深入浅出ReentrantLock(可重入锁)

//一个尝试插队的过程

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

//获取state值

int c = getState();

//比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁

if (c == 0) {

//则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程)

if (compareAndSetState(0, acquires)) {

// 设置成功标识独占锁

setExclusiveOwnerThread(current);

return true;

}

}

//如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程

else if (current == getExclusiveOwnerThread()) {

//如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁

int nextc = c + acquires;

//重入次数太多,大过Integer.MAX

if (nextc < 0) // overflow

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

setState(nextc);

return true;

}

//如果以上两种情况都不通过,则返回失败false

return false;

}

  • tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。

代码如下:

//AbstractQueuedSynchronizer.addWaiter()

private Node addWaiter(Node mode) {

// 初始化一个节点,用于保存当前线程

Node node = new Node(Thread.currentThread(), mode);

// 当CLH队列不为空的视乎,直接在队列尾部插入一个节点

Node pred = tail;

if (pred != null) {

node.prev = pred;

//如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

// 当CLH队列为空的时候,调用enq方法初始化队列

enq(node);

return node;

}

private Node enq(final Node node) {

//在一个循环里不停的尝试将node节点插入到队尾里

for (;😉 {

Node t = tail;

if (t == null) { // 初始化节点,头尾都指向一个空节点

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

  • 将节点增加到CLH队列后,进入 acquireQueued() 方法

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

//在一个循环里不断等待前驱节点执行完毕

for (;😉 {

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁

setHead(node);//将当前节点设置成头节点

p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉

failed = false;//将failed标记设置成false

return interrupted;

}

//中断

if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞

parkAndCheckInterrupt())// 阻塞,返回线程是否被中断

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

  • 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞

/**

  • 确保当前结点的前驱结点的状态为SIGNAL

  • SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程

  • 只有确保能够被唤醒,当前线程才能放心的阻塞。

*/

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

if (ws == Node.SIGNAL)

//如果前驱节点状态为SIGNAL

//表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点

return true;

if (ws > 0) {//ws > 0 代表前驱节点取消了

do {

node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列

} while (pred.waitStatus > 0);

pred.next = node;

} else {

//初始化状态,将前驱节点的状态设置成SIGNAL

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

  • 当进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:

// 从方法名可以看出这个方法做了两件事

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this);//挂起当前的线程

// 如果当前线程已经被中断了,返回true,否则返回false

// 有可能在挂起阶段被中断了

return Thread.interrupted();

}

4.2 非公平锁 NonfairSync.unlock()

2.1 unlock()方法的示意图

在这里插入图片描述

2.1 unlock()方法详解

  1. 调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。

  2. 进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。

  3. 如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。

  4. 一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。

代码如下:

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

//尝试在当前锁的锁定计数(state)值上减1,

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。

unparkSuccessor(h);

//成功返回true

return true;

}

//否则返回false

return false;

}

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

//如果waitStatus < 0 则将当前节点清零

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

//若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

LockSupport.unpark(s.thread);

}

当然在 release() 方法中不仅仅只是将 state - 1 这么简单,-1 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。

protected final boolean tryRelease(int releases) {

int c = getState() - releases;

//判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常

if (Thread.currentThread() != getExclusiveOwnerThread())

throw new IllegalMonitorStateException();

boolean free = false;

//c == 0,释放该锁,同时将当前所持有线程设置为null

if (c == 0) {

free = true;

setExclusiveOwnerThread(null);

}

//设置state

setState©;

return free;

}

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

// 从后往前找到离head最近,而且waitStatus <= 0 的节点

// 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

LockSupport.unpark(s.thread);// 唤醒挂起线程

}

重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁

4.3 公平锁 FairSync

FairSync相对来说就简单很多,只有重写的两个方法跟NonfairSync不同

final void lock() {

acquire(1);

}

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;

}

五、公平锁和非公平锁的区别


  • 锁的公平性是相对于获取锁的顺序而言的。

  • 如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

;

setState(nextc);

return true;

}

return false;

}

五、公平锁和非公平锁的区别


  • 锁的公平性是相对于获取锁的顺序而言的。

  • 如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

[外链图片转存中…(img-p0XgKYUe-1720117765771)]

[外链图片转存中…(img-JOgQRNhk-1720117765772)]

[外链图片转存中…(img-zopKYRPx-1720117765772)]

[外链图片转存中…(img-qurppBHf-1720117765773)]

[外链图片转存中…(img-bHRSqWe6-1720117765774)]

  • 27
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值