前言

本篇文章将对基于AbstractQueuedSynchronizer实现的锁进行学习,同时对LockSupportCondition的应用进行整顿和剖析。内容参考了《Java并发编程的艺术》第5章。在之前的多线程学习-队列同步器中曾经对AbstractQueuedSynchronizer的原理进行了详尽剖析,如果不相熟AbstractQueuedSynchronizer,能够先查阅该篇文章。

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

注释

一. 重入锁

重入锁,即ReentrantLock,继承于Lock接口,提供锁重入性能。重入锁与不可重入锁的区别在于,重入锁反对曾经获取锁的线程反复对锁资源进行获取,Java中的synchronized关键字能够隐式的反对锁重入性能,思考如下一个例子。

public class HelloUtil {    public static synchronized void sayHello() {        System.out.print("Hello ");        sayWorld();    }    public static synchronized void sayWorld() {        System.out.println("World");    }}

已知拜访由synchronized关键字润饰的静态方法时须要先获取办法所在类的Class对象作为锁资源,所以当A线程调用HelloUtilsayHello()办法时,须要获取的锁资源为HelloUtil类的Class对象,此时B线程再调用HelloUtilsayHello()sayWorld()办法时会被阻塞,然而A线程却能够在sayHello()办法中再调用sayWorld()办法,即A线程在曾经获取了锁资源的状况下又获取了一次锁资源,这就是synchronized关键字对锁重入的反对。

联合下面的例子,曾经对重入锁有了直观的意识,上面将剖析ReentrantLock是如何实现重入锁的。ReentrantLock的类图如下所示。

ReentrantLock有三个动态外部类,其中Sync继承于AbstractQueuedSynchronizer,而后FairSyncNonfairSync继承于Sync,因而SyncFairSyncNonfairSync均是ReentrantLock组件中的自定义同步器,且FairSync提供偏心获取锁机制,NonfairSync提供非偏心获取锁机制。偏心和非偏心获取锁机制当初暂且不谈,上面先看一下SyncFairSyncNonfairSync实现了哪些办法,如下所示。

NonfairSyncFairSync提供获取锁机制的不同就在于其实现的lock()tryAcquire()办法的不同,具体是应用哪一种获取锁机制,是在创立ReentrantLock时指定的,ReentrantLock的构造函数如下所示。

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

由上述可知,ReentrantLock默认应用非偏心获取锁机制,而后能够在构造函数中依据传入的fair参数决定应用哪种机制。当初先对下面的探讨做一个大节:ReentrantLock是可重入锁,即曾经获取锁资源的线程能够反复对锁资源进行获取,ReentrantLock外部有三个自定义同步器,别离为SyncNonfairSyncFairSync,其中NonfairSyncFairSync能别离提供非偏心获取锁机制和偏心获取锁机制,具体应用哪一种获取锁机制,须要在ReentrantLock的构造函数中指定。

接下来联合NonfairSyncFairSynclock()tryAcquire()办法的源码,对非偏心获取锁机制和偏心获取锁机制进行阐明。

NonfairSynclock()办法如下所示。

final void lock() {    if (compareAndSetState(0, 1))        setExclusiveOwnerThread(Thread.currentThread());    else        acquire(1);}

非偏心获取锁调用lock()办法时会先将stateCAS形式从0设置为1,设置胜利示意竞争到了锁,因而非偏心获取锁意味着同时获取锁资源时会存在竞争关系,不能满足先到先获取的准则。如果将stateCAS形式从0设置为1失败时,会调用模板办法acquire(),已知acquire()办法会调用tryAcquire()办法,而NonfairSynctryAcquire()办法会调用其父类SyncnonfairTryAcquire()办法,上面看一下其实现。

final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (compareAndSetState(0, acquires)) {            setExclusiveOwnerThread(current);            return true;        }    }    //以后线程如果是获取到锁资源的线程,则将state字段加1    //以后线程如果不是获取到锁资源的线程,则返回false而后退出同步队列    else if (current == getExclusiveOwnerThread()) {        int nextc = c + acquires;        if (nextc < 0)            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}

nonfairTryAcquire()办法中次要是对获取锁资源的线程进行判断,如果以后线程就是曾经获取到锁资源的线程,那么就会将state加1,因为每次都是将state加1,所以能够反复获取锁资源。

接下来再看一下偏心获取锁机制的FairSync的实现,首先FairSynclock()办法会间接调用模板办法acquire(),并已知在acquire()办法中会调用tryAcquire()办法,所以这里间接看FairSynctryAcquire()办法的实现。

protected final boolean tryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    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);        return true;    }    return false;}

FairSynctryAcquire()办法与NonfairSync的不同在于当state为0时多了一个hasQueuedPredecessors()办法的判断逻辑,即判断以后的同步队列中是否曾经有正在期待获取锁资源的线程,如果有,则返回true因而偏心获取锁意味着相对工夫上最先申请锁资源的线程会最先获取锁,以及期待获取锁资源工夫最长的线程会最优先获取锁,这样的获取锁机制就是偏心的。

当初最初剖析一下ReentrantLock的解锁逻辑。无论是非偏心获取锁机制还是偏心获取锁机制,如果反复对锁资源进行了n次获取,那么胜利解锁就须要对锁资源进行n次开释,前(n - 1)次开释锁资源都应该返回falseReentrantLockunlock()办法会间接调用AbstractQueuedSynchronizer的模板办法release(),并已知在release()办法中会调用tryRelease()办法,这里调用的是Sync实现的tryRelease()办法,如下所示。

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);    return free;}

tryRelease()办法中每胜利开释一次锁资源,就会将state减1,所以当state为0时,就判断锁资源被全副开释,即开释锁资源胜利。

二. 读写锁

读写锁,即ReentrantReadWriteLock,同一时刻能够容许多个读线程获取锁,但当写线程获取锁后,读线程和其它写线程应该被阻塞。上面以一个单例缓存的例子来阐明ReentrantReadWriteLock的应用。

public class Cache {    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();    private final Lock rLock = rwLock.readLock();    private final Lock wLock = rwLock.writeLock();    private final Map<String, String> map = new HashMap<>();    private static Cache instance = null;    private Cache() {}    public Cache getCacheInstance() {        if (instance == null) {            synchronized (Cache.class) {                if (instance == null) {                    instance = new Cache();                }            }        }        return instance;    }    public String getValueByKey(String key) {        rLock.lock();        try {            return map.get(key);        } finally {            rLock.unlock();        }    }    public void addValueByKey(String key, String value) {        wLock.lock();        try {            map.put(key, value);        } finally {            wLock.unlock();        }    }    public void clearCache() {        wLock.lock();        try {            map.clear();        } finally {            wLock.unlock();        }    }}

依据例子可知,ReentrantReadWriteLock提供了一对锁:写锁读锁,并且应用规定如下。

  • 以后线程获取读锁时,读锁是否被获取不会影响读锁的获取;
  • 以后线程获取读锁时,若写锁未被获取或者写锁被以后线程获取,则容许获取读锁,否则进入期待状态;
  • 以后线程获取写锁时,若读锁曾经被获取,无论获取读锁的线程是否是以后线程,都进入期待状态;
  • 以后线程获取写锁时,若写锁曾经被其它线程获取,则进入期待状态。

上面将联合源码对写锁和读锁的获取和开释进行剖析。首先看一下ReentrantReadWriteLock的类图。

ReentrantReadWriteLock一共有五个外部类,别离为SyncFairSyncNonfairSyncWriteLockReadLock,同时能够看到,只有WriteLockReadLock实现了Lock接口,因而ReentrantReadWriteLock的写锁和读锁的获取和开释实际上是由WriteLockReadLock来实现,所以这里对ReentrantReadWriteLock的工作原理进行一个简略概括:ReentrantReadWriteLock的写锁和读锁的获取和开释别离由其内部类WriteLockReadLock来实现,而WriteLockReadLock对同步状态的操作又是依赖于ReentrantReadWriteLock实现的三个自定义同步器SyncFairSyncNonfairSync

上面持续剖析写锁和读锁的同步状态的设计。通过下面的剖析能够晓得WriteLockReadLock依赖同一个自定义同步组件Sync,因而WriteLockReadLock对同步状态进行操作时会批改同一个state变量,即须要在同一个整型变量state上保护写锁和读锁的同步状态,而Java中整型变量一共有32位,所以ReentrantReadWriteLockstate的高16位示意读锁的同步状态,低16位示意写锁的同步状态。鉴于读写锁同步状态的设计,对读写锁同步状态的运算操作归纳如下。

  • 获取写锁同步状态: state & 0x0000FFFF
  • 获取读锁同步状态: state >>> 16
  • 写锁同步状态加一: state + 1
  • 读锁同步状态加一: state + (1 << 16)

理分明了ReentrantReadWriteLock的组件之间的关系和读写锁同步状态的设计之后,上面开始剖析写锁和读锁的获取和开释。

1. 写锁的获取

WriteLocklock()办法间接调用了AbstractQueuedSynchronizer的模板办法acquire(),在acquire()办法中会调用自定义同步器Sync重写的tryAcquire()办法,上面看一下tryAcquire()办法的实现。

protected final boolean tryAcquire(int acquires) {    Thread current = Thread.currentThread();    //c示意state    int c = getState();    //w示意写锁同步状态    int w = exclusiveCount(c);    if (c != 0) {        //state不为0,然而写锁同步状态为0,示意读锁曾经被获取        //获取写锁时只有读锁被获取过,就不容许获取写锁        //因为写锁是独占锁,所以持有写锁的线程不是以后线程也不容许获取写锁        if (w == 0 || current != getExclusiveOwnerThread())            return false;        if (w + exclusiveCount(acquires) > MAX_COUNT)            throw new Error("Maximum lock count exceeded");        //执行到这里示意写锁重入        setState(c + acquires);        return true;    }    //非偏心获取锁时writerShouldBlock()返回false    //偏心获取锁时writerShouldBlock()会调用hasQueuedPredecessors()办法    if (writerShouldBlock() ||        !compareAndSetState(c, c + acquires))        return false;    setExclusiveOwnerThread(current);    return true;}

上述tryAcquire()办法中,在获取写锁之前会判断读锁是否被获取以及写锁是否被其它线程获取,任意一个条件满足都不容许以后线程获取写锁。同时如果写锁和读锁均没有被获取,即state为0时,还会调用writerShouldBlock()办法来实现非偏心或偏心锁的语义,如果是非偏心锁,writerShouldBlock()办法会返回false,此时以后线程会以CAS形式批改state,批改胜利则示意获取读锁胜利,如果是偏心锁,writerShouldBlock()办法会调用hasQueuedPredecessors()办法来判断同步队列中是否曾经有正在期待获取锁资源的线程,如果有,则以后线程须要退出同步队列,后续依照等待时间越久越优先获取锁的机制来获取写锁。

2. 写锁的开释

WriteLockunlock()办法间接调用了AbstractQueuedSynchronizer的模板办法release(),在release()办法中会调用自定义同步器Sync重写的tryRelease()办法,上面看一下tryRelease()办法的实现。

protected final boolean tryRelease(int releases) {    if (!isHeldExclusively())        throw new IllegalMonitorStateException();    int nextc = getState() - releases;    boolean free = exclusiveCount(nextc) == 0;    if (free)        setExclusiveOwnerThread(null);    setState(nextc);    return free;}

因为写锁反对重入,所以在开释写锁时会对写锁状态进行判断,只有写锁状态为0时,才示意写锁被胜利开释掉。

3. 读锁的获取

ReadLocklock()办法间接调用了AbstractQueuedSynchronizer的模板办法acquireShared(),在acquireShared()办法中会调用自定义同步器Sync重写的tryAcquireShared()办法,tryAcquireShared()办法并不残缺,其最初会调用fullTryAcquireShared()办法,该办法的正文阐明如下。

获取读锁同步状态的残缺版本,可能实现在tryAcquireShared()办法中未能实现的CAS设置状态失败重试和读锁重入的性能。

JDK1.6ReentrantReadWriteLock提供了getReadHoldCount()办法,该办法用于获取以后线程获取读锁的次数,因为该办法的退出,导致了读锁的获取的逻辑变得更为简单,上面将联合tryAcquireShared()fullTryAcquireShared()办法的实现,在抛开为实现getReadHoldCount()办法性能而新增的逻辑的状况下,给出读锁获取的简化实现代码。

final int fullTryAcquireShared(Thread current) {    for (;;) {        //c示意state        int c = getState();        //如果写锁被获取并且获取写锁的线程不是以后线程,则不容许获取读锁        if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)            return -1;        if (sharedCount(c) == MAX_COUNT)            throw new Error("Maximum lock count exceeded");        //平安的将读锁同步状态加1        if (compareAndSetState(c, c + SHARED_UNIT))            return 1;    }}

由上述可知,读锁在写锁被获取并且获取写锁的线程不是以后线程的状况下,不容许被获取,以及读锁的同步状态为所有线程获取读锁的次数之和。

4. 读锁的开释

ReadLockunlock()办法间接调用了AbstractQueuedSynchronizer的模板办法releaseShared(),在releaseShared()办法中会调用自定义同步器Sync重写的tryReleaseShared()办法,该办法同样在JDK1.6中退出了较为简单的逻辑,上面给出其简化实现代码。

protected final boolean tryReleaseShared(int unused) {    for (;;) {        int c = getState();        int nextc = c - SHARED_UNIT;        if (compareAndSetState(c, nextc))            return nextc == 0;    }}

由上述可知,只有在state为0时,即读锁和写锁均被开释的状况下tryReleaseShared()办法才会返回true,在官网的正文中给出了这样设计的起因,如下所示。

开释读锁对读线程没有影响,然而当读锁和写锁均被开释的状况下,在同步队列中期待的写线程就有可能去获取写锁。

三. Condition接口

Condition接口定义了一组办法用于配合Lock实现期待/告诉模式,与之作为比照的是,用于配合synchronized关键字实现期待/告诉模式的定义在java.lang.Object上的监视器办法wait()notify()等。《Java并发编程的艺术》5.6大节对两者的差别进行了比照和总结,这里间接贴过来作参考。

比照项Object Monitor MethodsCondition
以后线程开释锁并进入期待状态反对反对
以后线程开释锁并进入期待状态,期待过程中不响应中断不反对反对
以后线程开释锁并进入超时期待状态反对反对
以后线程开释锁并期待至未来某个工夫点不反对反对
唤醒队列中的一个线程反对反对
唤醒队列中的多个线程反对反对

通常基于LocknewCondition()办法创立Condition对象并作为对象成员变量来应用,如下所示。

public class MyCondition {    private Lock lock = new ReentrantLock();    private Condition condition = lock.newCondition();        ......    }

队列同步器AbstractQueuedSynchronizer的外部类ConditionObject实现了Condition接口,后续将基于ConditionObject的实现进行探讨。首先给出Condition接口定义的办法。

public interface Condition {    void await() throws InterruptedException;    void awaitUninterruptibly();    long awaitNanos(long nanosTimeout) throws InterruptedException;    boolean await(long time, TimeUnit unit) throws InterruptedException;    boolean awaitUntil(Date deadline) throws InterruptedException;    void signal();    void signalAll();    }

上述办法的阐明如下表所示。

办法阐明
await()调用此办法的线程进入期待状态,响应中断,也能够被signal()signalAll()办法唤醒并返回,唤醒并返回前须要获取到锁资源。
awaitUninterruptibly()await(),但不响应中断。
awaitNanos()await(),并可指定等待时间,响应中断。该办法有返回值,示意残余等待时间。
awaitUntil()await(),并可指定期待截止工夫点,响应中断。该办法有返回值,true示意没有到截止工夫点就被唤醒并返回。
signal()唤醒期待队列中的第一个节点。
signalAll()唤醒期待队列中的所有节点。

针对下面的办法再做两点补充阐明:

  • 期待队列是Condition对象外部保护的一个FIFO队列,当有线程进入期待状态后会被封装成期待队列的一个节点并增加到队列尾;
  • 从期待队列唤醒并返回的线程肯定曾经获取到了与Condition对象关联的锁资源,Condition对象与创立Condition对象的锁关联。

上面将联合ConditionObject类的源码来对期待/告诉模式的实现进行阐明。await()办法的实现如下所示。

public final void await() throws InterruptedException {    if (Thread.interrupted())        throw new InterruptedException();    //基于以后线程创立Node并增加到期待队列尾    //这里创立的Node的期待状态为CONDITION,示意期待在期待队列中    Node node = addConditionWaiter();    //开释锁资源    int savedState = fullyRelease(node);    int interruptMode = 0;    //Node从期待返回后会被增加到同步队列中    //Node胜利被增加到同步队列中则退出while循环    while (!isOnSyncQueue(node)) {        LockSupport.park(this);        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)            break;    }    //让Node进入自旋状态,竞争锁资源    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)        interruptMode = REINTERRUPT;    //遍历期待队列,将曾经勾销期待的节点从期待队列中去除链接    if (node.nextWaiter != null)        unlinkCancelledWaiters();    //Node如果是被中断而从期待返回,则抛出中断异样    if (interruptMode != 0)        reportInterruptAfterWait(interruptMode);}

了解await()办法的整个执行流程前,先看一下期待队列的一个示意图,如下所示。

Condition对象别离持有期待队列头节点和尾节点的援用,新增加的节点会增加到期待队列尾,同时lastWaiter会指向新的尾节点。

当初回到await()办法,在await()办法中,会做如下事件。

  • 首先,会基于以后线程创立Node并增加到期待队列尾,创立Node有两个留神点:1. 这里创立的Node复用了同步队列中的Node定义;2. 在创立Node前会判断期待队列的尾节点是否曾经完结期待(即期待状态不为Condition),如果是则会遍历期待队列并将所有曾经勾销期待的节点从期待队列中去除链接;
  • 而后,以后线程会开释锁资源,并基于LockSupport.park()进入期待状态;
  • 再而后,以后线程被其它线程唤醒,或者以后线程被中断,无论哪种形式,以后线程对应的Node都会被增加到同步队列尾并进入自旋状态竞争锁资源,留神,此时以后线程对应的Node还存在于期待队列中;
  • 再而后,判断以后线程对应的Node是否是期待队列尾节点,如果不是则触发一次革除逻辑,即遍历期待队列,将曾经勾销期待的节点从期待队列中去除链接,如果是期待队列尾节点,那么以后线程对应的Node会在下一次创立Node时从期待队列中被革除链接;
  • 最初,判断以后线程从期待返回的起因是否是因为被中断,如果是,则抛出中断异样。

下面探讨了期待的实现,上面再联合源码看一下告诉的实现。首先是signal()办法,如下所示。

public final void signal() {    if (!isHeldExclusively())        throw new IllegalMonitorStateException();    Node first = firstWaiter;    if (first != null)        doSignal(first);}

signal()办法可知,调用signal()办法的线程须要持有锁,其次signal()办法会唤醒期待队列的头节点,即能够了解为唤醒等待时间最久的节点。上面再看一下signalAll()办法,如下所示。

public final void signalAll() {    if (!isHeldExclusively())        throw new IllegalMonitorStateException();    Node first = firstWaiter;    if (first != null)        doSignalAll(first);}

能够发现,signalAll()signal()办法大体雷同,只不过前者最初会调用doSignalAll()办法来唤醒所有期待节点,后者会调用doSignal()办法来唤醒头节点,上面以doSignal()办法进行阐明。

private void doSignal(Node first) {    do {        if ( (firstWaiter = first.nextWaiter) == null)            lastWaiter = null;        first.nextWaiter = null;    } while (!transferForSignal(first) &&             (first = firstWaiter) != null);}

理论就是在transferForSignal()办法中将头节点增加到同步队列尾,而后再调用LockSupport.unpark()进行唤醒。

四. LockSupport

在本篇文章的最初,对java.util.concurrent.locks包中的一个重要工具类LockSupport进行阐明。LockSupport提供了一组静态方法用于阻塞/唤醒线程,办法签名如下所示。

public static void park()public static void park(Object blocker)public static void parkNanos(long nanos)public static void parkNanos(Object blocker, long nanos)public static void parkUntil(long deadline)public static void parkUntil(Object blocker, long deadline)public static void unpark(Thread thread)

LockSupport的办法进行整顿如下。

办法阐明
park()将调用park()办法的线程阻塞,响应中断。
parkNanos()将调用parkNanos()办法的线程阻塞,并指定阻塞工夫,响应中断。
parkUntil()将调用parkUntil()办法的线程阻塞,并指定截止工夫点,响应中断。
unpark(Thread thread)唤醒传入的线程。

当初已知让线程睡眠(阻塞或期待)的形式有四种,别离是Thread.sleep(time)LockSupport.park()Object.wait()Condition.await(),在本篇文章的最初,对上述四种形式进行一个简略比照,如下表所示。

形式阐明
Thread.sleep(time)调用该办法必须指定线程睡眠的工夫,睡眠中的线程能够响应中断并抛出中断异样,调用该办法时不须要线程持有锁资源,然而持有锁资源的线程调用该办法睡眠后不会开释锁资源
LockSupport.park()调用该办法的线程会被阻塞,被阻塞中的线程能够响应中断但不会抛出中断异样,调用该办法时不须要线程持有锁资源,然而持有锁资源的线程调用该办法睡眠后不会开释锁资源
Object.wait()调用该办法的线程会进入期待状态,期待状态中的线程能够响应中断并抛出中断异样,调用该办法时须要线程曾经持有锁资源,调用该办法后会开释锁资源
Condition.await()调用该办法的线程会进入期待状态,期待状态中的线程能够响应中断并抛出中断异样,调用该办法时须要线程曾经持有锁资源,调用该办法后会开释锁资源

总结

本篇文章次要对重入锁读写锁的应用和原理进行了学习,重入锁读写锁的实现均是基于队列同步器AbstractQueuedSynchronizer。而后又对Condition的应用和原理进行了学习,通过Condition进入期待状态的线程会在期待队列上进行期待,被唤醒或中断时又会进入同步队列退出到对锁资源的竞争中,只有获取到了锁资源能力从期待状态返回。最初对LockSupport工具类进行了一个简略阐明,并针对线程进入睡眠的四种形式做了一个简略比照。