当前位置:天才代写 > tutorial > JAVA 教程 > 深入领略Java内存模子(五) 锁

深入领略Java内存模子(五) 锁

2017-11-02 08:00 星期四 所属: JAVA 教程 浏览:52

副标题#e#

锁的释放-获取成立的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 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。

按照监督器锁法则,3 happens before 4。

按照happens before 的通报性,2 happens before 5。

上述happens before 干系的图形化表示形式如下:

深入明确Java内存模型(五) 锁

在上图中,每一个箭头链接的两个节点,代表了一个happens before 干系。玄色箭头暗示措施顺序规 则;橙色箭头暗示监督器锁法则;蓝色箭头暗示组合这些法则后提供的happens before担保。

上 图暗示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens before 5。因此,线程A 在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立即变得对B线程可见。


#p#副标题#e#

锁释 放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的当地内存中的共享变量刷新到主内存 中。以上面的MonitorExample措施为例,A线程释放锁后,共享数据的状态示意图如下:

深入明确Java内存模型(五) 锁

当线程获取锁时,JMM会把该线程对应的当地内存置为无效。从而使得被监督器掩护的临界区代码必需 要从主内存中去读取共享变量。下面是锁获取的状态示意图:

深入明确Java内存模型(五) 锁

比拟锁释放-获取的内存语义与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的类图(仅画出与本文相关的部门):

深入明确Java内存模型(五) 锁

查察本栏目

#p#副标题#e#

ReentrantLock分为公正锁和非公正锁,我们首先阐明公正锁。

利用公正锁时,加锁要领lock ()的要领挪用轨迹如下:

ReentrantLock : lock()

FairSync : lock()

AbstractQueuedSynchronizer : acquire(int arg)

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()的要领挪用轨迹如下:

ReentrantLock : unlock()

AbstractQueuedSynchronizer : release(int arg)

Sync : tryRelease(int releases)

#p#分页标题#e#

在第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()的要领挪用轨迹如下:

ReentrantLock : lock()

NonfairSync : lock()

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前缀提供的内存屏 障结果)。

#p#副标题#e#

intel的手册对lock前缀的说明如下:

确保对内存的读-改-写操纵原子执行。在Pentium及Pentium之前的处理惩罚器中,带有lock前缀的指令在 执行期间会锁住总线,使得其他处理惩罚器临时无法通过总线会见内存。很显然,这会带来昂贵的开销。从 Pentium 4,Intel Xeon及P6处理惩罚器开始,intel在原有总线锁的基本上做了一个很有意义的优化:假如要 会见的内存区域(area of memory)在lock前缀指令执行期间已经在处理惩罚器内部的缓存中被锁定(即包括 该内存区域的缓存行当前处于独有或以修改状态),而且该内存区域被完全包括在单个缓存行(cache line)中,那么处理惩罚器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理惩罚器无法 读/写该指令要会见的内存区域,因此能担保指令执行的原子性。这个操纵进程叫做缓存锁定(cache locking),缓存锁定将大大低落lock前缀指令的执行开销,可是当多处理惩罚器之间的竞争水平很高可能指 令会见的内存地点未对齐时,仍然会锁住总线。

克制该指令与之前和之后的读和写指令重排序。

把写缓冲区中的所有数据刷新到内存中。

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

颠末上面的这些阐明,此刻我们终于能大白为什么JDK文档说CAS同时具有volatile读和volatile 写的内存语义了。

此刻对公正锁和非公正锁的内存语义做个总结:

公正锁和非公正锁释放时,最后都要写一个volatile变量state。

公正锁获取时,首先会去读这个volatile变量。

非公正锁获取时,首先会用CAS更新这个volatile变量,这个操纵同时具有volatile读和volatile写的 内存语义。

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

操作volatile变量的写-读所具有的内存语义。

操作CAS所附带的volatile读和volatile写的内存语义。

concurrent包的实现

由于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变量。

#p#分页标题#e#

Java的CAS会利用现代处理惩罚器上提供的高效呆板级别原子指令,这些原子指令以原子方法对内存执行读 -改-写操纵,这是在多处理惩罚器中实现同步的要害(从本质上来说,可以或许支持原子性读-改-写指令的计较机 器,是顺序计较图灵机的异步等价呆板,因此任何现代的多处理惩罚器城市去支持某种能对内存执行原子性读 -改-写操纵的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合 在一起,就形成了整个concurrent包得以实现的基石。假如我们仔细阐明concurrent包的源代码实现,会 发明一个通用化的实现模式:

首先,声明共享变量为volatile;

然后,利用CAS的原子条件更新来实现线程之间的同步;

同时,共同以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据布局和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包 中的基本类都是利用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基本类来实现的。从 整体来看,concurrent包的实现示意图如下:

深入明确Java内存模型(五) 锁

 

    关键字:


天才代写-代写联系方式