上篇在介绍Conditon时,关于Condition的实现,提到了AQS队列同步器,这篇就简单介绍下。
1、AbstractQueuedSynchronizer
队列同步器,简称AQS,是用来构建锁或者其他同步组件的基础框架,拿一个int成员变量表示同步状态,内置的FIFO队列完成资源获取线程的排队工作,JUC作者希望它能够实现大部分同步需求的基础。
刚在Condition的实现分析中也见识到了lock.newConditon()返回了一个新的CondtionObject对象,这是一个AQS内部类。不仅在Conditon,在FutureTask、ReentrantReadWriteLock、Semaphore、CountDownLatch中均有体现。
AQS一般不直接被程序员使用,它是实现各种同步组件的关键。同步组件是面向程序员的,AQS是面向同步组件实现的实现,屏蔽了同步状态管理、线程排队、等待唤醒等底层操作。
①AQS的接口与示例
AQS抽象类定义同步器设计基于模板方法模式,使用者需继承同步器并重写指定方法,随后将同步器组合在自定义同步组件实现中,调用同步器提供的的模板方法,这些方法将会调用重写过的方法。
重写同步器指定方法,要使用getState():获取当前同步状态、setState(int newState):设置当前同步状态、compareAndSetState(int expect, int update)使用CAS设置当前状态。
可重写的方法:
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
protected boolean isHeldExclusively()
提供的模板方法
void acquire(int arg)
void acquireInterruptibly(int arg)
boolean tryAcquireNanos(int arg, long nanos)
void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean tryAcquireSharedNanos(int arg, long nanos)
boolean release(int arg)
boolean releaseShared(int arg)
Collection<Thread> getQueuedThreads()
以Semaphore为例,可看到重写方法tryAcquireShared被重写,模板方法acquireShared调用重写过的tryAcquireShared。
重写tryAcquireShared acquireShared调用tryAcquireShared②同步队列
同步器依赖内部一个FIFO双向队列作为同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器以当前线程及等待信息构造一个Node加入同步队列,阻塞当前线程,当同步状态释放时,唤醒首节点,使其再次尝试获取同步状态。
Node属性
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus; //等待状态
volatile Node prev; //前驱节点
volatile Node next; //后继节点
volatile Thread thread; //同步状态的线程
Node nextWaiter; //等待队列中的后继节点
}
节点是构成同步队列的基础,同步器有首节点、尾节点;如果没能成功获取同步状态就会构建节点加入尾部;首节点线程在释放同步状态时,唤醒后继节点,后继节点获取同步状态成功时将自己设为首节点。
注意:设置尾节点需要基于CAS,设置头节点不需要基于CAS。
原因:因为获取同步状态失败的线程可能有多个,就会构建多个节点,都去设置尾节点,所以需要CAS来保证;而设置头节点是通过获取同步状态成功的线程来完成,只有一个线程能够成功获取,所以不需要。
同步状态的获取和释放分为独占式和共享式,它两最主要的区别在于同一时刻能否有多个线程同时获取同步状态。
2、CAS
上面提到在设置尾节点时要基于CAS,CAS到底是个啥?
jdk5之前,同步是靠synchronized实现的,锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁用到的机制就是CAS,Compare and Swap。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
案例分析
public int a = 1;
public boolean compareAndSwapInt(int b) {
if (a == 1) {
a = b;
return true;
}
return false;
}
- 线程A执行到 a==1,正准备执行a = b时,线程B也正在运行a = b,并在线程A之前把a修改为2;最后线程A又把a修改成了3。结果就是两个线程同时修改了变量a,显然这种结果是无法符合预期的,无法确定a的值。
- 解决方法也很简单,在compareAndSwapInt方法加锁同步,变成一个原子操作,同一时刻只有一个线程才能修改变量a。
CAS中的比较和替换是一组原子操作,不会被外部打断,先根据paramLong/paramLong1获取到内存当中当前的内存值V,在将内存值V和原值A作比较,要是相等就修改为要修改的值B,属于硬件级别的操作,效率比加锁操作高。
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
CAS缺点
CAS存在一个很明显的问题,即ABA问题。
如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。
总结
此篇粗略介绍了AQS队列同步器的作用,它是各种同步组件实现的关键。根据AQS中设置尾节点需要基于CAS,又对CAS做了简单介绍,CAS是JUC并发包中的很多同步工具类、原子操作类、同步组件底层都靠的是CAS。
本篇参考书籍《并发编程的艺术》,需要深入了解AQS的同学可以阅读此书。
略陈固陋,如有不当之处,欢迎各位看官批评指正!
网友评论