Java并发:深入浅出AQS之独占锁模式源码分析

PROPAGATE = -

3

;

volatile

int

waitStatus;

// 前置节点

volatile

Node

prev;

// 后置节点

volatile

Node

next;

// 节点所代表的线程

volatile

Thread

thread;

Node

nextWaiter;

}

acquire(int arg)

基于上面所讲的独占锁获取释放的大致过程,我们再来看下源码实现逻辑:

首先来看下获取锁的方法acquire()

public

final

void

acquire(

int

arg) {

if

(!tryAcquire(arg) &&

acquireQueued(addWaiter(

Node

.EXCLUSIVE), arg))

selfInterrupt();

}

代码虽然短,但包含的逻辑却很多,一步一步看下:

1、首先是调用开发人员自己实现的 tryAcquire() 方法尝试获取锁资源,如果成功则整个 acquire()方法执行完毕,即当前线程获得锁资源,可以进入临界区。

2、如果获取锁失败,则开始进入后面的逻辑,首先是 addWaiter(Node.EXCLUSIVE)方法。来看下这个方法的源码实现

addWaiter(Node)

此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的节点。

//注意:该入队方法的返回值就是新创建的节点

private

Node

addWaiter(

Node

mode) {

//基于当前线程,节点类型(Node.EXCLUSIVE)创建新的节点

//由于这里是独占模式,因此节点类型就是Node.EXCLUSIVE

Node

node =

new

Node

(

Thread

.currentThread(), mode);

Node

pred = tail;

//这里为了提搞性能,首先执行一次快速入队操作,即直接尝试将新节点加入队尾

if

(pred !=

null

) {

node.prev = pred;

//这里根据CAS的逻辑,即使并发操作也只能有一个线程成功并返回,其余的都要执行后面的入队操作。即enq()方法

if

(compareAndSetTail(pred, node)) {

pred.next = node;

return

node;

}

}

//上一步失败则通过enq入队。

enq(node);

return

node;

}

enq(final Node node)

此方法用于将node加入队尾

//完整的入队操作

private

Node

enq(

final

Node

node) {

// CAS 自旋 ,直到成功加入队尾

for

(;😉 {

Node

t = tail;

//如果队列还没有初始化,则进行初始化,即创建一个空的头节点

if

(t ==

null

) {

//同样是CAS,只有一个线程可以初始化头结点成功,其余的都要重复执行循环体

if

(compareAndSetHead(

new

Node

()))

tail = head;

}

else

{

//新创建的节点指向队列尾节点,毫无疑问并发情况下这里会有多个新创建的节点指向队列尾节点

node.prev = t;

//基于这一步的CAS,不管前一步有多少新节点都指向了尾节点,这一步只有一个能真正入队成功,其他的都必须重新执行循环体

if

(compareAndSetTail(t, node)) {

t.next = node;

//该循环体唯一退出的操作,就是入队成功(否则就要无限重试)

return

t;

}

}

}

}

上面的入队操作有两点需要说明:

1、初始化队列的触发条件就是当前已经有线程占有了锁资源,因此上面创建的空的头节点可以认为就是当前占有锁资源的节点(虽然它并没有设置任何属性)。

2、注意 enq(finalNodenode)代码是,是一个经典的CAS自旋操作,直到成功加入队尾,否则一直重试。

经过上面的操作,我们申请获取锁的线程已经成功加入了等待队列,通过文章最一开始说的独占锁获取流程,那么节点现在要做的就是挂起当前线程,等待被唤醒,这个逻辑是怎么实现的呢?来看下源码:

acquireQueued(final Node node, int arg)

通过 tryAcquire()和 addWaiter(),如果线程获取资源失败,已经被放入等待队列尾部了。

如果线程获取资源失败,下一步进入等待状态休息,直到其他线程彻底释放资源后,唤醒自己再拿到资源,在等待队列中排队拿号,直到拿到号后再返回。(队列先进先出)

通过上面的分析,该方法入参node就是刚入队的包含当前线程信息的节点

final

boolean

acquireQueued(

final

Node

node,

int

arg) {

//锁资源获取失败标记位

boolean

failed =

true

;

try

{

//等待线程被中断标记位

boolean

interrupted =

false

;

//这个循环体执行的时机包括新节点入队和队列中等待节点被唤醒两个地方

//又是一个“自旋”

for

(;😉 {

//获取当前节点的前置节点

final

Node

p = node.predecessor();

//如果前置 需要zi料+ 绿色徽【vip1024b】

节点就是头结点,则尝试获取锁资源

if

(p == head && tryAcquire(arg)) {

//当前节点获得锁资源以后设置为头节点,这里继续理解我上面说的那句话

//头结点就表示当前正占有锁资源的节点

setHead(node);

p.next =

null

;

//setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!

//表示锁资源成功获取,因此把failed置为false

failed =

false

;

//返回中断标记,表示当前节点是被正常唤醒还是被中断唤醒

return

interrupted;

}

//如果没有获取锁成功,则进入挂起逻辑

if

(shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true

interrupted =

true

;

}

}

finally

{

//最后会分析获取锁失败处理逻辑

if

(failed)

cancelAcquire(node);

}

}

挂起逻辑是很重要的逻辑,这里拿出来单独分析一下,首先要注意目前为止,我们只是根据当前线程,节点类型创建了一个节点并加入队列中,其他属性都是默认值。

shouldParkAfterFailedAcquire(Node pred, Node node)

此方法主要用于检查状态,看看自己是否真的可以去休息了,进入 waiting状态

//首先说明一下参数,node是当前线程的节点,pred是它的前置节点

private

static

boolean

shouldParkAfterFailedAcquire(

Node

pred,

Node

node) {

//获取前置节点的waitStatus

int

ws = pred.waitStatus;

if

(ws ==

Node

.SIGNAL)

//如果前置节点的waitStatus是Node.SIGNAL则返回true,然后会执行parkAndCheckInterrupt()方法进行挂起

return

true

;

if

(ws >

0

) {

//由waitStatus的几个取值可以判断这里表示前置节点被取消

do

{

node.prev = pred = pred.prev;

}

while

(pred.waitStatus >

0

);

//这里我们由当前节点的前置节点开始,一直向前找最近的一个没有被取消的节点

//注,由于头结点head是通过new Node()创建,它的waitStatus为0,因此这里不会出现空指针问题,也就是说最多就是找到头节点上面的循环就退出了

pred.next = node;

}

else

{

//根据waitStatus的取值限定,这里waitStatus的值只能是0或者PROPAGATE,那么我们把前置节点的waitStatus设为Node.SIGNAL然后重新进入该方法进行判断

compareAndSetWaitStatus(pred, ws,

Node

.SIGNAL);

}

return

false

;

}

上面这个方法逻辑比较复杂,它是用来判断当前节点是否可以被挂起,也就是唤醒条件是否已经具备,即如果挂起了,那一定是可以由其他线程来唤醒的。该方法如果返回false,即挂起条件没有完备,那就会重新执行 acquireQueued()方法的循环体,进行重新判断,如果返回 true,那就表示万事俱备,可以挂起了,就会进入 parkAndCheckInterrupt()方法看下源码:

parkAndCheckInterrupt()

private

final

boolean

parkAndCheckInterrupt() {

LockSupport

.park(

this

);

//调用park()使线程进入waiting状态

//被唤醒之后,返回中断标记,即如果是正常唤醒则返回false,如果是由于中断醒来,就返回true

return

Thread

.interrupted();

}

注意: Thread.interrupted()会清除当前线程的中断标记位。

park()会让当前线程进入 waiting状态。在此状态下,有两种途径可以唤醒该线程:1,被 unpark();2,被 interrupt()

看 acquireQueued方法中的源码,如果是因为中断醒来,那么就把中断标记置为 true。

不管是正常被唤醒还是由与中断醒来,都会去尝试获取锁资源。如果成功则返回中断标记,否则继续挂起等待。

最后:学习总结——MyBtis知识脑图(纯手绘xmind文档)

学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。下方即为我手绘的MyBtis知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的MyBtis知识脑图原件(包括上方的面试解析xmind文档)

image

除此之外,前文所提及的Alibaba珍藏版mybatis手写文档以及一本小小的MyBatis源码分析文档——《MyBatis源码分析》等等相关的学习笔记文档,也皆可分享给认可的朋友!

看下源码:

parkAndCheckInterrupt()

private

final

boolean

parkAndCheckInterrupt() {

LockSupport

.park(

this

);

//调用park()使线程进入waiting状态

//被唤醒之后,返回中断标记,即如果是正常唤醒则返回false,如果是由于中断醒来,就返回true

return

Thread

.interrupted();

}

注意: Thread.interrupted()会清除当前线程的中断标记位。

park()会让当前线程进入 waiting状态。在此状态下,有两种途径可以唤醒该线程:1,被 unpark();2,被 interrupt()

看 acquireQueued方法中的源码,如果是因为中断醒来,那么就把中断标记置为 true。

不管是正常被唤醒还是由与中断醒来,都会去尝试获取锁资源。如果成功则返回中断标记,否则继续挂起等待。

最后:学习总结——MyBtis知识脑图(纯手绘xmind文档)

学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。下方即为我手绘的MyBtis知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的MyBtis知识脑图原件(包括上方的面试解析xmind文档)

[外链图片转存中…(img-Gwc2asCb-1710355887903)]

除此之外,前文所提及的Alibaba珍藏版mybatis手写文档以及一本小小的MyBatis源码分析文档——《MyBatis源码分析》等等相关的学习笔记文档,也皆可分享给认可的朋友!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值