前言
从并发编程中的原子性、可见性、有序性三个角度出发,最后结合单例模式的双重校验锁模式进一步分析volatile的实战场景。
一、并发编程核心问题
并发编程的核心问题就是解决三个特性:可见性、有序性、原子性。
什么是可见性?
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性
什么是有序性?
有序性指的是程序按照代码的先后顺序执行
注意:编译器和处理器可能会对指令进行重排序,会影响到多线程并发执行的正确性.
什么是原子性?
操作是不可再分割的。
例如:int i=0 就是一条原子性操作。
为什么i++不是线程安全的? 因为i++不是一个原子性操作。
例如:i++ ,需要三条指令
(1)把变量i从内存加载到工作内存;
(2)在工作内存执行 +1 操作;
(3)将结果写入内存;
在多线程环境下,两个线程各自执行100次i++操作,i的值为多少?
答:i的取值范围可能是[2,200]之间。取值为200很容易理解,这是我们希望看到的状况。但为什么会出现取值为2的情况?
i刚开始为0时,两个线程的状态如下:线程A和线程B的工作内存的i的值都为0
1.线程A执行了99次操作,把i加到了99,然后写入了主内存,但是线程B的工作内存值还是为0,随后线程B进行了一次i++操作,把i修改成了1写入了主内存,这样线程B的i值就覆盖了线程A的值。
2.然后线程A读取主内存的值为1,线程B也读取主内存的值为1,线程B进行了99次操作,把i加到了100写入主内存,线程A执行了最后一次+1操作,把i=2写入了主内存,覆盖了主内存的100,所以最后i的值为2。
二、解决思路
原子性:考虑atomic包下的类。
可见性和有序性:考虑volatile关键字。
注意:,volatile关键字是无法保证原子性,使用synchronize能保证原子性和可见性以及有序性:因为只有一个线程能获取到synchronize锁,当然不会出现并发修改问题,但是效率太低,建议不要一上来就使用sychonize。
三、双重校验锁
双重校验锁是单例模式的一种实现方法,之前的文章中已经介绍了,但为详细解释为什么要使用volatile关键字修饰单例对象。首先我们来回顾一下双重校验锁的实现代码。
public class LockSingleton {
private volatile static LockSingleton lockSingleton;
private LockSingleton(){}
public static LockSingleton getInstance(){
if(lockSingleton==null){
synchronized (LockSingleton.class){
if (lockSingleton==null){
lockSingleton=new LockSingleton();
}
}
}
return lockSingleton;
}
}
创建一个对象实例lockSingleton=new LockSingleton(),可以分为三步:
1.分配对象内存
2.调用构造器方法,执行初始化
3.将对象引用赋值给变量。
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
例如:线程1准备创建单例对象,但发生了重排序,没有初始化对象就直接把对象赋值给了单例变量lockSingleton,此时线程2准备访问lockSingleton,由于lockSingleton不是null,直接返回使用,但未进行初始化,访问可能会出现问题。
正确的双重检查锁定模式需要需要使用volatile
1.保证可见性。使用 volatile定义的变量,将会保证对所有线程的可见性。
2.禁止指令重排序优化,由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
总结
本章内容介绍了并发编程核心问题以及具体的解决思路,并结合设计模式的内容列出了volatile关键字的实战场景,有助于加深理解。