java并发系列:锁的内存语义

一 序:

  本文根据《Java并发编程的艺术》第3.5部分整理而成。书上的3.4 volatile 参见之前整理。

 jdk里面Javadoc关于lock有这样的介绍:

Memory Synchronization

All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in The Java Language Specification (17.4 Memory Model):

  • A successful lock operation has the same memory synchronization effects as a successful Lock action.
  • A successful unlock operation has the same memory synchronization effects as a successful Unlock action.
Unsuccessful locking and unlocking operations, and reentrant locking/unlocking operations, do not require any memory synchronization effects.

    意思是j.u.c.locks.Lock接口的实现类具有和synchronized内置锁一样的内存同步语义。

     我们熟悉锁是临界区互斥执行,但是内存语义是如何实现的呢?不同于jvm底层实现的内置锁,lock是基于Java代码实现的。

二 锁的内存语义

  2.1 happens-before

    书上3.15节 介绍了happens-before,下为引用: 从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

常见的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 管理锁定(监视器锁定)规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

还有4个不太常用的,一共8条规则,我补充如下:

   1、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
  2、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    3、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
    4、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。

书上3.7节 对此又做了详细阐述:

《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下。

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的1)是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!

上面的2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

2.2 锁的内存语义

   在下一节看具体的Java代码之前,先看下锁的内存语义。

    当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

    当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。


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

三 锁内存语义的实现

  先看下jvm实现的synchronized,synchronized有两种用法,一种可以用来修饰方法,另外一种可以用来修饰代码块。我们以synchronized代码块为例:

synchronized (object) {
		//code	
		}

    因为synchronized代码块是互斥访问的,只有一个线程释放了锁,另一个线程才能进入代码块中执行。根据前面的happens-before原则第二条。可以看做内置锁的释放锁操作发生在该锁随后的加锁操作之前。假设当线程a释放锁后,线程b拿到了锁并且开始执行代码块中的代码时,线程b必然能够看到线程a看到的所有结果,所以synchronized能够保证线程间数据的可见性。

再看书上的reentrantlock实现例子。可以参考之前整理过reentrantlock

这里只保留部分代码,

先来看ReentrantLock类的lock方法和unlock方法的实现:

public void lock() {
    sync.lock();
}

// sync.lock()实现
final void lock() {
    acquire(1);
}

public void unlock() {
    sync.release(1);
}

lock方法和unlock方法的具体实现都代理给了sync对象,来看一下sync对象的定义:

 abstract static class Sync extends AbstractQueuedSynchronizer {

可见最终还是依赖于java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,后面会有代码。

static final class FairSync extends Sync

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁。

上面lock方法和unlock方法的具体实现都是由acquire和release方法完成的,而FairSync类中并没有定义acquire方法和release方法,这两个方法都是在Sync的父类AQS类中实现的。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
     public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    我们可以看出,获取锁和释放锁的具体操作是在tryAcquire和tryRelease中实现的,而tryAcquire和tryRelease在父类AQS中只是接口,具体实现留给子类Sync。也是真正的加锁,释放的逻辑。   
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); //获取锁的真正开始,首先读取volatile变量state
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    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); //写state
                return true;
            }
            return false;
        }
protected final boolean tryRelease(int releases) {
            int c = getState() - releases; //读取state
            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变量。根据volatile的happens-before规则(一个volatile变量的写操作发生在这个volatile变量随后的读操作之前),释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变的对获取锁的线程可见。

  非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。

     final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
      protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
  这里我们分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义。
        前文我们提到过,编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
 书上还对X86处理器就为cmpxchg指令加上lock前缀(lock cmpxchg).lock说明如下
         确保对内存的读-改-写操作原子执行。
        禁止该指令与之前和之后的读和写指令重排序。
        把写缓冲区中的所有数据刷新到内存中。

   我插一下,因为openjdk的代码C++的,我是不懂的,作者结合了源码及CPU的指令来分析,真是大神。看看JVM那本书就知道了。我自己吧这里分为三个层次,1会用及看了部分源码,2.深入理解源码 3. 从更底层的JVM,CPU多角度剖析。

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

从本文对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式:
   利用volatile变量的写-读所具有的内存语义。
   利用CAS所附带的volatile读和volatile写的内存语义。

无论是公平还是非公平性,这也解释了Lock接口的实现类能实现和synchronized内置锁一样的内存数据可见性。

四 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变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

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

    

参考:

http://ifeve.com/java-memory-model-5/

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/80798930