ReentrantLock 的源码分析

ReentrantLock 的源码分析

ReentrantLock 的时序图

ReentrantLock.lock()

这个是 reentrantLock 获取锁的入口

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

​ sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,而 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能。

sync的实现类

所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能 Sync 有两个具体的实现类,分别是:

NofairSync

​ 表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁。

FailSync

​ 表示所有线程严格按照 FIFO 来获取锁。

NofairSync.lock

以非公平锁为例,来看看 lock 中的实现

1.非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有没有线程排队,我先上来 cas 去抢占一下

2.CAS 成功,就表示成功获得了锁

3.CAS 失败,调用 acquire(1)走锁竞争逻辑

final void lock() {
 if (compareAndSetState(0, 1))
	setExclusiveOwnerThread(Thread.currentThread());
 else
 	acquire(1);
}
CAS 的实现原理
protected final boolean compareAndSetState(intexpect, int update) {
 // See below for intrinsics setup to support this
 return unsafe.compareAndSwapInt(this,stateOffset, expect, update);
}

​ 通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的 state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返 回 false。这个操作是原子的,不会出现线程安全问题,

state属性

​ state是AQS中的一个属性,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示:

1、当 state=0 时,表示无锁状态

2、当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为 ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增, 比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁

Unsafe 类

​ Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、 Hadoop、Kafka 等;

​ Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、 线程的挂起和恢复、CAS、线程同步、内存屏障。

compareAndSwapInt方法

​ CAS就是Unsafe 类中提供的一个原子操作

​ 在 unsafe.cpp文件中,可以找到 compareAndSwarpInt的实现。

​ 第一个参数为需要改变的对象, 第二个为偏移量(你某个字段相对于对象的起始内存地址的字节偏移),第三个参数为期待的值,第四个为更新后的值

​ 整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false。

AQS.accquire

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续 acquire(1)操作。

acquire(1)方法的作用

1、 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false

2、如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加 到 AQS 队列尾部

3、acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。

public final void acquire(int arg) {
 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 	selfInterrupt();
}
NonfairSync.tryAcquire

​ 这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false 。

protected final boolean tryAcquire(int acquires) {
 	return nonfairTryAcquire(acquires);
}
ReentrantLock.nofairTryAcquire

方法的作用

1、 获取当前线程,判断当前的锁的状态

2、 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值

3、 当前线程是属于重入,则增加重入次数

final boolean nonfairTryAcquire(int acquires) {
 	final Thread current = Thread.currentThread();//获取当前执行的线程
 	int c = getState();//获得 state 的值
 	if (c == 0) {//表示无锁状态
 		if (compareAndSetState(0, acquires)) {//cas 替换 state 的值,cas 成功表示获取锁成功
 			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;
}
AQS.addWaiter

​ 当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node.参数mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能。

1、 将当前线程封装成 Node

2、 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node 添加到 AQS 队列

3、如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列

private Node addWaiter(Node mode) {
 	Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
 	Node pred = tail; //tail 是 AQS 中表示同比队列队尾的属性,默认是 null
 	if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
 		node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
 		if (compareAndSetTail(pred, node)) {//通过cas 把node加入到 QS队列,也就是设置为 tail
 		pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
 	return node;
 		}
 	}
	 enq(node);//tail=null,把 node 添加到同步队列
 	return node;
}
enq

enq 就是通过自旋操作把当前节点加入到队列中

private Node enq(final Node node) {
 	for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
            }else {
                node.prev = t;
        		if (compareAndSetTail(t, node)) {
            	t.next = node;
        		return t;
 				}
 			}
 	}
}

图解分析

在这里插入图片描述

假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter 方法结束后,AQS 中的链表结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ho1r6WHu-1608643288588)(img/11.jpg)]

AQS.acquireQueued

​ 通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给 acquireQueued 方法,去竞争锁

1、 获取当前节点的 prev 节点

2、如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占 锁

3、抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点

4、如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程

5、 最后,通过 cancelAcquire 取消获得锁的操作

final boolean acquireQueued(final Node node, int arg) {
 	boolean failed = true;
 	try {
 	boolean interrupted = false;
	for (;;) {
 		final Node p = node.predecessor();//获取当前节点的 prev 节点
		if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁
 			setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
			 p.next = null; //把原 head 节点从链表中移除
 			 failed = false;
			 return interrupted;
		 }
		//ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
 		if (shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt())
			 interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
		 }
 	} finally {
 		if (failed)
 		cancelAcquire(node);
 	}
}
NofairSync.tryAcquire

​ 通过 state 的状态来判断是否处于无锁状态,然后在 通过 cas 进行竞争锁操作。

​ 成功表示获得锁,失败表示获得锁失败。

shouldParkAfterFailedAcquire

如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会 失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法

这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是 否应该被挂起。

返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt 挂起当前线程

1、 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程

2、通过循环扫描链表把 CANCELLED 状态的节点移除

3、 修改 pred 节点的状态为 SIGNAL,返回 false

Node的状态

CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取 消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状 态后的结点将不会再变化

SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程

CONDITION: 满足某种条件就释放锁

PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态

0:初始状态

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;//前置节点的waitStatus
 	if (ws == Node.SIGNAL)//如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放,
	 	return true;//返回 true,意味着可以直接放心的挂起了
    if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
 		do {
 			node.prev = pred = pred.prev;//相当于: pred=pred.prev;
			node.prev=pred;
		 } while (pred.waitStatus > 0); //这里采用循环,从双向列表中移除 CANCELLED 的节点
 			pred.next = node;
	 } else {//利用 cas 设置 prev 节点的状态为 SIGNAL(-1)
		 compareAndSetWaitStatus(pred, ws,Node.SIGNAL);
	 }
 	 return false;
}
parkAndCheckInterrupt

​ 使用 LockSupport.park 挂起当前线程编程 WATING 状态

​ Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是 thread.interrupt();

​ 如果有触发过中断请求,那么这个方法会返回当前的中断标识 true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味 着在 acquire 方法中会执行 selfInterrupt()。

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
 	return Thread.interrupted();
}

selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一 个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求 的

static void selfInterrupt() {
 	Thread.currentThread().interrupt();
}
图解分析

在这里插入图片描述

​ 通过 acquireQueued 方法来竞争锁,如果 ThreadA 还在执行中没有释放锁的话, 意味着 ThreadB 和 ThreadC 只能挂起了。

LockSupport

LockSupport是一个线程阻塞工具类

park(阻塞线程)和unpark(启动唤醒线程)。

​ unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有 点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。

​ permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1。调用一次 park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已 经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都 有一个相关的 permit,permit 最多只有一个,重复调调用 unpark 不会累积。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值