一. 概述

ReentrantLock底层基于AQS,其构造方法返回的就是NonfaireSyncfaireSync
两种同步器都继承自Sync,Sync又继承自AQS!在这里插入图片描述

在这里插入图片描述
new ReentrantLock时,返回的就是同步器,默认是非公平的;

基本语法
创建reentranLock对象:
在这里插入图片描述
上锁:
在这里插入图片描述

ReentrantLock的lock()方法用来让执行的线程获取锁;
try放的是临界区的代码,finally为了不管是否出现异常,都去释放ReentrantLock对象的锁;
ReentrantLock.lock 放在try 外面和里面都可以,《阿里手册》说放try的外面;
保证lock和unlock是成对出现! 最后一定要解锁!

补充
不要将获取锁写在 try 语句块中!因为一旦 try 抛出异常未获取到锁,会导致finallly语句块中无故释放!

二. 分析

非公平锁、可重入锁、可中断锁、条件变量锁 都是基于底层 state属性(volatile修饰)和阻塞队列来配合实现的;

1. 非公平锁的加锁

1. 1 加锁成功

调用 lock() 方法,使用CAS机制尝试将AQS底层 volatile修饰的 state 属性从0改为1,并将当前线程设为exclusiveOwnerThread即占有锁;
若CAS修改失败即出现竞争,则进入 acquire() 方法;
在这里插入图片描述

1. 2 加锁失败

  1. 当锁已经被占用,共享内存中的 state属性是1,compareAndSet(0,1) 会失败,CAS更改state属性失败,会进入 acquire() 方法;
  2. 进入else中 AQS的 acquire() 方法;
  3. 调用 tryAcquire() 再次尝试,如果还失败,再执行acquireQueued()
    如果是第一次会创造一个哨兵节点Node,然后再创造一个Node节点对象关联当前线程,并把节点添加到FIFO等待队列(双向链表)中;
  4. 节点进入队列后,会再次尝试加锁,失败后会被 park 阻塞,节点前驱节点的 waitStatus 会被置为 -1,-1表示可以去唤醒后继节点;

AQS中的acquire()方法:
在这里插入图片描述
假设多个线程多竞争失败,进入了FIFO等待队列:
在这里插入图片描述

1. 非公平锁的释放

1.1 unlock() 释放锁+唤醒线程

  1. finally{ } 中调用 unlock() 方法,底层调用了 release() 方法,使用 tryRelease()state 置为0,并将exclusiveOwnerThread置为null,即释放锁;
  2. tryRelease() 返回true后,判断head哨兵节点是否为null ,如果否且哨兵节点的waitStatus 也不会0(为-1),则哨兵节点的后继节点被 unParkSuccessor 唤醒 !线程就恢复了,就有机会去竞争锁;原来的节点就从队列中断开;

在这里插入图片描述

在这里插入图片描述

1.2 竞争失败

如果此时又来了一个新的线程,那么就会和队列中要被释放的线程一起竞争锁,
如果新的线程获取到了锁成为了exclusiveOwnerThread,则队列中的线程解锁失败,
在for中循环tryAcquire返回false,又被阻塞住;
在这里插入图片描述

2. 可重入锁

2.1 加锁(state自增)

加锁最终调用nonFairTryAcquire( 1 ), 先获取state值
①如果state=0即无锁,并使用CAS将state改为1,并将当前线程设为exclusiveOwnerThread,即占有锁;
②如果state>0,则判断当前线程是否等于exclusiveOwnerThread,如果是即锁重入,就让state值++;
在这里插入图片描述

2.2 解锁(state自减)

1.调用 tryRelease(1) 方法,使state减1
2.判断 如果减1之后state为0,则将exclusiveOwnerThread设为null,即释放锁,并返回true;
如果state减1之后不为0,则返回fasle;
在这里插入图片描述

:在主线程调用m1进行加锁,m1中调用m2进行加锁;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
打印日志:
在这里插入图片描述

3. 可中断机制(跳出阻塞)

加锁时执行的是 reentrantlock对象.lockInterrupitly() 方法,而不是lock()方法!
执行 线程.interrupt() 方法时就会被中断;

【不中断的模式】下,即使它被打断,会仍然驻留在AQS的等待队列中被阻塞;
【可打断模式】下,如果没有获得锁,进入 doAcquireInterruptibly() 方法而不是acquireQueued(),在线程被park阻塞的状态时,如果被interrupt则会抛出异常,线程就不会在AQS中的等待队列继续等待;
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
结果:
在这里插入图片描述

4. 公平锁(加锁时 看排队位置)

reentrantlock默认也是不公平,但是可以设置为公平锁;参数可以设置boolean,true即公平;
公平锁即先入先得
公平锁会降低并发度;
在这里插入图片描述
在这里插入图片描述

非公平锁:
加锁时,谁最先调用nonFairAcquire(),如果state=0,则直接用CAS机制将state改为1,设置Owner线程,不去检查AQS的等待队列;

公平锁:
1.最终调用 tryAcqure() 方法,会用hasQueuePredecessors()方法检查当前线程在AQS等待队列中的位置
2.hasQueuePredecessors即 检查当前这个线程在AQS队列中的位置是否处于head.next(最优先的位置,head是哨兵节点不关联线程),
如果线程是 就使用CAS机制将state设为1,并设置当前线程为exclusiveOwnerThread
不是就不会往下执行,不会用当前线程占有锁;

在这里插入图片描述

在这里插入图片描述

5. 锁超时

通过设置超时时间来避免线程无限制等待下去,以防止死锁;
reentrantlock.trylock(超时时间)会返回布尔类型,在超时时间内都去尝试获取锁;
reentrantlock.trylock() 不带参数即尝试获取锁,获取不到锁立刻结束等待


在这里插入图片描述
此时主线程一直占有锁,t1尝试获取锁,等待了1秒,再返回false;
在这里插入图片描述

6. 条件变量

ConditionObject条件变量的等待队列 类似Moniter的waitset队列 !
每个条件变量其实就对应着一个等待队列(双向链表),实现类是AQS中的ConditionObject

6.1 await() 加入条件变量队列+fullRelease

前提:线程占有锁,state=1,当前线程-0是Owner线程即占用着锁,调用 await()

  1. 执行await()方法,进入addConditionWaiter()方法,会 创建Node节点关联当前线程-0,并将节点加入到 ConditionObject条件变量的队列 中去,然后将节点的waitStatus状态设为 -2 ,并且执行 park() 阻塞该线程;
    (-2在条件变量里是等待的状态);
    (如果队列为空,就将节点作为firstNode,如果不为空就加到队列的尾部);
  2. 执行 fullyRelease():然后将当前这个线程占用的锁都 释放 掉,因为可能有锁重入;
  3. 既然是释放锁,就会在同步器的阻塞队列中head的后继节点 唤醒,即unparkSuccesser(),后面的线程就能去竞争锁;

在这里插入图片描述
在这里插入图片描述

6.2 singal()

只有Owner线程才有资格唤醒条件变量中的线程

假设Thread-1 唤醒Thread-0:

  1. 判断 当前线程是不是锁的持有者,如果不是则报错;
  2. 获取ConditionObject条件变量队列的头元素firstWaiter,如果不为空,就调用doSignale()将其 移除 等待队列,
    然后再将这个节点 转移 到同步器的阻塞队列中并将waitStatues改为0(因为阻塞队列最后一个节点的waitStatues是0), 这样线程就有机会抢占锁了;
    如果遇到中断等,就取消转移,将等待队列的下一个节点来唤醒;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

synchronized和ReentrantLock的区别 ?

ReentrantLock底层基于AQS实现,用volatile修饰的state属性和阻塞队列来实现线程的同步执行,从而达到线程安全性的目的;

ReentrantLock是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活(有更多的方法),能解决synchronized关键字在一些并发场景下不适用的问题;

  1. synchronized 属于 JDK 层面的, 而 ReentrantLock 依赖于 juc并发包 的API;

  2. ReentrantLock可以指定是 公平锁 还是非公平锁。⽽synchronized只能是 非公平锁

  3. 可以设置 中断:synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待;

  4. 可以设置 超时时间:synchronized一旦阻塞会进入EntryList,一直等待不会放弃;
    而ReentrantLock可以设置一个超时时间,超过一定时间可以放弃争抢锁;

  5. 可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐