
深入理解AbstractQueuedSynchronizer
前言
建议先看一下上一个分享:CAS实现原理
JUC中的许多并发工具类ReentrantLock,CountDownLatch等的实现都依赖AbstractQueuedSynchronizer
AbstractQueuedSynchronizer定义了一个锁实现的内部流程,而如何上锁和解锁则在各个子类中实现,典型的模板方法模式
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤
下面举个简单的例子说明一下
咖啡因饮料抽象类
茶饮料
咖啡饮料
测试类
这里有几个注意的地方
1. boilWater(煮水)和pourInCup(导入杯子)的修饰符为private,表示不让用子类直接用这些方法,也不能重写
2. prepareRecipe(准备食物)方法的修饰符为protected(不写默认为protected)和final,目的是为了让子类用但是不允许子类覆盖、
3. brew(酿造)和addCondiments(添加调味料)的修饰符为abstract,表示子类必须自己定义实现
属性介绍
基于jdk1.8.0_20,AQS内部维护一个FIFO的队列,通过该队列来实现线程的并发访问控制,队列中的元素是一个Node节点
这里先说一下park方法,后面会提到,park方法是Unsafe类中的方法,与之对应的是unpark方法。简单来说,当前线程如果执行了park方法,也就是阻塞了当前线程,反之,unpark就是唤醒一个线程。
waitStatus表示节点的状态,包含的状态有
- CANCELLED 1 当前节点被取消
- SIGNAL 2 表示当前节点的的后继节点将要或者已经被阻塞,在当前节点释放的时候需要unpark后继节点
- CONDITION -2 表示当前节点在等待condition,即在condition队列中
- PROPAGATE -3 表示releaseShared需要被传播给后续节点(仅在共享模式下使用)
- 0 当前节点在队列中等待获取锁
再来看AbstractQueuedSynchronizer这个类的属性
AQS提供了独占锁和共享锁两种方式,每种方式都有响应中断和不响应中断的区别,所以AQS的锁可以分为如下四类
- 不响应中断的独占锁(acquire)
- 响应中断的独占锁(acquireInterruptibly)
- 不响应中断的共享锁(acquireShared)
- 响应中断的共享锁(acquireSharedInterruptibly)
而释放锁的方式只有两种
- 独占锁的释放(release)
- 共享锁的释放(releaseShared)
我们只看一下不响应中断的独占锁,其他的类似
不响应中断的独占锁
从加锁这一部分开始
1. 先尝试获取,如果获取到直接退出,否则进入2
2. 以独占模式将线程包装成Node放到队列中
3. 因为在放到队列的过程中,锁有可能释放了,所以再尝试获取,如果获取到锁则将当前节点设为head节点,退出,否则进入4
4. 一直尝试获取,如果满足阻塞条件,则阻塞,如果被唤醒,则继续尝试获取
5. 当获取到锁时,如果线程获取过程发生中断,则最后将中断补上,即执行selfInterrupt()方法
1. tryAcquire(尝试获取锁)是让子类实现的
这里通过抛出异常来告诉子类要重写这个方法,为什么不将这个方法定义为abstract方法呢?因为AQS有2种功能,独占和共享,如果用abstract修饰,则子类需要同时实现两种功能的方法,对子类不友好
2
3.4
5将中断补上
处理异常退出的node
取消有三种状态
1. 当前节点是tail
2. 当前节点不是head的后继节点,也不是tail
3. 当前节点是head的后继节点
当前节点是tail,将该节点的前继节点的next指向null,也就是把当前节点移出队列
当前节点不是head的后继节点,也不是tail,这里将node的前继节点的next指向了node的后继节点,即compareAndSetNext(pred, predNext, next)
当前节点是head的后继节点,直接unpark后继节点
注意的是最后两种状态都会执行node.next = node,即将next指针指向自己,这关系着后面唤醒的时候是从尾部向前遍历,并且修改指针的这些操作都是CAS操作,并不保证成功
这里要注意一点是从队尾向前遍历,不是从队首向后遍历,可以看一下cancelAcquire方法的处理过程,cancelAcquire只是设置了next的变化,没有设置prev的变化,在最后有这样一行node.next=node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的
接着来看解锁的过程
让子类去实现
后记
最后说一个用的特别巧的方法,即parkAndCheckInterrupt()方法
看acquireQueued这个方法,里面是一个死循环,但是并不会引起CPU使用率飙升,因为获取失败后会被挂起,这个判断是否中断的方法用的特别巧,先举2个例子
这里先说一下isInterrupted和interrupted的方法的异同点,2个方法都能返回线程是否是中断状态,所不同的是isInterrupted不会清除这种状态,而interrupted则会清除这种这种状态(即中断状态的复位),所以两次调用interrupted,第一次为true,第二次则为false
为什么AbstractQueuedSynchronizer要用Thread.interrupted()这种方法来返回中断状态呢,我再写2个例子
可以看到当线程被中断时,调用park()方法并不会被阻塞
park与wait的作用类似,但是对中断状态的处理并不相同。如果当前线程不是中断的状态,park与wait的效果是一样的;如果一个线程是中断的状态,这时执行wait方法会报java.lang.IllegalMonitorStateException,而执行park时并不会报异常,而是直接返回。
到这我们就能理解为什么要进行中断的复位了
1. 如果当前线程是非中断状态,则在执行park时被阻塞,返回中断状态false
2. 如果当前线程是中断状态,则park方法不起作用,返回中断状态true,interrupted将中断复位,变为false
3. 再次执行循环的时候,前一步已经在线程的中断状态进行了复位,则再次调用park方法时会阻塞
所以这里要对中断进行复位,是为了不让循环一直执行,让当前线程进入阻塞状态,如果不进行复位,前一个线程在获取锁之后执行了很耗时的操作,那么岂不是要一直执行死循环,造成CPU使用率飙升
文章转载自公众号:Java识堂
