Lock
Lock介绍
在Lock接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合所有并发场景。但是java5之后,Lock的出现可以解决synchronized在某些场景下的短板,它比synchronized更加灵活。
Lock
Lock本质上是一个接口,它定义了释放锁和获取锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现,实现Lock接口的类有很多的,以下为几个常见的锁实现
ReentrantLock:表示重入锁,它是唯一一个现实Lock接口的类,重入锁指的是线程在获取锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥,读和写互斥,写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock:StampeLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,就会引起写线程的饥饿。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程
互斥锁
锁的可重入性
可重入锁是指当一个线程调用object.lock()获取到锁,进入临界区后,再次调用object.lock(),让然可以获取到该锁。显然,通常的锁都要设计成可重入的,否则就会发生死锁
synchronized关键字,就是可重入锁。如下所示:
在一个synchronized方法method1()里面调用另外一个synchronized方法method2()。如果synchronized关键字不可重入,那么在method2()处就会发生阻塞,这显然不可行
public void synchronized method1() {
// ...
method2();
// ...
}
public void synchronized method2() {
// ...
}
ReentrantLock重入锁
重入锁,表示支持重新进入的锁,也就是说,如果当前线程t1通过调用lock方法获取了锁之后,再次调用lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized和ReentrantLock都是可重入锁。很多同学不理解为什么锁会存在重入的特性,那是因为对于同步锁的理解程度还不够,比如在下面这类的场景中,存在多个加锁的方法是相互调用的,其实就是一种重入特性的场景
重入锁的设计目的
比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获取demo2的对象锁而阻塞,就会产生死锁,。重入锁的设计目的是避免线程的死锁
public class ReentrantDemo{
public synchronized void demo(){
System.out.println("begin:demo");
demo2(); }
public void demo2(){ System.out.println("begin:demo1"); synchronized (this){
}
}
public static void main(String[] args) {
ReentrantDemo rd=new ReentrantDemo();
new Thread(rd::demo).start(); }
}
ReentrantLock的使用案例
public class AtomicDemo {
private static int count=0;
static Lock lock=new ReentrantLock();
public static void inc(){
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->{AtomicDemo.inc();}).start();;
} Thread.sleep(3000);
System.out.println("result:"+count);
}
}
上面这个代码,如果不加lock的话,是会发生线程不安全问题,也就是+的数字最后可能不是1000,但是加上lock锁之后,就会跟synchronized加锁实现的效果一样
思考锁的实现
互斥
- 锁的互斥特性-》共享资源()-》标记(0无锁,1代表有锁)
- 没有抢占到锁的线程?-》释放cpu资源,[等待 -> 唤醒]
- 等待的线程怎么存储?-》数据结构去存储一些列等待中的线程,FIFO(等待队列)
- 公平和非公平(能否插队)
- 重入的特性(识别是否是同一个人?ThreadID)
技术方案
- volatile state=0(无锁),1表示是持有锁,>1代表重入
- wait/notify | condition需要唤醒指定线程。[lockSupport.park(); ->unpark(thread)] unsafe类中提供的一个方法
- 双向链表
- 逻辑层面去实现
- 在某一个地方存储当前获取锁的线程ID,判断下次抢锁的
类继承层次
在正式介绍锁的实现原理之前,先看一下Concurrent包中的与互斥锁(ReentrantLock)相关的类的继承层次,如下图所示:
ReentrantLock的实现原理
我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。那么在ReentrantLock中,多线程竞争重入锁的时候,竞争失败的线程是如何实现阻塞以及被唤醒的?
AQS是什么
在Lock中,用到了一个同步队列AQS,全称AbstractQueuedSynchronizer,它是一个同步工具也是Lock用来实现线程同步的核心组件。,如果你搞懂了AQS,那么J.U.C中绝大部分的工具都能轻松掌握。
AQS的两种功能
从使用层面来说,AQS的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁。
共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
AQS的内部实现
AQS队列内存维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后续节点和直接前驱节点,所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个Node其实是由线程封装,当前线程争抢锁失败后,会封装成Node加入到AQS队列中去。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)
初始的时候,head=tail=Null;然后,在往队列中加入阻塞的线程时,会新建一个空的Node,让head和tail都指向这个空Node;之后,在后面加入被阻塞的线程对象。所以,当head=tail的时候,说明队列为空。
首先看一下Lock接口的定义:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
根据上面给的示例大家也知道,最常用的方法就是lock和unlock。lock不能被中断,对应的lockInterruptibly()可以被中断。
ReentrantLock本身没有代码逻辑,实现都在其内部类Sync中
我们先从lock方法进入看流程:
public void lock() {
sync.lock();
}
sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,我们前面说过AQS是一个同步队列,它能够实现线程的阻塞和唤醒,但它并不具备业务能力,所以在不同的同步场景中,会继承AQS来实现对应的功能
sync有两个具体的实现类,分别是:
- NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列是否存活其他线程等待,新线程都有灰机抢占锁
- FailSync:表示所有线程严格按照FIFO来获取锁
NofairSync非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
看到lock方法:
- 非公平锁和公平锁最大的区别在于,在非公平锁中抢占锁的逻辑是,不管有没有线程排队,我先上来cas去抢占一下
- cas成功,就表示成功获得了锁
- cas失败,调用acquire(1)走锁竞争逻辑
CAS的实现原理(compareAndSetState)
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通过cas乐观锁的方式来做比较替换,这段代码的意思是,如果当前内存中的state的值和预期值expect相等,则替换为update。更新成功返回true,否则返回false
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到state这个属性的意义。
state是AQS中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
- 当state=0时,表示无锁状态
- 当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,线程同步,内存屏障
而CAS就是Unsafe类中提供了一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的headOffset的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值var4相等,则更新为新的期望值var5,如果更新成功,则返回true,否则返回false。
stateOffset
一个Java对象可以看成是一段内存,每个字段都按照一定的顺序放在这段内存里,通过这个方法可以准确的告诉你某个字段相对于对象的启始内存地址的字节偏移。用于在后面compareAndSwapInt中,去根据偏移量找到对象在内存中的具体位置
所以stateOffset表示state这个字段在AQS类的内存中相对于该类的首地址的偏移量
compareAndSwapInt
在unsafe.cpp文件中,可以找到compareAndSwarpInt的实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobjectunsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //将 Java 对象解析成 JVM 的 oop(普通
对象指针),
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p
和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比较并替换, x 表
示需要更新的值,addr 表示 state 在内存中的地址,e 表示预期值
UNSAFE_END
AQS.acquire
acquire是AQS中的方法,如果CAS操作未能成功,说明state已经不为0,此时继续acquire(1)操作
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法主要逻辑是:
- 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
- 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
- acquireQueued,将Node作为参数,通过自旋去尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这个方法的作用是尝试获取锁,如果成功返回true,不成功返回false
接下来看一下nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
//获取当前执行的线程
final Thread current = Thread.currentThread();
//获取state的值
int c = getState();
//表示无锁
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;
}
到这里tryAcquire方法就完事了
接下来看一下addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为Node
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//tail是AQS中表示同比队列队尾的属性,默认是null
if (pred != null) {//tail不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的Node的prev指向tail
if (compareAndSetTail(pred, node)) {//通过cas把node加入到AQS队列,也就是设置为tail
pred.next = node;//设置成功之后,把原tail节点的next指向当前node
return node;
}
}
enq(node);//tail=null,把node添加到同步队列
return node;
}
enq方法
private Node enq(final Node node) {
//自旋
for (;;) {
//tail默认是null
Node t = tail;
if (t == null) { // Must initialize
//因为tail默认是null,所以首次一定会进来
//也就是首次一定会把一个空的node设置为head
//这里compareAndSetHead也是比较替换
if (compareAndSetHead(new Node()))
//到这里就是tail和head都会指向new Node
tail = head;
} else {
//进入到这里是第二次循环了,这里t就不是空了,node的上一个节点指向t
node.prev = t;
//compareAndSetTail也是比较替换,把tail节点赋值为新的传入的node
if (compareAndSetTail(t, node)) {
//这里t指的是原来的tail节点,tail指向一开始的new Node
t.next = node;
return t;
}
}
}
}
private final boolean compareAndSetHead(Node update) {
//当前的head字段,和null值比对,默认是null,所以相等,所以赋值为update,也就是new node()
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
//当前的tail字段和期望值exepct,即t进行比较,一定是相等的,以为t=tail么,所以更新赋值为update,
//即新传进来的node
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
enq就是通过自旋操作把当前节点加入到队列中
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过addWaiter方法把线程添加到链表后,会接着把Node作为参数传递给acquireQueued方法,去竞争锁
- 获取当前节点的prev节点
- 如果prev节点为head节点,那么它就有资格去争抢锁,调用tryAcquire抢占锁
- 抢占锁成功之后,把获得锁的节点设置为head,并且移除原来的初始化head节点
- 如果获得锁失败,则根据waitStatus决定是否需要挂起线程
- 最后,通过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);
}
}
tryAcquire
这个方法在前面分析过,就是通过state的状态来判断是否处于无锁状态,然后通过cas进行竞争锁操作。成功表示获得锁,失败表示获得锁失败
shouldParkAfterFailedAcquire
如果ThreadA的锁还没有释放的情况下,ThreadB和ThreadC来争抢锁肯定会失败,那么失败以后会调用shouldParkAfterFailedAcquire方法
Node有5种状态,分别是:CANCELLED(1),SIGNAL(1),CONDITION(-2),PROPAGATE(-3),默认状态(0)
CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取 消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状 态后的结点将不会再变化
SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
CONDITION: 和 Condition 有关系
PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态
0:初始状态
这个方法主要作用通过Node的状态来判断,ThreadA竞争锁失败以后是否应该被挂起。
-
如果ThreadA的pred节点状态为SIGNAL,那就表示可以放心挂起当前线程
-
通过循环扫描链表把CANCELLED状态的节点移除
-
修改pred 节点的状态为 SIGNAL,返回 false
返回false时,也就是不需要挂起,返回true,则需要调用 parkAndCheckInterrupt挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前置节点waitStatus
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;//返回true,意味着可以直接放心的挂起了
if (ws > 0) {//ws大于0,意味着prev节点取消了排队,直接移除这个节点就行
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
//相当于:pred=pred.prev;node.prev=rred
} while (pred.waitStatus > 0);//这里采用循环,从双向列表中移除CANCELLED的节点
pred.next = node;
} else {//利用cas设置prev节点的状态为SIGNAL(-1)
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
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 类是 Java6 引入的一个类,提供了基本的线程同步原语。LockSupport 实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute,long time);
unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有 点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。 permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已 经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都 有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积
锁释放流程
如果这个时候ThreadA释放锁了,那么我们来看锁被释放后会产生什么效果
unlock:
public void unlock() {
sync.release(1);
}
release:
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放锁成功
Node h = head;//得到aqs中head节点
if (h != null && h.waitStatus != 0)//如果head节点不为空并且状态!=0 调用unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值 (参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时 候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下 才会返回 true。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;//获得head节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//设置head节点状态为0
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;//得到head节点的下一个节点
if (s == null || s.waitStatus > 0) {
//如果下一个节点为null或者status>0表示cancelled状态
//如果从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0的节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//next节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}
为什么在释放锁的时候是从 tail 进行扫描
我们再回到 enq 那个方法、。在标注为红色部分的代码来看一个新的节点是如何加入到链表中的
- 将新的节点的 prev 指向 tail
- 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
- t.next=node;设置原 tail 的 next 节点指向新的节点
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; }
} }
}
在 cas 操作之后,t.next=node 操作之前。 存在其他线程调用 unlock 方法从 head 开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。 就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题
通过锁的释放,原本的结构就发生了一些变化。head节点的waitStatus变成了0,ThreadB被唤醒
原本挂起的线程继续执行
通过 ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行,应该从哪里执 行大家还有印象吧。 原来被挂起的线程是在 acquireQueued 方法中,所以被唤 醒以后继续从这个方法开始执行
acquireQueued 这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。
由于ThreadB 的 prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这 个时候调用 tryAcquire 方法时,可以顺利获取到锁
- 把 ThreadB 节点当成 head
- 把原 head 节点的 next 节点指向为 null
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; for (;;) {
node) &&
final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p,
parkAndCheckInterrupt()) interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
- 设置新 head 节点的 prev=null
- 设置原 head 节点的 next 节点为 null
快写猝死了,剩下的后面写吧。。。