【Java并发编程】Java内存模型(上)

Java并发/多线程编程系列blog(一)内存模型(上)含Java内存模型基础/重排序/顺序一致性/volatile内存语义/锁的内存语义

内存模型基础

并发编程模型的两个关键问题

  1. 线程之间如何通信?

    通信是指线程之间以何种机制来交换信息。在命令式编程 中,线程之间有两种通信机制:共享内存和消息传递

    • 共享内存的并发模型:线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
    • 消息传递的并发模型:线程之间没有公共状态,线程之间必须通过发送消 息来进行显式通信
  2. 线程之间如何同步?

    同步是指程序中用于控制不同线程间操作发生相对顺序的机制

    • 共享内存的并发模型:同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行;
    • 消息传递的并发模型:由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

3.Java并发采用的机制

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对 程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Java内存模型(JMM)的抽象结构

1. JMM涵盖的范围

在Java中,所有实例域、静态域和数组元素(总体称之为共享元素)都存储在堆内存中,堆内存在线程之间共享。局部变量),方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

2. JMM的描述

Java线程之间的通信由JMM控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
C4hPK.png

3.线程间的通信过程

C4s1G.png
由图可知线程AB之间的通信过程为:

  • 本地内存A和本地内存B由主内存中共享变量x的副本(默认为0);
  • 线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1;
  • 当线程A和线程B需要通信时,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

总体来说就是线程A与线程B通过主内存来进行通信,而JMM就是通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证的。

从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常常会对源代码指令做重排序。重排序分3种类型(实际中会分别按以下顺序进行从源代码到最终执行的指令序列做重排序)。

  1. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句 的执行顺序;
  2. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序;
  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执行。

    上述的1属于编译器重排序2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。 JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

重排序所带来的问题与解决方案

C41al.png

在这个小程序的运行中,很可能结果会为a=b=0(因为拿处理器A来说,它执行的步骤为A1->A2->A3)。虽然处理器A执行内存操作的顺序为:A1→A2,但内存操作实际发生的顺序却是A2→A1。此时,处理器A的内存操作顺序被重排序了,这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令禁止特定类型的处理器重排序。JMM把内存屏障指令分为4类:
C4w78.png
其中StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

happens-before简介

。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

一些有关happens-before的规则

  • 程序顺序规则:一个线程中的每个操作,happens-before该线程中的任意后续操作
  • 监视器锁规则:对一个锁的解锁,happens-before随后对这个锁的加锁
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

注意

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见且前一个操作按顺序排在第二个操作之前的定义很微妙,后文会具体说明happens-before为什么要这么定义。,一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对 于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

重排序

定义

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间 就存在数据依赖性。
CM2CD.png
这三种情况只要重新排序两个操作的执行顺序,执行结果就会发生改变
前编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变单个处理器中执行的指令序列和单个线程中执行的操作中存在数据依赖关系的两个操作的执行顺序不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义,也就是说存在数据依赖的操作的执行顺序不会被改变,但是不存在数据依赖的操作的执行顺序可能会被改变,如 1.int a=1; 2.int b=1; 3.int c = a+b;在这个情况中,1和2依赖3所以3不会在1 2之前执行,但是1和2并不一定顺序执行。由于结果是一样的,但是要注意单线程程序并不一定是按照程序的顺序执行的!as-if-serial把单线程程序保护了起来,使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

程序顺序规则

上面举例中的操作1的执行结果不需要对操作2可见;而且重排序操作1和操作1后的执行结果,与操作1和操作2按happens-before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法,JMM允许这种重排,所以说在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。编译器和处理器遵从这一目标,从happens-before的定义我们可以看出,JMM同样遵从这一目标。

重排序对多线程的影响

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语 言的内存模型都会以顺序一致性内存模型作为参照。

数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。Java内存模型规范对数据竞争的定义:在一个线程中写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序。当代码中包含数据竞争时,程序的执行往往产生错误结果。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。JMM对正确同步的多线程程序的内存一致性做了如下保证: 如果程序是正确同步的,程序的执行将具有顺序一致性程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。马上我们就会看到,这对于程序员来说是一个极强的保证。这里的同步是指广义上的同步,包括对常用同步原语 (synchronized、volatile和final)的正确使用。

顺序一致性内存模型

CMBlI.png

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。当多个线程并发 执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中,所有操作之间具有全序关系)。顺序一致性内存模型有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见
    CMdjG.png
    CMsXw.png
    但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本没有被当前线程执行。只有当前线程把本地内存中写过的数据刷 新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其他线程看到的操作执行顺序将不一致。

同步程序的顺序一致性效果

对程序ReorderExample用锁来同步,看看正确同步的程序如何具有顺序 一致性。
CM1G2.png
CMqw8.png
从这里我们可以看到,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有如下几个差异:

  • 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行;
  • 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
  • JMM不保证对64位的long型和double型变量的写操作具有原子性(因为32位处理器可能在总线执行机制中被拆分的两个事务其间穿插其他事务),而顺序一致性模型保证对所有的内存读/写操作都具有原子性。但从JDK5开始,仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

数据总线的执行机制:

CMWdY.png
在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步
骤称之为总线事务。总线事务包括读事务和写事务。每个事务会 读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写。假设处理器A,B和C同时向总线发起总线事务,这时总线仲裁(Bus Arbitration)会对竞争做出裁决。总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写 操作具有原子性。

volatile的内存语义

volatile的特性

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写-读建立的happens-before关系

volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的语义效果,例子:
CMVOR.png
1.根据程序次序规则,1happens-before2;3happens-before4。
2.根据volatile规则,2happens-before3。
3.根据happens-before的传递性规则,1 happens-before 4。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

volatile写-读的内存语义

  • volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
  • volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主 内存中读取共享变量。

也就是说:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

volatile内存语义的实现

根据JMM针对编译器制定的bolatile重排序的规则表:
CXxH1.png

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class text{
int a;
volatile int i;
volatile int j;
void readAndWrite(){
int v1 = i; //第一个volatile读
int v2 = j; //第二个volatile读
a = v1 + v2; //第一个普通写
i = v1+1; //第一个volatile写
j = v2+1; //第二个volatile写
}
}

在这个例子中对于readAndWrite,编译器在生成字节码时会做如下的优化:

CXlNm.png
注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return

继续优化:

上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以X86处理器为例,除了最后的StoreLoad屏障外,其他的屏障都会被省略。因为X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。
CXSsd.png

volatile小总结

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性在功能上,锁比volatile更强大在可伸缩性和执行性能上,volatile更有优势。如果读者想在程序中用volatile代替锁,请一定谨慎,具体详情请参阅Brian Goetz的文章《Java理论与实践:正确使用Volatile变量》。

锁的内存语义

众所周知,锁可以让临界区互斥执行。这里将介绍锁的另一个同样重要,但常常被忽视的功能:锁的内存语义。

锁的释放-获取建立的happens-before关系

锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。来看一个锁释放-获取的例子:

1
2
3
4
5
6
7
8
9
10
clss text{
int a = 0;
public synchronized void write(){ //1
a++; //2
} //3
public synchronized void read(){ //4
int i = a; //5
...
} //6
}

这个类的代码中包含着三类happens-before关系:

  • 程序次序规则:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens- before 6。
  • 监视器锁规则:3happens-before4
  • happens-before传递性:2 happens-before 5

综合来看:
CmDBn.png

每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示 程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的happens- before保证。
图表示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens-before 5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见

锁的释放和获取的内存语义

  • 线程释放锁时:JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。以上面的程序为例,A线程释放锁后,共享数据的状态就被刷新了;
  • 线程获取锁时:JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。相当于线程A向线程B发送了消息

总结:

  • 锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

锁内存语义的实现

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ReentrantLockExample { 
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock(); // 获取锁
try {
a++;
}finally {
lock.unlock(); // 释放锁
}
}
public void reader () {
lock.lock(); // 获取锁
try {
int i = a;
……
} finally {
lock.unlock(); // 释放锁
}

}
}

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。 ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。
CmbVh.png
ReentrantLock分为公平锁和非公平锁,逐一分析:

公平锁

使用公平锁时,加锁方法lock()调用轨迹如下
1.ReentrantLock:lock();
2.FairSync:lock()。
3.AbstractQueuedSynchronizer:acquire(int arg)。
4.ReentrantLock:tryAcquire(int acquires)。
在第4步真正开始加锁,下面是该方法的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final boolean tryAcquire(int acquires) { 
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的开始,首先读volatile变量state if (c == 0) {
if (isFirst(current) && compareAndSetState(0, acquires)){
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

从上面源代码中我们可以看出,加锁方法首先读volatile变量state。

在使用公平锁时,解锁方法unlock()调用轨迹如下
1.ReentrantLock:unlock()。
2.AbstractQueuedSynchronizer:release(int arg)。
3.Sync:tryRelease(int releases)。
第3步真正开始释放锁,下面是该方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases) { 
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 释放锁的最后,写volatile变量state
return free;
}

从上面的源代码可以看出,在释放锁的最后写volatile变量state。 公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据 volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。

非公平锁

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下:
1.ReentrantLock:lock()。
2.NonfairSync:lock()。
3.AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。
在第3步真正开始加锁,下面是该方法的源代码。

1
2
3
protected final boolean compareAndSetState(int expect, int update) { 
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

该方法以原子操作的方式更新state变量,本文把Java的compareAndSet()方法调用简称为 CAS。JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。

在常见的intel X86处理器中,CAS是如何同时具有volatile读和volatile写的内存语义的?

下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码。

1
public final native boolean compareAndSwapInt(Object o, long offset, int expected)

可以看到,这是一个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为: unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147- 27_jun_2011\openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于Windows操作系统,X86处理器)。

下面是对应于intel X86处理器的源代码的片段:

1
2
3
4
5
6
inlinej int Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) 
{ // alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx
}
}

如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前 缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明

  • 1.确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache Locking) 来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
  • 2.禁止该指令,与之前和之后的读和写指令重排序。
  • 3.把写缓冲区中的所有数据刷新到内存中。

上面的第2点和第3点所具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义。

公平锁和非公平锁的内存语义总结

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
  • 公平锁获取时,首先会去读volatile变量。
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile 写的内存语义。

从本文对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:

  • 1.利用volatile变量的写-读所具有的内存语义。
  • 2.利用CAS所附带的volatile读和volatile写的内存语义。

concurrent包的实现

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现 在有了下面4种方式。

  • 1.A线程写volatile变量,随后B线程读这个volatile变量。
  • 2.A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  • 3.A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  • 4.A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

    Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式。首先,声明共享变量为volatile。 然后,使用CAS的原子条件更新来实现线程之间的同步。同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的 通信。AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。

从整体来看,concurrent包的实现示意图:
Cp4cw.png

参考:
《Java并发编程的艺术》

如果觉得还不错的话,把它分享给朋友们吧(ง •̀_•́)ง