介绍
AQS:AbstractQueuedSynchronizer
jdk并发包中最基础&最重要的一个类,几乎所有的并发工具类都直接或间接的关联到这个类,所以如果想要深入学习和使用jdk中并发相关的源码,这个是躲不掉的一关。另外如果吃透了这个类的核心,那么在后面继续学习并发相关知识时也会达到事半功倍的效果。
废话不多说,下面进入正题。
部分准备知识
如果想要学习AQS,那么有两个类是必须要先了解的,至少需要知道是干啥的。
- UnSafe
这个类在rt.jar中,是一个很基础的类,然后打开源码一看,几乎全部是native 方法,是不是很懵?其实这个类是方便我们在Java中直接像C/C++一样去操作内存的,只不过不像C那么“随便”,UnSafe类只提供了封装好了的API,目的是防止开发“胡乱”使用。这里只简单的介绍我们会用到的几个方法。
1. 操作对象属性的
+ public native Object getObject(Object o, long offset);<br>
通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。同理还有putObject方法.
+ public native Object getObjectVolatile(Object o, long offset);<br>强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。同理还有putObjectVolatile。
2. 线程挂起和恢复
+ public native void park(boolean isAbsolute, long time);<br>阻塞当前线程,一直等道unpark方法被调用。
+ public native void unpark(Object thread);<br> 释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。
3. CAS机制
+ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
+ public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
+ public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这三个都是对应的CAS操作,也是贯穿整个并发包的几个方法。
- LockSupport
LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。 它的内部其实两类主要的方法:park(阻塞线程)和unpark(唤醒线程)。 和Object对象的wait和notify类似,但是区别是后者需要获得锁,而LockSupport不需要,而且可以指定需要阻塞或者唤醒的线程。
怎么使用
如果需要使用AQS那么必须自己去继承这个基类,然后重写以下的方法:
- tryAcquire
定义独占模式加锁,一般是通过CAS对state进行修改来定义加锁,比如ReentrantLock的实现就是将state+1等 - tryRelease
定义独占模式解锁,如ReentrantLock则是判断出事持有线程就将state-1. - tryAcquireShared 定义共享模式加锁,如
- tryReleaseShared
分别是独占模式和共享模式下面的锁的获取和释放。
独占模式:同时只能有一个线程持有锁
共享模式:同时可以有多个线程持有锁
在JDK源码中ReentrantLock 就是AQS的具体的使用,从Java层实现了独占模式下的可重入锁 。
剖析原理
几个重要属性
- state 同步状态
加锁和释放锁基本都是基于这个属性,修饰是volatile,保证所有线程都能看到 比如对于可重入锁的实现就是state=1表示锁定状态,state=0 表示无锁,也是独占锁的模式 再比如计数器或者信号量使用state来计数,也是共享锁实现的一种方式。
- head
同步队列的头节点,每次释放锁的时候都会去唤醒这个节点的线程
- tail
同步队列的尾节点
Node 是AQS中同步队列(双向链表)的实体,head和tail都是Node,包装了线程和线程的等待状态.
节点状态
SIGNAL=1: 表示后续节点的线程需要unpark
CANCELLED=-1: 表示当前节点的线程取消锁等待了
CONDITION=-2: 表示当前节点的线程处于条件等待状态
PROPAGATE=-3
执行流程
抽出几个步骤详细说明
加锁
AQS最初的入口acquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
比如Lock的lock方法实现就有直接调用acquire的。
竞争
tryAcquire 正如上面说的,这个是需要子类实现的,也是加锁动作的定义(通俗的讲就是怎么“上锁”);这边也是会发生竞争的地方,所以很多实现就是使用CAS这样的操作来完成的。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
返回true:加锁成功,返回false:加锁失败
等待
竞争失败的线程需要被挂起,AQS框架中也是实现了,不需要实现者去自己实现了,在上图中这个一阶段也是比较复杂的。
首先是通过addWaiter 往同步队的末尾插入,通过CAS+自旋(循环)保证最终都会插入成功,并且将tail指向同步队列的队尾节点。
然后通过acquireQueued进行等待,该方法中会有一个死循环,每次都判断前一个节点是不是head,然后如果是则会执行一次锁抢占,目的是为了抢占成功的那个线程在释放锁前就异常了,只会有一点性能损耗,然后会进入park。
最后调用shouldParkAfterFailedAcquir判断是否应该挂起等待,此时会去判断前一个节点的waitstatus是否=SINGAL,如果是0,将其置为SINGAL,并返回true,然后进行park。
这边是整个AQS相对比较核心的地方需要多看源码,然后也可以跟踪调试,走一遍也就清楚了。
解锁
和加锁对应的解锁通过调用release开始进行,然后调用tryRelease,成功后就通过unparkSuccessor唤醒同步队列中的第一个线程,然后继续在acquireQueued方法中进行循环然后尝试去获取锁tryAcquire
条件对象
前面说过,AQS中有一个条件对象,它是Condition接口的实现,其主要作用是配合AQS实现更精确的线程同步。
Condition 主要方法 + await : 调用线程会进入条件队列,必须是获取了锁的线程才能调用,否则会抛出异常 + signal : 调用线程会唤醒条件队列里面的第一个线程,然后将其转入同步队列,也必须是获取了锁的线程才能调用,否则会抛出异常
类比Object的wait和notify方法,两者都是要在持有锁或者在同步方法块中(本质也是持有锁)的场景下使用。
个人理解
AQS :提供了一个实现了阻塞锁以及依赖等待队列的相关同步器的框架
核心使用了两个队列:同步队列和条件队列,加锁失败进入同步队列,拿到锁后通过条件对象可以将线程放入条件队列,也可以将条件队列中的线程转移到同步队列
同步队列(AQS)<->锁池(Object)
条件队列(AQS)<->等待池(Object)
不同之处: 一个AQS实现的锁,只有一个同步队列,但是可以有多个条件队列,竞争时全局的,但是条件队列是和条件对象绑定的,只能被同一个条件进行阻塞和唤醒,不同的条件之间没有关系,唯一的就是唤醒时都会到同步队列里面去等待竞争
基于AQS的同步器
ReentrantLock
可重入独占锁,独占模式,实现了Lock接口,lock方法是对state进行赋值,如果赋值成功则加锁成功,否则进入等待队列并阻塞等待,如果是同一个线程多次加锁则是进行state的自增;解锁则是对state的自减,如果state=0表示已经没有线程持有锁了,并且通过AQS框架去通知等待队列的下一个线程去尝试加锁。
CountDownLatch
简介
用处
1. 开关,多个线程通过调用await从而进入等待状态,直到另外有一个线程进行countDown调用,从而唤醒等待的线程继续往下执行
2. 合并结果, 主线程通过调用await进行等待,直到其他多个线程全部调用countDown,从而唤醒等待的主线程继续执行
共享锁的一种实现
使用方法
1. 构造方法
public CountDownLatch(int count) 初始化锁的个数
2. await() 本质是尝试获取共享锁 tryAcquireShared,state == 0 ?成功 : 失败
3. countDown() 本质是释放共享锁 tryReleaseShared ,state -= 1
原理解释
1. 内部类Sync继承了AQS 实现了tryAcquireShared 和 tryReleaseShared 即共享锁
2. 新建时申明锁的个数N(state = N)
使用注意
1. 该同步器是一次性的,使用完不能被重置
2. 注意countDown的调用时机,避免出现并发错误
CyclicBarrier
简介
用处
每个线程都调用await进行等待,直到等待的线程数达到指定个数后,同时唤醒所有的等待线程往下继续执行
可以用来做并发测试,同时发出指定请求
使用方法
1. 构造方法
public CyclicBarrier(int parties) 指定需要等待的线程个数
2. await 通过加锁对parties 进行减1 操作,并判断是否=0,如果没有达到屏障条件则使用condition.await将线程进行等待,如果达到了会调用condition.signalAll, 唤醒所有等待线程
原理解释
1. 内部构造了一个Lock(独占锁)和对应的Condition,通过条件对象让线程等待
2. 内部使用一个Generation 对象来标记是否到达屏障点,或者响应了线程中断
3. 每次达到屏障点后,会重置parties和内部使用一个Generation对象,从而达到可以循环使用
使用注意
1. CyclicBarrier在最后一个线程到达时会唤醒所有线程,
Semaphore
最后
膜拜一下大神 Doug Lea
拜一拜,妈妈再也不用担心我的并发学习了!