您的位置:首页 > 其它

ReentrantLock可重入锁—源码详解

2021-12-23 10:38 155 查看

开始这篇博客之前,博主默认大家都是看过AQS源码的~什么居然没看过🤬猛戳下方👇👇👇 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量

介绍

ReentrantLock
是可重入锁,是JUC提供的一种最常用的锁。“可重入”的意思就是:同一个线程可以无条件地反复获得已经持有的锁

ReentrantLock
公平锁非公平锁两种模式,底层使用的正是
AbstractQueuedSynchronizer
这个伟大的并发工具

ReentrantLock
的结构如下图所示:

ReentrantLock
实现了
Lock
接口,该接口定义了一个锁应该具备的基本功能,即加锁、解锁、创建条件变量等功能。源码如下:

public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock();

Condition newCondition();
}

使用

ReentrantLock
,主要使用
lock
unlock
方法,也会用到
newCondition
来创建条件变量,实现一些条件同步功能

回到上面的结构图,可以看到,

ReentrantLock
的功能主要是借助其内部类
Sync
来实现,而
Sync
类是继承了
AbstractQueuedSynchronizer
,并衍生出两个子类
FairSync
NonfairSync
,分别对应公平锁和非公平锁两种模式。实际应用中,一般非公平锁的效率要高于公平锁。具体原因见最后一节“相关面试题"

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15719681.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

Sync

ReentrantLock
中的
sync
域是是一个
Sync
类对象,
ReentrantLock
使用
sync
来实现主要的功能。
Sync
类是
ReentrantLock
的内部类:

/** Synchronizer providing all implementation mechanics */
private final Sync sync;

Sync
类继承了AQS,使用AQS的
state
作为锁的重入数,其源码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

// 用于辅助ReentrantLock执行Lock接口的lock方法,所以定义了一个同名lock方法
abstract void lock();

// 执行非公平的tryLock,是非公平锁子类NonFairSync实现的tryAcquire方法的主要逻辑,也是ReentrantLock的tryLock的主要逻辑?
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;
}

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

// 为了支持使用条件变量,Sync实现了AQS中的isHeldExclusively,并提供了newCondition方法创建条件变量

protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}

final ConditionObject newCondition() {
return new ConditionObject();
}

// ReentrantLock的三个同名方法,都委托给了下面这三个方法,用于获取锁的一些信息

final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}

final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}

final boolean isLocked() {
return getState() != 0;
}

/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}

总结一下,

Sync
类有以下几个作用:

  • 定义抽象
    lock
    方法:
    Sync
    定义了
    lock
    这一个抽象方法,强迫两个子类去实现,而
    ReentrantLock
    lock
    方法就可以直接委托
    Sync
    lock
    方法去执行
  • 非公平模式的尝试获取锁:
    Sync
    提供了
    nonfairTryAcquire
    方法,提供非公平尝试获取锁的方法。不仅非公平子类
    NonfairSync
    实现
    tryAcquire
    方法需要委托给
    nonfairTryAcquire
    来处理,而且
    ReentrantLock
    中的
    tryLock
    方法也会委托给它来处理
  • 尝试释放锁:
    Sync
    实现了AQS中的
    tryRelease
    方法,因为不管是公平模式还是非公平模式,释放锁的逻辑都是相同的,因此在
    Sync
    这一层就提供了具体的实现,而没有下放给子类来实现
  • 条件变量的支持:
    Sync
    实现了AQS中的
    isHeldExclusively
    方法(该方法会被AQS中的
    ConditionObject
    signal
    方法调用),并提供了
    newCondition
    方法创建条件变量
  • 获取锁信息的方法:
    Sync
    提供了
    getOwner
    getHoldCount
    isLocked
    三个方法用于获取锁的信息,外围类
    ReentrantLock
    的三个同名方法会委托这三个方法来执行

获取锁

要利用AQS实现获取锁的功能,需要实现

tryAcquire
方法。但是由于公平模式和非公平模式下获取锁的逻辑不同,因此
tryAcquire
交给两个子类去实现,
Sync
并不实现

但是对于非公平模式的获取锁,

NonFairSync
子类实现的
tryAcquire
方法实际上委托了
Sync
类的
nonfairTryAcquire
方法来处理。
nonfairTryAcquire
的源码分析放在后面的非公平模式去讲解

释放锁

要利用AQS实现释放锁的功能,需要实现

tryRelease
方法。不同于获取锁,对于公平模式和非公平模式来说,释放锁的逻辑是相同的,因此
tryRelease
的实现直接交给
Sync
这一层来实现,而没有下放给子类来实现

tryRelease
是尝试释放资源,而在
ReentrantLock
中的语义环境下就是尝试释放锁。其源码如下:

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;		// 返回锁是否空闲,如果空闲则为true
}

tryRelease
releases
参数说明: 由于每次只释放一个锁,所以调用
lock
释放锁时
tryRelease
releases
参数恒为1 但是
ReentrantLock
支持条件变量,条件变量的
await
方法也会调用
tryRelease
方法一次性释放所有的锁资源,此时
tryRelease
的参数
releases
不一定为1

AQS中的

release
方法会调用
tryRelease
方法并接收其返回值,如下:

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

如果

tryRelease
返回true,说明锁为空闲,那么就需要唤醒等待获取锁而阻塞,且等待最久的线程,让它来获取锁。因此
release
会唤醒同步队列的队首线程。如果锁不是空闲,就不需要唤醒任何线程

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15719681.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

非公平锁

非公平锁是借助

Sync
和其子类
NonfairSync
来实现的。
NonfairSync
实现了
Sync
定义的
lock
抽象方法,以及实现了AQS中的
tryAcquire
方法以获取锁。源码如下:

static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

// 实现了Sync中定义的lock方法
final void lock() {
// 上来就CAS,一点也不客气————非公平性
if (compareAndSetState(0, 1))		// 如果state为0,说明锁是空闲的,直接CAS获取
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);	// 如果锁不空闲,或者CAS竞争失败,就调用acquire去获取1个锁,可能会被阻塞
}

// 非公平竞争锁,实际上委托Sync.nonfairTryAcquire来执行
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

lock
方法先直接CAS修改
state
,如果锁空闲且修改成功,则说明获取到了锁,这里也体现出非公平性,因为它不会谦让已经在同步队列中等待的线程 如果锁非空闲或者竞争失败,则会调用
acquire
方法。
acquire
会调用非公平锁实现的
tryAcquire
方法,再次进行竞争,可能直接获取到锁,也可能再次失败,进入同步队列阻塞等待,这里同样体现了非公平性

非公平锁实现的

tryAcquire
实际委托
Sync.nonfairTryAcquire
方法来执行,该方法源码如下:

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果锁空闲,那么直接CAS修改————非公平性
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);		// 直接设置不用CAS
return true;
}
return false;				// 获取失败
}

第一个

if
体现了该方法的非公平性,获取锁的线程不会给同步队列的队首线程“谦让”,而是直接上去CAS竞争,如果竞争成功,将比队首线程更先获得锁,这体现了不公平性

ReentrantLock
默认创建出来的是非公平锁,因为非公平锁的效率一般要高于公平锁:

public ReentrantLock() {
sync = new NonfairSync();
}
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15719681.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

公平锁

公平锁是借助

Sync
和其子类
FairSync
来实现的。
FairSync
实现了
Sync
定义的
lock
抽象方法,以及实现了AQS中的
tryAcquire
方法以获取锁。源码如下:

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();
if (c == 0) {
// 先检查有无排队等待的线程,如果有就不去CAS竞争——公平性
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;
}
}

lock
方法直接调用
acquire
方法获取锁,而
acquire
会调用非公平锁实现的
tryAcquire
方法,而
tryAcquire
也遵循公平性,因此该
lock
方法整体上就是公平的

tryAcquire
方法会检查锁是否空闲,如果空闲,也不会立即去CAS争夺,而是调用AQS的
hasQueuedPredecessors
方法检查是否有线程在同步队列中等待,如果没有才会CAS竞争。如果有就说明不能竞争,返回false

AQS中的

hasQueuedPredecessors
方法会检查是否有线程在同步队列中等待,源码如下:

public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head等于tail,说明是空队列
// 如果队首的thread域不是当前线程,说明有别的线程先于当前线程等待获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

要使用公平模式的锁,需要将`ReentrantLock`的构造参数`fair`设为true。如果是false或不设置,则创建的都是非公平模式的锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15719681.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

Lock接口的实现

ReentrantLock
实现了
Lock
接口的所有方法,如下:

public void lock() {
sync.lock();
}

public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public void unlock() {
sync.release(1);
}

public Condition newCondition() {
return sync.newCondition();
}

可以看到,

ReentrantLock
实现的所有
Lock
方法其实都是委托给了
Sync
(AQS)来执行

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15719681.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

相关面试题

ReentrantLock
是如何实现可重入的

无论是公平锁还是非公平锁,获取锁调用

tryAcquire
方法时,获取成功后都会设置当前持有锁的线程是自己。如果再次获取该锁,当发现锁已经被持有时,会判断持有锁的线程是否是自己,如果是就可以不用竞争而直接获取锁

简述非公平锁和公平锁之间的区别

  • 从定义角度来说:

    公平锁:获取锁的顺序和请求锁的顺序是一致的,即谁申请得早(等待得久),谁就最先获取锁
  • 非公平锁:竞争锁时,等待时间最长的线程和刚刚过来竞争锁(不在阻塞同步队列中)的线程都有可能获取锁,CPU时间片轮询到哪个线程,哪个就能获得锁
  • 从源码角度来说: 当锁被占用时,请求锁的所有线程都会按照FIFO的顺序在同步队列中阻塞等待。在锁被释放的时候,如果是非公平锁,则队首线程和刚刚过来请求锁而不在阻塞队列中的线程,都可能获得锁。如果是公平锁,就一定是队首线程获得锁,刚刚过来请求锁得线程会被加入同步队列阻塞等待

  • 从效率上来说:公平锁效率低于非公平锁,主要是两方面的开销

      代码执行上的开销:公平模式下会多执行一个方法,该方法用于判断是否有其他线程正在同步队列中等待
    • 系统层面的开销:公平模式下,队首线程是阻塞的,所以必须先将队首线程唤醒,这涉及到操作系统上下文切换的操作,开销较大。而在非公平模式下,可能是刚刚过来请求锁的线程获得锁,而该线程已经是唤醒状态,不需要上下文切换

    为什么

    ReentrantLock.lock
    方法不能被其他线程中断

    因为

    lock
    方法调用的是AQS中的
    acquire
    方法,该方法忽略中断。而
    acquire
    方法又会调用
    acquireQueued
    方法,该方法执行过程中如果有其他线程中断了当前线程,只会将中断记录下来,不会响应中断。如果锁已经被获取,那么该线程需要被阻塞,阻塞调用的是
    LockSupport.unpark
    方法,该方法接收到中断信号后,不会抛出中断异常,而是返回。返回之后又会进入
    acquireQueued
    的循环,如果不是队首,就重新被阻塞。所以整个过程都不会被其他线程中断,只会将中断记录下来

    ReentrantLock
    synchronized
    之间的相同和不同点

    相同点: 它们都是通过加锁实现同步,而且都是阻塞式同步,而不是非阻塞式(自旋锁),即当一个线程获取锁后,其他线程再请求锁就会失败而被阻塞,等到锁释放才有机会被唤醒

    不同点

    • ReentrantLock
      :是Java 5之后提供的API层面的互斥锁;需要
      lock
      unlock
      配合
      try
      finally
      使用;支持定时获取锁功能;支持可中断的加锁方法
      lockInterruptibly
      ,在等待获取锁时响应中断,会抛出中断异常
    • synchronized
      :是Java语言的关键字,通过JVM实现;使用便捷;不支持定时获取锁功能;
      synchronized
      在等待获取锁时不响应中断,不抛出中断异常,只记录中断状态

    公平锁和非公平锁之间最大的区别在哪里(一句话版本)?

    • 公平锁:先到临界区的线程一定会比后到的先获得锁
    • 非公平锁:先到临界区的线程不一定比后到的先获得锁

    synchronized
    加锁是公平锁还是非公平锁?

    synchronized
    是非公平锁

    如果使用

    synchronized
    锁,在线程到达临界区时就直接CAS尝试获取锁,如果失败则升级为轻量级锁,再不断CAS请求锁。当CAS失败到达一定次数之后,升级为重量级锁,放入monitor对象的队列中阻塞等待。而且入队之前也会先尝试获取锁,获取不到才进入等待队列

    因此,线程获取

    synchronized
    锁都不会关心有没有其他线程之前获取过,所以
    synchronized
    是非公平锁

    为什么要设置前驱节点的状态为

    SIGNAL

    为了表示前驱节点的后继节点对应的线程需要被唤醒,就这么简单

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: