java并发编程(7)-ReentrantLock,ReentrantReadWriteLock以及Condition原理剖析

java并发编程系列前文

  1. java并发编程(1)-并发编程基础(上)
  2. java并发编程(2)-并发编程基础(下)
  3. java并发编程(3)-ThreadLocal原理剖析
  4. java并发编程(4)-Random以及ThreadLocalRandom原理剖析
  5. java并发编程(5)-CAS以及原子性操作类原理剖析
  6. java并发编程(6)-AQS原理剖析

本章是java并发编程系列的第七章,经过前面几章的铺垫,我们从本章开始就要正式进入JUC锁的内容。本章是面向一定基础的读者,需要读者对AQS有一定基础的,如果对AQS没了解过,可以先去看一下我们并发编程系列的第六章。当然并不是有AQS基础才能看懂本章,而是有AQS基础以后才能对本章内容有更深的理解和体会。

ReentrantLock

ReentrantLock是JUC包下的一个互斥锁,他是基于AQS实现的。我们在第二章的时候已经见过了java中互斥锁的其中一个实现synchronized。

和synchronized一样,ReentrantLock也保证了同一时刻只有一个线程能是持有锁。ReentrantLock除了拥有和synchronized一样的互斥性,还让我们可以自由选择我的锁类型是公平锁还是非公平锁。我们先来上一段测试代码,看看ReentrantLock是如何使用的,再来讨论一下它和synchronized的区别和优缺点。

java 复制代码
int i = 0;
ReentrantLock nonFairLock = new ReentrantLock();
nonFairLock.lock();
i++;
nonFairLock.unlock();

ReentrantLock fairLock = new ReentrantLock(true);
fairLock.lockInterruptibly();
i++;
fairLock.unlock();

可以看到如果我们要使用ReentrantLock,就需要先创建对象,然后通过调用lock方法获取锁,获取锁以后还需要手动调用unlock释放锁。除此之外我们还可以通过有参构造方法传入的boolean类型变量来选择我们创建的锁是公平锁还是非公平锁,true为公平锁,无参构造方法创建的是非公平锁。

另外我们还可以看到ReentrantLock除了提供lock方法获取锁,还提供了lockInterruptibly方法,lockInterruptibly方法与lock方法的区别在于lockInterruptibly会对中断响应。比如说有个线程在调用lockInterruptibly方法获取锁资源的时候被阻塞了,这时候如果我们对该线程进行中断操作,会抛异常出来,这样我们就可以选择线程被中断以后是否让他继续向下执行锁域外的代码。

接下来,我们通过ReentrantLock的类图来了解一下它的内部结构

可以看到ReentrantLock内部有个Sync类型的成员变量,Sync以及它的子类都是ReentrantLock的内部类。这个成员变量根据构造方法的不同,创建时使用的实际类型也不同。当我们调用构造方法创建非公平锁时,内部的成员变量sync是NonfairSync类型,反之则是FairSync类型。而且我们实际使用时调用的lock方法其实就是这个成员变量的lock方法。

对于ReentrantLock,我们已经有了一个基本的了解,接下来我们看一下在调用lock以及unlock时,它的底层发生了什么。

获取非公平锁资源

实际场景中,非公平锁可能会用的比较多一点,所以我们先来看非公平锁下获取锁资源时的底层机制。

java 复制代码
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

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

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

我们在前面提到过在使用时实际调用的是sync的lock方法,而非公平锁的实现是NonfairSync,所以我们直接看这个类就可以了。

可以看到NonfairSync内部代码不多,因为NonfairSync继承Sync,而Sync又继承AQS,所以在lock方法中我们会先去尝试使用CAS操作修改AQS中的state,如果操作成功就会修改AbstractOwnableSynchronizer中exclusiveOwnerThread,标识当前线程获取到了锁。

如果修改失败则会调用acquire,最后其实调用的就是AQS中的acquire方法,这个方法的内部流程已经在上一章详细说过了,本章不再赘述了。

如果是看过上一章内容的读者,应该会记得当时我们在说到AQS中的tryAcquire方法时,说过它的作用是尝试获取锁,并且我们还说过它是由子类实现。所以本章我们来NonfairSync的tryAcquire逻辑,补全上一章我们一笔带过的内容。

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看到获取锁的逻辑还是比较简单的,判断AQS中的state标识位,如果为0说明锁没有被持有,使用CAS操作修改state值,修改成功以后exclusiveOwnerThread变为当前线程并且该方法返回true。

如果state不为0就会再去判断当前线程是否已经持有锁了,持有锁了就讲state值在原有的基础上+1(因为独占锁acquires默认情况下传的是1),修改完成以后返回true。

获取公平锁资源

公平锁的获取逻辑在FairSync的tryAcquire中

java 复制代码
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;
}

可以看到公平模式下获取锁资源的逻辑和非公平模式主要区别在公平锁在第5行调用了hasQueuedPredecessors。

java 复制代码
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

现在我们对这个方法return中的条件逐一分解。

h != t代表着AQS的队列已完成初始化并且至少已经有一个Node入过队了。 (s = h.next) == null,因为第一个Node入队时是先修改尾节点,然后再改head节点的后驱节点(具体代码请看我们的java并发编程第六章的acquire小节的enq方法),所以这个条件代表第一个Node正处于入队过程当中。 s.thread != Thread.currentThread()则表示下一个等待被唤醒的Node所属线程是否非当前线程。

我们将整体逻辑串起来梳理一下。公平锁相较于非公平锁,会在抢占时进行判断,AQS队列尚未进行初始化或者第一个Node已经完成入队并且下一个等待唤醒的线程为当前线程才会去尝试获取锁。

释放锁

不管是公平锁还是非公平锁,释放锁的方法的逻辑都是同一个,最后都会走到Sync中的tryRelease

java 复制代码
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;
}

这个方法的逻辑非常简单,如果state的值经过本次释放动作后变成0了,则认为锁资源已经完全释放掉了,就会将AbstractOwnableSynchronizer中的exclusiveOwnerThread提前重置为null。修改state,返回结果为boolean,代表锁资源是否已经完全释放掉。

ReentrantReadWriteLock

ReentrantLock为我们保证了只会有一个线程持有锁,其他线程会被阻塞直到线程释放锁。但有的时候我们的项目可能会存在这么一个比较特殊的情况,项目中有明确的读写场景区分,读场景下时只会对资源进行读取,写场景下时会对资源进行写入和读取。

在这种情况下ReentrantLock就显得不太合适了。对于这种情况,JUC为我们提供了ReentrantReadWriteLock,它是一个读写锁,它的核心思想就是将锁细分成读锁和写锁以此来实现读时共享写时互斥的需求。 我们在前面提到的它的读锁和写锁就是类图中的readerLock和writerLock。当我们要使用读锁时,需要调用readLock() 获取读锁对象。当我们要使用写锁时,需要调用writeLock() 获取写锁对象。接下来我们再来看一下ReentrantReadWriteLock的构造方法,结合类图和构造方法我们先简单分析一遍整体流程。

java 复制代码
public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

从上面的代码中可以看到在ReentrantReadWriteLock构造方法中会去创建sync以及读锁和写锁,ReentrantReadWriteLock的sync也有公平锁和非公平锁之分。

而从类图上我们有能看到ReadLock和WriterLock内部都有sync,而创建它们时又会把当前ReentrantReadWriteLock对象传入到它们的构造方法中。

种种迹象已经能让我们猜到背后的真相。没错,这三个sync其实是同一个,都是ReentrantReadWriteLock对象中的那个sync,传入this只不过是为了获取它的sync。所以可以得出一个结论,那就是读锁和谢锁实际上用的是同一个AQS队列(Sync继承AQS)。

ReentrantReadWriteLock是需要做到读时共享,写时互斥。用同一个AQS队列如果去管理两种模式呢?

在java并发编程第六章的时候,我们提到过AQS的state在不同实现下代表的含义也不同。ReentrantReadWriteLock正是利用state这个成员变量来标识当前处于哪种模式下。

AQS中的state是int类型,ReentrantReadWriteLock将state的高16位作为读锁被获取到的次数,低16位代表写锁的可重入次数。换言之,如果高16位不为0那么就代表处于读模式,如果低16位不为0就代表处于写模式。

下面我们上一段ReentrantReadWriteLock源码类简单的讲讲是如何利用state的

java 复制代码
static final int SHARED_SHIFT   = 16;
// 00000000 00000001 00000000 00000000
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
// 00000000 00000000 11111111 11111111
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
// 00000000 00000000 11111111 11111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/**
 * 读锁被成功获取的次数
 */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

/**
 * 写锁可重入次数
 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

可以看到获取读锁被获取的次数是通过无符号右移16位实现的,而获取写锁可重入则通过按位与实现的。上面代码第六行注释中的就是EXCLUSIVE_MASK在二进制中的表示,按位与时相同位都为1时才为1,因为它高16位都为0,所以exclusiveCount结果就是写锁的可重入次数。

至于SHARED_UNIT,大家应该也能从它的二进制表示猜出它作用,就是用于在成功获取读锁时,将读锁被获取次数加1。

获取写锁

还是老样子,因为入队的操作已经由AQS替我们实现了,所以我们只需要关注子类中重写的tryAcquire方法就可以了。因为Sync继承AQS,所以我们直接去看Sync的tryAcquire方法。

java 复制代码
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 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;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

整体逻辑不算复杂,首先我们会去判断state是否不等于0,state不等于0则说明当前写锁或者读锁已被获取,紧接着就会去判断低16位是否等于0,如果低16位等于0说明当前读锁已被获取不能再去获取写锁所以直接返回false,如果不等于0就会判断当前线程是否锁持有线程,如果不是返回false。

当不符合第六行的条件那就说明当前写锁已被获取,并且持有线程是当前线程,这个时候会再判断是否会超过最大可重入次数,如果没超过对重入次数加1(acquires默认为1)。

如果不符合第五行的条件,说明现在读锁和写锁都没有被获取,那么就可以尝试竞争锁资源,成功竞争到锁资源则记录当前线程为持有锁的线程。writerShouldBlock方法就是判断当前线程是否应该被阻塞,再非公平锁下这个方法写死返回false,在公平锁下这个方法内部会调用hasQueuedPredecessors方法,AQS队列中有节点存在时不会立刻竞争锁资源。

释放写锁

java 复制代码
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;
}

写锁释放逻辑极其简单,判断当前线程为持有锁的线程才能执行释放锁的操作,否则发生异常。通过调用exclusiveCount方法判断本次释放以后写锁可重入次数是否为0,如果是则将exclusiveOwnerThread置null,标识当前没有线程持有锁。

经过上面的判断以后修改state,结束本次释放操作。后续唤醒队列中节点的逻辑参考java并发编程第六章的内容。

获取读锁

获取读锁的逻辑在Sync中的tryAcquireShared中。

java 复制代码
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

在获取读锁时,我们会先去判断写锁是否已被获取以及持有写锁的线程是否是当前线程,如果写锁未被获取或者持有写锁的线程为当前线程则会进入后续逻辑。这段逻辑也很合理,写锁已被获取了并且不是当前线程持有锁,那自然而然的不能让你去获取到读锁。

紧接着会去调用readerShouldBlock方法,这个方法就是判断当前是否应该被阻塞,在公平锁模式下这个方法和写锁的writerShouldBlock方法别无二致,对于这个方法我们只看非公平锁模式下的逻辑。

在非公平锁模式下,这个方法最后调用到的是AQS中的apparentlyFirstQueuedIsExclusive方法

java 复制代码
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

本章内容看到这,理解apparentlyFirstQueuedIsExclusive方法的逻辑对于你而言应该已经没什么难度了,几乎稍微看一下就知道这个方法的逻辑就是判断如果AQS已被初始化并且最少已经有一个节点入队,且这个节点的类型不是共享的则返回true。

因为在tryAcquireShared代码块第8行中的子条件前带了!,所以apparentlyFirstQueuedIsExclusive为false,也就是AQS未被初始化或尚未有节点入队或节点类型是共享的才会再去判断读锁被获取次数是否小于阈值,如果小于阈值的话就会去尝试竞争锁,如果CAS操作成功执行则相当于竞争到了读锁。

成功通过上面的条件后就会进一步判断如果本次加锁操作是第一次加锁(读锁被获取次数等于0)则记录当前线程为第一个获取读锁的线程并初始化firstReaderHoldCount。如果本次不是第一次加锁但当前线程是第一个获取读锁的线程则对firstReaderHoldCount进行++操作。

如果当前线程不是第一个获取到读锁的线程就会进入17-22行的代码块,在这里会去获取HoldCounter缓存,HoldCounter记录了对应线程的读锁获取次数。如果缓存为null或者缓存的HoldCounter不是当前线程的那么就会去更新缓存,readHolds的类型是ThreadLocalHoldCounter,它继承ThreadLocal,所以readHolds.get()就是获取当前线程的HoldCounter,最后将当前线程的读锁获取次数++。

可以看到如果8-10行没有成功获取到读锁,就会去调用fullTryAcquireShared方法。

java 复制代码
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        } else if (readerShouldBlock()) {
            if (firstReader == current) {
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh;
            }
            return 1;
        }
    }
}

这段代码就不细说了,就稍微贴出来给大家看一下。只要你通过上面的内容对tryAcquireShared已经有了一定理解的话,可以很容易的看出来fullTryAcquireShared就是自旋获取读锁而已。

释放读锁

java 复制代码
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    },
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

释放读锁时会判断当前线程是否为第一个获取到读锁的线程,如果是直接操作则减少firstReaderHoldCount的值,当第一个线程完全释放掉自身的读锁资源时则会重置firstReader。

如果当前线程不是第一个获取到读锁的线程,那么就去先去看缓存的HoldCounter是否是当前线程的HoldCounter,如果是则直接操作HoldCounter,如果不是则从readHolds中获取当前线程的HoldCounter,当完全释放掉当前线程的读锁资源时会清除当前线程的HoldCounter。

因为readHolds的类型是ThreadLocalHoldCounter,它继承ThreadLocal。所以当调用到readHolds.get()时,如果readHolds中没有当前线程的数据或触发ThreadLocalHoldCounter重写的初始化方法返回一个空的HoldCounter对象,空对象的count是0,所以如果当前线程在没有获取到读锁的情况下去调用unlock方法就会抛出unmatchedUnlockException异常。所以ThreadLocalHoldCounter的作用就是记录线程是否获取到了读锁以及成功获取的次数。

读写锁总结

最后我们对读写锁加锁流程做个总结。

ReentrantReadWriteLock根据场景将锁细分成读锁和写锁。读锁可以多个线程同时持有,但读锁被持有时,任何线程都无法再去持有写锁。写锁只能被一个线程持有,已经持有写锁的线程可以再去持有读锁,除此之外的其他线程无法在此时获取读锁。

为了实现上面所提到的特性,ReentrantReadWriteLock利用state来记录读写锁的状态。state的高16位记录读锁被获取次数,低16所记录写锁的可重入次数。采用左移获取高16位的值,采用按位与获取低16位的值。

读锁使用firstReader和ThreadLocalHoldCounter记录当前线程是否持有读锁,毕竟读锁是共享的,光靠一个state是无法判断当前线程是否已经获取到读锁。

Conditon

在使用synchronized时,我们可以通过调用锁对应的对象的wait或notify方法来阻塞当前线程或唤醒其中一个因为调用wait方法而被阻塞的线程。在使用JUC锁时则需要使用Conditon,Condition就相当于JUC包中的wait和notify方法,并且它的功能更强大。

java 复制代码
public class Test {

    private static final List<String> list = new ArrayList<>();
    private static final ReentrantLock lock;
    private static final Condition producerCondition;
    private static final Condition consumerCondition;

    static {
        lock = new ReentrantLock();
        consumerCondition = lock.newCondition();
        producerCondition = lock.newCondition();
    }

    private static void put(String str) {
        boolean isLocked = false;
        try {
            lock.lock();
            isLocked = true;
            if (list.size() >= 10) {
                producerCondition.await();
                if (!Thread.interrupted()) {
                    list.add(str);
                } else {
                    isLocked = false;
                }
            } else {
                list.add(str);
            }
            consumerCondition.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (isLocked) {
                lock.unlock();
            }
        }
    }

    private static String take() {
        lock.lock();
        try {
            if (list.isEmpty()) {
                producerCondition.signal();
                consumerCondition.await();
            }
            return list.remove(0);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                put(String.valueOf(i));
            }
        });
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println(take());
            }
        });
        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

上面是一个简单的生产者消费者例子,当集合长度过长时生产者会阻塞自己,当消费者消费不到数据时阻塞自己。可以看到相较于wait和notify,Condition可以做到只唤醒自身内部的被阻塞的线程,不会影响其他Condition中的线程。

newCondition实际创建的类型是ConditionObject,它是AQS的内部类。我们先简单的看一下它的类图,看一下它的内部结构。

可以看到ConditionObject内部是一个单向队列,并没有直接使用AQS队列。这也很合理,因为AQS中的都是因为竞争锁失败而被阻塞的线程,我们不可能将因await而被阻塞的线程也放到AQS队列中。

await

现在我们深入到await源码中,看看当我们调用这个方法时内部发生了什么事情

java 复制代码
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

private Node addConditionWaiter() {
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null)
        return true;
    return findNodeFromTail(node);
}

private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

从addConditionWaiter方法可以证明Conditon队列是单向的,而且没用AQS的同步队列(本章后续提到的同步队列,如无特殊申明,都指的是AQS中的同步队列)。

在调用await方法后,首先会将Node添加到Condition队列中,并且完全释放掉当前线程的锁资源(锁可重入,只释放一次不行)。紧接着,就会去判断当期线程的是否处于同步队列中,判断的条件也非常简单,因为只有被加入到同步队列,prev或next才有可能不为null,所以waitStatus为CONDITION或prev为null就说明还没加入到同步队列,当我们刚进去await方法,这里必定为false,所以会在代码第8行被阻塞挂起。

当我们调用signal时,则会将当前线程的Node从Condition队列放到同步队列中并唤醒线程,这时候就会退出while循环。

接着就会通过调用acquireQueued等待获取到锁,不管是成功获取到锁还是获取锁中途被中断了,都会都会对Condition队列进行一次清洗,剔除waitStatus不是CONDITION的节点。

在await的最后,我们会根据interruptMode做一次特殊处理。如果interruptMode不等于0,那么说明发生了两种情况,一种是THROW_IE(Condition节点在调用signal方法前就被中断了),另外一种是REINTERRUPT(Condition节点在调用signal方法后还未真正被唤醒时被中断或者调用acquireQueued方法获取锁时被中断)。对于THROW_IE会直接抛出InterruptedException,而REINTERRUPT则只会在当前线程中记录中断状态,所以我们的测试用例中在被唤醒时还去判断了线程的中断状态,没有被中断才会写数据到集合中。

signal

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

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

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

signal的逻辑还是挺好理解的,就是将Condition节点从Condition队列放到同步队列中,并唤醒指定线程。并且从doSignal方法中可以看到Condition的唤醒是顺序唤醒的。

总结

本章参照《java并发编程之美》以及《java并发编程 深度解析与实战》,主要内容是从ReentrantLock,ReentrantReadWriteLock以及Condition源码分析它们的机制。随着我们的逐步深入,我们也开始明白AQS的state字段代表含义根据子类的不同而不同。如果工作中遇到只读场景以及写场景混合的情况下,可以尝试使用读写锁。合理使用Condition也能帮助我们在并发场景中更优雅的去解决问题。

相关推荐
闲猫14 分钟前
go orm GORM
开发语言·后端·golang
4277240015 分钟前
IDEA使用git不提示账号密码登录,而是输入token问题解决
java·git·intellij-idea
丁卯40436 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo37 分钟前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李长渊哦39 分钟前
常用的 JVM 参数:配置与优化指南
java·jvm
计算机小白一个39 分钟前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
南宫生3 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
计算机毕设定制辅导-无忧学长4 小时前
Maven 基础环境搭建与配置(一)
java·maven
bing_1584 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白5 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式