1. 简介
AQS是AbstractQueuedSynchronizer的简写,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。
2. 工作原理
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
- 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础框架。
- 利用了一个int类型表示状态。
- 使用方法是继承,子类通过继承并通过实现它的方法管理其状态{ acquire 和release }的方法操纵状态。
- 可以同时实现排它锁和共享锁模式(独占、共享)。
3.AQS组件
3.1 CountDownLatch
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
@Slf4j
public class CountDownLatchExample1 {
private final static int threadCount = 200;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
log.error("exception", e);
} finally {
countDownLatch.countDown();
}
});
}
//countDownLatch.await(100,TimeUnit.MILLSECONDS),设定等候时间
countDownLatch.await();
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("{}", threadNum);
Thread.sleep(100);
}
}
3.2 Semaphore
Semaphore负责协调各个线程,以保证它们能够正确、合理的使用公共资源,也是操作系统中用于控制进程同步互斥的量。Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
Semaphore主要方法:
//构造方法,创建具有给定许可数的计数信号量并设置为非公平信号量
Semaphore(int permits)
//构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量
Semaphore(int permits,boolean fair)
//从此信号量获取一个许可前线程将一直阻塞。相当于一辆车占了一个车位
void acquire()
//从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。比如n=2,就相当于一辆车占了两个车位
void acquire(int n)
//尝试获取许可,停车场有车位就进入,没有就走
tryAcquire()
//释放一个许可,将其返回给信号量。就如同车开走返回一个车位
void release()
//释放n个许可
void release(int n)
//当前可用的许可数
int availablePermits()
下面一起看看如何使用Semaphore
@Slf4j
public class SemaphoreExample1 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
//semaphore.acquire(3); 获取多个许可
//semaphore.tryAcquire() 尝试获取许可
//semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS) 尝试等待获取许可
semaphore.acquire(); // 获取一个许可
test(threadNum);
semaphore.release(); // 释放一个许可
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
3.3 CyclicBarrier
CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。
@Slf4j
public class CyclicBarrierExample3 {
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
//此处代码在barrier满足条件时优先执行
log.info("callback is running");
});
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.error("exception", e);
}
});
}
executor.shutdown();
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
3.4 ReetrantLock
,JDK6.0版本之后synchronized 得到了大量的优化,二者性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。除此之外, ReetrantLock又自带一系列高逼格BUFF:可中断响应、锁申请等待限时、公平锁。另外可以结合Condition来使用,使其更是逼格满满。
ReentrantLock与Synchronized区别
- 可重入性(都可重入)
- 锁的实现:Synchronized是依赖于JVM实现的,而ReentrantLock是依赖于程序实现。
- 性能区别:在JDK5.0版本之前, ReetrantLock的性能远远好于synchronized关键字,但是随着锁的不断优化(自旋锁、轻量级锁、偏向锁),两者性能也差不太多。在两者都能满足需求的情况,更推荐使用synchronized,简单。
- 功能区别:Synchronized的使用便于ReentrantLock,并且它是由编译器是保证锁的加锁和释放的,而ReentrantLock是由我们自己控制的;第二点锁定粒度与灵活度,明显ReentrantLock优于Synchronized。
- ReentrantLock独有功能:1,可指定公平锁或非公平锁。2,提供了一个Condition类,可以分组唤醒需要唤醒的线程。3,提供能够中断等待锁的线程的机制,lock.lockInterruptibly()。
Condition的使用:Condition可以非常灵活的操作线程的唤醒,下面是一个线程等待与唤醒的例子,其中用1234序号标出了日志输出顺序
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();//创建condition
//线程1
new Thread(() -> {
try {
reentrantLock.lock();
log.info("wait signal"); // 1
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal"); // 4
reentrantLock.unlock();
}).start();
//线程2
new Thread(() -> {
reentrantLock.lock();
log.info("get lock"); // 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();//发送信号
log.info("send signal"); // 3
reentrantLock.unlock();
}).start();
}
3.5 ReentrantReadWriteLock读写锁
在没有任何读写锁的时候才可以取得写入锁(悲观读取,容易写线程饥饿),也就是说如果一直存在读操作,那么写锁一直在等待没有读的情况出现,这样我的写锁就永远也获取不到,就会造成等待获取写锁的线程饥饿。 平时使用的场景并不多。
public class LockExample3 {
private final Map map = new TreeMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public Data get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
public Set getAllKeys() {
readLock.lock();
try {
return map.keySet();
} finally {
readLock.unlock();
}
}
public Data put(String key, Data value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
class Data {}
}
3.6 StampedLock
StampedLock是Java8引入的一种新的锁机制,可以认为它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观锁策略。如果有大量的读线程,他也有可能引起写线程的饥饿。而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。
@Slf4j
@ThreadSafe
public class LockExample5 {
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static int count = 0;
private final static StampedLock lock = new StampedLock();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
long stamp = lock.writeLock();
try {
count++;
} finally {
lock.unlock(stamp);
}
}
}
3.7 Callable、Future、FutureTask
线程的创建方式中有两种,一种是实现Runnable接口,另一种是继承Thread,但是这两种方式都有个缺点,那就是在任务执行完成之后无法获取返回结果,于是就有了Callable接口,Future接口与FutureTask类的配合取得返回的结果。
@FunctionalInterface
public interface Callable {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
该接口声明了一个名称为call()的方法,同时这个方法可以有返回值V,也可以抛出异常。
下面看下Future接口定义
public interface Future {
//如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执
//行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停
//止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执
//行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经/
//完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中
//断执行中的线
boolean cancel(boolean mayInterruptIfRunning);
//如果任务完成前被取消,则返回true
boolean isCancelled();
//如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true
boolean isDone();
//获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成
V get() throws InterruptedException, ExecutionException;
//获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限
//制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
通过方法我们可知Future提供了3种功能:(1)能够中断执行中的任务(2)判断任务是否执行完成(3)获取任务执行完成后的结果。
下面我们再看下FutureTask的定义
public class FutureTask implements RunnableFuture {
}
public interface RunnableFuture extends Runnable, Future {
void run();
}
FutureTask除了实现了Future接口外还实现了Runnable接口,
通过上面的介绍,我们对Callable,Future,FutureTask都有了比较清晰的了解了,那么它们到底有什么用呢?我们前面说过通过这样的方式去创建线程的话,最大的好处就是能够返回结果,加入有这样的场景,我们现在需要计算一个数据,而这个数据的计算比较耗时,而我们后面的程序也要用到这个数据结果,那么这个时Callable岂不是最好的选择?我们可以开设一个线程去执行计算,而主线程继续做其他事,而后面需要使用到这个数据时,我们再使用Future获取不就可以了吗?下面我们就来编写一个这样的实例。
@Slf4j
public class FutureExample {
static class MyCallable implements Callable {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
Future future = executorService.submit(new MyCallable());
log.info("do something in main");
Thread.sleep(1000);
String result = future.get();
log.info("result:{}", result);
}
}
下面我们用FutureTask再次实现类似功能
@Slf4j
public class FutureTaskExample {
public static void main(String[] args) throws Exception {
FutureTask futureTask = new FutureTask(new Callable() {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
});
new Thread(futureTask).start();
log.info("do something in main");
Thread.sleep(1000);
String result = futureTask.get();
log.info("result:{}", result);
}
}
3.8 Fork/Join框架
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。此处我们不做过多的说明,其原理是基于工作窃取算法,指某个线程从其他队列里窃取任务来执行。下面我们以一个demo来介绍如何使用
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask {
public static final int threshold = 2;
private int start;
private int end;
public ForkJoinTaskExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//如果任务足够小就计算任务
boolean canCompute = (end - start) <= threshold;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果任务大于阈值,就分裂成两个子任务计算
int middle = (start + end) / 2;
ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待任务执行结束合并其结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
// 合并子任务
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();
//生成一个计算任务,计算1+2+3+4
ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
//执行一个任务
Future result = forkjoinPool.submit(task);
try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}
3.9 Queue
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都集成自Queue。
ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。
BlockingQueue接口:
- ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没有实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫
有界队列
。 - LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持一个数据缓冲队列(由列表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并发,是一个无界队列。
- SynchronousQueue:一种没有缓冲的队列,生产者生产的数据直接会被消费者获取并消费。
- PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说
传入队列的对象必须实现Comparable接口
),在实现PriorityBlockingQueue时,内部控制线程同步得锁采用的是公平锁,它是一个无界
的队列。 - DelayQueue:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素,DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲链接的关闭等等。