并发包和内存模型
Contents
Java内存模型的基础
并发编程中,需要处理两个关键问题,线程之间如何通信和线程之间如何同步。通信是指线程之间以何种机制来交换信息,线程间的通信机制有两种,共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信,
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
Java 内存模型
Java线程之间的通信由Java内存模型JMM控制,JMM决定一个线程对共此享变量的写入何时对另一个线程可见,从抽象的角度讲,JMM定义了线程和主内存之间的抽象关系。
Java 内存模型把虚拟机内部划分为线程栈和堆。 每一个运行在Java虚拟机中的线程都有自己的线程栈。线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程只能访问自己的线程栈。一个线程创建的本地变量对其他线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。所有原始类型的本地变量都存放在线程栈上,因此对其他线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。
一个本地变量可能是原始类型,这种情况下,它总是待在线程栈上。
一个本地变量也可能是指向一个对象的一个引用,这种情况下,这个引用存放在线程栈上,对象本身还是在堆中。
一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
静态成员变量跟随着类定义一起也存放在堆上。
存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。
happens - before规则
JDK5开始,使用happens-before的概念来阐述操作之间的内存可见性,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这两个操作可以是一个线程内,
1 | 1 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 |
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,happens-before仅仅要求前一个操作的结果对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
as-if-serial 语义
As-if-serial 的意思是 不管怎么重排序,程序的执行结果不能被改变,编译器,runtime和处理器都必须遵守as-if-serial 语义。
为了遵守as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果,但是,如果操作之前不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
顺序一致性(理想化的模型)
顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
顺序一致性模型有两大特性1 一个线程中的所有操作必须按照程序的顺序来执行。
2(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
volatile 的内存语义
- 特性:可见性 原子性
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile 变量的读,总是能看到任意线程对这个volatile 变量最后的写入。
同时,对任意单个volatile变量的读、写具有原子性,但类似volaile++这种复合操作不具有原子性。
- 写-读的内存语义
线程A写一个volatile变量,实质上是线程A向接下来将要读这个变量的某个线程发出了(其对共享变量所做修改的)消息
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出(在写这个volatile变量之前对共享变量所做修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile 变量,这个过程实质上是线程A通过主内存向线程B发送消息。
内存语义的实现主要通过插入内存屏障来实现
锁的内存语义
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量做修改的)消息
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
Current包的实现
由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
- A线程写volatile变量,随后B线程读这个volatile变量。
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
##
Author: corn1ng
Link: https://corn1ng.github.io/2018/01/19/Java并发/并发包和内存模型/
License: 知识共享署名-非商业性使用 4.0 国际许可协议