当前位置: 代码迷 >> 综合 >> 【并发编程进阶6】ReentrantLock 和 LockSupport
  详细解决方案

【并发编程进阶6】ReentrantLock 和 LockSupport

热度:82   发布时间:2023-12-16 02:34:06.0

???欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
img

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
    • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
    • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
    • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
    • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
    • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。??? ?? 欢迎订阅本专栏 ??

博客目录

      • 1.什么是可重入锁?
      • 2.ReentrantLock 非公平锁获取与释放?
      • 3.ReentrantLock 公平锁获取和释放?
      • 4.公平锁和非公平锁总结?
      • 5.为什么非公平锁会造成线程饥饿?
      • 6.ReentrantLock 使用和理解?
      • 7.什么是读写锁?
      • 8.ReentrantReadWriteLock 原理?
      • 9.ReentrantReadWriteLock 写锁的获取与释放?
      • 10.读写锁 c 值的含义
      • 11.ReentrantReadWriteLock 读锁的获取与释放?
      • 12.ReentrantReadWriteLock 的锁降级问题?
      • 13.LockSupport 工具是什么?
      • 14.parkNanos 中 blocker 含义
      • 15.park unpark 原理
      • 16.Synchronized 和 Lock 区别?
      • 17.Lock 的 Condition 接口作用?
      • 18.Condition 的实现原理?

1.什么是可重入锁?

首先明确下 synchronized 和 lock 接口均为可重入锁。

重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

实现原理如下:

  • 线程再次获取锁.锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 锁的最终释放.线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其他线程能够获取到该锁.锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已经成功释放。

2.ReentrantLock 非公平锁获取与释放?

使用非公平锁加锁过程:

  • ReentrantLock:lock()。
  • NonfairSync:lock()。
  • AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。
final boolean nonfairTryAcquire(int acquires){
    final Thread current = Thread.currentThread();int c = getState();if (c == 0){
    if (compareAndSetState(0, acquires)){
    setExclusiveOwnerThread(current);//以cas原子操作更新statereturn true;}} else if (current == getExclusiveOwnerThread()){
    int nextc = c + acquires;if (nextc < 0)// overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求 ReentrantLock 在释放同步状态时减少同步状态值

在使用非公平锁时,解锁方法 unlock()调用轨迹如下。

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)。
  3. Sync:tryRelease(int releases)。
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;
}

3.ReentrantLock 公平锁获取和释放?

公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。

使用公平锁时,加锁方法 lock()调用轨迹如下。

  1. ReentrantLock:lock()。
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)。
  4. ReentrantLock:tryAcquire(int acquires)。

在第 4 步真正开始加锁,下面是该方法的源代码。

在使用公平锁时,解锁方法 unlock()调用轨迹如下。

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)。
  3. Sync:tryRelease(int releases)。

在第 3 步真正开始释放锁,下面是该方法的源代码。

c==0 代表是第一次进入,不是重复获取锁,所以不需要加其他的判断,第一步需要读取 volatile 变量 state

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。该方法与 nonfairTryAcquire(int acquires)比较,唯一不同的置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;final void lock(){
    acquire(1);}protected final boolean tryAcquire(int acquires){
    final Thread current = Thread.currentThread();int c = getState();//获取锁,读取volatile变量stateif (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;}
}

释放锁的过程和非公平锁一样

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);//释放锁,写volatile变量statereturn free;
}

4.公平锁和非公平锁总结?

ReentrantLock 是 Java 并发包提供的一种重入锁,它可以作为公平锁或非公平锁来使用。下面对 ReentrantLock 的公平锁和非公平锁进行总结:

  1. 公平锁(Fair Lock):
    • 特点:公平锁遵循"先到先得"的原则,即线程按照请求锁的顺序依次获取锁。当有多个线程竞争锁时,等待时间最长的线程会被优先获取锁。
    • 实现:公平锁的实现需要维护一个等待队列,当线程请求锁时,如果锁已被其他线程占用,则该线程会进入等待队列,按照 FIFO 的顺序等待。
    • 优点:公平锁能够避免饥饿现象,所有线程都有机会获得锁,公平性比较高。
    • 缺点:由于涉及到频繁的线程切换和调度,可能会导致性能下降。
  2. 非公平锁(Non-Fair Lock):
    • 特点:非公平锁是一种抢占式的锁策略,线程在请求锁时,不管是否有其他线程在等待,它都会直接尝试获取锁,不会考虑等待队列中的顺序。
    • 实现:非公平锁的实现不维护等待队列,当线程请求锁时,直接尝试获取锁,如果锁被其他线程占用,则进入自旋等待或阻塞状态。
    • 优点:非公平锁由于省去了等待队列的维护和线程调度的开销,通常比公平锁具有更高的吞吐量和更低的延迟。
    • 缺点:非公平锁可能会导致某些线程长时间无法获取锁,产生线程饥饿现象,不具备公平性。

5.为什么非公平锁会造成线程饥饿?

首先说下公平锁.假设目前 AQS 的同步队列中有 A B C 三个线程.线程 A 排在最前边.当线程 A 获取锁的同时,线程 D 也要获取锁.此时 D 线程先会通过 tryAcquire 方法判断是否自己是同步队列的头结点,如果不是,则乖乖的去同步队列中等待.线程 A 处于无竞争状态下获取锁.因此说公平锁完全按照线程先后进行 FIFO 的获取锁。非公平锁不必唤醒所有线程,cpu 开销小.

  1. ReentrantLock:lock()。

  2. FairSync:lock()。

  3. AbstractQueuedSynchronizer:acquire(int arg)。

  4. ReentrantLock:tryAcquire(int acquires)

再说非公平锁.假设目前 AQS 的同步队列中有 A B C 三个线程.线程 A 排在最前边.当线程 A 获取锁的同时,线程 D 也要获取锁.此时 D 线程直接进行争夺,虽说 D 是后来的,但是作为非公平锁,会直接进行 cas 的竞争.如果竞争成功,线程 A 继续作为头结点,等待 D 线程释放锁,参与下一轮竞争.如果竞争失败,线程 D 也需要乖乖的到同步队列中排队。从这段话我们可以看到,如果线程 A 一直竞争不到锁,那么就会一直留在同步队列中等待,造成线程饥饿,没事儿可干。

使用非公平锁时,加锁方法 lock()调用轨迹如下。

  1. ReentrantLock:lock()。
  2. NonfairSync:lock()。
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。

6.ReentrantLock 使用和理解?

在 ReentrantLock 中,调用 lock()方法获取锁;调用 unlock()方法释放锁。

注意:lock()是在 try 的外面,为了防止获取锁失败,还去释放锁.

ReentrantLock 的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer(本文简称之之为 AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维护同步状态,这个 volatile 变量是 ReentrantLock 内存语义实现的关键。

ReentrantLock 默认是非公平锁,如果需要公平锁,参数传入 true

image-20220425184350325

7.什么是读写锁?

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

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

ReentrantReadWriteLock 作为 ReadWriteLock 的子类.ReadWriteLock 仅定义了获取读锁和写锁的两个方法,即 readLock()方法和 writeLock()方法

public interface ReadWriteLock {
    /** * Returns the lock used for reading *@return the lock used for reading */Lock readLock();/** * Returns the lock used for writing *@return the lock used for writing */Lock writeLock();
}

而其实现 ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法以及描述如下所示。

方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如,仅一个线程,它连续获取(重进入)了 n 次读锁,那么占据读锁的线程数是 1.但该方法返回 n
int getReadHoldCount() 返回当前线程获取读锁的次数。该方法在 Java6 中加入到 ReentrantReadWriteLock 中,使用 ThreadLocal 保存当前线程获取的的次数,这也使得 Java6 的实现变得更加复杂
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

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

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();static Lock r = rwl.readLock();static Lock w = rwl.writeLock();//获取一个key对应的valuepublic static final Object get(String key){
    r.lock();try {
    return map.get(key);} finally {
    r.unlock();}}//设置key对应的value,并返回旧的valuepublic static final Object put(String key, Object value){
    w.lock();try {
    return map.put(key, value);} finally {
    r.unlock();}return null;}//清空所有的内容public static final void clear(){
    w.lock();try {
    map.clear();} finally {
    w.unlock();}}
}

8.ReentrantReadWriteLock 原理?

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态 state.回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写,划分方式如下图所示:

image-20220415163159873

9.ReentrantReadWriteLock 写锁的获取与释放?

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,**读锁已经被获取(读状态不为 0)**或者当前线程不是已经获取写锁的线程(意思是非重入状态),则当前线程进入等待状态:

protected final boolean tryAcquire(int acquires){
    Thread current = Thread.currentThread();int c = getState();//读锁和写锁的总体状态int w = exclusiveCount(c);//写锁状态if (c != 0){
    //(Note: if c != 0 and w == 0 then shared count != 0)//存在读锁或者当前获取线程不是已经获取写锁的线程if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires)> MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock()||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}

从代码上看,如果 c!= 0,w=0。说明 r!=0。则 return false,当前线程进入同步队列等待。否则进行加锁,更改 state 的值,如果 c==0,这说明读写锁都没有。则进行判断是否写线程需要 block 或者进行更新同步状态失败。否则设置当前线程为 owner 线程,获取锁成功。写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为 0 时表示写锁已被释放.

ReentrantReadWriteLock是 Java 并发包提供的读写锁,用于实现读写分离的并发控制。在ReentrantReadWriteLock中,有一个名为"c"的整型变量,用于表示读写状态。

10.读写锁 c 值的含义

c 的值及其含义如下:

  1. c = 0:
    • 表示当前没有任何线程持有读锁或写锁。即没有线程正在读取或写入共享资源。
  2. c > 0:
    • 表示当前有一个或多个线程持有读锁。c 的值等于持有读锁的线程数量。
  3. c = -1:
    • 表示当前有一个线程持有写锁。读写锁中只能有一个线程持有写锁,因此 c 的值只会是 0 或-1。

读写锁允许多个线程同时获取读锁,只要没有线程持有写锁,而写锁是独占的,只能有一个线程获取写锁。读锁和写锁之间是互斥的,即当有线程持有写锁时,其他线程无法获取读锁或写锁,直到写锁被释放。

11.ReentrantReadWriteLock 读锁的获取与释放?

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为 0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。类似于修改文件的时候不能打开查看.

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

在 tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。

12.ReentrantReadWriteLock 的锁降级问题?

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

为什么先获取读锁才能释放写锁:

因为 state 是包含读写锁的状态,如果先释放写锁,则获取读锁的时候,写锁被其他线程获取,不能再次获取到读锁.而拥有写锁的时候可以同时获取读锁,所以需要先获取读锁才能释放写锁.原理上来说只能有一个线程持有写锁.

RentrantReadWriteLock 为什么不支持锁升级:

读锁升级为写锁。首先自己有读锁,之后拿到写锁。如果有两个读锁同时升级为写锁。那么只有一个能升级成功。但是这两个线程同时拥有读锁。其中一个线程还一直在申请写锁。其他线程获取到读锁时,不能获取写锁.这就会造成死锁。但是锁降级是可以的。因为写锁只有一个线程占有。

13.LockSupport 工具是什么?

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

方法名称 描述
void park() 阻塞当前线程,如果调用 unpar ik(Thread thread)方法或者当前线程被中断,才能从 park()方法返回
void parkNanos(long nanos) 阻塞当前线程,最长不超过 na anos 纳秒,返回条件在 park(的基础上增加了超时返回
void parkUntil(long deadline) 阻塞当前线程,直到 deadline 日时间(从 1970 年开始到 deadline 时间的毫秒数)
void unpark(Thread thread) 唤醒处于阻塞状态的线程 thread

先唤醒线程,再阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。

LockSupport 就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

  • 当调用park()方法时,会将_counter 置为 0,同时判断前值,小于 1 说明前面被unpark过,则直接退出,否则将使该线程阻塞。
  • 当调用unpark()方法时,会将_counter 置为 1,同时判断前值,小于 1 会进行线程唤醒,否则直接退出。
    形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有 1 个。当调用 park 方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而 unpark 则相反,它会增加一个凭证,但凭证最多只能有 1 个。
  • 为什么可以先唤醒线程后阻塞线程?
    因为 unpark 获得了一个凭证,之后调用 park 因为有凭证消费,故不会阻塞。
  • 为什么唤醒两次后阻塞两次会阻塞线程。
    因为凭证的数量最多为 1,连续调用两次 unpark 和调用一次 unpark 效果一样,只会增加一个凭证;而调用两次 park 却需要消费两个凭证。

14.parkNanos 中 blocker 含义

parkNanos(long nanos)和 parkNanos(Object blocker,long nanos)异同?

对比 parkNanos(long nanos)方法和 parkNanos(Object blocker,long nanos)方法在使用场景上有什么不同?

LockSupport 增加了 3 个方法

  • park(Object blocker)
  • parkNanos(Object blocker,long nanos)
  • parkUntil(Object blocker,long deadline)

用于实现阻塞当前线程的功能,其中参数 blocker 是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。

image-20220418155623994

从右图的线程 dump 结果可以看出,代码片段的内容都是阻塞当前线程 10 秒,但从线程 dump 结果可以看出,有阻塞对象的 parkNanos 方法能够传递给开发人员更多的现场信息.这是由于在 Java 5 之前,当线程阻塞(使用 synchronized 关键字)在一个对象上时,通过线程 dump 能够查看到该线程的阻塞对象,方便问题定位,而 Java 5 推出的 Lock 等并发工具时却遗漏了这一点,致使在线程 dump 时无法提供阻塞对象的信息.因此,在 Java 6 中,LockSupport 新增了上述 3 个含有阻塞对象的 park 方法,用以替代原有的 park 方法。

15.park unpark 原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter _cond _mutex

打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)

  • 调用 park 就是要看需不需要停下来歇息

    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足

    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进

因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

16.Synchronized 和 Lock 区别?

synchronized Lock
原始构成 sync是 JVM 层面的,底层通过monitorentermonitorexit来实现的 Lock是 JDK API 层面的
使用方法 sync不需要手动释放锁 Lock需要手动释放
是否可中断 sync不可中断,除非抛出异常或者正常运行完成 Lock是可中断的,通过调用interrupt()方法
是否为公平锁 sync只能是非公平锁 Lock既能是公平锁,又能是非公平锁
绑定多个条件 sync不能,只能随机唤醒 Lock可以通过Condition来绑定多个条件,精确唤醒

synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

Lock 接口提供的 synchronized 关键字不具备的主要特性

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 与 synchronized 不同,获取到锁的线程能能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

Lock 的 API:

方法名称 描述
void lock() 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
void lockInterruptibly()) throws InterruptedException 可中断地获取锁,和 lock0 方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回 true,否则返回 false
boolean tryLock(long time.TimeUnit unit)throws InterruptedException 超时的获取锁,当前线程在以下 3 种情况下会返回:
① 当前线程在超时时间内获得了锁
② 当前线程在超时时间内被中断
③ 超时时间结束,返回 false
void unlock() 释放锁
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的 waitO 方法,而调用后,当前线程将释放锁

synchronized和lock区别:

  1. synchronized 是一个关键词,lock 是一个接口
  2. synchronized 是隐式调用,lock 是显示调用
  3. synchronized 可以作用到方法,lock 只能作用到代码块
  4. Lock 支持非阻塞式加锁,
  5. Lock 支持超时加锁
  6. Lock 支持可中断加锁
  7. synchronized 采用的 monitor 监视器,lock 采用的是 AQS
  8. synchronized 只有 2 个同步队列,一个等待队列,lock 可以有多个等待队列,一个同步队列
  9. synchronized 只支持非公平锁,lock 支持公平锁和非公平锁
  10. synchronized 与 wait 和 notify 和 notifyAll 配合使用,lock 和 condition 和 await 和 signal 和 signalAll 配合使用
  11. lock 是模板方法模式,可以自定义实现锁机制
  12. lock 有读写锁,可以支持同时读读

17.Lock 的 Condition 接口作用?

任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、 wait(long timeout)、 notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式.Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。

ConditionObject 是 AQS 的一个内部类,Condition 操作需要获取同步状态.节点类型和 AQS 中的节点是一样的.

image-20220609194933608

通过对比 Object 的监视器方法和 Condition 接口,可以更详细地了解 Condition 的特性

Object 的监视器方法与 Condition 接口的对比

对比项 Obiect Monitor Methods Condition
前置条件 获取对象的锁 调用 Locklock 获取锁
调用 LocknewCondition 获取 Condition 对象
调用方式 直接调用
如:object.wait()
直接调用
如:condition.await()
等待队列个数 一个 多个
当前线程释放锁并进人等待状态 支持 支持
当前线程释放锁并进人等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来的某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

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

public class Juc_15_keyword_Condition {
    final Lock lock = new ReentrantLock();//定义锁对象//通过锁对象获取Condition实例final Condition notFull = lock.newCondition(); //用于控制put操作final Condition notEmpty = lock.newCondition(); //用于控制take操作final Object[] items = new Object[100];//缓冲队列,初始容量100int putptr, takeptr, count;//分别记录put,take当前的索引,count用于记录当前item的个数/*** 往缓冲队列中添加数据*/public void put(Object x) throws InterruptedException {
    //上锁,作用和synchronized一样,保证代码同一时刻只有一个线程可以操作,也保证了和take方法的互斥lock.lock();try {
    while (count == items.length) {
    notFull.await();//如果队列满了,则put线程等待被唤醒}items[putptr] = x; //队列未满,则添加数据if (++putptr == items.length) putptr = 0;//添完后,如果记录读数据的索引到了最后一个位置,则重置为0++count;//item总数自增notEmpty.signal();//唤醒take线程取数据} finally {
    lock.unlock();//put操作完后,释放锁.}}/*** 从缓冲队列中取数据*/public Object take() throws InterruptedException {
    lock.lock();try {
    while (count == 0) {
    notEmpty.await();//如果队列空了,则take线程等待被唤醒}Object x = items[takeptr]; //队列未空,则取数据if (++takeptr == items.length) takeptr = 0;//取完后,如果记录取数据的索引到了最后一个位置,则重置为0--count;//item总数自减notFull.signal();//唤醒put线程添加数据return x;//返回取得的数据} finally {
    lock.unlock();//take操作完后,释放锁对象}}
}

当调用 await()方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal()方法,通知当前线程后,当前线程才从 await()方法返回,并且在返回前已经获取了锁。一个线程进入等待状态----释放锁----线程被唤醒----线程获取锁----等待状态结束。

Condition 相关的方法,Condition 其实就是一个队列,好理解

方法 描述
void await() 造成当前线程在接到信号或被中断之前一直处于等待状态。
boolean await(long time, TimeUnit unit) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout) 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
void awaitUninterruptibly() 造成当前线程在接到信号之前一直处于等待状态。
boolean awaitUntil(Date deadline) 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
void signal() 唤醒一个等待线程。
void signalAll() 唤醒所有等待线程。

18.Condition 的实现原理?

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

如果一个线程调用了 Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。

一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter).当前线程调用 Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列

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

image-20220418160002688

在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock (更确切地说是同步器)拥有一个同步队列和多个等待队列

image-20220418160040057

如果从队列(同步队列和等待队列)的角度看 await()方法,当调用 await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。

//AbstractQueuedLongSynchronizer中的内部类ConditionObject,实现了Condition接口
public final void signal() {
    if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);
}

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

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

觉得有用的话点个赞 ?? 呗。
??????本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!???

???如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!? ? ?

???Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!???

img