硬核干货:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer (2)

上面是一个简单的CLH队列锁的实现,内部类QueueNode只使用了一个简单的布尔值locked属性记录了每个线程的状态,如果该属性为true,则相应的线程要么已经获取到锁,要么正在等待锁,如果该属性为false,则相应的线程已经释放了锁。新来的想要获取锁的线程必须对tail属性调用getAndSet()方法,使得自身成为队列的尾部,同时得到一个指向前驱节点的引用pred,最后线程所在节点在其前驱节点的locked属性上自旋,直到前驱节点释放锁。上面的实现是无法运行的,因为一旦自旋就会进入死循环导致CPU飙升,可以尝试使用下文将要提到的LockSupport进行改造。

CLH队列锁本质是使用队列(实际上是单向链表)存放等待获取锁的线程,等待的线程总是在其所在节点的前驱节点的状态上自旋,直到前驱节点释放资源。从实际来看,过度自旋带来的CPU性能损耗比较大,并不是理想的线程等待队列的实现

j-a-q-s-ex-17

基于原始的CLH队列锁中提供的等待队列的基本原理,AQS实现一种了CLH锁队列的变体(Variant)。AQS类的protected修饰的构造函数里面有一大段注释用于说明AQS实现的等待队列的细节事项,这里列举几点重要的:

AQS实现的等待队列没有直接使用CLH锁队列,但是参考了其设计思路,等待节点会保存前驱节点中线程的信息,内部也会维护一个控制线程阻塞的状态值。

每个节点都设计为一个持有单独的等待线程并且"带有具体的通知方式"的监视器,这里所谓通知方式就是自定义唤醒阻塞线程的方式而已。

一个线程是等待队列中的第一个等待节点的持有线程会尝试获取锁,但是并不意味着它一定能够获取锁成功(这里的意思是存在公平和非公平的实现),获取失败就要重新等待。

等待队列中的节点通过prev属性连接前驱节点,通过next属性连接后继节点,简单来说,就是双向链表的设计

CLH队列本应该需要一个虚拟的头节点,但是在AQS中没有直接提供虚拟的头节点,而是延迟到第一次竞争出现的时候懒创建虚拟的头节点(其实也会创建尾节点,初始化时头尾节点是同一个节点)。

Condition(条件)等待队列中的阻塞线程使用的是相同的Node结构,但是提供了另一个链表用来存放,Condition等待队列的实现比非Condition等待队列复杂。

线程阻塞与唤醒

线程的阻塞和唤醒在JDK1.5之前,一般只能依赖于Object类提供的wait()、notify()和notifyAll()方法,它们都是JNI方法,由JVM提供实现,并且它们必须运行在获取监视器锁的代码块内(synchronized代码块中),这个局限性先不谈性能上的问题,代码的简洁性和灵活性是比较低的。JDK1.5引入了LockSupport类,底层是基于Unsafe类的park()和unpark()方法,提供了线程阻塞和唤醒的功能,它的机制有点像只有一个允许使用资源的信号量java.util.concurrent.Semaphore,也就是一个线程只能通过park()方法阻塞一次,只能调用unpark()方法解除调用阻塞一次,线程就会唤醒(多次调用unpark()方法也只会唤醒一次),可以想象是内部维护了一个0-1的计数器。

LockSupport类如果使用得好,可以提供更灵活的编码方式,这里举个简单的使用例子:

public class LockSupportMain implements Runnable { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); private Thread thread; private void setThread(Thread thread) { this.thread = thread; } public static void main(String[] args) throws Exception { LockSupportMain main = new LockSupportMain(); Thread thread = new Thread(main, "LockSupportMain"); main.setThread(thread); thread.start(); Thread.sleep(2000); main.unpark(); Thread.sleep(2000); } @Override public void run() { System.out.println(String.format("%s-步入run方法,线程名称:%s", FORMATTER.format(LocalDateTime.now()), Thread.currentThread().getName())); LockSupport.park(); System.out.println(String.format("%s-解除阻塞,线程继续执行,线程名称:%s", FORMATTER.format(LocalDateTime.now()), Thread.currentThread().getName())); } private void unpark() { LockSupport.unpark(thread); } } // 某个时刻的执行结果如下: 2019-02-25 00:39:57.780-步入run方法,线程名称:LockSupportMain 2019-02-25 00:39:59.767-解除阻塞,线程继续执行,线程名称:LockSupportMain

LockSupport类park()方法也有带超时的变体版本方法,遇到带超时期限阻塞等待场景下不妨可以使用LockSupport#parkNanos()。

独占线程的保存

AbstractOwnableSynchronizer是AQS的父类,一个同步器框架有可能在一个时刻被某一个线程独占,AbstractOwnableSynchronizer就是为所有的同步器实现和锁相关实现提供了基础的保存、获取和设置独占线程的功能,这个类的源码很简单:

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable { private static final long serialVersionUID = 3737899427754241961L; protected AbstractOwnableSynchronizer() { } // 当前独占线程的瞬时实例 - 提供Getter和Setter方法 private transient Thread exclusiveOwnerThread; protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; } protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; } }

它就提供了一个保存独占线程的变量对应的Setter和Getter方法,方法都是final修饰的,子类只能使用不能覆盖。

CLH队列变体的实现

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zywszj.html