Volatile关键字
保证线程间共享变量的可见性、有序性。
如何保证可见性:多了一个Lock的汇编指令。
什么是可见性
- 硬件
- 运行速度:CPU > 内存 > IO设备;所以在硬件的使用中,为了提高效率
- CPU增加了高速缓存
- 读数据时,从内存读,然后缓存到CPU缓存,线程修改变量时会先同步到缓存,再同步到主内存。这时候会出现缓存一致性问题,CPU层面解决该问题的方案是总线锁,缓存锁。
- 缓存一致性协议(MESI)S:Shared;E:Exclusive;I:Invalid;M:Modify。缓存处于I状态时,必须去主内存读;缓存处于S状态时,写数据必须先让其他CPU缓存变为I状态。
- MESI协议又会导致在Invalidate其他线程变量时有阻塞,引入了storebuffer,指令写入到storebuffer,降低阻塞。但是这又会导致执行乱序,引发新的可见性问题。CPU层面提供了指令->内存屏障来解决可见性问题。三种屏障:写屏障(之前的结果必须对后面可见)、读屏障(读屏障之后的指令一定能读到最新数据)、全屏障。
- 引入线程、进程
- 指令优化:重排序
- CPU增加了高速缓存
- 运行速度:CPU > 内存 > IO设备;所以在硬件的使用中,为了提高效率
- JMM(Java Memory Model)
- 为了解决平台差异化的问题,Java提供了JMM(语言级别的抽象内存模型),核心价值是解决了可见性和有序性的问题。
- 概念
- 工作内存:线程本地内存
- 主内存:线程共享的内存
- 线程会先读工作内存,再读主内存。在这种情况下,还是会存在可见性问题。为了解决这些问题,java引入了jvm级别的4种屏障:loadload,storestore,loadstore,storeload
- loadload:该屏障之前的读一定能被后面读到
- storestore:前面的写先于后面的写(后面写能知道前面的写)
- loadsotre:前面的读先于后面的写(后面能写到前面的读)
- storeload:前面的写先于后面的读(后面能读到前面的写)
还引入了volatile、synchronized、final、happens-before一些关键字和原则。
- 重排序:java源代码->编译期重排序->cpu重排序->最终执行的顺序;重排序不能改变程序单线程运行下的结果(as-if-serial)。
- volatile
- volatile通过语言级别的屏障指令,保证缓存每次都被刷到主内存,读取均从主内存读,来保证可见性
- volatile同时还保证了有序性(经典用法就是解决double-check单例模式对象new过程中的重排序)
- volatile不能保证原子性
- happens-before原则
- 1、程序的顺序规则:一个线程中的任意操作,happens-before于该线程中的任意后续操作。
- 2、volatile规则:对一个volatile字段的写,happens-before于任意后续对该字段的读。
- 3、传递性规则:如果A happens-before B,B happens-before C,那么A一定 happens-before C。
- 4、start()规则:如果A线程操作执行B线程的start(),那么A线程的B.start() happens-before B线程中的任意操作。
- 5、join()规则:如果A线程执行操作B线程的join()并成功返回,那么B线程的任意操作一定happens-before 线程A的 B.join()。
- 6、监视器锁规则:对锁的释放 happens-before 后续对该锁的获取。
- 7、程序中断规则:线程A对线程B interrupted()调用 happens-before B线程检测到被中断线程的中断时间(抛出interruptException)。
- 8、对象finalize规则:对象的初始化完成 happens-before 它的finalize()方法。