线程间通信机制有两种:共享内存、消息传递,Java并发采用的前者(堆内存和线程本地内存见得数据同步);
指令重排序
概念:编译器或处理器为了优化程序性能而对指令序列重排序的手段;
从源代码到最终的指令序列经过了3个重排序:编译器优化重排序(Java编译器处理)、指令集并行重排序、内存系统重排序(处理器处理);
如何保证内存可见性:通过禁止特定类型的重排序保证可见性和正确性,编译器通过编译规则禁止,处理器通过插入内存屏障处理;
内存屏障指令分为4类:LoadLoad、LoadStore、StoreStore、StoreLoad
happens-before
概念:一个操作执行结果对另一个操作可见,则存在happens-before关系。两个操作可以是一个线程也可以是不同线程的;
并不是一个操作先于一个操作执行,而是结果可见且顺序上在前面,规则比如:
程序员层面:happens-before规则; -> JMM实现:禁止特定类型的编译重排序;-> JMM规则:编译器重排序规则+处理器重排序规则;
即一个happens-before规则对应一个或多个编译器和处理器重排序规则;
happens-before规则包括:
程序顺序规则:一个线程中的每个操作happens-before该线程的后续操作;
volatile规则:对一个volatile域的写happens-before任意后续的读;
监视器锁规则:对一个锁的解锁happens-before随后对这个锁的加锁;
传递性:A happens-before B,B happens-before C,则A happens-before C;
start()规则:A线程中执行B.start(),则A中的B.start() happens-before B中的任意操作;
join()规则:A线程中执行B.join(),则B的任意操作happens-before A从B.join()返回;
as-if-serial
概念:不管怎么重排序,单线程执行的结果不会被改变;
重而造成单线程按顺序执行的幻觉,实际上也是经过重排序的,只是不影响结果;
允许控制依赖的重排序:如if(flag){int i=a*a},编译器可能会先计算好a*a的值放入缓冲区,为true时再将结果写入i;
as-if-serial保证单线程内程序执行结果不被改变,happens-before保证正确同步的多线程执行结果不被改变,目的都是在不改变执行结果的前提下提升性能;
顺序一致性模型
是一个理想的理论参考模型,就是程序的执行顺序和它编写的顺序一致,特性:一个线程的所有操作必须按照程序的顺序执行;每个操作都原子且对所有线程立即可见,所有线程看到的总体执行顺序一致;模型中有一个单一的全局内存,任一时间还能有一个线程连接到内存;
Java没使用该模型,因为:
编译器和处理器无法对程序做到优化,在Java中我们使用的是可以进行指令重排序的JMM模型。
顺序一致模型要求线程的每一个操作都具有原子性,也就是说,读写都会操作主存,这样的效率低;
数据依赖性
写后读、读后写、写后写存在数据依赖,读后读不存在;
编译器和处理器不会改变存在数据依赖的两个操作;
数据依赖的判断只针对单处理器和单线程,多处理器和多线程不会被编译器和处理器考虑,所以存在并发问题;
同步的3个源语:volatile、锁、final
volatile
volatile变量的单个读或写等价于一个普通变量的读或写使用同一个锁同步操作,volatile变量的特性:
可见性:对volatile变量的读总是能看到任意线程对他最后的写;
原子性:单个volatile变量的读或写具有原子性,类似volatile++的符合操作没有;(这也是不能替代锁的原因)
volatile写的内存语义:JMM把该线程对应的本地内存的变量值刷新到主内存;
volatile读的内存语义:JMM把该线程对应的本地内存置为无效,线程将从主内存读取;
内存语义的实现:在volatile读写操作的前后加入不同类型的内存屏障(LoadlLoad、LoadStore等)
锁
锁的作用:让临界区互斥执行;释放锁的线程向获取同一锁的线程发送消息;
释放锁的内存语义:JMM把该线程对应的本地内存的变量值刷新到主内存;(同volatile写)
获取锁的内存语义:JMM把该线程对应的本地内存置为无效,线程将从主内存读取;(同volatile读)
final
final域对象的读不能重排序;final域的写不能重排序到构造函数之外;
网友评论