一、并发编程的三要素
原子性 :原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
可见性 :可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他 线程可以立即看到修改的结果。
有序性 :有序性,即程序的执行顺序按照代码的先后顺序来执行。
为了使程序正确执行,我们需要满足以上三个条件,但是在并发环境中,往往是会产生冲突的。
1、JAVA初始化对象字节码执行乱序问题(this溢出问题)
我们来看new对象的执行步骤:
- new ThreadThisTest() :为对象申请空间,并将成员变量mk初始化化为0;
- dup:压栈操作
- invokespecial:调用构造函数初始化对象,将成员变量mk设置为10;
- astore_1:建立关联,将栈中的变量thisTest指向new 出来的对象;
如果invokespecial与astore_1指令执行顺序改变,我们的代码执行的结果就会是0;由此问题我们就知道为什么在double check 单例模式中我们为何要加上volatile关键字了
二、volatile
volatile的两大作用:
- 保证共享变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,并且每次使用都会从主内存获取。
- 禁止指令重排序优化,有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),单核CPU无需内存屏障;
汇编指令lock的作用:
- 重排序时不能把后面的指令重排序到内存屏障之前的位置
- 使得本CPU的Cache写入内存
- 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见
volatile指令的有序性:为了禁止指令的重排序,编译器会在编译指令的时候,插入内存屏障来保证volatile指令的有序性:
- 对于写操作:在每个volatile写操作的前面插入一个StoreStore屏障,再在其后面插入一个StoreLoad屏障。
- 对于读操作:在每个volatile读操作的后面插入一个LoadLoad屏障,再在其后面插入一个LoadStore屏障。
- StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
- StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
- LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
- LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
double-check singleton Pattern:
public class DoubleSingle {
//volatile 作用防止创建对象时指令的重排序
private static volatile DoubleSingle doubleSingle;
private DoubleSingle(){
}
public static DoubleSingle getInstance(){
//第一层判断,如果不为空直接返回
if(doubleSingle==null){
//加锁防止多线程同时来创建对象
synchronized (DoubleSingle.class){
//再判断是防止多个线程一起竞争锁,当某个线程创建对象后,
// 其他获得锁的线程进入代码后做再次检查,避免重复创建
if(doubleSingle==null){
doubleSingle=new DoubleSingle();
}
}
}
return doubleSingle;
}
}
加volatile的原因:
对象初始化的逻辑:如果2和3重排序,那么single指向的是一个未初始化的对象
- single = allocate(); // 为对象开辟内存空间
- ctorInstance(DoubleSingle); // 2:初始化对象
- instance = single; // 3:设置instance指向刚分配的内存地址
volatile不能保证原子性:例如i++
volatile的integer自增(i++),其实要分成3步:
- 1)读取volatile变量值到local;
- 2)增加变量的值;
- 3)把local的值写回,让其它的线程可见。
这3步的jvm指令为:
- mov 0xc(%r10),%r8d ; Load
- inc %r8d ; Increment
- mov %r8d,0xc(%r10) ; Store
- lock addl $0x0,(%rsp) ; StoreLoad Barrier
例如:线程A先读取i,然后A被阻塞了,这时线程B也去读取变量i,由于线程A只是读取,没有做修改操作,所以线程B读取的数据与A相同都是0,线程B继续执行i=i+1=1,并把1写入工作内存,最后写入主存;然后线程A获取到了CPU资源,执行i=i+1=1;并把1写入工作内存,最后写入主存;由此可见对于非原子操作voalitle是无法保证其执行结果的。
使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中