《Java并发编程的艺术》Java内存模型(三)
Java内存模型
一、Java内存模型的基础
1.并发编程模型的两个关键问题:
两个关键问题,线程之间如何通信和如何同步。两种方式,共享内存和消息传递。Java里线程的通信是通过共享内存,线程的同步是显示进行的,而通信则是隐式进行的。
2.Java内存模型的抽象结构:
实际上就是说Java线程之间的通信是通过共享内存来进行交互。
3.从源代码到指令序列的重排序:
重排序,让指令序列重新排序而更高效地运行程序。Java中根据不同的处理器会禁用对应的重排序规则(内存屏障),从而保证了内存可见性。
4.并发编程模型的分类:
对于处理器来说主要需要重排序的操作是:写-读操作,否则可能会导致数据的脏读。不同的处理器分别在“读-读”、“写-写”、“读-写”、“写-读”操作上可能都有可能进行重排序。而在不需要重排序的处理器上,Java提供了内存屏障指令来避免没有必要的重排序。
5.happens-before概念:
操作A happens-before 操作B,那么操作A的结果就对操作B可见,happens-before概念并不意味着操作A就一定发生在操作B之前。
二、重排序
对指令的重排序是对程序执行的一种优化;
1.数据依赖性
如果两个操作操作同一数据,只要有个操作是“写”,那么这两个操作之间存在数据依赖性;
2.as-if-serial语义
不管怎么重排序,单线程的处理结果不能被改变;
3.重排序对多线程的影响
两个线程分别操作两个数据,线程A的两个数据没有数据依赖性,而线程B的两个数据存在数据依赖性,那么线程A中可能发生的重排序可能会影响到线程B的操作的结果。
三、顺序一致性
顺序一致性模型是一个理想化的内存模型;
1.数据竞争与顺序一致性
数据竞争:两个线程之间,线程A在写一个数据,线程B又在读这个数据,并且两个线程没有做同步;而在JMM中,做了同步操作的结果可以保证和顺序一致性模型的结果保持一致;
2.顺序一致性内存模型
顺序一致性的两大特性:
(1)一个线程的所有操作必须按照程序的顺序执行;
(2)不管是否同步,所有线程只能看到单一的操作执行顺序;
3.同步程序的顺序一致性效果
同步的程序之间,临界区内的代码可能会进行重排序,但是最终的结果和顺序一致性模型是一样的,这样就既保证了顺序一致性的效果,又能对性能进行优化;
4.未同步程序的执行特性
四、volatile的内存语义
1.volatile特性:
有 volatile 变量的方法相当于加了 synchronized 锁;
2.volatile内存语义:
两个分别对同一个 volatile 变量进行写和读的线程:
(1)写线程在写了之后,读线程才去读这个写线程发出(修改)的信息;写线程在把本地内存的信息写了之后,刷新到共享内存中;
(2)读线程读一个变量,相当于读了写线程写了之后的变量;
(3)相当于写线程和读线程之间的通信;
五、锁的内存语义
1. 锁获取与锁释放内存语义:
synchronized 锁与 volatile 的内存语义:锁获取与volatile读有相同内存语义,锁释放与volatile写有相同内存语义;
2.锁的源码分析:
以 ReentrantLock 为例:
公平锁:
(1)lock()方法获取锁,最终调用tryAcquire()方法,读取volatile变量;
(2)unlock()方法释放上锁,最终调用tryRelease()方法,写volatile变量;
非公平锁:
(1) lock() 方法最终会去调用CAS方法去更新volatile变量,从而有volatile读写共有的内存语义;
六、final域的内存语义:
1.final域修饰基本数据类型成员变量:
JMM不会将构造函数中的写final域与其它线程中的读final域进行重排序,不会将写final域操作重排序在构造函数外面;
2.final域修饰引用类型成员变量:
构造函数中,final域修饰的引用成员变量的任何操作都不会重排序在构造函数外部,比如在构造函数中读引用变量,使用引用变量给赋值,而在其它线程方法中给此引用变量赋值则可能被重排序到读操作之后;