AQS原理概览
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,类似于synchronize关键字加的锁。
功能目标
-阻塞获取锁:acquire(),获取不到锁阻塞。
-阻塞尝试获取锁:tryAcquire(),获取不到锁快速失败返回。
-获取锁超时机制:tryAcquireNanos()指定时间内获取不到锁返回。
-打断取消机制:acquireInterruptibly()获取锁阻塞可以被打断。
-独占机制及共享机制
-条件不满足时的等待机制
设计思想
AQS 的基本思想其实很简单 获取锁的逻辑
while(state 状态为已加锁) {
if(队列中还没有此线程) {
//入队阻塞,等待被唤醒去竞争锁
}
}
//当前线程出队
释放锁的逻辑
if(state 状态允许释放锁) {
//唤醒阻塞的线程去竞争锁
}
设计要点
1.原子操作+volatile 维护state状态
2.park、unpark阻塞及恢复线程
3.维护同步队列和等待队列
state设计
state使用volatile配合compareAndSetState即cas保证其修改时的原子性。
state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想。
使用state属性来表示资源的状态,子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
共享方式
AQS定义两种资源共享方式:
Exclusive(独占,只有一个线程能执行,如ReentrantLock)
Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源。
独占模式
以ReentrantLock(独占)为例,state初始化为0,表示未锁定状态。
A线程lock()时,会调用tryAcquire()独占该锁并将state+1。
此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止。
其它线程才有机会获取该锁。
当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。
但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE
= new AtomicReference<Singleton>();
private Singleton() {
System.out.println("我被初始化了");
CasSingletonTest.objectcount.getAndIncrement();
}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
package com.xttblog.canal.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class CasSingletonTest {
public static AtomicInteger objectcount = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
//0.创建1个总门栓
final CountDownLatch begin = new CountDownLatch(1);
//0.创建1000个分门栓
final CountDownLatch last = new CountDownLatch(1000);
for(int i=0;i<1000;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
//1.所有的线程都会阻塞在这
begin.await();
System.out.println
(Thread.currentThread().getName()+":begin...");
//3.阻塞的1000个线程并发执行
Singleton sba = Singleton.getInstance();
System.out.println(Thread.currentThread().getName()+":OK");
//4.每执行1次 释放门栓
last.countDown();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}).start();
}
//2.释放门栓
begin.countDown();
//5.等待1000个线程释放门栓 即执行完毕
last.await();
System.out.println("new objects: "+objectcount.get());
}
}
共享模式
以CountDownLatch(共享)以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。
这N个子线程是并行执行的,调用await()函数会park()住主调用线程,每个子线程执行完后countDown()一次,state会CAS减1。
等到所有子线程都执行完后(即state=0),然后主调用线程就会从await()函数返回,继续后续动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式。
他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。
但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
阻塞恢复设计
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter ,_cond 和 _mutex 。
打个比喻线程就像一个旅人,Parker 就像他随身携带的背包, _cond条件变量就好比背包中的帐篷。
_counter 就好比背包中的备用干粮,0 为没有干粮或者干粮被耗尽,1 为干粮充足,默认是没有干粮的即0。
调用 park 的时候就是看有没有干粮,有干粮就继续。
参考链接: zhuanlan.zhihu.com/p/110746871
使用 park & unpark 来实现线程的暂停和恢复,先 unpark 再 park 也没问题。
在调用park的时候会停止,再unpark 会先补充一份干粮,然后结束park继续往下走。
先 unpark再park 会先补充一份干粮,然后继续往下走,在调用park的时候不会停止。
park&unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细,park 线程还可以通过 interrupt 打断。它们是 LockSupport 类中的方法
//先park再unpark
// 暂停当前线程
LockSupport.park();
// 恢复t1线程的运行
LockSupport.unpark(t1)
//先 park 再 unpark:先暂停再继续往下执行
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}
,"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
输出:
18:42:52.585 c.TestParkUnpark [t1] - start...
18:42:53.589 c.TestParkUnpark [t1] - park...
18:42:54.583 c.TestParkUnpark [main] - unpark...
18:42:54.583 c.TestParkUnpark [t1] - resume...
//先 unpark 再 park:发现park还是继续往下执行
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}
, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
18:43:50.765 c.TestParkUnpark [t1] - start...
18:43:51.764 c.TestParkUnpark [main] - unpark...
18:43:52.769 c.TestParkUnpark [t1] - park...
18:43:52.769 c.TestParkUnpark [t1] - resume...
CLH同步队列
使用了 FIFO 先入先出队列,并不支持优先级队列。
设计时借鉴了 CLH 队列,CLH是一种单向无锁队列。注意只是借鉴了。
CLH 好处: 无锁,使用自旋 快速,无阻塞。
AQS 在一些方面改进了 CLH
条件等待队列
使用了 FIFO 先入先出队列,并不支持优先级队列。
设计时借鉴了 CLH 队列,它是一种单向无锁队列。
提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList。
使用条件变量来实现等待、唤醒机制,支持多个条件变量。
一个条件变量类似于一个 Monitor 的 WaitSet。
队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 waitStatus 维护节点状态。
入队伪代码,只需要考虑 tail 赋值的原子性
自定义同步器API
不同的自定义同步器争用共享资源的方式也不同。
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
自定义同步器实现时需要继承AbstractQueuedSynchronizer主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- 以上方法默认抛出 UnsupportedOperationException