本文主要介绍AbstractQueuedSynchronizer(AQS)及其常用的一些实现,如 CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock、ReentrantReadWriteLock等。
本文目的以实用为主,概述基本原理,说明使用场景,引入基本案例。
AbstractQueuedSynchronizer
AQS定义了一套多线程访问共享资源的同步器框架,它维护了一个 volatile int state(代表共享资源)和一个 FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
已提供的state的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
CountDownLatch
CountDownLatch 是基于AQS的一个同步工具类,它允许一个或多个线程一直等待,直到其他一个或者多个线程的操作执行完后再执行。
CountDownLatch 通过一个计数器来实现的,计数器的初始化值为同步状态数量。
每当一个线程完成了自己的任务后,就会消耗一个同步状态,计数器的值会减1。
当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务了。
常用API
// count 初始化计数值,一旦count初始化完成后,就不可重新初始化或者修改 CountDownLatch 对象的内部计数器的值。
public CountDown(int count){}
// 使当前线程挂起,直到计数值为0时,才继续往下执行。
public void await() {};
// 有超时的等待
public boolean await(long timeout , TimeUnit timeUnit) throws InterruptExcetion {};
//将count值减1
public void countDown() {}
常用场景
多线程做资源初始化,主线程先暂停等待初始化结束;
每个线程初始化结束后都 countDown 一次,等全部线程都初始化结束后(state=0),此时主线程再继续往下执行。
Demo
主线程main中,等任务Task1和任务Task2都跑完之后,再继续处理业务逻辑:
/**
* 测试 CountDownLatch 功能
*/
public class CountDownLatch_ {
public static void main(String[] args) throws Exception {
long now = System.currentTimeMillis();
// 定义任务数:2
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(new Task1(countDownLatch)).start();
new Thread(new Task2(countDownLatch)).start();
// 等待上述两个任务结束
countDownLatch.await();
System.out.println("all tasks are finished, time :" + (System.currentTimeMillis() - now));
}
}
class Task1 implements Runnable {
private CountDownLatch countDownLatch;
Task1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println("begin doing task1...");
Thread.sleep(3000);
System.out.println("stop doing task1(3s)...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 放在final中,确保释放锁资源
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
}
}
class Task2 implements Runnable {
private CountDownLatch countDownLatch;
Task2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println("begin doing task2...");
Thread.sleep(4000);
System.out.println("stop doing task2(4s)...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 放在final中,确保释放锁资源
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
}
}
测试结果:
CyclicBarrier
CyclicBarrier 是基于AQS的一个同步工具类。
CyclicBarrier 的作用是让一组线程之间相互等待,任何一个线程到达屏障点后就阻塞,直到最后一个线程到达,才都继续往下执行。
常用API
// 参数 parties 表示屏障拦截的线程数量
public CyclicBarrier(int parties){}
// 参数 parties 表示屏障拦截的线程数量,在屏障被打开时将优先执行barrierAction,方便处理更负责的业务场景
public CyclicBarrier(int parties, Runnable barrierAction){}
// await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。
public int await() {};
// 有超时的等待
public int await(long timeout, TimeUnit unit);
// 用来了解阻塞的线程是否被中断
public boolean isBroken() {}
// 将屏障重置为其初始化状态即重置为构造函数传入的parties值。
public void reset() {}
// 获得 CyclicBarrier 阻塞的线程数量
public int getNumberWaiting() {}
常用场景
用于多线程计算数据,最后合并计算结果的场景。每个parter负责一部分计算,最后的线程barrierAction线程进行数据汇总。
与 CountDownLatch 的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
- CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
- CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
Demo
此demo只是简单实例,并不典型。
import java.util.concurrent.CyclicBarrier;
/**
* 测试 CyclicBarrier 功能
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount);
for (int i = 0; i < threadCount; i++) {
System.out.println("创建工作线程:" + i);
new Thread(new Worker(cyclicBarrier)).start();
}
}
}
class Worker implements Runnable {
private CyclicBarrier cyclicBarrier;
Worker(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println(threadName + " 开始等待其他线程...");
cyclicBarrier.await();
System.out.println(threadName + " 开始执行...");
// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
Thread.sleep(1000);
System.out.println(threadName + " 执行完毕(1s)...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果:
Semaphore
Semaphore 是基于AQS的一个同步工具类。
Semaphore 是基于计数的信号量,可以用来控制能同时访问特定资源的线程数量。
可以通过设定一个阈值,基于此,多个线程争抢获取许可信号,做完自己的操作后归还许可信号,超过阈值后,线程申请许可信号将会被阻塞,直到有其他线程释放许可信号。
Semaphore也是基于AQS实现的,state值为初始化时传入的permits信号量,Semaphore也重写了tryAcquireShared方法,tryAcquireShared方法返回>=0,才表示获得同步量。
有一点不同的是Semaphore实现了公平抢占和非公平抢占,公平抢占就是抢占前先判断自己是否是同步队列中第一个要出队列的,不是则进入同步队列等待。非公平抢占,则不关心同步队列等待情况,直接尝试获取。
常用API
// 用给定的允许数量和默认的非公平设置创建Semaphore对象。
Semaphore(int permits)
//用给定的允许数量和给定的公平设置创建一个Semaphore对象。
Semaphore(int permits , boolean fair)
// 从信号量里获取一个可用的许可,如果没有可用的许可,那么当前线程将被禁用以进行线程调度,并且处于休眠状态。
void acquire()
// 尝试获取信号量,获取失败立刻返回
void tryAcquire()
// 释放一个许可,将其返回给信号量
void release()
// 返回此信号量中当前可用的许可数量。
int availablePermits()
// 查询是否有线程正在等待获取。
boolean hasQueuedThreads()
常用场景
Semaphore可以用来做流量控制,特别公用资源有限的应用场景。
应用:https://www.cnblogs.com/iou123lg/p/9689491.html
Demo
此demo只是简单实例,并不典型。
import java.util.concurrent.CyclicBarrier;
/**
* 测试 CyclicBarrier 功能
*/
public class CyclicBarrierTest {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount);
for (int i = 0; i < threadCount; i++) {
System.out.println("创建工作线程:" + i);
new Thread(new Worker(cyclicBarrier)).start();
}
}
}
class Worker implements Runnable {
private CyclicBarrier cyclicBarrier;
Worker(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println(threadName + " 开始等待其他线程...");
cyclicBarrier.await();
System.out.println(threadName + " 开始执行...");
// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
Thread.sleep(1000);
System.out.println(threadName + " 执行完毕(1s)...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果:
可以看出,资源可用数限制在5以下。
重入锁ReentrantLock
ReentrantLock 是基于AQS的一个同步工具类。
可重入锁:锁具备可重入性。
常用API
// 创建独占锁
ReentrantLock() {}
// 创建独占锁时指定锁类型(公平/非公平)
ReentrantLock(boolean fair) {}
// 获得锁,如果锁被占用则等待
void lock() {}
// 获得锁,但优先响应中断
void lockInterruptibly(){}
// 尝试获得锁,如果成功,则返回true;失败返回false。(此方法不等待,立即返回)
boolean tryLock() {}
// 在指定时间内尝试获取锁
boolean tryLock(long timeout, TimeUnit unit)(){}
// 释放锁
void unlock() {}
实现原理
ReentrantLock 其实是 AQS 独占式获取同步状态的一种具体实现,
-
可重入实现原理:
可重入需要记录重入次数,在 ReentrantLock 中是用 state 来记录重入次数的。
一个线程尝试获取同步状态时,会判断当前线程是否是同步状态的独占拥有者,如果是,则将state加上请求同步量(对于锁一般都是1),来记录重入次数,如果不是,则进入同步队列争抢同步状态。
释放时,也会首先判断当前线程是否是同步状态的独占拥有者,不是则抛出异常。如是,则减去释放量,减到state为0时,释放对同步状态的独占,其实就是将 setExclusiveOwnerThread(null);。
-
公平锁与非公平锁实现原理
和 Semaphore 一样,公平锁在尝试争抢同步状态时的时候,会判断当前线程是否是同步队列中的第一个节点 hasQueuedPredecessors(),如果不是则争抢失败,进入同步队列等待。非公平锁则直接争抢。
常用场景
与 synchronized 类似,但更为灵活。
ReentrantLock和synchronized的异同
相同点
-
ReentrantLock 和 synchronized 都是独占锁,只允许线程互斥的访问临界区
synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。
一般并发场景使用synchronized的就够了,ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。
ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
-
ReentrantLock 和 synchronized 都是可重入的
synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;
而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
不同点
-
ReentrantLock可以实现公平锁
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权,而非公平锁则随机分配这种使用权。
ReentrantLock 默认实现是非公平锁,因为相比公平锁,非公平锁性能更好。
当然公平锁能防止饥饿,某些情况下也很有用。
-
ReentrantLock可响应中断
当使用 synchronized 实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。
而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法 lockInterruptibly(),该方法可以用来解决死锁问题。
-
获取锁时限时等待
参考:https://www.cnblogs.com/takumicx/p/9338983.html
ReentrantReadWriteLock
ReentrantLock 是基于AQS的一个同步工具类。
读写锁维护了一对锁(一个读锁和一个写锁),通过分离读锁和写锁,使得同一时刻可以允许多个读线程访问。
但是在写线程进行访问时,所有的读线程和其他写线程均被阻塞。
读写就是AQS中共享式争抢同步状态的具体实现。
写锁就是AQS中独占式争抢同步状态的具体实现。
常用API
// 创建读写锁
ReentrantReadWriteLock() {}
// 创建读写锁时指定锁类型(公平/非公平)
ReentrantReadWriteLock(boolean fair) {}
// 获得写锁对象
ReentrantReadWriteLock.WriteLock writeLock() {}
// 获得读锁对象
ReentrantReadWriteLock.ReadLock readLock() {}
// 其他 加锁/解锁 操作都是在 上述两个锁对象上进行
实现原理
参考:https://segmentfault.com/a/1190000018963066
常用场景
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。
在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
在常见的开发中,我们经常会定义一个共享的用作内存缓存的数据结构;比如一个大Map,缓存全部的城市Id和城市name对应关系。这个大Map绝大部分时间提供读服务(根据城市Id查询城市名称等);而写操作占有的时间很少,通常是在服务启动时初始化,然后可以每隔一定时间再刷新缓存的数据。但是写操作开始到结束之间,不能再有其他读操作进来,并且写操作完成之后的更新数据需要对后续的读服务可见。