文章目录
前言
编写一个并发程序的过程中,极其容易出现各种问题,其中最合性的问题就是并发编程的三大核心问题:原子性,可见性和有序性。本文会浅谈并发的三大核心问题以及其解决方法
一、并发编程的三大核心问题是什么?
原子性:操作要么全部执行,要么全不执行。
例:A给B转账100元,转账有两个步骤:1、A账户里的钱减少100;2、B账户里的钱增加100.若此时系统发生了故障,只完成了1的步骤,那么A的钱少了,B也没得到钱。这就是原子性问题。
可见性:并发过程中,多线程访问同一个变量时,一个线程修改了变量的值后,其它线程可以立即看到修改后的变量的值。
//例:
//线程1:
int i = 0;
i = 10;
//线程2
j = i;
//执行时,线程1的i已经变为10,但是没有写入主存中
//线程2读取时i任然是0。
有序性:程序执行的顺序按照代码的先后顺序执行。
//例:
1 int i = 0;
2 boolean flag = false;
3 i = 1;
4 flag = true;
以上代码的执行顺序并不一定是1-2-3-4,而可能是1-2-4-3或2-1-4-3。
原因:JVM在执行以上代码时可能会发生指令重排序
指令重排序:处理器为了提高运行效率,可能会对输入的代码进行优化,不保证各行数据的先后顺序一致,但能保证处理的结果一致。
在单线程编程中,指令重排序没有问题,但是当多线程工作时就会出现问题:
//线程1
context = loadContext(); //语句1
inited = true; //语句2
//线程2
while(!inited){
sleep()
}
doSomethingwithconfig(context);
以上代码语句1和语句2没有数据依赖性,所以可能发生指令重排序,先执行语句2,此时线程2会以为初始化工作已完成,跳出while循环,执行下面的操作,但此时context还没有初始化,导致线程出错。
二、Java内存模型(Java Memory Moder,JMM)
1.JMM作用
JMM用来屏蔽各个硬件平台和操作系统的内存访问差异,以实现java程序在各平台下都能达到一致的内存访问结果。
它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。
注意,为了获得较好的执行性能,**Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。**也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
2.Java本身对原子性、可见性和有序性的解决方案。
原子性:
Java内存模型只保证了基本的读取和赋值是原子性操作(例:i+1是原子性操作,i=x;和i++;不是),如果要进行原子性的操作,最好使用synchronized和Lock来实现。
可见性:
Java提供了volatile关键字来保证可见性。
volatile关键字:当一个共享变量被volatile关键字修饰时,他能够保证修改的值会立即被更新到主存中,其它线程读取时回去主存中读取。一般变量修改值后,什么时候更新到主存中是不一定的。
synchronized和Lock也可以保证可见性。
有序性:
Java中,volatile可以保证一定的有序性,synchronized和Lock也可以保证性。
另外,Java内存模型也具备一些先天的有序性,即“happens-before”原则。如果两个语句不能通过“happens-before”原则判断出执行次序,那么就不能保证它们的有序性,虚拟机会随意对他们指令重排序。
happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于后面的操作;
- 锁定规则:一个unLock操作先行发生于同一个锁的Lock操作;
- volatile规则:一个变量的写操作先行发生于同一个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,那么A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个操作;
- 线程中断规则:对线程的interrupt()方法先行发生于被中断线程的代码检测到中断时间的发生;
- 线程终结规则:先生中所有的操作都先行发生于线程终止操作。可以通过Thread.join()方法结束。Thread.idAlive()方法返回值等手段检测线程是否已经终止运行;
- 对象终结规则:一个对象的初始化完成先行于它的finalize()方法的开始。
以上8条规则摘自《深入理解Java虚拟机》
三、Volatile关键字
1.volatile关键字作用
当一个变量被volatile关键字修饰时,它会被赋予两个语义:
- 保证了不同线程对这个变量的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的;
- 禁止指令重排序。
volatile关键字不能保证原子性: 想要原子性可以采用synchronized、Lock和Atomic原子类。
Atomic原子类: 在jdk1.5中的java.util.concurrent.atomic包下提供了这一些原子操作类,能对基 本数据类型进行一些增减操作,保证这些操作是原子性操作。
atomic是利用CAS(Compare and swap)来保证原子性的,CAS实际上是用处理器提供的CMPXCHG指令来实现的,而处理器执行CMPXCHG指令是一个原子性操作。
Volatile关键字能保证一定的有序性:
volatile可以禁止指令重排序,所以能保证一定程度上的有序性。
- 当线程执行到volatile变量的读或写操作时,能保证前面的操作已经执行,且结果对后面操作可见,在其后面的操作肯定没有进行。
- 在进行指令优化时,不能将volatile前面的语句放在其后面执行,也不能把volatile变量后面的语句放在其前面之余还能行。
2.volatile原理及实现机制
观察加入volatile关键字和没有加入volatile关键字时所产生的汇编语言发现,加入volatile关键字时,会多出一个Lock前缀指令。(摘自《深入理解Java虚拟机》)
Lock前缀指令实际上相当于一个内存屏障,也成为“内存栅栏”,内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障前面的位置,也不会把前面的指令排到没存屏障的后面;即在执行到内存屏障这句指令时,前面的操作已经全部完成;
- 它会强制将对缓存的修改立即写入主存中;
- 如果是写操作,它会导致其它CPU对应的缓存行无效。
3.volatile适用场景
synchronized防止多线程同时执行一段代码,会很影响程序执行效率,而volatile在某些情况下性能优于synchronized,但是volatile并不能替代它,因为其无法保证原子性,一般来说,使用它有两个条件:
- 对变量的操作不依赖于当前值;
- 该变量不包含在具有其它变量的不变式中。
例:状态标记量、double check
[1]: https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析