前言
本文是对之前AQS系列文章的一个小结,首先看看以下几个问题:
1、ReentrantLock和ReentrantReadWriteLock的可重入特性是如何实现的?
2、哪个变量控制着锁是否被占用?
3、多个线程竞争一个排它锁时,未抢到锁的线程是如何阻塞的?
4、读读真的可以一直共享不阻塞吗?
对于以上问题,你是否都能知道答案 ?是否都清楚其原理?如果是的话,就没必要阅读本文了,否则还请慢慢读来。
正文
可重入性是通过exclusiveOwnerThread和state一起实现的。在AQS的父类AbstractOwnableSynchronizer中有一个成员变量exclusiveOwnerThread来存放获取到独占锁时的线程,可重入的特性就是通过此变量实现的。如果根据state判断当前锁已被占用,那么再判断这个变量中存的是不是当前线程,如果是则获取到锁,锁计数+1,否则获取不到锁。(此处针对的是排它锁,共享锁不属于这个范畴)
state为volatile int类型,用于记录加锁状态和重入次数,但针对不同的实现类其记录方式又不同,下面分别进行说明:
【ReentrantLock】 : state用于记录锁的持有状态和重入次数 ,state=0表示没有线程持有锁;state=1表示有一个线程持有锁;state=N表示exclusiveOwnerThread这个线程N次重入了这个锁。
【ReentrantReadWriteLock】 : state用于记录读写锁的占用状态和持有线程数量(读锁)、重入次数(写锁) ,state的高16位记录持有读锁的线程数量,低16位记录写锁线程重入次数,如果这16位的值是0,表示没有线程占用锁,否则表示有线程持有锁。另外针对读锁,每个线程获取到的读锁次数由本地线程变量中的HoldCounter记录。
【Semaphore】 : state用于计数。 state=N表示还有N个信号量可以分配出去,state=0表示没有信号量了,此时所有需要acquire信号量的线程都等着;
【CountDownLatch】 :state也用于计数 ,每次countDown都减一,减到0的时候唤醒被await阻塞的线程。
waitStatus为volatile int 类型,有5种值,作用分别为:
1: CANCEL,即取消状态,这种状态的节点是无效节点,在执行时会直接略过,一般只有在特殊的异常场景中才会出现这种状态;
0:初始化状态,新建的Node节点都是这个状态;
-1:SIGNAL,即可唤醒状态,如果一个节点加锁失败需进入阻塞状态,必须先将它的前一个节点置为-1,自己才会进入park状态,在AQS中搜parkAndCheckInterrupt()可以发现,只要是这个方法出现的地方,前面一定有shouldParkAfterFailedAcquire(p, node)方法先将p的waitStatus置为-1;
-2:CONDITION,跟条件队列相关的状态,ReentrantReadWriteLock和ReentrantLock中暂未涉及;
-3:PROPAGATE,可传播状态,没看出来有什么用处。
当第一个线程过来获取锁时,是不会初始化队列的,只是将exclusiveOwnerThread这个变量变为当前线程(独占锁的情况下),此时head和tail都是null;
第一个线程没执行完,第二个线程又来了,这时会初始化队列,new一个空Node赋值给head和tail,然后在tail(也就是head)后面拼上这个要排队的线程(详见下面的enq方法),此时形成了一个有两个节点的双向队列,head是new Node(),tail是新加入需排队的Node;然后再获取不到锁就会将head的waitStatus置为-1,自身挂起,等待unparkSuccessor(head)来唤醒。
addWaiter方法:如果tail不为空,则将当前的node节点赋值给tail,原先的tail变为新tail在链表上的前置节点。由此可知此链表的新增方式是后入式。这也解释了为什么唤醒线程时是从tail往前遍历找排在最前面符合条件的node节点。
1 private Node addWaiter(Node mode) { 2 Node node = new Node(Thread.currentThread(), mode); 3 // Try the fast path of enq; backup to full enq on failure 4 Node pred = tail; 5 if (pred != null) { 6 node.prev = pred; 7 if (compareAndSetTail(pred, node)) { 8 pred.next = node; 9 return node; 10 } 11 } 12 enq(node); 13 return node; 14 }
在AQS类中的enq方法代码如下:跟addWaiter类似,只是多了一步初始化head/tail的步骤。
1 private Node enq(final Node node) { 2 for (;;) { 3 Node t = tail; 4 if (t == null) { // Must initialize 5 if (compareAndSetHead(new Node())) // 初始化头节点,这时head指针指向的就是new Node()对象 6 tail = head; // 将tail也一起初始化了,这时会继续再走一遍for循环 7 } else { 8 node.prev = t; // 先将node的prev指针指向局部变量t; 9 if (compareAndSetTail(t, node)) { // 将tail所在内存地址的对象值换成node 10 t.next = node; // 将tail的next指针指向node节点,这时node节点就成了新的队尾了 11 return t; // 返回旧的队尾t 12 } 13 } 14 } 15 }
ReentrantReadWriteLock中,读锁和读锁并不是永远都可以同时进行,如果当前运行的是读锁,而后面第一个排队的是写锁,那么再来新的读锁会进入链表排队阻塞而不是直接获取读锁,因为不这样设定则可能存在由于一直有读锁过来导致后面的写锁总是处于阻塞状态获取不到锁。
可参见readShouldBlock方法的非公平读锁实现-apparentlyFirstQueuedIsExclusive()方法
1 final boolean apparentlyFirstQueuedIsExclusive() { 2 Node h, s; 3 return (h = head) != null && 4 (s = h.next) != null && 5 !s.isShared() && 6 s.thread != null; 7 }
LockSupport对线程的唤醒和挂起,是基于Thread直接信号量进行的,而wait/notify是基于对象的,需要依赖于Monitor对象。
park/unpark的语义更容易理解,park是针对当前线程阻塞,unpark是针对指定线程唤醒,不像wait/notify那样违反常识。
unpark为指定线程提供一个许可,有这个许可线程就能继续执行;而park是等待一个许可。unpark可以在park之前执行,线程不会阻塞。但unpark不能重入叠加,多个unpark也会被一个park直接用掉。
在底层,UNSAFE的park/unpark方法操作的是Paker对象(每个线程都有一个Parker实例),对象中维护了一个_counter变量,调用unpark方法后变量从0变为1,表示拥有继续执行的许可,执行之后恢复为0,调用park方法后变量如果不是1则线程阻塞,直到变为1再唤醒继续执行。
关于AQS暂时就写这些,后面还会针对JUC包里的其他工具类进行学习。