文章目录
- 概述
- synchronized的缺陷
- 1)synchronized不能控制阻塞,不能灵活控制锁的释放。
- 2)在读多写少的场景中,效率低下。
- 独占锁ReentrantLock原理
- ReentrantLock概述
- AQS同步队列
- 1. AQS实现原理
- 2. 线程被唤醒时,AQS队列的变化
- 锁的获取
- 1. F. lock
- 2. AQS. acquire
- 3. FairSync. tryAcquire
- 4. AQS. addWaiter(Node. EXCLUSIVE)
- 5. AQS. acquireQueued(newNode,1)
- 6. AQS. shouldParkAfterFailedAcquire
- 7. AQS. parkAndCheckInterrupt:将当前线程阻塞挂起
- 锁的释放
- 1. ReentrantLock. unlock
- 2. AQS. release
- 3. Sync. tryRelease
- 4. AQS. signalNext (JDK17)
- 公平锁和非公平锁实现区别
- 1. 非公平锁与非公平锁的释放过程没有任何差异
- 2. 非公平锁与非公平锁获取锁的差异
- (1)tryAcquire差异 (JDK17)
概述
Java提供了种类丰富的锁,synchronized为Java的关键字,是Java最早提供的同步机制。当它用来修饰一个方法或一个代码块时,能够保证在同一时刻最多只能有一个线程执行该代码。
当使用synchronized修饰代码时,并不需要显式的执行加锁和解锁过程,所以它也被称之为隐式锁。
除了Java提供的synchronized关键字,从Java 5. 0起,JUC包中引入了显式锁作为synchronized的补充,提供了比synchronized同步锁更广泛的锁定操作,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。
JUC中的锁全部位于Java. util. concurrent. locks
下,是并发包中最核心组件之一,是保证线程安全的重要组件。
查看JUC工具类的源码,就会发现JUC中绝大部分组件都用到了它们。
JUC中的锁分为两类:重入锁和读写锁。
重入锁:ReentrantLock类,它是一种可重入的独占锁,具有与使用synchronized相同的一些基本行为和语义,但是它拥有了更广泛的API,它的功能更强大。ReentrantLock相当于synchronized的增强版,具有synchronized很多所没有的功能。
读写锁:读写锁的实现有两个,分别为ReentrantReadWriteLock和StampedLock。ReentrantReadWriteLock
维护了一对关联锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。一个是ReadLock(读锁)用于读操作的,一个是WriteLock(写锁)用于写操作。读写锁适合于读多写少的场景,基本原则是读锁可以被多个线程同时持有进行访问,而写锁只能被一个线程持有。
ReentrantReadWriteLock
可以使得多个读线程同时持有读锁,而写锁是写线程独占的。读写锁如果使用不当,很容易产生“饥饿”问题:比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”。虽然使用公平策略可以一定程度上缓解这个问题,但是公平策略是以牺牲系统吞吐量为代价的。
StampedLock
,是在JDK 1. 8引入的锁类型,是对读写锁ReentrantReadWriteLock
的增强版。StampedLock
采用一种乐观的读策略,使得读锁完全不会阻塞写线程。
synchronized的缺陷
Java已经提供了synchronized同步锁可以对临界资源同步互斥访问,为什么还要使用JUC锁呢?
主要是因为synchronized同步锁存在以下两种问题:
1)synchronized不能控制阻塞,不能灵活控制锁的释放。
synchronized同步锁提供了一种排他式的同步机制,当多个线程竞争锁资源时,同时只能有一个线程持有锁,当一个线程获取了锁,其他线程就会被阻塞,只有等占有锁的线程释放锁后,才能重新进行竞争锁。
当使用synchronized同步锁,线程会在三种情况下释放锁:
- ① 线程执行完同步代码块/方法,释放锁;
- ② 线程执行时发生异常,此时JVM会让线程自动释放锁;
- ③ 在同步代码块/方法中,锁对象执行了wait方法,线程释放锁。
使用synchronized同步锁,假如占有锁的线程被长时间阻塞(IO阻塞、sleep方法、join方法等),由于线程在阻塞时不会释放锁,一旦其他线程此时尝试获取锁,就会被阻塞而一直等待下去,甚至可能会发生死锁,这样就会造成大量线程的堆积,严重降低服务器的性能。
所以synchronized同步锁的线程阻塞,存在两个致命的缺陷:无法控制阻塞时长,阻塞不可中断。
JUC锁可以解决这种情况。
- 使用ReentrantLock重入锁时,线程可以使用
tryLock(long time、TimeUnit unit)
方法获取锁,限定一个获取锁的超时时间,如果在规定的时间内获取锁失败,线程就会放弃锁的获取。 - 也可以使用
lockInterruptibly()
方法获取锁,如果线程长时间没有获取锁,在等待时如果其他线程调用该线程的interrupt方法中断线程,该线程就可以通过响应中断放弃获取锁。
2)在读多写少的场景中,效率低下。
在读多写少的场景中,当多个读线程同时操作共享资源时,读操作不会对共享资源进行修改,所以读线程和读线程是不需要同步的。
如果这时采用synchronized关键字,就会导致一个问题,当多个线程都只进行读操作时,所有线程都只能同步进行,只能有一个读线程可以进行操作,其他读线程只能被阻塞而无法进行读操作。
因此,在读多写少的场景中,我们需要实现一种机制,当多个线程都只是进行读操作时,使得线程可以同时进行读操作。ReentrantReadWriteLock
锁可以解决这种情况。所以通过JUC锁可以很容易解决以上问题,但synchronized同步锁却对此无能为力。
独占锁ReentrantLock原理
ReentrantLock概述
ReentrantLock
是一个显式的互斥锁,提供比 synchronized 关键字更强大的功能,例如可中断、超时和公平性选择。其底层基于 AbstractQueuedSynchronizer (AQS)
,其主要作用是控制线程的同步。
从图可以看到ReentrantLock实现了Lock接口,在ReentrantLock中有非公平锁NonfairSync和公平锁FairSync的实现,NonfairSync和FairSync都继承自抽象类Sync。
Sync类是ReentrantLock的内部抽象类,继承自抽象类AbstractQueuedSynchronizer(简称AQS)。
如果大家看过JUC的源代码,会发现不仅重入锁用到了AQS,JUC中绝大部分的同步工具也都是基于AQS构建的。
Lock接口定义了一套锁实现的标准规范,定义了获得锁和释放锁一系列方法,所以Lock锁可以提供比synchronized更广泛的锁操作,可以更灵活地控制锁的粒度.
ReentrantLock中提供了两个构造器:其中一个构造器可以指定锁的类型公平锁/非公平锁,默认构造器默认使用非公平锁
- 公平锁:多个线程按照申请锁的顺序,按照先来后到的原则获得锁。
- 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,允许“插队”,有可能出现后申请的线程比先申请的线程优先获取锁的情况。
一般情况下使用公平锁的程序在多线程访问时,在线程调度上面的开销比较大,所以总体吞吐量会比较低。
ReentrantLock和synchronized一样都是独占排他锁,所有的线程都是同步互斥的访问同步块代码。同时只能有一个线程获得锁,其他线程只能等待锁被释放后才能有机会获得锁的使用权。
ReentrantLock锁的使用比较简单
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 获取锁try {// 执行临界区代码System.out.println("执行临界区代码");} finally {lock.unlock(); // 释放锁}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();example.doSomething();}
}
使用一个示例来感受下ReentrantLock的同步效果,代码如下所示
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 获取锁try {// 执行临界区代码System.out.println(Thread.currentThread().getName() + " 进入临界区");Thread.sleep(2000); // 模拟耗时操作System.out.println(Thread.currentThread().getName() + " 离开临界区");} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println(Thread.currentThread().getName() + " 被中断");} finally {lock.unlock(); // 释放锁}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();Thread thread1 = new Thread(() -> example.doSomething(), "Thread1");Thread thread2 = new Thread(() -> example.doSomething(), "Thread2");thread1.start();thread2.start();}
}
输出
Thread1 进入临界区
Thread1 离开临界区
Thread2 进入临界区
Thread2 离开临界区
Thread1先于Thread2执行lock. lock(),会先获得锁,随后Thread2尝试获得锁会被阻塞,Thread1运行完毕需要执行lock. unlock()释放锁,这时Thread2才能通过lock. lock()获得锁运行同步代码块
AQS同步队列
重入锁ReentrantLock的实现底层是使用AbstractQueuedSynchronizer实现的。Abstract-QueuedSynchronizer是一个抽象同步类,简称AQS。它是实现JUC中绝大部分同步工具的核心组件,都是基于AQS构建的.
AQS提供了一套通用的机制来管理同步状态、阻塞/唤醒线程、管理等待队列等。除了JUC中的锁,JUC中的其他同步工具如CountDownLatch、CyclicBarrier
等,也都是通过内部类实现了AQS的API,来实现各自的同步器功能。如果掌握了AQS,那JUC中绝大多数的工具类也就可以轻松掌握.
AQS 是 ReentrantLock
等同步器的基础。它通过一个同步队列来管理线程的等待和唤醒。AQS 通过维护一个双向链表来表示等待线程的顺序.
AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。
其中
-
Node中的thread变量用来存放进入AQS队列里面的线程;
-
Node节点内部的prev记录当前节点的前驱节点,next记录当前节点的后继节点;
-
SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,
-
EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;
-
waitStatus记录当前线程等待状态,可以为
CANCELLED(取消线程)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点
1. AQS实现原理
AQS维护同步队列,内部使用一个FIFO的双向链表,管理线程同步时的所有被阻塞线程。双向链表这种数据结构,它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,都可以很方便地访问它的前驱节点和后继节点。
AQS的同步队列内部结构如下图所示,AQS中的内部静态类Node为链表节点,AQS会在线程获取锁失败后,线程会被阻塞并被封装成Node加入到AQS队列中;当获取锁的线程释放锁后,会从AQS队列中唤醒一个线程(节点)。
【AQS同步队列结构】
线程抢夺锁失败时加入AQS,AQS队列的变化如下
1)AQS的head、tail分别代表同步队列头节点和尾节点指针,默认为null,如下图所示【AQS初始状态】。
2)当第一个线程抢夺锁失败,同步队列会先初始化,随后线程会被封装成Node节点追加到AQS队列中 【AQS初始化】
3)接下来将ThreadB封装成Node节点,追加到AQS队列。
设置新节点的prev指向AQS队尾节点;将队尾节点的next指向新节点;最后将AQS尾节点指针指向新节点。此时AQS变化 【ThreadB阻塞加入AQS】
4)当下一个线程抢夺锁失败时,重复上面步骤即可。将线程封装成Node,追加到AQS队列
假设此次抢占锁失败的线程为ThreadC,此时AQS变为如下 【ThreadC阻塞加入AQS】
2. 线程被唤醒时,AQS队列的变化
ReentrantLock唤醒阻塞线程时,会按照FIFO的原则从AQS中head头部开始唤醒首个节点中线程。head节点表示当前获取锁成功的线程ThreadA节点。当ThreadA释放锁时,它会唤醒后继节点线程ThreadB,ThreadB开始尝试获得锁,如果ThreadB获得锁成功,会将自己设置为AQS的头节点。ThreadB获取锁成功后,此时AQS变化,如图
- 1)head指针指向ThreadB节点。
- 2)将原来头节点的next指向Null,从AQS中删除。
- 3)将ThreadB节点的prev指向Null,设置节点的thread=null。
线程被唤醒时AQS队列的变化:
- ThreadA 释放锁。
- AQS 唤醒 ThreadA 的后继节点 ThreadB。
- ThreadB 尝试获取锁。
- ThreadB 获取锁成功。
- AQS 更新 head 指针,使其指向 NodeB。
- 将原头节点 NodeA 的 next 指向 null,从AQS中删除。
- 将 NodeB 的 prev 指向 null。
- 将 NodeA 的 thread 设置为 null
以上就是线程在竞争锁时,线程被阻塞和被唤醒时AQS同步队列的基本实现过程。
锁的获取
AQS使用了一种典型的设计模式:模板方法。AQS中大多数方法都是final或private的,也就是说AQS并不希望用户覆盖或直接使用这些方法,只能重写AQS规定的部分方法。
使用模板方法时,父类(AQS框架)定义好一个操作算法的骨架,而一些步骤延迟到子类去实现。这样子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗地讲,就是将子类相同的方法,都放到其抽象父类中。
下面,以锁中相对简单的公平锁实现为例,以获取锁的lock方法为入口,分析多线程是如何同步获取ReentrantLock锁的
1. F. lock
/*** 获取锁。** <p>如果锁没有被其他线程持有,则立即获取锁并返回,将锁的持有计数设置为1。** <p>如果当前线程已经持有锁,则将锁的持有计数增加1,并立即返回。** <p>如果锁被其他线程持有,则当前线程将被禁用以进行线程调度,* 并处于休眠状态,直到获取到锁为止,此时锁的持有计数将被设置为1。*/
public void lock() {sync.lock();
}
可以看到ReentrantLock获取锁调用了lock方法,该方法的内部调用了sync. lock()。
sync是Sync类的一个实例,Sync类是ReentrantLock的抽象静态内部类,它集成了AQS来实现重入锁的具体业务逻辑。AQS是一个同步队列,实现了线程的阻塞和唤醒,没有实现具体的业务功能。在不同的同步场景中,需要用户继承AQS来实现对应的功能
Sync有两个实现类公平锁FairSync和非公平锁NoFairSync.
/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
重入锁实例化时,可以根据参数fair为属性sync创建对应锁的实例。以公平锁为例,调用sync. lock事实上调用的是FairSync的lock方法
该方法的内部执行了方法
acquire为AQS中的final方法,用于竞争锁
2. AQS. acquire
线程进入AQS中的acquire方法,arg=1。arg参数作用:在重入锁中,计算重入次数时使用。获取锁成功后,锁状态标识state=state+arg
/*** 以独占模式获取锁,忽略中断。通过至少调用一次 {@link #tryAcquire} 实现,* 如果获取成功则返回。否则,线程将被排队,可能多次阻塞和解除阻塞,直到调用 {@link #tryAcquire} 成功。* 此方法可以用于实现 {@link Lock#lock} 方法。** @param arg 获取参数。此值传递给 {@link #tryAcquire},但除此之外没有特定的解释,可以表示任何你想要的内容。*/
public final void acquire(int arg) {// 尝试获取锁if (!tryAcquire(arg) &&// 如果获取失败,将当前线程加入等待队列,并尝试获取锁直到成功acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 如果在等待过程中被中断,则恢复中断状态selfInterrupt();
}
这个方法的逻辑为:先尝试抢占锁,抢占成功,直接返回。抢占失败,将线程封装成Node节点,追加到AQS队列中并使线程阻塞等待。
- 1)首先会执行tryAcquire(1)尝试抢占锁,成功返回true,失败返回false。
- 2)抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将线程封装成Node节点追加到AQS队列。
- 3)然后调用acquireQueued将线程阻塞。
- 4)线程阻塞后,接下来就只需等待其他线程(其他线程释放锁时)唤醒它,线程被唤醒后会重新竞争锁的使用。
3. FairSync. tryAcquire
尝试获取锁。若获取锁成功,返回true;获取锁失败,返回false
/*** 公平版本的 tryAcquire。除非是递归调用、没有等待者或当前线程是第一个等待者,否则不授予访问权限。*/
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread(); // 获取当前线程int c = getState(); // 获取当前同步状态(即锁的状态)// 如果当前状态为0,表示锁未被任何线程持有if (c == 0) {// 检查是否有前驱节点(即是否有其他线程在等待)if (!hasQueuedPredecessors() &&// 使用 CAS 操作将状态从0设置为acquirescompareAndSetState(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; // 获取锁失败
}
这个方法逻辑:获取当前的锁状态,如果为无锁状态,当前线程会执行CAS操作尝试获取锁;若当前线程是重入获取锁,只需增加锁的重入次数即可
- 1)首先通过getState()会获取锁的状态state的值,如果state=0表示当前锁为无锁状态。通过CAS更新state的值为1,若CAS成功说明当前线程获取锁成功,直接返回true。若CAS失败,说明锁已经被其他线程占用,当前线程获取锁失败,返回false。
- 2)如果state>0,表示当前锁已被占用并且占用锁线程是否为当前线程。增加锁重入次数,获取锁成功,直接返回true。
- 3)否则说明锁被其他线程占有,抢占锁失败,线程需要等待锁被释放。
CAS更新state的值为1时,使用了AQS的compareAndSetState方法,这个方法比较并替换state的值,如果当前对象state的值和预期值0相等,则将state的值替换为1,否则不替换。替换成功返回true,替换失败返回false。这个方法是原子操作,其实底层使用了Unsafe这个类的compareAndSwapInt方法,所以不存在线程安全问题.
state是AQS同步队列中的一个属性,表示一个重入锁的锁状态。① state=0:表示无锁状态。② state>0:表示锁已经被线程占有,同时state的值还代表重入次数。
ReentrantLock是重入锁,允许同一个线程多次获得同一个锁。线程初次获得锁时state=1,再次获得锁时state会递增加1,state=state+1。比如重入3次,那么state=3。而在释放锁的时候,必须释放相同的次数,state=0时其他线程才能获得锁
4. AQS. addWaiter(Node. EXCLUSIVE)
线程抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)
将线程封装成Node节点追加到AQS队列中。
/*** 为当前线程和给定模式创建并入队一个节点。** @param mode Node.EXCLUSIVE 表示独占模式,Node.SHARED 表示共享模式* @return 新创建的节点*/
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode); // 创建新节点// 尝试快速路径入队;如果失败则使用完整入队方法Node pred = tail; // 获取尾节点if (pred != null) { // 如果尾节点不为空node.prev = pred; // 设置新节点的前驱节点为当前尾节点if (compareAndSetTail(pred, node)) { // 使用 CAS 操作将尾节点设置为新节点pred.next = node; // 设置当前尾节点的后继节点为新节点return node; // 返回新节点}}enq(node); // 如果 CAS 操作失败,使用完整入队方法return node; // 返回新节点
}
- 1)将当前线程封装成Node节点。
- 2)若AQS同步队列的tail节点不为null,将当前线程节点追加到链表中。
- 3)若同步队列为空,初始化链表并将当前线程追加到链表中。
addWaiter(Node mode)
中的mode参数表示节点的类型,Node.EXCLUSIVE表示是独占排他锁,也就是说重入锁是独占锁,用到了AQS的独占模式
Node定义了两种节点类型:
- 1)共享模式:Node. SHARED。共享模式,可以被多个线程同时持有,如读写锁的读锁
- 2)独占模式: Node. EXCLUSIVE。独占很好理解,即自己独占资源,如独占排他锁同时只能由一个线程持有。
所以,相应的AQS支持两种模式:独占模式和共享模式
5. AQS. acquireQueued(newNode,1)
这个方法的主要作用就是将线程阻塞。参数node为新加入等待队列线程节点。
/*** 以独占且不可中断模式获取锁,适用于已经在队列中的线程。条件等待方法和获取锁方法都会使用此方法。** @param node 节点* @param arg 获取锁的参数* @return 如果在等待期间被中断,则返回 {@code true}*/
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)) { // 如果前驱节点是头节点且成功获取锁setHead(node); // 将当前节点设置为头节点p.next = null; // 帮助垃圾回收failed = false; // 标记获取锁成功return interrupted; // 返回是否在等待期间被中断}if (shouldParkAfterFailedAcquire(p, node) && // 检查是否需要阻塞当前线程parkAndCheckInterrupt()) // 阻塞当前线程并检查是否被中断interrupted = true; // 标记在等待期间被中断}} finally {if (failed) // 如果获取锁失败cancelAcquire(node); // 取消获取锁}
}
主要逻辑如下
- 1)若同步队列中,若当前节点为队列第一个线程,则有资格竞争锁,再次尝试获得锁。
- 2)尝试获得锁成功,移除链表head节点,并将当前线程节点设置为head节点。
- 3)尝试获得锁失败,判断是否需要阻塞当前线程。
- 4)若发生异常,取消当前线程获得锁的资格
6. AQS. shouldParkAfterFailedAcquire
这个方法的主要作用是:线程竞争锁失败以后,通过Node的前驱节点的waitStatus状态来判断,线程是否需要被阻塞
/*** 检查并更新未能获取锁的节点的状态。如果线程应该阻塞,则返回 true。* 这是在所有获取循环中的主要信号控制。要求 pred == node.prev。** @param pred 节点的前驱节点,持有状态* @param node 节点* @return 如果线程应该阻塞,则返回 {@code true}*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus; // 获取前驱节点的状态if (ws == Node.SIGNAL) {/** 该节点已经设置了状态,请求释放时通知它,因此它可以安全地阻塞。*/return true;}if (ws > 0) {/** 前驱节点已被取消。跳过前驱节点并指示重试。*/do {node.prev = pred = pred.prev; // 更新当前节点的前驱节点} while (pred.waitStatus > 0); // 继续跳过已取消的前驱节点pred.next = node; // 更新前驱节点的后继节点} else {/** waitStatus 必须是 0 或 PROPAGATE。指示我们需要一个信号,但还不阻塞。* 调用者需要重试以确保在阻塞之前无法获取锁。*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 使用 CAS 操作设置前驱节点的状态为 SIGNAL}return false; // 返回 false,表示不应该阻塞
}
- 1)如果前驱节点状态为SIGNAL,当前线程可以被放心的阻塞,返回true。
- 2)若前驱节点状态为CANCELLED,向前扫描链表把CANCELLED状态的节点从同步队列中移除,返回false。最终结果,当前节点的前驱节点为非取消状态。之后,当前线程执行轨迹如下:
① 再次返回方法acquireQueued,再次循环,尝试获取锁。② 再次执行shouldParkAfterFailedAcquire判断是否需要阻塞。
- 3)若前驱节点状态为默认状态或PROPAGATE,修改前驱节点的状态为SIGNAL,返回false。之后,当前线程执行轨迹如下:
① 再次返回方法acquireQueued,再次循环,尝试获取锁。② 再次执行shouldParkAfterFailedAcquire判断是否需要阻塞。③ 当前节点前驱节点为SIGNAL状态,可以放心被阻塞。
根据shouldParkAfterFailedAcquire返回值的不同,线程会继续执行不同的操作。
- 1)若返回false,会退回到acquireQueued方法,重新执行自旋操作。自旋会重复执行acquireQueued和shouldParkAfterFailedAcquire,有两个结果:① 线程尝试获得锁成功或者线程异常,退出acquireQueued,直接返回。② 若执行shouldParkAfterFailedAcquire成功,当前线程可以被阻塞。
- 2)若返回true,调用parkAndCheckInterrupt阻塞当前线程。
在AQS定义了Node的5种状态,分别是:
- ①0:默认状态。
- ②1:CANCELLED,取消/结束状态。表明线程已取消争抢锁。线程等待超时或者被中断,节点的waitStatus为CANCELLED,线程取消获取锁请求。需要从同步队列中删除该节点。
- ③ -1:SIGNAL,通知。状态为SIGNAL节点中的线程释放锁时,就会通知后续节点的线程。
- ④ -2:CONDITION,条件等待。表明节点当前线程在condition队列中。
- ⑤ -3:PROPAGATE,传播。
在一个节点成为头节点之前,是不会跃迁为P ROPAGATE状态的。用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制.
7. AQS. parkAndCheckInterrupt:将当前线程阻塞挂起
LockSupport. park(this)会阻塞当前线程,会使当前线程(如ThreadB)处于等待状态,不再往下执行。
/*** 方便方法,用于阻塞当前线程并检查是否被中断。** @return 如果线程被中断,则返回 {@code true}*/
private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 阻塞当前线程return Thread.interrupted(); // 检查并清除线程的中断状态
}
- 1)当其他线程调用了LockSupport. unpark(ThreadB),当前线程才能接着往下执行。
- 2)若在等待过程中,若其他线程调用了ThreadB.interrupt(),则此时ThreadB的中断标识为true。当前线程需要响应中断请求,将中断标识复位为false(初始状态),并且将中断标识返回。
- 3)acquireQueued线程获取锁成功后,会同时将中断标识返回。
- 4)若中断标识为false,回到acquire方法,会直接返回。
- 5)若中断标识为true,回到acquire方法,会执行AQS.selfInterrupt(),将线程中断状态复位。后面当前线程获得锁成功,在处理业务代码时可以检查中断标志的状态来判断是否需要终止当前线程
锁的释放
AQS公平锁的释放时序如下
1. ReentrantLock. unlock
/*** 尝试释放此锁。** <p>如果当前线程是此锁的持有者,则持有计数会递减。如果持有计数现在为零,则锁被释放。如果当前线程不是此锁的持有者,则抛出 {@link IllegalMonitorStateException}。** @throws IllegalMonitorStateException 如果当前线程未持有此锁*/public void unlock() {sync.release(1);}
释放锁时,需调用ReentrantLock的unlock方法。这个方法内部,会调用 sync.release(1)
,release方法为AQS类的final方法
2. AQS. release
/*** 以独占模式释放锁。如果 {@link #tryRelease} 返回 true,则通过解除一个或多个线程的阻塞来实现。* 此方法可用于实现 {@link Lock#unlock} 方法。** @param arg 释放参数。此值传递给 {@link #tryRelease},但除此之外没有特定含义,可以表示任何内容。* @return 从 {@link #tryRelease} 返回的值。*/public final boolean release(int arg) {if (tryRelease(arg)) { //释放锁 若释放后锁状态为无锁状态,需唤醒后继线程signalNext(head); //同步队列头节点return true;}return false;}
先执行方法tryRelease(1)
。tryRelease方法为ReentrantLock中Sync类的final方法,用于释放锁。释放锁成功后, signalNext(head)
会从同队队列中唤醒一个线程重新尝试获取锁.
3. Sync. tryRelease
@ReservedStackAccessprotected final boolean tryRelease(int releases) {// 计算新的状态值,即当前状态减去释放次数int c = getState() - releases;// 检查当前线程是否是锁的持有者if (getExclusiveOwnerThread() != Thread.currentThread())throw new IllegalMonitorStateException();// 判断新的状态值是否为零boolean free = (c == 0);// 如果新的状态值为零,表示锁已完全释放if (free)setExclusiveOwnerThread(null); // 清除锁的持有者线程// 更新锁的状态setState(c);// 返回是否锁已完全释放return free;}
尝试释放锁,释放成功返回true,失败返回false。
- 1)判断当前线程是否为锁持有者,若不是持有者,不能释放锁,直接抛出异常。
- 2)若当前线程是锁的持有者,将重入次数减1,并判断当前线程是否完全释放了锁。
- 3)若重入次数为0,则当前新线程完全释放了锁,将锁拥有线程设置为null,并将锁状态置为无锁状态(state=0),返回true。
- 4)若重入次数>0,则当前新线程仍然持有锁,设置重入次数=重入次数-1,返回false. 返回true说明,当前锁被完全释放,这时需要唤醒同步队列中的一个线程,执行unparkSuccessor唤醒同步队列中的节点线程。
4. AQS. signalNext (JDK17)
/*** 唤醒给定节点的后继节点(如果存在),并取消其 WAITING 状态以避免 park 竞争条件。* 当一个或多个线程被取消时,这可能会失败,但 cancelAcquire 确保活跃性。*/private static void signalNext(Node h) {Node s;// 检查给定节点不为空,且其后继节点不为空,且后继节点的状态不为0if (h != null && (s = h.next) != null && s.status != 0) {// 取消后继节点的 WAITING 状态s.getAndUnsetStatus(WAITING);// 唤醒后继节点的等待线程LockSupport.unpark(s.waiter);}}
唤醒后继线程
公平锁和非公平锁实现区别
公平锁的获取锁和释放锁的流程我们已经梳理完了,公平锁和非公平锁在获取锁和释放锁时有什么区别?
1. 非公平锁与非公平锁的释放过程没有任何差异
释放锁时调用的方法都是AQS的方法,所以非公平锁与非公平锁的释放过程没有任何差异
2. 非公平锁与非公平锁获取锁的差异
公平锁实现中,线程获得锁的顺序是按照请求锁的顺序,按照“先来后到”的规则获取锁。如果线程竞争公平锁失败后,则都会到AQS同步队列队尾排队,将自己阻塞等待锁的使用资格,锁被释放后,会从队首开始查找可以获得锁的线程并唤醒。
而在非公平锁中,允许新线程请求锁时“插队”,不管AQS同步队列是否有线程在等待,新线程都会先尝试获取锁,如果获取锁失败后,才会进入AQS同步队列队尾排队。
主要差异有以下两处
(1)tryAcquire差异 (JDK17)
ReentrantLock.lock
@ReservedStackAccessfinal void lock() {if (!initialTryLock())acquire(1);}
AQS.acquire
public final void acquire(int arg) {if (!tryAcquire(arg))acquire(null, arg, false, false, false, 0L);}
公平锁
/*** Acquires only if thread is first waiter or empty*/protected final boolean tryAcquire(int acquires) {if (getState() == 0 && !hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}}
公平锁获取锁,若锁为无锁状态时,本着公平原则,新线程在尝试获得锁前,需先判断AQS同步队列中是否有线程在等待(hasQueuedPredecessors
),若有线程在等待,当前线程只能进入同步队列等待。若AQS同步无线程等待,则通过CAS抢占锁compareAndSetState
非公平锁
/*** Acquire for non-reentrant cases after initialTryLock prescreen*/protected final boolean tryAcquire(int acquires) {if (getState() == 0 && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}
非公平锁,不管AQS是否有线程在等待,若当前锁状态为无锁状态,则会先通过CAS抢占锁(compareAndSetState
)。
公平锁和非公平锁获取锁时,其他方法都是调用AQS的final方法,没有什么不同之处。