一、LockSupport 工具类
JDK 中的 rt.jar 包中的 LockSupport 类 是个很重要的工具类 ,它的主要作用是挂起 和 唤醒 线程,该工具类是创建 锁 和 其他同步类的基础。LockSupport 是使用 Unsafe 类实现的。
LockSupport 类与每个使用它的线程都会关联一个许可证。默认情况下,调用 LockSupport 类的方法的线程是不持有这个许可证的。源码中关于许可证的注释:
(Unlike with Semaphores though, permits do not accumulate. There is at most one.)
译:不过与 信号量 不同,许可证 不会累积。最多只能有一个。
1、void park() 方法
源码:
public static void park() {
UNSAFE.park(false, 0L);
}
(Unsafe 类里都是 native 方法,点不进去了。)
如果调用 park() 方法的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 时会马上返回,否则,调用线程会被禁止参与线程的调度,也就是被阻塞挂起。
例👀: 在 main 函数中调用 park 方法,当前线程会被挂起,因为默认情况下调用线程是不持有许可证的。
运行结果:
在其他线程调用 unpark(Thread thread)方法并且将当前线程作为参数时,调用 park 方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的 interrupt() 方法,设置了中断标志 或者 线程被虚假唤醒,则 阻塞线程也返回。所以在调用 park() 方法时 最好也使用循环条件判断方式。
✨需要注意的是,因 调用 park() 方法 而被阻塞的线程,被其他线程中断而返回时,并不会抛出 InterruptedException 异常。✨
2、void unpark(Thread thread) 方法
源码:
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
当一个线程调用 unpark 时,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有,如果 thread 之前因为调用 park() 方法而被挂起,则调用 unpark() 方法后,该线程被唤醒。如果 thread 之前没有调用 park ,则调用 unpark 方法后,再调用 park() 方法,会立刻返回。
例1 👀:修改之前的代码:
public class ParkTest {
public static void main(String[] args) {
// 使当前线程获取许可证
LockSupport.unpark(Thread.currentThread());
System.out.println("begin park!");
LockSupport.park();
System.out.println("end park!");
}
}
运行结果:
例2 👀 :
package LockPack;
import java.util.concurrent.locks.LockSupport;
public class ParkTest2 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("child thread begin park!");
// 调用park方法,挂起自己
LockSupport.park();
System.out.println("child thread unpark!");
}
});
// 启动子线程
thread.start();
// 主线程休眠 1s
Thread.sleep(1000);
System.out.println("main thread begin unpark!");
// 调用 unpark 方法让 child 线程持有许可证,然后 park 方法返回
LockSupport.unpark(thread);
}
}
运行结果:
以上代码创建了一个子线程 thread,子线程启动后,调用 park 方法,由于默认情况下子线程没有持有许可证,所以它会把自己挂起。接下来 CPU 给到主线程,主线程休眠 1s,再下来调用 unpark 方法让子线程持有许可证,这样的话,子线程调用的 park 方法就返回了。
🙌注意:park 方法返回时不会告诉我们是因为何种原因返回,所以调用者需要根据之前调用 park 方法的原因,再次检查条件是否满足,如果不满足则还需要再次调用 park 方法。
例3 👀:根据调用前后中断状态的对比,判断是否因为中断而返回。
import java.util.concurrent.locks.LockSupport;
public class ParkTest3 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("child thread begin park!");
// 调用 park 方法,挂起自己,只有被中断时才会退出循环
while (!Thread.currentThread().isInterrupted()){
LockSupport.park();
}
System.out.println("child thread unpark!");
}
});
// 启动子线程
thread.start();
// 主线程休眠 1s
Thread.sleep(1000);
System.out.println("main thread begin unpark!");
// 中断子线程
thread.interrupt();
}
}
运行结果:
3、void parkNanos(long nanos) 方法
源码:
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
和 park 方法类似,如果调用 park 方法的线程已经拿到了与 LockSupport 关联的许可证,则 调用 LockSupport.parkNanos(long nanos)
方法后会立马返回。该方法的不同之处在于 如果没有拿到许可证,则调用线程会被挂起 nanos 时间后修改为自动返回。
✨另外 park 方法还支持带有 blocker 参数的方法 void park(Object blocker)
方法,当线程在没有持有许可证的情况下调用 park 方法而被阻塞挂起时,这个 blocker 对象会被记录到该线程内部。✨
park(Object blocker) 方法源码:
public static void park(Object blocker) {
// 获取调用线程
Thread t = Thread.currentThread();
// 设置该线程的 blocker 变量
setBlocker(t, blocker);
// 挂起线程
UNSAFE.park(false, 0L);
// 线程被激活后清除 blocker 变量,因为一般是在线程阻塞时才分析原因
setBlocker(t, null);
}
Thread 类里有个变量 parkBlocker,用来存放 park 方法传递的 blocker 对象 :
volatile Object parkBlocker;
使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用 getBlocker(Thread)
的方法来获取 blocker 对象的,所以 JDK 推荐我们使用带有 blocker 参数的 park 方法,并且把 blocker 方法设置为 this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。
例如以下代码:
package LockPack;
import java.util.concurrent.locks.LockSupport;
public class ParkTest4 {
public void testPark(){
LockSupport.park();
}
public static void main(String[] args) {
ParkTest4 parkTest4=new ParkTest4();
parkTest4.testPark();
}
}
jstack pid 命令的使用
在 dos 模式下,先进入 jdk 安装目录,输入 jps 获取当前进程信息,可以看到,这个类的进程 id 是 17060,然后在输入 jstack 17060,就可以看到进程下每个线程的状态。
修改代码为:
LockSupport.park(this);
重复上述过程,观察线程被阻塞原因,可以看到:
✨ 使用带 blocker 参数的 park 方法,线程堆栈可以提供更多有关线程阻塞对象的信息。✨
4、void parkUntil(Object blocker,long deadline) 方法
源码:
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
参数 deadline 的单位为 ns,该时间是从 1970 年到现在某一个时间点的毫秒值。这个方法和 parkNanos(Object blocker,long nanos)
方法的区别是,后者是从当前算 等待 nanos 时间,而前者是指定一个时间点,需要计算那个时间点转换为 从 1970 年到这个时间点的总毫秒数。
二、抽象同步队列 AQS 概述
1、AQS——锁的底层支持
AbstractQueuedSynchronizer 抽象同步队列,简称 AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用 AQS 实现的。
(1) AQS 类图
AQS 是一个 FIFO 的 双向队列 ,其内部通过节点 head 和 tail 记录队首 和 队尾元素,队列元素的类型是 Node,其中 Node 中的 thread 变量用来存放进入 AQS 队列里面的线程:Node 节点内部的 SHARED 用来标记 该线程是 获取共享资源时被阻塞挂起后放入 AQS 队列的,EXCLUSIVED 用来标记 线程是获取独占资源时 被挂起后放入 AQS 队列的;waitStatus 记录当前线程等待状态,可以为 CANCELLLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev 记录当前节点的前驱节点,next 记录当前节点的后继节点。
在 AQS 中维持了一个 单一的 状态信息 state :
/**
* The synchronization state.
*/
private volatile int state;
可以通过 getState、setState、compareAndSetState 方法修改 state 的值。
对于读写锁 ReentReadWriteLock 来说,state 的高 16 位表示 读状态,即获取 读锁 的次数,低 16 位表示写锁的线程的可重入次数。
对于 semaphore 来说,state 表示当前可用信号的个数。
对于 CountDownlatch 来说, state 表示计数器当前的值。
(2)条件变量 ConditionObject
AQS 有个内部类 ConditionObject,用来结合锁 实现线程同步。ConditionObject 可以直接访问 AQS 对象内部的变量,比如 state 状态值和 AQS 队列。ConditionObject 是条件变量,每个条件变量对应一个条件队列(单向链表队列) ,(🙌 注意区分 AQS 队列 和 条件队列) 用来存放调用条件变量的 await 方法后 被阻塞的线程。这个条件队列的头、尾元素分别是 firstWaiter 和 lastWaiter。
✨对于 AQS 来说,线程同步的关键是对状态值 state 进行操作。✨
根据 state 是否属于一个线程,操作 state 的方式分为 独占方式 和 共享方式。
使用独占方式获取的资源是与具体线程绑定的,就是说 如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作 state 就会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如,独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,在 AQS 内部会首先使用 CAS 操作把 state 的值从 0 变为1 ,然后设置当前锁的持有者为当前线程,当该线程再一次获取锁时发现它就是锁的持有者,则会把状态值从 1 变为 2,也就是设置可重入数,而当另一个线程获取锁时,发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。
对应共享方式的资源 与 具体线程是不相关的,当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取许可。比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。
👉独占方式👈
在独占方式下,获取和释放资源 的流程如下:
- 当一个线程调用 acquire(int arg) 方法 获取独占资源时,会首先使用 tryAcquire 方法尝试获取资源,具体是 设置状态变量 state 的值 ,成功 则直接返回,失败则将当前线程封装为 Node.EXCLUSIVE 的 Node 节点后 插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this) 方法挂起自己。
源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))、
//(1)
selfInterrupt();
// 成功则直接返回
}
(1):
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
- 当一个线程调用 release(int arg) 方法时,会尝试使用 tryRelease 释放资源,具体是设置状态变量 state 的值,然后调用
LockSupport.unpark(thread)
方法激活 AQS 队列里 被阻塞的一个线程。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。
源码:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
👉共享方式👈
在共享方式下,获取和释放资源的流程如下:
- 当一个线程调用 acquire(int arg) 方法获取共享资源时,会首先使用 tryAcquireShared 方法尝试获取资源,具体是设置状态变量 state 的值,成功 则直接返回,失败则将当前线程封装为 Node.SHARED 的 Node 节点后 插入到 AQS 阻塞队列的尾部,并调用 LockSupport.park(this) 方法挂起自己。
源码:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- 当一个线程调用 releaseShared(int arg) 方法时,会尝试使用 tryReleaseShared 释放资源,具体是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 方法激活 AQS 队列里 被阻塞的一个线程。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。
源码:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
需要注意的是,AQS 类并没有提供可用的 tryAcquire 、tryAcquireShared 和 tryRelease、tryReleaseShared 方法,正如 AQS 是锁 阻塞 和 同步器的基础框架一样,这些方法需要由具体子类来实现,子类在实现这些方法时要根据具体场景使用 CAS 算法尝试修改 state 状态值 ,成功则返回 true,否则返回 false。子类还需要定义调用这些方法时 state 状态值的增减代表什么含义。
👻比如:
- 继承自 AQS 实现的独占锁 ReentrantLock ,定义 当 status 为 0 时,表示 锁空闲,为 1 时表示锁已经被占用。在重写 tryAcquire 时,在内部需要使用 CAS 算法查看当前 state 是否为 0,如果为 0 则使用 CAS 设置为 1,并设置当前锁的持有者为当前线程,而后返回 true,如果 CAS 失败则返回 false。
- 继承自 AQS 实现的独占锁在实现 tryRelease 时,在内部需要使用 CAS 算法把当前 state 值从 1 修改为 0,并设置当前锁的持有者为 null,然后 返回 true,如果 CAS 失败,则返回 false。
- 继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里的读锁在重写 tryAcquireShared 时,首先看 写锁 是否被其他线程持有,如果是 则直接返回 false,否则使用 CAS 递增 state 的高 16 位(在 ReentrantReadWriteLock 中,state 的高 16 位为获取读锁的次数。)
- 继承自 AQS 实现的 读写锁 ReentrantReadWriteLock 里的读锁在重写 tryReleaseShared 时,在内部需要使用 CAS 算法 把当前 state 的值的高 16 位减1 ,然后返回 true,如果 CAS 失败 则返回 false。
注意到还有 :
public final void acquireInterruptibly(int arg)
public final void acquireSharedInterruptibly(int arg)
不带 Interruptibly 关键字 的意思是 不对中断进行响应,也就是 线程在调用不带 Interruptibly 关键字的方法 获取资源时 或者 获取资源失败被挂起时,其他线程中断了该线程,该线程不会因为被中断而抛出异常,它还是继续获取资源 或者 被挂起。而带 Interruptibly 关键字的方法,是要抛出 InterruptedException 异常而返回的。
来看看如何维护 AQS 提供的队列:
👉入队 enq(final Node node) 👈
当一个线程获取锁失败后,该线程会被转换为 Node 节点,然后就会使用 enq(final Node node)
方法将该节点插入到 AQS 的阻塞队列。
源码:
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;
//(1)
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
(1):
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
第一次循环时,队列 头 尾 都指向 null,然后节点 t 也会指向 head,接下来通过 CAS 算法设置一个哨兵节点为头节点,如果 CAS 成功,则让尾部节点也指向哨兵节点,如图所示:
这样只是插入了一个哨兵节点,还需要插入 node 节点,所以第二次循环中,t 先指向尾节点,接下来设置前驱节点为尾节点,再下来,t 引用存放的是 tail 的值,通过 CAS 操作把 node 的值存在 tail 引用中,即 tail 指向 node;CAS 成功后,再设置哨兵节点的后驱节点为 node,这样就完成 双向链表的插入 啦:
2、AQS——条件变量的支持
正如 notify 和 wait,是配合synchronized 内置实现线程间同步 ,条件变量的 signal 和 await 方法也是用来配合 使用 AQS 实现的锁,实现线程间同步的。
它们的不同在于 synchronized 同时只能与一个共享变量的 notify 或 wait 方法实现同步,而 AQS 的一个锁可以对应多个条件变量 。
调用共享变量的 notify 和 wait 方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的 signal 和 await 方法前也必须先获取条件变量对应的锁。否则会抛出 java.lang.IllegalMonitorStateException 异常。
例 👀:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionObjectTest {
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
// // new 一个在 AQS 内部声明的 ConditionObject 对象
Condition condition=lock.newCondition();
lock.lock();
try{
System.out.println("begin wait!");
// 阻塞挂起当前线程,
// 当其他线程调用条件变量的 signal 方法时
// 被阻塞的线程才会从 await 处返回
condition.await();
System.out.println("end wait!");
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
lock.lock();
try{
System.out.println("begin signal!");
condition.signal();
System.out.println("end signal!");
} catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
运行结果:
✨ 这里的 Lock 对象等价于 synchronized 加上共享变量,调用 lock.lock() 方法就相当于进入了 synchronized 块,调用 lock.unlock() 方法就相当于退出 synchronized 块。调用条件变量的 await() 方法就相当于调用共享变量的 wait() 方法,调用条件变量的 signal() 方法就相当于调用共享变量的 notify() 方法。调用条件变量的 signalAll() 方法就相当于调用共享变量的 notifyAll() 方法。✨
(1)await() 方法
源码:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 创建新的 node 节点,并尾插
Node node = addConditionWaiter();
// 释放当前线程的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 阻塞当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
当线程调用变量的 await() 方法时,在内部会构造一个类型为 Node.CONDITION 的 node 节点,然后将该节点插入到条件队列的末尾,之后当前线程会释放获取的锁,也就是 会操作锁对应的 state 变量的值。并被阻塞挂起。(确实像极了 Object 的 waIt)这时,如果有其他线程调用 lock.lock() 尝试获取锁,就会有一个线程获取到锁,其他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里,并做自旋 CAS 尝试获取锁。
(2)signal() 方法
源码:
/**
* Moves the longest-waiting thread, if one exists, from the
* wait queue for this condition to the wait queue for the
* owning lock.
*
* @throws IllegalMonitorStateException if {@link #isHeldExclusively}
* returns {@code false}
*/
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 将条件队列头元素移动到 AQS 队列
doSignal(first);
}
注释译为:将等待时间最长的线程(如果存在)从该条件的等待队列 移至 拥有锁的等待队列。 注释里说的 ”等待队列“ 其实就是上面说的 AQS 维护的队列。
源码中的注释:
/**
* Wait queue node class.
*
* <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
* Hagersten) lock queue. CLH locks are normally used for
* spinlocks. We instead use them for blocking synchronizers, but
* use the same basic tactic of holding some of the control
* information about a thread in the predecessor of its node. A
* "status" field in each node keeps track of whether a thread
* should block. A node is signalled when its predecessor
* releases. Each node of the queue otherwise serves as a
* specific-notification-style monitor holding a single waiting
* thread. The status field does NOT control whether threads are
* granted locks etc though. A thread may try to acquire if it is
* first in the queue. But being first does not guarantee success;
* it only gives the right to contend. So the currently released
* contender thread may need to rewait.
*
* <p>To enqueue into a CLH lock, you atomically splice it in as new
* tail. To dequeue, you just set the head field.
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*
* <p>Insertion into a CLH queue requires only a single atomic
* operation on "tail", so there is a simple atomic point of
* demarcation from unqueued to queued. Similarly, dequeuing
* involves only updating the "head". However, it takes a bit
* more work for nodes to determine who their successors are,
* in part to deal with possible cancellation due to timeouts
* and interrupts.
*
* <p>The "prev" links (not used in original CLH locks), are mainly
* needed to handle cancellation. If a node is cancelled, its
* successor is (normally) relinked to a non-cancelled
* predecessor. For explanation of similar mechanics in the case
* of spin locks, see the papers by Scott and Scherer at
* http://www.cs.rochester.edu/u/scott/synchronization/
*
* <p>We also use "next" links to implement blocking mechanics.
* The thread id for each node is kept in its own node, so a
* predecessor signals the next node to wake up by traversing
* next link to determine which thread it is. Determination of
* successor must avoid races with newly queued nodes to set
* the "next" fields of their predecessors. This is solved
* when necessary by checking backwards from the atomically
* updated "tail" when a node's successor appears to be null.
* (Or, said differently, the next-links are an optimization
* so that we don't usually need a backward scan.)
*
* <p>Cancellation introduces some conservatism to the basic
* algorithms. Since we must poll for cancellation of other
* nodes, we can miss noticing whether a cancelled node is
* ahead or behind us. This is dealt with by always unparking
* successors upon cancellation, allowing them to stabilize on
* a new predecessor, unless we can identify an uncancelled
* predecessor who will carry this responsibility.
*
* <p>CLH queues need a dummy header node to get started. But
* we don't create them on construction, because it would be wasted
* effort if there is never contention. Instead, the node
* is constructed and head and tail pointers are set upon first
* contention.
*
* <p>Threads waiting on Conditions use the same nodes, but
* use an additional link. Conditions only need to link nodes
* in simple (non-concurrent) linked queues because they are
* only accessed when exclusively held. Upon await, a node is
* inserted into a condition queue. Upon signal, the node is
* transferred to the main queue. A special value of status
* field is used to mark which queue a node is on.
*
* <p>Thanks go to Dave Dice, Mark Moir, Victor Luchangco, Bill
* Scherer and Michael Scott, along with members of JSR-166
* expert group, for helpful ideas, discussions, and critiques
* on the design of this class.
*/
static final class Node {
Node 类是等待队列节点类 ,等待队列是“ CLH”(Craig,Landin 和* Hagersten)锁队列的变体。 CLH 锁通常用于 自旋锁 ,我们还用它们代替 阻塞同步器 ,但是使用相同的基本策略,即在其节点的前身中包含有关线程的某些控制信息。每个节点中的 “status 状态” 字段跟踪线程,表示线程是否应阻塞。 当节点 node 的前任predecessor 释放时,会被唤醒。否则,队列的每个节点都充当*特定通知样式的监视器,每个节点都持有一个等待线程。虽然状态字段不控制线程是否被授予锁等。如果节点在队列头部 first,线程可能会尝试获取资源 acquire。但是头节点并不能保证成功。 它仅授予竞争权。当前释放了资源的竞争者线程可能需要重新等待。
<p>
要加入CLH锁,您可以自动将其作为新的尾部拼接。要出队,您只需设置头字段。
<p>
插入到 CLH 队列中,只需要对 尾节点 tail 执行一个原子性操作,因此存在一个简单的分界的原子点,即从未排队到排队。同样,出队仅涉及更新头节点 head 。但是,节点需要花费更多的精力来确定其后继者是谁,部分原因是由于超时和中断而可能导致的取消。
<p>
主要需要 前驱 prev(在原始CLH锁中不使用)来处理取消。如果取消某个节点,则其后继节点(通常)会重新链接到一个未取消的前任节点。
<p>
我们也使用 后继 next 以实现阻塞机制。 每个节点的线程 ID 保留在其自己的节点中,因此前任 predecessor 通过遍历 下一个链接 next 确定它是哪个线程,从而唤醒下一个节点的。确定后继者必须避免与新排队的节点竞争以设置其前任的“下一个”字段。 必要时,当一个节点的后继 successor 看起来可能会是 null 时,自动更新 尾部 tail 从而 向后检查,来解决此问题。 (或者换句话说,next-links是一种优化,因此我们通常不需要向后扫描。)
<p>
由于我们必须让取消状态的节点出队 poll,因此我们可能会遗漏一个 在前面 或者 后面 被取消的节点。要解决此问题,必须始终 unpark 取消节点的后继者 successor,让它们拥有稳定的前任。
<p>
CLH 队列需要一个虚拟标头节点才能开始。但是我们不会在构造方法中创建它们,因为如果从不发生冲突,那将会浪费时间精力。而是,在第一次出现冲突时 构造节点,设置头和尾指针。
<p>
等待条件的线程使用相同的节点,但是使用附加的链接。条件只需要在简单(非并行)链接队列中链接节点,因为仅当它们被独占时才可以访问它们。等待时,将节点插入条件队列。收到信号后,该节点被转移到主队列。 status 字段的特殊值用于标记节点所在的队列。
当另外一个线程调用条件变量的 signal 方法时,在内部会把条件队列里队头的一个线程节点从条件队列里移除,并放入 AQS 的阻塞队列里,然后激活这个线程。
需要注意的是,AQS 只提供了 ConditionObject 的实现,并没有提供 newCondition 方法,该方法用来 new 一个 ConditionObject 对象,需要 AQS 的子类来提供 newCondition 方法。
🎭 总结:
一个锁对应一个 AQS 阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
3、基于 AQS 实现自定义同步器
来基于 AQS 实现一个不可重入的独占锁,自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。我们定义:
- state 为 0 表示锁没有被线程持有
- state 为 1 表示锁已经被某一个线程持有。
由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数,而且,自定义的锁支持条件变量。
(1)代码实现
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 基于 AQS 实现一个不可重入的独占锁,
* 自定义 AQS 需要重写一系列函数,还需要定义原子变量 state 的含义。我们定义:
* state 为 0 表示锁没有被线程持有
* state 为 1 表示锁已经被某一个线程持有。
* 由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数,
* 而且,自定义的锁支持条件变量。
*/
public class NonReentrantLock implements Lock,Serializable {
// 内部帮助类
private static class Sync extends AbstractQueuedSynchronizer{
@Override
// 锁是否已经被持有
protected boolean isHeldExclusively(){
return getState()==1;
}
// 如果 state 为0,则尝试获取锁
@Override
public boolean tryAcquire(int acquires){
assert acquires == 1;
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁
@Override
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
// 设置状态值 为 0
setState(0);
return true;
}
// 提供条件变量接口
Condition newCondition(){
return new ConditionObject();
}
}
// 创建 Sync 对象
private final Sync sync=new Sync();
@Override
public void lock() {
// 尝试获取资源,也就是设置 state 的值
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked(){
return sync.isHeldExclusively();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(timeout));
}
}
(2)使用自定义锁实现生产-消费模型
👇 生产 - 消费模型:
通过容器解决生产者和消费者的强耦合问题,生产者 和 消费者 之间不直接通讯,而通过 阻塞队列来进行通信。
代码:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
public class ProducerAndConsumer {
final static NonReentrantLock lock=new NonReentrantLock();
final static Condition notFull=lock.newCondition();
final static Condition notEmpty=lock.newCondition();
final static Queue<String> queue=new LinkedBlockingQueue<>();
final static int queueSize=10;
public static void main(String[] args) {
// 生产者
Thread producer=new Thread(new Runnable() {
@Override
public void run() {
// 获取独占锁
lock.lock();
try{
// 如果队列满了,则等待
// while 循环避免虚假唤醒
while (queue.size() == queueSize){
notEmpty.await();
}
// 添加元素到队列
queue.add("ele");
// 唤醒消费线程
notFull.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
}
});
// 消费者
Thread consumer=new Thread(new Runnable() {
@Override
public void run() {
// 获取独占锁
lock.lock();
try{
// 队列空,则等待
while (0 == queue.size()){
notFull.await();
// 消费一个元素
String ele=queue.poll();
// 唤醒生产线程
notEmpty.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
}
});
// 启动线程
producer.start();
consumer.start();
}
}
在 main 方法里,首先创建了 producer 生产者线程,在线程内部先调用 lock.lock();
获取独占锁,然后判断当前队列是否已满,如果满了就调用 notEmpty.await(); 阻塞挂起当前线程,如果队列不满,则直接向队列里添加元素,然后调用 notFull.signalAll() 唤醒所有因为无消费元素而被阻塞的线程,最后释放获取的锁。
然后,创建了 consumer 消费线程,在线程内部先调用 lock.lock();
获取独占锁 ,然后判断当前队列里面是不是有元素,如果队列为空,则调用 notFull.await() 阻塞挂起当前线程,如果队列不为空,就直接从队列里面获取并移除元素,然后唤醒因为队列满而被阻塞的线程,最后释放获取的锁。
(说白了就是:有两个条件变量,两个队列,一个队列 notFull 管”空“,空的时候,消费者线程封装成节点,到这个队列排队,而生产者正常生产完,就不空了,所以要从这个队列里出队一个,进行唤醒,进而消费;另一个队列 notEmpty 管”满“,满的时候,生产者线程封装成节点,到这个队列排队,而消费者正常消费完,就不满了,所以要从这个队列里出队一个,进行唤醒,进而生产。)