文章目录
一、AQS介绍
AQS的设计内容:
- 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础挂架。
- 利用了一个int类型表示状态。
- 使用方法是继承。
- 子类通过继承并通过实现它的方法管理其状态{ acquire和 release}的方法操纵状态。
- 可以同时实现排它锁和共享锁模式(AQS的功能两类:独占、共享。这两类不会同时实现)
- 内部维护了一个队列来管理锁,线程会首先尝试获取锁,如果失败就将当前线程以及等待状态等信息包装成一个node节点,加入到之前介绍的同步队列中,接下来继续不断地尝试获取锁,条件是当前节点为head的直接后继才会尝试,如果失败就会阻塞自己,直到自己被唤醒,而当持久锁的线程释放锁的时候,会唤醒队列中的后继线程。
- 基于这些基础的设计,jdk提供了许多基于AQS的子类,就是我们常用的同步的组件。
二、AQS常用同步组件
2.1、CountDownLatch(倒计时器)
2.1.1、CountDownLatch简介
- 使用一个计数器,来完成类似于阻塞的操作,如果计数器的值不为0,则组织进程进入,可以用作防止存在任务未完成就开始进入下一步。
- 是一个同步辅助类,可以完成类似于阻塞当前线程的功能。一个线程或多个线程一直等待,直到其他线程执行的操作完成。通过一个给定的计数器来实现,该计数器的操作是具备原子性的操作,就是同时只能有一个线程去操作该计数器。
- 调用线程
await
方法的线程会一直处于阻塞状态,直到其它线程调用countDown
方法,使当前计数器的值变为0,每次调用的时候计数器的值会-1,计数器的值被减到0 的时候,所有因调用await
的方法而处于等待状态的线程就会继续往下执行,这个操作只能被实现一次,因为计数器的值是不能被重置的。- 使用场景:并行计算,通过
countDownLatch
保证所有请求都被处理完。
2.1.2、CountDownLatch 使用
@Slf4j
public class CountDownLatchExample1 {
private static int threadCount = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
//Thread.sleep(1);
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
log.error("exception ", e);
} finally {
countDownLatch.countDown();
}
});
}
//第一个是值,第二个是时间的单位
//countDownLatch的计数器减到0的时候才执行下面这句话
countDownLatch.await(10, TimeUnit.MILLISECONDS);
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
Thread.sleep(100);
log.info("{}", threadNum);
}
}
2.2、Semaphore(信号量)(牛逼)
2.2.1、Semaphore 简介
Semaphore
称为信号量,可以控制并发访问的线程个数- 信号量是用来对某一共享资源所能访问的最大个数进行控制
- 可以轻松完成操作系统中信号量功能。
- 实用场景:常用于仅能提供优先访问的资源,比如数据库的最大连接数只有20,而上层系统的并发数可能远远大于20,如果同时对数据库进行操作,可能导致无法获取数据库连接数而导致异常,这个时候就可以通过信号量semaphore来做并发访问控制,当
semaphore
控制到1 的时候就跟单线程差不多了。
2.2.2、Semaphore 使用
方法 | 介绍 |
---|---|
void acquire() | 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 |
void release() | 释放一个许可,将其返回给信号量。 |
int availablePermits() | 返回此信号量中当前可用的许可数。 |
boolean hasQueuedThreads() | 查询是否有线程正在等待获取。 |
(1)直接使用,不限定每次获取和每次释放的信号量个数
每次信号量区间(acquire和release之间)中只能塞入20个信号量,如果当信号量满了,则进程将在acquire
处等待空闲信号量。由于下面的进程每次执行都会在获取到信号量之后睡眠一秒,所以每次输出都是20个,但是不保证执行顺序。
@Slf4j
public class SemaphoreExample1 {
private static int threadCount = 200;
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
//创建Semaphore信号量,初始化许可大小为20
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
semaphore.acquire();//获取一个许可
test(threadNum);
semaphore.release();//释放一个许可
} catch (Exception e) {
log.error("exception ", e);
} finally {
}
});
}
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
(2)限定每次获取和每次释放的信号量个数
semaphore.acquire(4);//获取多个许可,表示之后每一个进程都会耗费4个许可。
semaphore.release(4);//释放多个许可,表示之后每一个进程都会释放4个许可。感觉这两句话只有占用资源的用处,控制经过某部分的代码块的某进程的许可数量
@Slf4j
public class SemaphoreExample2 {
private static int threadCount = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
semaphore.acquire(4);//获取多个许可
test(threadNum);
semaphore.release(4);//释放多个许可
} catch (Exception e) {
log.error("exception ", e);
} finally {
}
});
}
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
(3)使用tryAcquire()尝试获取资源
@Slf4j
public class SemaphoreExample3 {
private static int threadCount = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
//尝试获取一个许可,获取到了就允许执行
if (semaphore.tryAcquire()) {
test(threadNum);
semaphore.release();//释放一个许可
}
} catch (Exception e) {
log.error("exception ", e);
} finally {
}
});
}
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
(4)使用tryAcquire()尝试获取资源的同时设置最长等待时间
@Slf4j
public class SemaphoreExample4 {
private static int threadCount = 200;
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
//尝试获取一个许可,获取到了就允许执行
//等一秒尝试获取许可
//只有5秒的执行,5秒之后再也不等待了
if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
test(threadNum);
semaphore.release();//释放一个许可
}
} catch (Exception e) {
log.error("exception ", e);
} finally {
}
});
}
log.info("finish");
exec.shutdown();
}
private static void test(int threadNum) throws InterruptedException {
log.info("{}", threadNum);
Thread.sleep(1000);
}
}
2.3、CyclicBarrier
2.3.1、CyclicBarrier简介
- 和CountDownLatch很像,都能阻塞进程,CountDownLatch是-1操作,CyclicBarrier是+1操作,CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的。
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
- 是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共的屏障点。
- 通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。
- 和CountDownLatch有一些相似的地方,都是通过计数器来实现的。当某个线程调用了await方法之后,该线程就进入了等待状态,计数器执行的是+1操作。当计数器的值达到初始值的时候,因为调用await方法进入等待的线程被唤醒,继续执行他们后续的操作,由于它在释放等待线程后可以重用,所以可以称它为循环屏障,可以重复使用。
- 场景和CountDownLatch很相似。主要是实现了多个线程相互等待。
- 可以用于多线程计算数据,最后合并计算结果的场景。
2.3.2、CyclicBarrier的方法
(1)构造方法
public CyclicBarrier(int parties)
//parties 是参与线程的个数
public CyclicBarrier(int parties, Runnable barrierAction)
//第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
(2)重要方法
public int await()
//线程调用 await() 表示自己已经到达栅栏
public int await(long timeout, TimeUnit unit)
//BrokenBarrierException报错 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
(3)其他方法
int getNumberWaiting()
//返回当前在屏障处等待的参与者数目。
int getParties()
//返回要求启动此 barrier 的参与者数目。
boolean isBroken()
//查询此屏障是否处于损坏状态。
void reset()
//将屏障重置为其初始状态。如果调用了该函数,则在等待的线程将会抛出BrokenBarrierException异常。
2.3.3、CyclicBarrier使用
(1) CyclicBarrier的基本使用
@Slf4j
public class CyclicBarrierExample1 {
//当barrier.await();上方堵住的进程有5个时,全部放行,计数器变为0,如果上方堵住进程没到5个时,就会继续堵住
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
final int threadNum = i;
Thread.sleep(1000);
executor.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
log.error("exception ",e);
}
});
}
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
(2)CyclicBarrier设置等待超时时间
@Slf4j
public class CyclicBarrierExample2 {
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
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);
}
});
}
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
try {
//如果在2秒内没有塞满,则会报错
barrier.await(2000, TimeUnit.MILLISECONDS);
}catch (Exception e){
System.out.println(barrier.getParties());
log.error("exception {}",e);
}
log.info("{} continue", threadNum);
}
}
(3)CyclicBarrier结合lambda表达式
@Slf4j
public class CyclicBarrierExample3 {
//线程达到屏障的时候优先执行里面的内容
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
log.info("callback is running");
});
public static void main(String[] args) throws InterruptedException {
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);
}
});
}
}
private static void race(int threadNum) throws Exception {
Thread.sleep(1000);
log.info("{} is ready", threadNum);
barrier.await();
log.info("{} continue", threadNum);
}
}
2.4、ReentrantLock与锁(特别牛逼,建议深入了解)
Java有两类锁,第一种是
synchronized
,一种就是ReentrantLock
,它的核心是lock和unlock。
2.4.1、ReentrantLock(可重入锁)和synchronized区别
- 可重入性:(在进入锁,两者都可以,两者在这里的差别不大)。
- 锁的实现:
synchronized
是通过JVM实现的,ReentrantLock
是通过JDK实现的(一个是操作系统控制实现,一个用户自己敲代码实现)后者可以看源码来看。- 性能的区别:偏向锁、轻量锁引入之后
synchronized
就比后者好一些了,就倾向于这个。- 功能:便利性(
synchronized
好一些)。后者需要手动声明加锁、手动释放锁。细腻度和灵活度后者好一些。
2.4.2、ReentrantLock独有功能
- 可指定是公平锁(先等待先获得)还是非公平锁。
synchronized
都是非公平锁。- 提供了一个
Condition
类,可以分组唤醒需要唤醒的线程。不像synchronized
必须全部唤醒。- 提供能够中断等待锁的线程的机制,
lock.lockInterruptibly()
。- 如果要实现这三个独有功能,就最好使用这个
ReentrantLock
。- Sync能做的reen都能做,但是反过来则不一定
2.4.3、ReectrantLock使用
(1)通过Lock父类接口实例化ReectrantLock
@Slf4j
@ThreadSafe
public class LockExample2 {
private static int threadTotal = 200;//同时最多200个请求同时执行
private static int clientTotal = 50000;//总共5000个请求
private static long count = 0;//获取到的数量
private final static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//使用线程池和信号量来模仿客户端
ExecutorService exec = Executors.newCachedThreadPool();//线程池
final Semaphore semaphore = new Semaphore(threadTotal);//信号量
//保证线程完全完整的执行
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int index = 0; index < clientTotal; index++) {
exec.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
exec.shutdown();
log.info("count:{}", count);
}
private static void add() {
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
}
(2)StampedLock的使用
@Slf4j
public class LockExample5 {
private static int threadTotal = 200;//同时最多200个请求同时执行
private static int clientTotal = 50000;//总共5000个请求
private static long count = 0;//获取到的数量
private final static StampedLock lock = new StampedLock();
public static void main(String[] args) throws InterruptedException {
//使用线程池和信号量来模仿客户端
ExecutorService exec = Executors.newCachedThreadPool();//线程池
final Semaphore semaphore = new Semaphore(threadTotal);//信号量
//保证线程完全完整的执行
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int index = 0; index < clientTotal; index++) {
exec.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
exec.shutdown();
log.info("count:{}", count);
}
private static void add() {
long stamp = lock.writeLock();
try {
count++;
}finally {
//有lock就一定要有unlock
lock.unlock(stamp);
}
}
}
(3)ReentrantReadWriteLock使用
@Slf4j
public class LockExample3 {
private final Map<String,Data> 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<String> 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{
}
public static void main(String[] args) throws InterruptedException {
}
}
(4)ReentrantLock 和 Condition配合使用
@Slf4j
public class LockExample6 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
new Thread(()->{
try {
reentrantLock.lock();
log.info("wait signal");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("get signal");
reentrantLock.unlock();
}).start();
new Thread(()->{
reentrantLock.lock();
log.info("get lock");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
log.info("send signal ~");
reentrantLock.unlock();
}).start();
}
}