Java ReentrantLock 原理 源码 多线程 模拟加锁解锁过程

前言

咱们下面都是公平锁相关的内容

参考视频:https://www.bilibili.com/video/BV1XJ411N7n8?p=1

 

基本原理

首先大概要知道总体原理:

多个线程去抢一个status状态,抢到的线程就获取成功,抢不到线程的会把自己当前线程放在一个队列里排队。抢到锁的线程unlock的时候会把排在队头的线程唤醒,然后队头的线程再去尝试获得锁。

大概的伪代码是这样的,混个脸熟

class Lock {

    int state;
    Queue q;


    lock() {

        for(;;) {
            // 多线程抢着设置成1
            if (CAS(state, 0, 1)) {
                return true;
            } else {
                // 抢不到的线程入队并且park等待唤醒
                enq(currentThread);
                park();
            }
        }
    }

    unlock() {
        state = 0;
        // 取出队头线程
        Thread t = getq();
        // 唤醒他
        t.unpark();
    }

}

模拟多线程加锁解锁过程

看并发的代码太蛋疼了,特用下面的表格模拟了一下多个线程交替运行的过程,这样能比干看代码相对好理解一点吧。

把你的大脑当做计算机,一行一行的运行代码吧!

 

备注

t1

t2

t3

重要变量

 

acquire(1);

 

 

 

 

tryAcquire(1)

 

 

 

 

获取当前thread=t1

 

 

 

 

初始化查state==0

 

 

state=0

 

hasQueuedPredecessors返回false,不需要排队

//队列为空:head==tail,

 

 

 

 

CAS设置state=1

设置当前获得锁的线程=t1

 

 

state=1

 

tryAcquire(1)返回true成功获得锁,返回

 

 

 

跟t1的前面流程是一样的流程

 

acquire(1);

tryAcquire(1)

查state==1,

tryAcquire(1)返回false

 

state=1

 

 

acquireQueued

(addWaiter(Node.EXCLUSIVE), arg))

 

// addWaiter在这时候会创建一个双向链表,

有两个结点,

头结点的thread=null,

尾结点的thread=t2

 

state=1

链表:

空头结点-t2

万一这时候t1已经跑完了

,t2再尝试获取锁就可以获取锁了,

不用park,减小性能消耗。

咱们就假设真的发生了这个神奇的事件,

继续推演

 

进入acquireQueued。

死循环,

获取t2的前一个节点,

发现是那个thread为空的头结点,

于是再尝试获取锁tryAcquire(arg)

 

 

t1还真的就释放锁了

(过程先不分析,但是大胆认为state==0)

state==0

hasQueuedPredecessors()

// 这时候t2之前建好了双向链表,

h!=t,(s=h.next)就是t2,

最后整体判断

hasQueuedPredecessors() 返回false

认为没人排队

 

state=0

链表:

空头结点-t2

 

 

CAS设置state=1

设置当前获得锁的线程=t2返回true

 

state=1

链表:

空头结点-t2

注意!

到这里我们大概可以猜测,

这个队列维护的时候,

一直是有个空节点当头结点,

排在他后面的才是实际上排第一的节点。

 

setHead(node);设置头是当前节点(会把当前节点的thread设置为null,这样就变成了跟原来造的空头结点一模一样了),头节点设置为null(help GC)返回interrupted=false;

 

state=1

链表:

空头结点

两个条件都返回false,不会调用selfInterrupt()

 

t2成功获取锁

 

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

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

        selfInterrupt();

}

 

 

t3来了,

这回可没有t2运气这么好了,

第2次tryAcquire也失败

 

 

t3也执行lock,也跟前面一大坨,来到acquireQueued,第2次尝试tryAcquire也失败

state=1

链表:

空头结点-t3

 

 

 

shouldParkAfterFailedAcquire(p, node)

// ws初始化都是0,进入最后一个else分支,CAS设置前面一个节点(头结点)的waitStatus=Node.SIGNAL=-1;

state=1

链表:

空头结点(ws=-1)-t3

 

 

 

注意现在还是在for(;;)死循环里。又会tryAcquire(arg),很不幸还是没有成功,又进入了shouldParkAfterFailedAcquire(p, node),这回pred前面那个空节点的ws==Node.SIGNAL了,返回true。

 

java.util.concurrent.locks.

AbstractQueuedLongSynchronizer

#parkAndCheckInterrupt

 

 

private final boolean parkAndCheckInterrupt() {

    LockSupport.park(this);

    return Thread.interrupted();

}

//终于可以park了,乖乖等着被人唤醒吧

 

那我们就假设t2忙完了要unlock了吧

 

release(1)

tryRelease(1)

int c = getState() - releases;

//正常是0

setExclusiveOwnerThread(null);

//设置当前持有锁的线程是null

setState(c);

//state被修改成了0,印证了我们前面的解锁会设置state=0的说法

返回true

 

state=0

链表:

空头结点(ws=-1)-t3

java.util.concurrent.locks.

AbstractQueuedSynchronizer#release

 

现在头结点不为空,头结点的ws被t3设置为了SIGNAL,所以要unparkSuccessor(Node node)

 

 

 

java.util.concurrent.locks.

AbstractQueuedSynchronizer

#unparkSuccessor

 

现在ws=-1

CAS设置state为0

获取s为头结点后面那个节点,就是排第一的节点,也就是t3的节点、

s!=null, s.waitStatus == 0

不进入if分支(这个if分支,按照注释,是用来处理一些cancel的节点,先不管)

于是就unpark了t3

 

state=0

链表:

空头结点(ws=-1)-t3

t3被唤醒了哟,回到了代码park的代码中

 

 

 

return Thread.interrupted();

//由于是唤醒,不是被中断,返回false。

这时候都忘了是从哪个函数进的了,翻翻上面,其实是

java.util.concurrent.locks.AbstractQueuedLongSynchronizer#acquireQueued

返回false,不会进入if 分支。

又继续死循环

 

现在链表的状态是头部空节点后面连着t3

 

 

tryAcquire(arg)

state被t2设置为0

hasQueuedPredecessors返回false

CAS state=1

设置当前持有锁的线程是t3,返回true

state=1

链表:

空头结点(ws=-1)-t3(ws=0)

java.util.concurrent.locks.

AbstractQueuedSynchronizer

#acquireQueued

 

 

设置头是t3,头的thread是null。这时候队列里只有一个空的头节点了

返回interrupted=false

成功获得了锁

state=1

链表:

空头结点(ws=0)

//因为把t3设置成了空的头结点所以ws是t3的ws=0

然后t3干完活再释放锁

 

 

偷懒抄下t2的释放锁

release(1)

tryRelease(1)

int c = getState() - releases;

//正常是0

setExclusiveOwnerThread(null);

//设置当前持有锁的线程是null

setState(c);

返回true

state=0

链表:

空头结点(ws=0)

 

 

留一个坑:

什么时候触发hasQueuedPredecessors的其他条件?

return h != t &&

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

以下这些情况都返回true:认为有人排队

头不等于尾 且 排第1的是空?? 这个是遗留问题

头不等于尾 且 排第一的不是当前线程 这个已经得到验证

 

结语

人脑代替电脑分析过程中发现了 interrupt 这种中断机制

后面有机会再分析是怎么个中断法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值