一、简介
aqs全称抽象队列同步器是AbstractQueuedSynchronize抽象类。
它是一个用来构建锁和同步器的框架,它底层用了CAS技术来保证操作的原子性,同时利用FIFO队列实现线程间的锁竞争,将基础的同步相关抽象细节放在AQS,它能够成为实现大部分同步需求的基础,也是JUC并发包同步的核心基础组件。Lock
、ReadWriteLock
、CountDowndLatch
、CyclicBarrier
、Semaphore
、ThreadPoolExecutor
等都是在AQS的基础上实现的。
二、AQS核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个volatile的state变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
1、具体加锁过程
1、执行tryAcquire方法尝试加锁
2、如果判断当前线程如果是自己,则把state+1,加锁成功。如果不是继续判断当前锁是否空闲(state是否为0),是的话通过cas加锁,返回加锁结果(由子类实现)
3、如果加锁失败,则把当前线程封装进Node对象中,把Node.prev指向tail组成链表结构,如果线程需要阻塞则执行LockSupport.park(this);
2、具体释放锁过程
1、执行tryRelease方法释放锁
2、State进行-1操作,如果是0,则把当前线程ExclusiveOwnerThread置为null。否则设置State-1值
3、如果有等待的线程,则使用LockSupport.unpark唤醒包装了Node的下一个节点的线程
3、aqs图示
主要包含4个字段
- state(同步状态)
- exclusiveOwnerThread(获得锁的 线程)
- head(头节点)
- tail尾节点
aqs分为两种“独占锁”和”共享锁“
- 独占锁 -- 锁在一个时间点只能被一个线程锁占有。 例如:ReentrantLock可重入锁和ReentrantReadWriteLock.WriteLock写锁。
独占锁又分为“公平锁”和“非公平锁”。
- 公平锁,是按照先来先得的规则,公平的获取锁;
- 非公平锁,则当线程要获取锁时,直接抢占获取锁
- 共享锁 -- 能被多个线程同时拥有,能被共享的锁。例如:ReentrantReadWriteLock.ReadLock读锁,CyclicBarrier循环屏障, CountDownLatch计数器和Semaphore信号量
三、aqs实现子类
1、CountDownLatch计数器(常用)
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,开门后所有线程一起跑”的感觉。
使用方法
首先建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。
常用方法有:
- await()方法表示阻塞线程
- countDown()方法表示,计数器减一。在线程阻塞后,计数器减至0后才可以唤醒所有阻塞的线程。
使用场景
- 让多个线程等待:模拟并发,让并发线程一起执行
- 让单个线程等待:多个线程(任务)完成后,进行汇总合并
2、Semaphore信号量
Semaphore(信号量)
可以通过其限制执行的线程数量(同时访问共享资源的线程数),达到限流的效果。
使用方法
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。
常用方法有:
- void acquire() throws InterruptedException 获取一个许可,会阻塞等待其他线程释放许可
- void acquire(int permits) throws InterruptedException 获取指定的许可数 ,会阻塞等待其他线程释放
- void acquireUninterruptibly() 获取一个许可 会阻塞等待其他线程释放许可 可被中断
- void acquireUninterruptibly(int permits) 获取指定的许可数 会阻塞等待其他线程释放许可 可被中断
- boolean tryAcquire() 尝试获取许可 不会进行阻塞等待
- boolean tryAcquire(int permits) 尝试获取指定的许可数 不会阻塞等待
- boolean tryAcquire(long timeout, TimeUnit unit) 尝试获取许可 可指定等待时间
- boolean tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取指定的许可数 可指定等待时间
使用场景
- 控制线程最大并发数(限流,例如数据库连接池)
- 异步任务,同步返回
- 加锁 (1个整形信号量即可模拟互斥锁)
3、CyclicBarrier循环屏障
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,它是 ReentrantLock 和 Condition 的组合使用,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
使用方法
常用方法有:
- public CyclicBarrier(int parties);//parties表示屏障拦截的线程数量
- public CyclicBarrier(int parties, Runnable barrierAction);//parties表示屏障拦截的线程数量,在线程到达屏障时,优先执行barrierAction
- await()
- isBroken()用来知道阻塞的线程是否被中断
- reset()
- getNumberWaiting()获得CyclicBarrier阻塞的线程数量
使用场景
- 用于多线程计算数据,最后合并计算结果的应用场景
与CountDownLatch区别
CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。
区别:
- CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
- CyclicBarrier还提供getNumberWaiting()、isBroken()等方法。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
4、Phaser阶段
Phaser允许并发多阶段任务。Phaser类机制是在每一步结束的位置对线程进行同步,当所有的线程都完成了这一步,才允许执行下一步。
使用场景
用来解决控制多个线程分阶段共同完成任务的情景问题。
5、Exchanger交换器
Exchanger用于线程间进行通信、数据交换。Exchanger提供了一个同步点exchange方法,两个线程调用exchange方法时,无论调用时间先后,两个线程会互相等到线程到达exchange方法调用点,此时两个线程可以交换数据,将本线程产出数据传递给对方。
6、ReentrantLock可重入锁、ReentrantReadWriteLock可重入读写锁
参考:【java并发编程】lock接口_现实、太残忍的博客-CSDN博客