并发编程的三个问题:原子性问题,可见性问题,有序性问题
原子性
概念简介
- 一个操作或者多个操作,要么全部执行,要么就都不执行
- 执行的过程不会被任何因素打断
- 在并发中要求互斥访问
只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。Java内存模型只保证了基本读取和赋值是原子性操作。
Java语言提供的保证:
通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
普通的共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值。
Java语言提供的保证
volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
程序执行的顺序按照代码的先后顺序执行。
指令重排序(Instruction Reorder)
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
处理器在进行重排序时是会考虑指令之间的数据依赖性。重排序会影响多个线程内程序执行的结果。换句话说,重排不会影响单个线程内程序执行的结果。
结论:指令重排的适用范围是线程,它不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
举个参考资料里的例子:
我相信,有经验的程序员,即使不懂的指令重排,本能上也不会用两个线程异步的执行下诉初始化操作。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingWithConfig(context);
计划执行:在线程1里执行语句1,初始化加载操作,完成后,标志位inited置为true,线程2,跳出睡眠,执行doSomethingWithConfig方法。
实际执行:由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
Java语言提供的保证:
通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性
volatile关键字
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2.禁止进行指令重排序。
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
volatile关键字的使用场景
volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。因此,当保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
- 状态标记量
- double check。
单例的最优写法:
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
参考资料