关于类的线程安全
如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的;不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全;
类的线程安全表现为:
-
操作的原子性
-
内存的可见性
-
栈封闭
所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态;
-
无状态
没有任何成员变量的类
-
-
加
final
关键字,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final
关键字,但是加上final
public class FinalRef { private final int a; private final int b; private final User user;//这里不能保证线程安全啦 public FinalRef(int a, int b) { this.a = a; this.b = b; this.user = new User(); } public int getA() { return a; } public int getB() { return b; } public User getUser() { return user; } public static class User{ private int age; public User(int age) { super(); this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public static void main(String[] args) { FinalRef ref = new FinalRef(12,23); User u = ref.getUser(); //这里能修改user的值 //u.setAge(35); } }
2.不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值;
-
volatile
volatile
可用于在多线程环境下,保证类的可见性,即一个线程修改了,别的线程能够读取到,但volatile
并不能保证原子性;
Java内存模型规定了所有的变量都存储在主内存中;每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(如果局部变量是一个引用类型,它引用的对象在Java堆中可被各个线程共享,但是引用本身在Java栈的局部变量表中,它是线程私有的),线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存的变量;不同的线程之间也无法直接访问工作内存中的变量,线程间变量的值传递均需要通过主内存完成;
public class RunThread extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean running) { isRunning = running; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " 进入run"); while (isRunning) { } System.out.println(Thread.currentThread().getName() + " 线程停止"); } public static void main(String[] args) throws InterruptedException { RunThread thread = new RunThread(); thread.start(); Thread.sleep(1000); thread.setRunning(false); System.out.println(Thread.currentThread().getName() + " 已经赋值false"); } }
变量isRunning存在于公共堆栈和线程私有堆栈中(这里的公共堆栈指的是主内存,线程的私有堆栈指的是线程的工作内存),程序运行后一直在线程的私有堆栈中取得isRunning的值为true,虽然在主线程中执行thread.setRunning(false),更新的是公共堆栈的isRunning变量,线程间对于变量的修改是无感知的,操作的是两块内存地址的数据,如下图:
使用关键字volatile可以禁止代码的重排序;
在Java程序运行时,JIT(即使编译器)可以动态地改变程序代码运行地顺序;例如,有如下代码:
A代码-重耗时 B代码-轻耗时 C代码-重耗时 D代码-轻耗时
在多线程环境下,JIT有可能进行代码重排序,重排序后地代码顺序有可能如下:
B代码-轻耗时 D代码-轻耗时 A代码-重耗时 C代码-重耗时
这样做地主要原因是CPU流水线是同时执行这4个指令的,那么轻耗时的代码在很大程度上先执行完成,以让出CPU流水线给其他指令,所以代码重排序是为了追求更高的程序运行的效率;
重排序发生在没有依赖关系时,例如,对于上面的A,B,C,D代码,B,C,D代码不依赖A代码的结果,C,D代码不依赖A,B代码的结果,D代码不依赖A,B,C代码的结果,这种情况下就会发生重排序,如果代码之间有依赖关系,则代码不会重排序;
使用关键字volatile可以禁止代码重排序,例如,有如下代码:
A变量的操作 B变量的操作 volatile Z变量的操作 C变量的操作 D变量的操作
那么会有4种情况发生:
- A,B可以重排序
- C,D可以重排
- A,B不可以重排到Z的后面
- C,D不可以重排到Z的前面
换言之,变量Z是一个屏障,Z变量之前或之后不可以跨越Z变量,这就是屏障的作用,关键字synchronized具有同样的特性;
1.关键字synchronized之前的代码不可以重排到synchronized之后
2.关键字synchronized之后的代码不可以重排到synchronized之前
使用双重检查锁实现多线程环境下的延迟加载单例模式
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
使用volatile修饰变量singleton使该变量在多个线程间达到可见性,另外也禁止了singleton = new Singleton()的代码重排序,singleton = new Singleton()代码在内部分为3部分:
1.memory = allocate(); //分配对象的内存空间 2.ctorInstance(memory); //初始化对象 3.instance = memory; //设置instance指向刚分配的内存地址
在一些JIT编译器上,这种指令重排是真实发生的;
1.memory = allocate(); //分配对象的内存空间 3.instance = memory; //设置instance指向刚分配的内存地址 2.ctorInstance(memory); //初始化对象
所有线程在执行Java程序时都必须要遵守intra-thread semantics;intra-thread semantics保证重排序不会改变单线程内的程序结果;换句话说,intra-thread semantics允许那些在单线程内,不会改变单线程程序执行结果的重排序;
当线程A,线程B执行时,B线程访问instance所引用的对象,但这个对象没有被线程A初始化,线程B将看到一个还没有被初始化的对象;
这里的A2和A3虽然重排序了,但Java内存模型的intra-thread semantics将确保A2一定会排在A4前面执行;因此,线程A的intra-thread semantics没有改变,但A2和A3的重排序,将会导致线程B判断instance实例不为空,线程B接下来将访问instance引用的对象(上图中线程B中的虚线),此时线程B访问到的是一个没有没有初始化的对象(没有进行赋值的对象),返回的是一个空的对象;
当一个变量被volatile
修饰后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的可见性是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的;而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如线程A修改一个普通变量的值,然后往主内存进行回写,另外一条线程B在线程A回写完成后再从主内存进行读取操作,读取至B线程的工作内存,新变量才会对线程B可见;
使用volatile
关键字,可以强制从主内存(公共内存)中读取变量的值,绕过了线程的工作内存,如下图:
更多关于volatile可参考这篇博文[https://www.cnblogs.com/dolphin0520/p/3920373.html]
上面的单例是用了DCL的写法,但是在《Java并发编程实战》上对于单例写法并不推荐用DCL;
下面摘自《Java并发编程实战》16.2.4节的一段话;
”在JMM的后续版本(Java5.0以及更高的版本)中,如果把resource声明为volatile类型,那么就能启用DCL,并且这种方式对性能的影响很小,因为voliatile变量读取操作的性能通常只是略高于非volatile变量读取操作的性能;然而,DCL的这种使用方法已经被广泛废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化策略;延迟初始化占位类模式能带来同样的优势,并且更容易理解;“
上面提到了volatile,JDK5+,延迟初始化的关键字,如果使用DCL写单例没有使用volatile修饰resource,那实例创建在并发过程是可能出问题的,再看以下当时梁飞大佬写的的ppt;
使用静态内部类这种延迟加载的方式较优的;
public class Singleton { private Singleton() { } public static class SingletonHolder { static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }
- 这种方式利用了类加载机制来保证只创建一个instance实例,对于Singleton首次调用getInstance方法的时候,SingletonHolder会进行类加载,类加载的时候就创建了instance实例,并且对instance实例保证唯一,因此不存在并发创建实例的问题;
- 它是在内部类里面去创建对象实例,JVM会保证一个类的类构造器<clinit>()在多线程环境中
被正确的加锁、同步
,这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载;也就是说这种方式可以同时保证延迟加载和线程安全;
不过这种方式也不是完全的线程安全,如果使用者通过反射创建Singleton实例,那单实例就不能保证了,一般大多情况下这种方式够用了;完全保证单例可使用枚举的方式;
public class Singleton { public static class SingletonHolder { final static Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } private Singleton() { } public static void main(String[] args) { Runnable runnable = () -> { try { Class<Singleton> claz = Singleton.class; Constructor<Singleton> constructor = claz.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton = constructor.newInstance(); System.out.println(singleton + " -- " + Thread.currentThread()); } catch (Exception e) { e.printStackTrace(); } }; Thread thread1 = new Thread(runnable, "thread-1"); Thread thread2 = new Thread(runnable, "thread-2"); thread1.start(); thread2.start(); } }
参考:[https://blog.csdn.net/fly910905/article/details/79286680]
[https://blog.csdn.net/sunkun2013/article/details/12764101]
-
使用原子操作类,synchronized,Lock锁
-
-
LockSupport定义一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能;
-
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程,如果调用unpark(Thread thread)方法或当前线程,才能从park()方法返回 |
void parkNanos() | 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回 |
void parkUntil(long deadline) | 阻塞当前线程,直到deadline时间(从1970年开始到deadline时间的毫秒数) |
void unpark(Thread thread) | 唤醒处于阻塞状态的thread |
注意:阻塞状态时线程阻塞在进入synchronized关键字修饰方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为 java.concurrent包中的Lock接口对于阻塞的实现均使用LockSupport类中的相关方法
-
-
Condition接口
任意一个Java对象,都拥有一组监视器(定义在java.lang.Object类),主要包括wait(),wiat(long timeout),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式;Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式;
-
Object的监视器方法与Condition接口的对比
Condition的(部分)方法以及描述
方法名称 | 描述 |
---|---|
void await() throws InterruptedException | 当前线程进入等待状态直到被通知(signal)或中断,当前线程进入运行状态且从await()方法返回的情况,包括:其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒;其他线程(调用interrupt()方法)中断当前线程;如果当前等待线程从await()方法返回,那么表面该线程已经获取了Condition对象所对应的锁 |
void awaitUninterruptibly() | 当前线程进入等待状态直到被通知,从方法名称上可以看出该方法对中断不敏感; |
long awaitNanos(long nanosTimeout) throws InterruptedException | 当前线程进入等待状态直到被通知,中断或超时;返回值表示剩余时间,如果在nancosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout - 实际耗时);如果返回值是0或负数,那么可以肯定已经超时 |
boolean awaitUntil(Date deadline) throws InterruptedException | 当前线程进入等待状态直到被通知,中断或到某个时间;如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false |
void signal() | 唤醒一个等待在Condtion上的线程,该线程从等待方法返回前必须获得与Condition相关联得锁 |
void signalAll() | 唤醒所有等待在Condition上得线程,该线程从等待方法返回前必须获得与Condition相关联得锁 |
使用线程本地变量
Servlet不是线程安全类,如需共享资源,会出现线程不安全;Servlet的生命周期是接收到请求,创建一个Servlet,返回一个应答时,销毁Servlet,都是由一个线程负责的;
- 死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁;
当资源多于1个,同时小于等于竞争的线程数;获取锁的顺序不一致会导致死锁;当资源只有一个,只会产生激烈的竞争;解决方法:jstack 查看应用的锁的持有情况;保证加锁的顺序性;
动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的;解决: 通过内在排序,保证加锁的顺序性;也可以通过尝试拿锁;
- 活锁
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程;处于活锁的实体是在不断的改变状态,活锁有可能自行解开;如下例子:
/** *类说明:不会产生死锁的安全转账方法,尝试拿锁 */ public class SafeOperate implements ITransfer { @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { Random r = new Random(); while(true) { if(from.getLock().tryLock()) { try { System.out.println(Thread.currentThread().getName() +" get "+from.getName()); if(to.getLock().tryLock()) { try { System.out.println(Thread.currentThread().getName() +" get "+to.getName()); //两把锁都拿到了 from.flyMoney(amount); to.addMoney(amount); break; }finally { to.getLock().unlock(); } } }finally { from.getLock().unlock(); } } //错开线程拿锁的时间 //Thread.sleep(r.nextInt(10)); } } }
上面的线程休眠是用于错开线程拿锁的时间,休眠看起来会耗费时间,但效率会得到提高,能够减少出现重复尝试——失败的次数;