java内存模型-锁

沿的假释-获取建立之 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 关系可分为两近乎:

  1. 冲程序次序规则,1 happens before 2, 2 happens before 3; 4 happens
    before 5, 5 happens before 6。
  2. 根据监视器锁规则,3 happens before 4。
  3. 依据 happens before 的传递性,2 happens before 5。

上述 happens before 关系之图形化表现形式如下:

C++ 1

当齐图被,每一个箭头链接的一定量个节点,代表了一个 happens before
关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示结合这些规则后提供的
happens before保证。

及图表示在线程A释放了锁之后,随后线程B获取同一个锁。在达成图被,2 happens
before
5。因此,线程A在放出锁之前所有可见的共享变量,在线程B获取同一个锁之后,将即刻变得对B线程可见。

吊释放以及得之内存语义

当线程释放锁经常,JMM
会把欠线程对应之地方内存中之共享变量刷新到主内存中。以地方的MonitorExample
程序也例,A线程释放锁后,共享数据的状态示意图如下:

C++ 2

当线程获取锁经常,JMM
会把欠线程对应的地头内存置为无效。从而让让监视器保护的临界区代码必须使自主内存中去读取共享变量。下面是沿得的状态示意图:

C++ 3

比锁释放-获取的内存语义与 volatile 写-读之内存语义,可以看出:锁释放以及
volatile 写来平等的内存语义;锁得与 volatile 读来同的内存语义。

脚对沿释放和钉得之内存语义做个小结:

  • 线程 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
的类图(仅打来与本文系的一部分):

C++ 4

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
前面和后面的妄动内存操作重排序。

脚我们来分析在广大的 intel x86 处理器中,CAS 是何等以负有 volatile
读与 volatile 写的内存语义的。

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

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

可看看就是独地面方法调用。这个地面方法在 openjdk 中相继调用的 C++
代码为:unsafe.cpp,atomic.cpp 和
atomicwindowsx86.inline.hpp。这个地面方法的终极落实以 openjdk
的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于
windows 操作系统,X86 处理器)。下面是针对许于 intel x86
处理器的源代码的一部分:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     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
    前缀的下令在推行中会锁住总线,使得其他计算机暂时无法透过总线访问内存。很扎眼,这会带昂贵的开销。从
    Pentium 4,Intel Xeon 及 P6 处理器开始,intel
    在原有总线锁之底蕴及做了一个那个有含义的优化:如果要访问的内存区域(area
    of memory)在 lock
    前缀指令执行中曾以处理器内部的休养生息存着吃锁定(即含该内存区域之复苏存行当前处垄断或因为改状态),并且该内存区域被完全含在么缓存行(cache
    line)中,那么处理器将直接执行该令。由于当指令执行中该缓存行会一直于锁定,其它计算机无法读/写该令要看的内存区域,因此会管令执行之原子性。这个操作过程叫做缓存锁定(cache
    locking),缓存锁定以大大降低 lock
    前缀指令的履行开销,但是当多处理器之间的竞争档次深高或指令访问的内存地址未对一起时,仍然会锁住总线。

  2. 禁绝该令和事先与下的朗诵与描绘指令重排序。

  3. 管写缓冲区C++中之所有数据刷新到内存中。

方的第2点和第3接触所怀有的内存屏障机能,足以同时落实 volatile 读与
volatile 写的内存语义。

经地方的这些分析,现在咱们算是会分晓为什么 JDK 文档说 CAS 同时有
volatile 读与volatile 写的内存语义了。

今昔本着公正锁与非公平锁的内存语义做只总结:

  • 公平锁与非公平锁释放时,最后还设描写一个 volatile 变量 state。
  • 公正无私锁得时,首先会见失掉念之 volatile 变量。
  • 非公平锁得时,首先会就此 CAS 更新是 volatile 变量,这个操作而负有
    volatile 读与 volatile 写的内存语义。

自从本文对 ReentrantLock
的分析可以看,锁释放-获取之内存语义的兑现至少发生下两种植方式:

  1. 采用 volatile 变量的刻画-读所具备的内存语义。
  2. 动用 CAS 所附带的 volatile 读与 volatile 写的内存语义。

concurrent 包的实现

由于 java 的 CAS 同时具有 volatile 读与 volatile 写的内存语义,因此 Java
线程之间的通信现在生矣底四种植艺术:

  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
包的源代码实现,会意识一个通用化的落实模式:

  1. 首先,声明共享变量为 volatile;
  2. 下一场,使用 CAS 的原子条件更新来兑现线程之间的共同;
  3. 还要,配合以 volatile 的读/写和 CAS 所负有的 volatile
    读与描绘的外存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic
包中的类),这些 concurrent 包被之基本功类都是动这种模式来贯彻的,而
concurrent
包着之高层类以是据让这些基础类来实现之。从完整来拘禁,concurrent
包的贯彻示意图如下:

C++ 5