并发编程的艺术--Java中的锁(下)【随笔】

读写锁

介绍

之前提到锁(如 CustomLockReentrantLock )基本都是独占锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问。但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的独占锁有了很大提升。

除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的所有读服务可见。

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠 synchronized 关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。

改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。 一般情况下,读写锁的性能都会比独占锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比独占锁更好的并发性和吞吐量

其中的读锁就相当于下面的共享锁,写锁就相当于下面的独占锁。共享锁访问时,阻塞所有独占锁,独占锁访问时,阻塞其它独占锁和所有共享锁。

20210105144703755

Java 并发包提供读写锁的实现是 ReentrantReadWriteLock,它提供的特性如下表所示:

20210105150221422

简单使用

ReentrantReadWriteLock 提供了获取读锁和写锁的两个方法,即 readLock() 方法和 writeLock() 方法。下面是一个读写锁的使用案例:

public class ReadWriteLockTest {

    public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();

        // 写操作
        for (int i = 1; i <= 6; i++) {
            final int temp = i;
            new Thread(()->{
                myCache.put(temp + "", "akieay" + temp);
            }, "write thread-"+i).start();
        }

        //读操作
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(()->{
                Object o = myCache.get(temp + "");
            }, "read thread="+i).start();
        }
    }
}

/**
 * 加锁的自定义缓存
 */
class MyCacheLock {

    private volatile Map<String, Object> map = new HashMap<>();
    /**
     * 读写锁:更加细粒度的控制
     */
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    /**
     * 读锁
     */
    private Lock read = lock.readLock();
    /**
     * 写锁
     */
    private Lock write = lock.writeLock();

    /**
     * 存储,写入的时候,只希望同时只有一个线程写
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        write.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入Ok");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            write.unlock();
        }
    }

    /**
     * 读取,读取的时候,所有人都可以读
     * @param key
     * @return
     */
    public Object get(String key) {
        read.lock();
        Object value = null;
        try {
            System.out.println(Thread.currentThread().getName() + " 读取" + key);
            value = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取Ok");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            read.unlock();
        }

        return value;
    }
}

运行程序,查看控制台打印:可以看到在当前线程获取写锁执行写操作时,不会有其他写操作和读操作执行,只有在当前写操作执行完成后,其它线程的写操作才能获取写锁并执行。在当前线程获取读锁执行读操作时,其它线程也可以获取读锁执行读操作【如示例中的读线程3在执行时,读线程4、5也在执行。】,其它执行读操作的线程并不会阻塞。

20210105154949069

上述示例中,MyCacheLock 使用一个非线程安全的 HashMap 作为缓存的实现,同时使用读写锁的读锁和写锁来保证 MyCacheLock 是线程安全的。在读操作 get(String key) 方法中,需要获取读锁,读锁使得并发访问该方法时不会被阻塞。写操作 put(String key, Object value) 方法,在更新 HashMap 时必须提前获取写锁,当获取写锁后,其它线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其它线程的读写操作才能继续。MyCacheLock 使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

读写锁的实现原理

要分析 ReentrantReadWriteLock 的实现原理,主要是分析以下几点:

  • 读写状态的设计
  • 写锁的获取与释放
  • 读锁的获取与释放
  • 锁降级
读写状态的设计

读写锁同样依赖自定义同步器【实现 AQS】来实现同步功能,而读写状态就是其同步器的同步状态。回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。如果在一个整型变量上维护多种状态,就一定需要 “按位切割使用” 这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如下图所示【int 占 4 个字节,共 32 位】:

20210105164857908

上面的同步状态表示一个线程已经获取了写锁,且重进入了两次【值为 3】,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为 S,写状态等于 S&0x0000FFFF(将高16位全部抹去,0x0000FFFF 可由 (1 << 16) -1 得到,左移右边补0),读状态等于 S>>>16(左边补0右移16位,将高16位抹去)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是 S+0x00010000 。

根据状态的划分能得出一个推论【下面用得到】:S不等于0 且 写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

写锁的获取与释放
写锁的获取

写锁是一个支持重进入的独占锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,ReentrantReadWriteLock 获取写锁的源码如下:

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取整体的同步状态【包括读锁、写锁】
    int c = getState();
    // 获取写锁
    int w = exclusiveCount(c);
    // 同步状态不为0【即存在读锁或写锁中至少一种】
    if (c != 0) {
        // 写锁状态为0,即存在读锁【总状态不为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;
    }
    
    /* 执行到这里表示不存在写锁与读锁 */
    /// 有资格获取写锁但被阻塞,或 CAS 修改同步状态失败
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 设置当前线程获取同步状态
    setExclusiveOwnerThread(current);
    return true;
}

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取。原因在于:

读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取。而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

另外注意 exclusiveCount 方法的实现,前面也介绍了:写状态等于 S&0x0000FFFF(将高16位全部抹去,左移右边补0),而 0x0000FFFF 可由 (1 << 16) -1 得到,所以这个方法是用来获取写锁同步状态的。

20210105173504352

写锁的释放

写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态;当写状态为 0 时,表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 写锁状态为低16位,可以直接减去
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
读锁的获取与释放
读锁的获取

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在 ThreadLocal 中,由线程自身维护,这使获取读锁的实现变得复杂。获取读锁的核心方法 tryAcquireShared 源码如下:

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);
    // 有资格获取读锁且没被阻塞 且 读锁的数量未达到最大值 且 CAS 修改读锁状态成功
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            // 设置 第一个获得读锁的线程
            firstReader = current;
            // 设置 firstReader保持计数
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // firstReader的保持计数自增1
            firstReaderHoldCount++;
        } else {
            // 缓存的保持计数器,维护了每个线程的读锁获取次数
            HoldCounter rh = cachedHoldCounter;
            // 若计数器为空 或 计数器的Tid 不是当前线程的ID
            if (rh == null || rh.tid != getThreadId(current))
                // 将缓存的保持计数器 的值修改为 当前线程持有的读锁的数量
                cachedHoldCounter = rh = readHolds.get();
            // 若读锁数量为0
            else if (rh.count == 0)
                // 设置保持计数器
                readHolds.set(rh);
            // 读锁数量+1
            rh.count++;
        }
        return 1;
    }
    // CAS 获取读锁失败时调用
    return fullTryAcquireShared(current);
}

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) {
                // assert firstReaderHoldCount > 0;
            } 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");
        // CAS 更新读锁数量
        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; // cache for release
            }
            return 1;
        }
    }
}

tryAcquireShared(int unused) 方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠 CAS 保证)增加读状态,成功获取读锁。

注意:更新同步状态需要加上 SHARED_UNIT (即:(1 << 16),也就是 0x00010000),表示读锁数量 + 1【读锁为高 16 位】。

读锁的释放

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)【读锁为高 16 位】。

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 如果当前线程是第一个获取读锁的线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        // 获取读锁的计数器
        HoldCounter rh = cachedHoldCounter;
        // 若计数器为空,或计数器的Tid 不是 当前线程ID
        if (rh == null || rh.tid != getThreadId(current))
            // 当前线程持有的读锁的计数器
            rh = readHolds.get();
        // 当前线程持有的读锁的数量
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        // 读锁数量-1
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        // 更新同步状态
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}
锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。接下来看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。示例如下:

class CachedData {
    String data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // 必须在获取写锁之前释放读锁
            rwl.readLock().unlock();

            // 获取写锁
            rwl.writeLock().lock();
            try {
                // 重新检查状态,因为另一个线程可能已经获得了写锁并在我们之前更改了状态。
                if (!cacheValid) {
                    // 准备数据的流程(略)
                    data = ....
                    cacheValid = true;
                }
                // 通过在释放写锁之前获取读锁来降级
                rwl.readLock().lock();
            } finally {
                // 释放写锁,仍然保持读锁
                rwl.writeLock().unlock();
            }
        }

        try {
            // 使用据的流程(略)
            use(data);
        } finally {
            // 释放读锁
            rwl.readLock().unlock();
        }
    }
}

上述示例中,当数据发生变更后,cacheValid 变量(布尔类型且 volatile 修饰)被设置为 true,此时所有访问 processCachedData() 方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的 lock() 方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。

锁降级中读锁的获取是否必要呢?

答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程 T )获取了写锁并修改了数据,那么当前线程无法感知线程 T 的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程 T 才能获取写锁进行数据更新。


LockSupport 工具类

当需要阻塞或唤醒一个线程的时候,可以使用 LockSupport 工具类来完成相应工作。LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而 LockSupport 也成为构建同步组件的基础工具。LockSupport 定义了一组以 park 开头的方法用来阻塞当前线程,以及 unpark(Thread thread) 方法来唤醒一个被阻塞的线程。这些方法如下:

20210106144054331

在 Java 6 中,LockSupport 增加了 park(Object blocker)parkNanos(Object blocker,long nanos)parkUntil(Object blocker,long deadline) 3个方法,用于实现阻塞当前线程的功能,其中参数 blocker 是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。

使用示例:

public class LockSupportDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
            System.out.println(Thread.currentThread().getName() + "被唤醒");
        }, "akieay-one");
        thread.start();


        String flag = "flag";
        Thread thread2 = new Thread(() -> {
            LockSupport.parkNanos(flag, TimeUnit.SECONDS.toNanos(10));
            System.out.println(Thread.currentThread().getName() + "被唤醒");
        }, "akieay-two");
        thread2.start();
    }
}

Condition 接口

Condition 介绍

任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()wait(long timeout)notify() 以及 notifyAll() 方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。通过对比 Object 的监视器方法和 Condition 接口,可以更详细地了解 Condition 的特性,如下图:

20210106160300864

Condition 使用示例

Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。Condition 对象是由 Lock 对象调用 newCondition()方法创建出来的,换句话说,Condition 是依赖 Lock 对象的。

一般都会将 Condition 对象作为成员变量。当调用 await() 方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal() 方法,通知当前线程后,当前线程才从 await() 方法返回,并且在返回前已经获取了锁。

Condition 定义的部分方法以及描述如下表:

20210106161123069

获取一个 Condition 必须通过 LocknewCondition() 方法。下面通过一个有界队列的示例来深入了解 Condition 的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现 “空位”。

以下为一个使用示例:

public class ConditionTest {

    public static void main(String[] args) {
        BoundedQueue boundedQueue = new BoundedQueue(4);

        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    String remove = (String) boundedQueue.remove();
                    System.out.println(Thread.currentThread().getName() + " 消费产品 " + remove);
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "consumer").start();

        new Thread(() -> {
                for (int i = 0; i < 20; i++) {
                try {
                    String product = "akieay-" + i;
                    System.out.println(Thread.currentThread().getName() + " 生产产品 " + product);
                    boundedQueue.add(product);
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "provider").start();
    }
}

class BoundedQueue<T> {

    private Object[] items;
    /**
     * 添加的下标
     */
    private int addIndex;
    /**
     * 删除的下标
     */
    private int removeIndex;
    /**
     * 数组当前数量
     */
    private int count;
    private Lock lock = new ReentrantLock();

    private Condition notEmpty = lock.newCondition();

    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size) {
        items = new Object[size];
    }

    /**
     * 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
     * @param t
     * @throws InterruptedException
     */
    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                System.out.println(Thread.currentThread().getName()+"----等待--");
                // 阻塞等待
                notFull.await();
            }
            items[addIndex] = t;
            if (++addIndex == items.length) {
                addIndex = 0;
            }
            ++count;
            // 通知消费
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
     * @return
     * @throws InterruptedException
     */
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println(Thread.currentThread().getName()+"----等待--");
                // 阻塞等待
                notEmpty.await();
            }
            Object x = items[removeIndex];
            if (++removeIndex == items.length) {
                removeIndex = 0;
            }
            --count;
            // 通知生产
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

上述示例中,BoundedQueue 通过 add(T t) 方法添加一个元素,通过 remove() 方法移出一个元素。以添加方法为例,首先需要获得锁,目的是确保数组修改的可见性和排他性。当元素数量等于数组长度时,表示数组已满,则调用 notFull.await(),当前线程随之释放锁并进入等待状态。如果元素数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知阻塞在 notEmpty上 的线程,数组中已经有新元素可以获取。 在添加和删除方法中使用 while 循环而非 if 判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。

查看我们 main 方法中的逻辑来推断运行结果:

​ 首先,我们启动类一个消费者线程,消费者每隔 2 秒消费一个产品;然后启动了一个生产者线程,生产者每隔 1 秒生产一个产品。由于先启动消费者,这时数组中还没有产品,此时消费者等待;生产者启动后,生产产品并通知消费者消费,消费者被唤醒开始消费产品。由于产品的生产速度远快于消费速度,所以当数组满了后,生产者进入等待状态,等待消费者消费产品后通知生产者生产。

运行程序,查看控制台打印信息如下:完全符合我们预期的结果。

20210107174746844

Condition 的实现原理【重点】

ConditionObject是同步器AbstractQueuedSynchronizer 的内部类,因为 Condition 的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个 Condition 对象都包含着一个队列(以下称为等待队列),该队列是 Condition 对象实现等待/通知功能的关键。下面将分析 Condition 的实现,主要包括:等待队列、等待和通知,下面提到的 Condition 如果不加说明均指的是 ConditionObject

等待队列

等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在 Condition 对象上等待的线程,如果一个线程调用了 Condition.await() 方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中同步队列节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node。一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用 Condition.await() 方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图所示:【值得注意的是与同步队列不同的是 等待队列是一个单向队列,而同步队列是一个双向队列

20210107094436596

如图所示,Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await() 方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

Object 的监视器模型上,一个对象拥有一个同步队列和等待队列;而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列【可以创建多个 Condition 对象,每个都包含一个等待队列】,其对应关系如图所示:

在这里插入图片描述

如图所示,Condition 的实现是同步器的内部类,因此每个 Condition 实例都能够访问同步器提供的方法,相当于每个 Condition 都拥有所属同步器的引用。

等待 await

调用 Conditionawait() 方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await() 方法返回时,当前线程一定获取了 Condition 相关联的锁。如果从队列(同步队列和等待队列)的角度看 await() 方法,当调用 await() 方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。Conditionawait() 方法源码如下:

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);
        // 检查线程是否被中断,没有被中断则返回 0
        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);
}

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

当等待队列中的节点被唤醒,则被唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal()/signalAll() 方法唤醒,而是对等待线程进行中断而唤醒,则会抛出 InterruptedException

如果从队列的角度去看,当前线程加入 Condition 的等待队列,同步队列的首节点并不会直接加入等待队列,而是通过 addConditionWaiter() 方法把当前线程构造成一个新的节点并将其加入等待队列中。

关于上面的实现过程还有以下几个细节需要了解:

  • 将当前线程添加到等待队列的过程
  • 释放锁的过程
  • await 方法退出并获取 Lock 锁的过程

一、将当前线程添加到等待队列,核心方法为 addConditionWaiter,其源码为:

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;
}

/** 遍历并释放等待队列中失效的节点 */
private void unlinkCancelledWaiters() {
    // 获取等待队列头节点
    Node t = firstWaiter;
    Node trail = null;
    // 循环遍历等待队列的节点
    while (t != null) {
        // 获取节点的后继节点
        Node next = t.nextWaiter;
        // 若节点状态不为 等待状态
        if (t.waitStatus != Node.CONDITION) {
            /* 删除失效节点 */
            // 使节点的尾指针失效
            t.nextWaiter = null;
            if (trail == null)
                // 设置等待队列头节点为其后继节点
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            // 若节点已遍历完成
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

以上源码其主要逻辑是:首先获取等待队列的尾节点,若尾节点不处于等待状态【即尾节点失效】,则遍历并释放等待队列中的失效节点,然后重新获取尾节点【保证获取到的尾节点为等待状态 或为 null】;然后将当前线程构造成节点,且状态为等待状态;在判断尾节点是否为空,若尾节点为空【等待队列为空】,则将当前节点设置为头节点,若不为空,则将当前节点设置为原尾节点的后继节点,并将当前节点设置为尾节点并返回。

二、释放锁的过程,其核心方法为 fullyRelease ,源码如下:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 获取同步状态
        int savedState = getState();
        // 释放同步状态
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            // 修改节点状态
            node.waitStatus = Node.CANCELLED;
    }
}

以上源码的主要逻辑是:获取同步状态,然后调用 AQS 的模板方法 release 方法释放 AQS 的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则返回获取到的同步状态,若失败的话就抛出异常且将节点状态设置为 CANCELLED【取消状态】。

三、从 await 方法退出并获取 Lock 锁的过程,重点关注 await 方法的以下源码:

// 若当前节点不处于同步队列上
while (!isOnSyncQueue(node)) {
    // 当前线程进入等待状态
    LockSupport.park(this);
    // 检查线程是否被中断,没有被中断则返回 0
    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);

分析以上源码可知:当线程第一次调用await() 方法时,会进入到这个 while() 循环中,然后通过 LockSupport.park(this) 方法使得当前线程进入等待状态;想要退出 await 方法第一个前提条件就是要先退出这个 while 循环;退出方式有两种:

  • 不满足 while 循环的条件,即:当且节点处于同步队列中
  • 满足 if 中的条件,从而 break 退出,即:线程被中断

总结一下就是,当前线程被中断,或者其它线程调用 condition.signal/condition.signalAll 方法,当前线程移动到同步队列后,即可退出 while 循环;当退出循环后,会调用 acquireQueued 尝试获取锁,该方法会通过 CAS 自旋不断尝试获取同步状态,直至成功(线程获取到lock);这样当退出 await 方法时,已经获得了 condition 引用(关联)的 Lock

至此,关于 await 方法的流程及原理介绍完毕,下面是整个方法的流程图:

20210107145920548

通知 signal

调用 Conditionsignal() 方法,将会唤醒在等待队列中等待时间最长的节点(头节点),在唤醒节点之前,会将节点移到同步队列中。 Conditionsignal() 方法,源码如下:

调用该方法的前置条件是当前线程必须获取了锁,可以看到 signal() 方法进行了 isHeldExclusively() 检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。

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) {
    // CAS 修改节点的状态为初始状态,如果不能修改waitStatus,表示该节点已经取消
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 将节点插入同步队列尾部,并返回其前驱节点
    Node p = enq(node);
	// 获取前驱节点状态
    int ws = p.waitStatus;
    // 若前驱节点状态 > 0 或 CAS 修改前驱节点状态【为 SIGNAL】失败
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 唤醒线程
        LockSupport.unpark(node.thread);
    return true;
}

private Node enq(final Node node) {
   for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

通过调用同步器的 enq(Node node) 方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用 LockSupport 唤醒该节点的线程。被唤醒后的线程,将从 await() 方法中的 while 循环中退出(isOnSyncQueue(Node node)方法返回t rue,节点已经在同步队列中),进而调用同步器的 acquireQueued() 方法加入到获取同步状态的竞争中。 成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的 await() 方法返回,此时该线程已经成功地获取了锁。 ConditionsignalAll() 方法,相当于对等待队列中的每个节点均执行一次 signal() 方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

节点从等待队列移动到同步队列的流程图如下:

20210107161154343

总结

Condition 的等待通知整体流程如下:

其中蓝色为:Thread 获取锁并调用 await 方法阻塞线程并释放锁。

其中绿色为:Thread 获取锁并调用 signal 方法唤醒被阻塞的线程。

其中程色为:Thread 获取锁失败,将线程构造成节点添加到同步队列,在其它线程释放锁后,参与锁竞争并获取锁后执行。

至此,关于 Condition 的介绍完毕,现在应该就能很容易的理解前面的使用示例了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值