Java并发学习(六)显式锁的实现,重入锁ReentrantLock、读写锁ReentrantReadWriteLock

上次对于显示锁和队列同步器的学习已经提到,我们可以使用Java提供的队列同步器AQS实现自定义的同步组件。同样,Java自身也为我们提供了很多可用的锁(或同步组件),如重入锁ReentrantLock和读写锁ReentrantReadWriteLock等,它们也是通过AQS实现的,并且继承了Lock接口。

与内置锁synchronized不同,Lock接口提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方式都是显式的,这也是显式锁的由来。java.util.concurrent.locks 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 

重入锁 

重入锁ReentrantLock,顾名思义,是指可重入的锁,即当前拥有锁的线程能过对此资源反复进行加锁,而不会被阻塞。在Java中关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入(具体可参考这篇文章)。

除了可重入外,ReentrantLock还支持公平锁和非公平锁两种方式。

1、实现可重入

要想实现可重入性,就要解决以下两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功
  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功

ReentrantLock是通过组合对自定义同步器来实现锁的获取和释放的,以非公平锁为例,重入获取锁时:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//获取同步状态值
    //为0,说明该锁未被任何线程占有,该锁能被当前线程获取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {//CAS重置同步状态值
            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;
}

成功获取锁的线程再次获取锁后,只是增加了同步状态值state,又由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功;所以ReentrantLock在每次释放同步状态时也必须减少同步状态值,锁释放如下代码:

protected final boolean tryRelease(int releases) {
	//参数值releases、acquires一般都为1
    int c = getState() - releases; //同步状态值减1
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//只有当同步状态值为0时,锁成功被完全释放,返回true
        free = true;
        setExclusiveOwnerThread(null);//设置锁当前所属线程为空
    }
	//同步状态值不为0,锁未被完全释放,返回false
    setState(c);
    return free;
}

需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放tryRelease方法必须返回false,只有被释放n次才算成功释放,返回true。

2、公平与非公平

所谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的无参构造方法默认是构造非公平锁,构造公平锁则需要布尔参数值为true。我们可以这样使用:

Lock fairLock = new ReentrantLock(true);

Lock unfairLock = new ReentrantLock(false);
//也可以无参
Lock unfairLock = new ReentrantLock();

 而ReentrantLock内部是通过将Sync继承AQS并设为抽象类,再用NonfairSyncFairSync两个静态内部类分别实现非公平和公平锁的语义。

//定义Sync抽象类继承AQS同步器
abstract static class Sync extends AbstractQueuedSynchronizer {....}
//非公平锁NonfairSync继承抽象类Sync,实现其语义
static final class NonfairSync extends Sync {....}
//公平锁FairSync继承抽象类Sync,实现其语义
static final class FairSync extends Sync {....}


//ReentrantLock默认构造为非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
}
//设置公平锁,构造参数为true
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

上文中已介绍非公平锁使用nonfairTryAcquire(int acquires)方法获取同步状态,对于非公平锁,只需要CAS设置同步状态成功,则表示当前线程获取到了锁,而公平锁则则多了一道防线,ReentrantLock的TryAcquire(int acquires)方法即为公平的获取同步状态。

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

该方法与上文nonfairTryAcquire(int acquires)方法完全类似,唯一的不同只有判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,表示有线程比当前线程更早的请求获取锁,因此为了公平需要等待前驱线程获取并释放锁之后才能继续获取锁。

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

如上所述,公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。似乎公平锁功能更好,那么我们为什么还要默认使用非公平锁呢?那是因为:

  • 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
  • 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量

对于ReentrantLock公平锁和非公平锁的理解,这篇文章讲的很生动,值得一看。

3、Lock锁和synchronized锁的区别

读写锁

在AQS同步器的实现原理学习中我们知道了获取同步状态的两种方式:独占式共享式。而ReentrantLock或者之前的synchronized都是独占式锁(或者说是排他锁),尽管ReentrantLock和synchronized都是可重入的(synchronized锁是隐式可重入)锁,但这些锁同一时间只允许一个线程对其进行访问,而共享式锁在同一时刻可允许多个线程访问。

读写锁的实现就依靠了共享锁的原理,其在读访问时,允许多个读线程访问;而在写访问时,只允许一个写线程,所有其他的读线程和写线程都被阻塞。

1、ReadWriteLock接口

读写锁维护了一对锁来实现其语义,分别是一个读锁(共享锁)一个写锁(独占锁),通过分离读锁和写锁,使得其并发性相对于一般的排他锁有了很大的提升。

ReadWriteLock是专门为了实现读写锁而设计的接口,它只定义了获取读锁和获取写锁两个方法,而实际的功能由它的实现类ReentrantReadWriteLock来详细实现。

2、ReentrantReadWriteLock读写锁

ReentrantReadWriteLock是ReadWriteLock的实现类,除了接口定义的方法外,它还提供了一些便于外界监控其内部工作状态的方法,ReentrantReadWriteLock提供的特性如下:

  1. 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
  2. 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
  3. 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

ReentrantReadWriteLock主要由以下方法实现其功能:

 ReentrantReadWriteLock.ReadLockreadLock() 
          返回用于读取操作的锁。
 ReentrantReadWriteLock.WriteLock

writeLock() 
          返回用于写入操作的锁。

 boolean

isWriteLocked() 
          判断写锁是否被获取。

 intgetWriteHoldCount() 
          查询当前线程在此锁上保持的重入写入锁数量。
 intgetReadLockCount() 
          返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如一个线程连续重入的获取读锁N次,那么占据读锁的线程数是1,而此方法返回N
 intgetReadHoldCount() 
          返回当前线程获取读锁的次数,即当前线程重入锁的次数。

读写锁的使用:

我们知道HashMap是线程不安全的,所以下面使用读写锁实现一个HashMap为基础的线程安全的缓存的简单例子

public class Cache {
    static Map<String,Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock read = rwl.readLock();   //创建读锁
    static Lock write = rwl.writeLock(); //创建写锁
    //get读取key键对应的value值,读锁
    public static final Object get(String key) {
        read.lock();
        try {
            return map.get(key);
        }finally {
            read.unlock();
        }
    }
    //put写入key键对应的value值,返回旧value值,写锁
    public static final Object put(String key,Object value) {
        write.lock();
        try {
            return map.put(key,value);
        }finally {
            write.unlock();
        }
    }
}

在上面的代码示例中,同时使用读写锁的读锁和写锁来保证Cache类的线程安全,使用非常的简单,那具体是怎么实现的呢

3、读写锁的实现

1.读写状态的设计

对于AQS同步器的学习后我们知道,同步器管理一个int类型整数信息state,这个整数状态信息可以表示任何的状态,例如,重入锁ReentrantLock用来表示所有者线程已经重复获取该锁的次数;信号量Semaphore用来表示剩余的许可数量;FutureTask用来表示任务的运行状态(尚未运行、正在运行、已完成已经已取消)等。

读写锁的自定义同步器,需要维护多个读线程和一个写线程的状态,但是AQS只提供了一个整型变量,那他是如何做的呢?

读写锁采取了”按位分割“的方法来充分的利用一个整型变量state,将此32位的int变量state分割成为了两个部分,高16位表示读,低16位表示写,如图所示:

如图的当前同步状态表示一个线程已经获取到了写锁,并且又重入了2次,同时也获取到了读锁。

读写锁通过高效的位运算来迅速确定读和写各种的同步状态,写状态为state & 0x0000FFFF(将高16位全部抹去),读状态等于state >>> 16(无符号右移16位);所以当写状态增加1(获取写锁)时,等于state+1,读状态增加1(获取读锁)时,等于state+(1<<16),即state+0x00010000。

2、写锁的获取与释放

写锁的获取:

 写锁是一个可重入的排他锁,这与ReentrantLock的语义是一样的。如果当前线程已经获取到了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(state高16位的读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,具体实现如下:

//重写AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
	//写锁当前的同步状态
    int c = getState();
	//写锁被获取的次数,低16位,即state & 0x0000FFFF 
    int w = exclusiveCount(c);
    if (c != 0) {
	//当存在读锁(c!=0,w=0即高16位不为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;
}

相较于一般的排他锁,该方法除了重入条件,还增加了读锁是否存在的判读;当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败。

写锁的释放:

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

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
	//同步状态减去写状态
    int nextc = getState() - releases;
	//当前写状态是否为0,为0则释放写锁
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
	//不为0则更新同步状态
    setState(nextc);
    return free;
}

3、读锁的获取和释放

读锁是一个支持重入的共享锁,它能被多个线程同时获取,在没有其他写线程访问(state低16位的写状态为0)时,读锁总能够被成功的获取,但是会增加读状态(state的高16位加1),此增加操作是线程安全的,依靠的是CAS来保证的。

共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法,读锁的获取实现方法为:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
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; }


protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
	//如果写锁已经被获取或者获取写锁的线程不是当前线程的话
	//当前线程获取读锁失败并返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);//获取state同步状态的高16位,采用state>>>16
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
		//当前线程获取读锁
        compareAndSetState(c, c + SHARED_UNIT)) {
		//下面的代码主要是新增的一些功能,比如getReadHoldCount()方法
		//返回当前获取读锁的获取次数
        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;
    }
	//处理在上述CAS操作失败的自旋实现重入性
    return fullTryAcquireShared(current);
}

读状态是所有线程获取读锁次数的总和,但不保存每个线程各自获取读锁的次数,这个次数由各自的线程保存在各自的ThreadLocal中,线程自身维护。

读锁状态的增加是依靠CAS实现的,如果当前线程获取了写锁或者写锁未被获取,则当前线程CAS增加读状态,成功获取读锁。

读锁的每次释放,也必须保证是线程安全的,可能多个线程同时释放读锁,同时减少读状态,减少值为(1<<16)。

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
	// 为了实现getReadHoldCount等新功能
    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;
    }
}

4、锁降级

锁降级指的是写锁降级为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程是不能称之为锁降级的。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先去拥有的)写锁的过程。

void processCachedData() {
      rwl.readLock.lock();
      if (!cacheValid) {
            //必须先释放读锁
            rwl.readLock().unlock();
            //锁降级从写锁获取到开始
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
                cacheValid = true;
                }
            //释放写锁前必须先获取读锁
            rwl.readLock().lock();
            } finally {
              rwl.writeLock().unlock(); //把持读锁,释放写锁
            }
      }
      try {
        use(data);//使用数据
      } finally {
        rwl.readLock().unlock();
      }
}

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

辅助工具类

1、LockSupport工具类

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。LockSupport提供了一组公共的静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,所以它也成为了构建同步组件的基本工具。

LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。

park()和unpark()不会有 “Thread.suspend和Thread.resume所可能引发的死锁” 问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。

如果调用线程被中断,则park方法会返回。同时park也拥有可以设置超时时间的版本。

需要特别注意的一点:park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。

2、Condition辅助接口

任何一个Java对象,都有一组监视器方法wait、notify 和 notifyAll等,这些方法与synchronized配合使用可以实现等待通知模式。而Condition则是将Object的这些通信方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set),其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 通信方法的使用。

Condition的使用:

通过对比两者在使用上的差异,可以更好的理解其不同的特性:

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

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }
    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

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

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

public class BoundedQueue<T> {
    private Object[] items;

    // 添加的下标,删除的下标和数组当前数量
    private int addIndex,removeIndex,count;

    private Lock lock = new ReentrantLock();

    private Condition notEmpty = lock.newCondition();

    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size){
        items = new Object[size];
    }
    //添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
    public void add(T t) throws InterruptedException{
        lock.lock();
        try{
            while(count == items.length){
                notFull.await();
            }
            items[addIndex] = t;
            if(++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal();
        }finally{
            lock.unlock();
        }
    }
 //由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException{
        lock.lock();
        try{
            while(count == 0)
                notEmpty.await();
            Object x = items[removeIndex];
            if(++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notFull.signal();
            return (T)x;
        }finally{
            lock.unlock();
        }
    }
}

首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时,表示数组已满,则调用notFull.await(),当前线程随之释放锁并进入等待状态。如果数组数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty上的线程,数组中已经有新元素可以获取。

在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。

Condition的实现原理:

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个等待队列,该队列是Condition对象实现等待/通知功能的关键。

下面将分析Condition的实现,主要包括:等待队列、等待和通知。

等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态 。一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。

事实上,Condition节点的定义服用了AQS队列同步器中节点的定义,也就是说,同步队列和等待队列的节点类型是一样的,都是AQS的静态内部类AbstractQueuedSynchronizer​​​​​​​.Node。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示:

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

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如下图所示:

等待方法

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

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

需要注意的是,同步队列的首节点不会直接加入等待队列(虽然节点类型是一样的),而是通过addConditionWaiter()方法把当前线程构造成一个新的节点再将其加入等待队列中。

唤醒方法

调用Conditionsignal()方法,将会唤醒在等待队列中等待时间最长的节点首节点),在唤醒节点之前,会将节点移到同步队列中。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。

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

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。

被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中(自旋)。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。

Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

 

 

参考文章:

《Java并发编程的艺术》

《Java并发编程实战》

《Java核心技术 卷1》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值