AQS源码解读(三)——ReentrantLock原理详解(Sync、NonfairSync

  • 文末有彩蛋

一、前言


ASQ实现的是一套通用的模板,并不能完全直接应用于实际并发生产中,ReentrantLock就是根据AQS实现的互斥可重入锁。ReentrantLocksynchronized类似,互斥、阻塞、可重入,不同之处在于synchronized基于Java语法层面实现隐式加锁和释放锁,ReentrantLock基于API层面实现显式加锁和释放锁。

ReentrantLock实现了接口Lock,相较于synchronized,对锁的操作更灵活可控,同时根据不同的环境实现了公平锁和非公平锁。


二、ReentrantLock基本结构


ReentrantLock本身没有什么代码逻辑,实现的方法调用的是Sync的模板方法,所以锁机制的所有实现逻辑都在ReentrantLock的内部类Sync中。Sync继承自AbstractQueuedSynchronizer,其还有两个子类NonfairSyncFairSync,分别具体实现非公平和公平情况下加锁的逻辑。如下是ReentrantLock的类关系图:

ReentrantLock的类关系图

public class ReentrantLock implements Lock, java.io.Serializable {

private static final long serialVersionUID = 7373984872572414699L;

/** Synchronizer providing all implementation mechanics */

//sync同步器提供所有的实现机制

private final Sync sync;

/**

  • Base of synchronization control for this lock. Subclassed

  • into fair and nonfair versions below. Uses AQS state to

  • represent the number of holds on the lock.

*/

abstract static class Sync extends AbstractQueuedSynchronizer {

abstract void lock();

… …

}

static final class NonfairSync extends Sync {

final void lock() {}

… …

}

static final class FairSync extends Sync {

final void lock() {}

… …

}

… …

}


三、锁的公平性


ReentrantLock默认情况下提供非公平锁,同时也可以在实例化的时候指定锁公平性。

public ReentrantLock() {

sync = new NonfairSync();

}

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

何为公平和非公平,例如实际生活中的排队,新来的人按规矩排在队尾是公平,插队就是非公平。对于锁也是同样的道理,新来线程排在阻塞队列队尾是公平,一上来就抢锁是非公平。不同于现实生活,ReentrantLock默认非公平锁是为了减少线程间的切换,从而提高效率。


四、锁的获取与释放


ReentrantLock锁的获取分别实现了不可中断获取锁lock,尝试获取锁tryLock(获取成功返回true,获取失败返回false),可中断获取锁lockInterruptibly;锁的释放unlock很简单,直接调用的AQS的模板方法release,不需要区分公平性。

1、ReentrantLock#lock

ReentrantLock#lock调用了sync.lock()Synclock()是一个抽象函数,具体的实现在其子类NonfairSyncFairSync中。

public void lock() {

sync.lock();

}

abstract static class Sync extends AbstractQueuedSynchronizer {

/**

  • Performs {@link Lock#lock}. The main reason for subclassing

  • is to allow fast path for nonfair version.

*/

abstract void lock();

}

(1)ReentrantLock.NonfairSync

ReentrantLock.NonfairSync#lock实现了Sync中的抽象方法lock,其非公平性体现在,一上来就抢锁设置statecompareAndSetState(0, 1),如果抢锁成功就设置当前线程为持有独占锁的线程setExclusiveOwnerThread(Thread.currentThread())

static final class NonfairSync extends Sync {

private static final long serialVersionUID = 7316153563782823691L;

/**

  • Performs lock. Try immediate barge, backing up to normal

  • acquire on failure.

*/

final void lock() {

//非公平,直接抢锁

if (compareAndSetState(0, 1))

//抢成功,设置当前独占锁的线程为当前线程

setExclusiveOwnerThread(Thread.currentThread());

else

//没有抢到

acquire(1);

}

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

}

如果一上来抢锁失败则进入AQS的模板方法acquire(1)acquire(1)中还会再进行一次抢锁tryAcquire,抢锁失败才进入AQS队列操作acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。(acquire详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》

//AbstractQueuedSynchronizer#acquire

public final void acquire(int arg) {

//若没有抢到锁,则进入等待队列

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

//自己中断自己

selfInterrupt();

}

加锁的通用模板acquire()已经在AQS中实现,子类只需要实现tryAcquire即可。NonfairSync#tryAcquire调用了父类的Sync#nonfairTryAcquire。具体逻辑如下:

  • 查看当前state,是否有线程持有锁,没有state=0则继续抢锁。

  • 有线程持有锁,判断持有锁的线程是否是当前线程,是就重入。

//ReentrantLock.Sync#nonfairTryAcquire

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;

}

(2)ReentrantLock.FairSync

FairSyncNonfairSync的主要区别在于,FairSync不会一上来就抢锁,而是先判断队列中是否有其他线程在等待锁,没有再抢锁。

代码的模板逻辑和NonfairSync类似,区别在于FairSynctryAcquire的实现。

static final class FairSync extends Sync {

private static final long serialVersionUID = -3000897897090466540L;

final void lock() {

acquire(1);

}

/**

  • Fair version of tryAcquire. Don’t grant access unless

  • recursive call or no waiters or is first.

*/

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)

t

必看视频!获取2024年最新Java开发全套学习资料 备注Java

hrow new Error(“Maximum lock count exceeded”);

setState(nextc);

return true;

}

return false;

}

}

FairSync的公平性主要体现在tryAcquire判断当前没有线程持有锁时,不会立即去抢锁,而是判断AQS队列中是否有其他线程也在等待锁,没有才去抢锁。如果有线程正持有锁,判断是否是当前线程,是则重入。

public final boolean hasQueuedPredecessors() {

// The correctness of this depends on head being initialized

// before tail and on head.next being accurate if the current

// thread is first in queue.

Node t = tail; // Read fields in reverse initialization order

Node h = head;

Node s;

return h != t &&

((s = h.next) == null || s.thread != Thread.currentThread());

}

2、ReentrantLock#lockInterruptibly

lock是不可中断锁,lockInterruptibly是可中断锁,其直接调用了AQS的模板方法acquireInterruptibly

public void lockInterruptibly() throws InterruptedException {

sync.acquireInterruptibly(1);

}

acquireInterruptiblydoAcquireInterruptibly代码中分别响应中断抛出异常InterruptedException

acquireInterruptiblydoAcquireInterruptibly详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》

3、ReentrantLock#tryLock

tryLock尝试获取锁,调用的是sync.nonfairTryAcquire(1),不会涉及到AQS队列操作,获取锁成功返回true,失败返回false。

public boolean tryLock() {

return sync.nonfairTryAcquire(1);

}

tryLock还有一个重载方法,可传入一个超时时长timeout和一个时间单位TimeUnit,超时时长会被转为纳秒级。

public boolean tryLock(long timeout, TimeUnit unit)

throws InterruptedException {

return sync.tryAcquireNanos(1, unit.toNanos(timeout));

}

tryLock(long timeout, TimeUnit unit)直接调用了AQS的模板方法tryAcquireNanos,也具备了响应中断,超时获取锁的功能:

  1. 若一开始获取锁tryAcquire失败则进入AQS同步队列doAcquireNanos

  2. 进入同步队列后自旋1000纳秒,还没有获取锁且判断应该阻塞,则会阻塞一定时长。

  3. 超时时长到线程自动唤醒,再自旋还没获取锁,且判断超时则返回false。

tryAcquireNanos详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》

4、ReentrantLock#unlock

ReentrantLock释放锁的过程没有区分公平性,调用的是AQS的模板方法release(),其基本逻辑如下:

  1. 释放锁tryRelease

  2. 唤醒后继节点unparkSuccessor。(unparkSuccessor详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》

//ReentrantLock

public void unlock() {

sync.release(1);

}

//AbstractQueuedSynchronizer

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

//释放锁成功后唤醒head后继节点。

unparkSuccessor(h);

return true;

}

return false;

}

释放锁的逻辑由ReentrantLock实现,释放锁之后唤醒后继由AQS负责。释放锁的代码ReentrantLock.Sync#tryRelease基本逻辑如下:

  1. 判断持有锁的线程是否是当前线程,不是当前线程则抛出异常。

  2. 判断释放之后的state==0,等于0说明完全释放锁,将持锁的线程设置为null。

  3. 修改state,因为是排它锁,只有当前线程才会走到这里,所以是线程安全的。

注:如果是当前线程多次重入,releases=1是不能完全释放锁的,free=false,也不会唤醒后继节点。

//ReentrantLock.Sync#tryRelease

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值