内存模型
在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量(Local Variables),方法定义参数(Java语言规范称之为Formal Method Parameters)和异常处理器参数(ExceptionHandler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(MainMemory)中,每个线程都有一个私有的本地内存(LocalMemory),本地内存中存储了该线程以读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
如下图Java内存模型示意图
如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
通信过程如下图所示:
本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程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属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
他有如下几种规则:
1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2、监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3、volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4、传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前
volatile的内存语义
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement() {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample2 {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement() { // 普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性。
·可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写
入。
·原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
具有原子性。
锁的内存语义
锁的释放-获取建立的happens-before关系
锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
下面是锁释放-获取的示例代码
class MonitorExample {
int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类。
1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happensbefore 6。
2)根据监视器锁规则,3 happens-before 4。
3)根据happens-before的传递性,2 happens-before 5。
上述happens-before关系的图形化表现形式如下图所示。
每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的happensbefore保证。图表示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens-before5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。
锁的释放和获取的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。以上面的MonitorExample程序为例,A线程释放锁后,共享数据的状态示意图如下图所示
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
下面对锁释放和锁获取的内存语义做个总结。
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发
送消息。
锁内存语义的实现
我们来看下ReentrantLock源码
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内存语义实现的关键。
我们来看下ReentrantLock类图
ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁。
使用公平锁时,加锁方法lock()调用轨迹如下。
1)ReentrantLock:lock()。
2)FairSync:lock()。
3)AbstractQueuedSynchronizer:acquire(int arg)。4)ReentrantLock:tryAcquire(int acquires)。
在第4步真正开始加锁,下面是该方法的源代码。
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步真正开始释放锁,下面是该方法的源代码。
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步真正开始加锁,下面是该方法的源代码。
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
该方法以原子操作的方式更新state变量,本文把Java的compareAndSet()方法调用简称为CAS。JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。这里我们分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义。
我们提到过,编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
现在对公平锁和非公平锁的内存语义做个总结。
·公平锁和非公平锁释放时,最后都要写一个volatile变量state。
·公平锁获取时,首先会去读volatile变量。
·非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。
从本文对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式。
1)利用volatile变量的写-读所具有的内存语义。
2)利用CAS所附带的volatile读和volatile写的内存语义。
由于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包的实现示意图