1.顺序一致性模型,这个是一个理论参考模型,为程序员提供了一个极强的内存可见性保证,主要有以下两个特征:
a.一个线程中所有操作必须按照程序中的顺序执行
b.多线程中(不管是否同步),所有线程只能看到一个单一的执行顺序。即在内存中有一个全局的顺序性。因为在同一时刻只有一个线程会连接到主内存,对主内存的操作会立刻同步到主内存,对其他的线程是可见的。
例如在做线程同步的情况下,A线程获取到锁,A有三个操作A1->A2->A3,B线程也有三个操作B1->B2->B3,这时顺序一致性模型可以保证最终A,B线程看到的执行顺序都是A1->A2->A3->B1->B2->B3。如果是未做同步的情况下,假如A,B同时执行,可能出现的一个执行顺序是B1->A1->B2->A2->B3->A3,但是由于在某一个时刻只有一个线程可以操作主内存,B线程的操作会立即同步到主内存,所以A线程和B线程最终看到的都是上面这个顺序。
但是可以看到这个保证实现的时候会有很大的性能问题
2.处理器重排序规则(处理器级别)
为了尽可能的提高执行效率,而且没有数据依赖的情况下不同的处理器有自己的内存模型,比如会对read-write,read-read,write-write,write-read这些指令操作进行重排序,具体数据如下:
但是这些处理器在重排序时都会遵循as-if-serial的规则,也就是不会对程序执行结果产生影响的情况下会执行相应的指令的重排序
3.编译器重排序规则happens-before(语言级别层面,面向程序员)
保证在单线程或者正确同步的多线程情况下,执行的结果与顺序一致性模型结果一致。
happens-before规则如下:
a.程序顺序规则:一个线程中的任意操作happens-before该线程中后续的任意操作。即保证一个线程中的执行结果会和顺序一致性模型执行的结果一致,重点是结果一致,但是对于没有数据依赖的,jmm会允许处理器进行重排序,只要保证最终的结果一致就可以
b.监视器锁规则:对一个锁的解锁操作happens-before对这个锁的加锁操作
c.volatile变量规则:对一个volatile变量的写操作happens-before对这个变量的读操作
d.顺序传递性规则,Ahappends-beforeB操作,Bhappends-beforeC操作,则Ahappends-beforeC操作
e.start()规则,对一个线程的启动操作happends-before对这个线程的后续所有操作
f.join()规则,A线程对B线程执行join操作,B结果返回前happends-beforeA线程后续的操作
总结:无论是编译器的重排序还是处理器的重排序都会参照顺序一致性模型,
由于处理器模型较弱(任意的读读,或者读写都可能重排序),jmm为了实现某些关键字的内存语义,会在编译成字节码的时候加入一些内存屏障防止处理器的指令重排序
volatile关键字主要有两个内存语义:
1.内存可见性,即对一个volatile变量的写操作,其他的线程是可见的。主要是通过线程每次读取volatile变量的时候jmm会将该线程的本地缓存清除,重新从主内存读取最新的数据
2.原子性,对单个volatile变量的单个读或者写操作是原子的,但是对于复合操作并不是原子的,例如volatile++这种,long或者double类型的变量在64位处理器执行读或者写操作时就不是原子的。
3.可以防止处理器指令重排序,这个主要是通过内存屏障实现,具体如下:
a.在每个volatile变量的写操作前面插入store-store内存屏障
b.在每个volatile变量的写操作后面插入store-load内存屏障
c.在每个valatile变量的读操作后面插入load-load屏障
d.在每个volatile变量的读操作的后面插入load-store屏障
如图:
总结:通过内存屏障可以保证volatile的写操作先行与对volatile变量的读操作,以及禁止volatile变量的读写与普通变量的读写进行重排序
volatile变量的一个重要应用:延迟初始化
这个延迟加载的方法实现就是有问题的,主要是因为new SingleInstance()这个过程不是一个原子的过程,涉及三个过程,1.分配内存空间2.初始化3.设置singleInstance。其中由于2过程和3过程不存在数据依赖,所以处理器可能会进行重排序。
例如如下场景:
A线程获取锁然后执行singleInstance = new SingleInstance()操作,但是由于2,3被重排序了,这时B线程进入getInstance方法,由于singleInstance对象已经不是null,所以直接返回了一个没有初始化的对象,从而产生问题。
解决方案:1.用volatile修饰singleInstance对象,由于volatile对象的写操作happens-before读操作,所以2,3过程不会被重排序,这样也就不会出现问题
2.通过类加载后触发类初始化时给这个类对象加锁的机制实现
由于调用getInstance方法时访问了InnerSingleInstance的静态变量,所以会触发InnerSingleInstance的类初始化方法,这个时候由于类初始化锁的存在,所以只会有一个线程获取到锁,获取到锁后就算会出现上面的2,3被处理器重排序的情况,但是对于其他的线程是无感知的,所以获取到的对象是被正确初始化的对象。
final关键字
对于final变量编译器要遵循以下规则:
1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域的内存语义的实现是通过在final域的写入之后,在构造函数的返回之前插入一个store-store内存屏障,所以在使用一个未初始化的final变量之前一定要在构造函数中对这个变量进行初始化
而读final域的内存语义的实现是通过在读final域的前面插入一个load-load屏障,防止处理器对初次读包含final域的对象引用和读这个final域变量进行重排序。
如下例:
假设A线程开始执行后,B线程立即执行,则B线程读取到的b变量值一定是2,但是a变量可能会是0,因为这个时候i可能还没有完成初始化,由于处理器的重排序可能会把i=1这个指令重排序到返回对象引用之后,但是由于final的内存语义却一定会先对j变量进行初始化
总结:对于多线程的问题,有时候通过final和volatile关键字也可以在一定程度上实现同步的效果