前言
之前分享了好几篇关于线程并发安全问题的文章,相信大家对多线程安全问题已经深有感触了
但是关于多线程不仅要知道如何保证数据的安全,还要知道怎么让多线程协同去工作
场景举例
1.某个接口执行比较复杂的业务时占用内存和CPU都比较高,如果请求量超过10个将会导致系统的性能整体下降
2.某个接口处理时间比较长,为了提高接口的响应速度决定将一件事分给多个线程一起去做,当多线程做完了再由主线程合并计算
....................
实际工作中还会有很多这样的应用场景,限流、协同、线程栅栏这些常见的多线程应对手段都值得我们去学习和探讨
在了解并发工具类之前还要先知道aqs机制,接下来我们一起理解aqs
AbstractQueuedSynchronizer
嗯!既然是一个抽象类就意味着这是之前理解的一个模板,我们接着往下看
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
// 前驱结点
volatile Node prev;
// 后驱结点
volatile Node next;
// 当前线程
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
看到前驱结点和后驱节点之后就可以断定它就是一个链表,从这个类描述中可以看到这是一个双端双向循环链表
/**
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*/
因为head指向头部节点所以它知道头部是哪个,接下来通过画一个图来理解一下Node链表的关系
除了链表以外它还维护了一个状态,这个状态由volatile修饰那就必然具备了可见性,也提供了这个状态的cas操作
对于aqs里面的所有操作都是在维护这个状态,通过对这个状态的操作来完成我们期望的加锁/解锁
它把我们之前想要实现的可重入锁/读写锁中的模板代码封装了起来
如果日后要实现关于锁方面的需求,可以直接继承aqs实现它里面的一些特定方法即可完成相对应的功能
需要重写的特定方法如下
| 方法 | 描述 |
|---|---|
| isHeldExclusively() | 该线程是否正在独占资源.只有用到Condition才需要实现它 |
| tryAcquire(int) | 独占方式尝试获取锁,成功返回true失败返回false |
| tryRelease(int) | 独占方式尝试释放锁,成功返回true失败返回false |
| tryAcquireShared(int) | 共享方式尝试获取锁,成功返回0,失败返回负数,正数表示成功且有剩余资源 |
| tryReleaseShared(int) | 共享方式尝试释放锁,如果释放后允许唤醒后续等待节点返回true否则返回false |
Semaphore
Semaphore是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目,简单来说它就是一种用来控制并发量的共享锁
使用
/**
* <p>
* 计数信号量使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:16
*/
public class SemaphoreDemo {
private static final Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
try {
// 获取信号量,当达到限制范围会阻塞该线程
semaphore.acquire();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "执行结束" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放信号量
semaphore.release();
}
});
}
executorService.shutdown();
}
}
从结果中可以看到每次只会有两个线程去执行,其余的都在等待直到有线程释放后才能获取,它能够起到限流的作用
实现
从它的表现可以猜得出来像是一把限制了数量的读锁,每次只能给限定数量的线程通过超过则阻塞等待
既然如此我们可以继承aqs重写共享锁对应的两个特定方法(tryAcquireShared、tryReleaseShared)来实现Semaphore
/**
* <p>
* 自定义Semaphore
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:31
*/
public class MySemaphore {
private final Sync sync;
class Sync extends AbstractQueuedSynchronizer {
private volatile int permits;
private Sync(int permits) {
this.permits = permits;
}
@Override
protected int tryAcquireShared(int arg) {
int state = getState();
int updateState = getState() + arg;
if (updateState <= permits) {
if (compareAndSetState(state, updateState)) {
return permits - updateState;
}
}
return -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
int state = getState();
int updateState = getState() - arg;
if (updateState >= 0) {
return compareAndSetState(state, updateState);
}
return false;
}
}
public MySemaphore(int permits) {
sync = new Sync(permits);
}
public void acquire() {
sync.acquireShared(1);
}
public void release() {
sync.releaseShared(1);
}
}
用了aqs以后发现代码量是很少的,因为很多重复的代码aqs都封装好了,改成用自己写的试一把
/**
* <p>
* 计数信号量使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:16
*/
public class SemaphoreDemo {
private static final MySemaphore semaphore = new MySemaphore(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
try {
// 获取信号量,当达到限制范围会阻塞该线程
semaphore.acquire();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放信号量
semaphore.release();
}
});
}
executorService.shutdown();
}
}
通过使用我们实现MySemaphore同样可以达到semaphore的效果
CountDownLatch
CountDownLatch是一个线程计数器,能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行
使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一
当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务
使用
它有两种用法:1.希望多个线程在某一刻并发执行 2.主线程等待多线程执行完后执行
希望多个线程在某一刻并发执行
/**
* <p>
* 线程计数器使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:50
*/
public class CountDownLatchDemo {
private static CountDownLatch countDownLatch = new CountDownLatch(9);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 9; i++) {
executorService.execute(() -> {
// 计数器减一
countDownLatch.countDown();
try {
// 等待计数器的值为0时再执行
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
主线程等待多线程执行完后执行
/**
* <p>
* 线程计数器使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:50
*/
public class CountDownLatchDemo {
private static CountDownLatch countDownLatch = new CountDownLatch(9);
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 9; i++) {
executorService.execute(() -> {
list.add(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
// 执行完毕计数器减一
countDownLatch.countDown();
});
}
executorService.shutdown();
try {
countDownLatch.await();
list.forEach(System.out::println);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
实现
/**
* <p>
* 自定义CountDownLatch
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:01
*/
public class MyCountDownLatch {
private Sync sync;
class Sync extends AbstractQueuedSynchronizer {
private Sync(int count) {
setState(count);
}
@Override
protected int tryAcquireShared(int arg) {
return getState() == 0 ? 0 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
// 循环操作cas直到成功为止
while (true) {
int state = getState();
int updateState = getState() - arg;
if (updateState < 0) {
return false;
} else {
if (compareAndSetState(state, updateState)) {
// 当最后一个线程执行完毕时再唤醒后续等待节点
return updateState == 0;
}
}
}
}
}
public MyCountDownLatch(int count) {
this.sync = new Sync(count);
}
public void countDown() {
sync.releaseShared(1);
}
public void await() {
sync.acquireShared(1);
}
}
/**
* <p>
* 线程计数器使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 17:50
*/
public class CountDownLatchDemo {
private static MyCountDownLatch countDownLatch = new MyCountDownLatch(9);
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 9; i++) {
executorService.execute(() -> {
list.add(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
// 执行完毕计数器减一
countDownLatch.countDown();
});
}
executorService.shutdown();
countDownLatch.await();
list.forEach(System.out::println);
}
}
通过使用我们实现MyCountDownLatch同样可以达到CountDownLatch的效果
CyclicBarrier
CyclicBarrier是一个循环栅栏,它必须要达到指定的线程数量才能往下执行
使用
/**
* <p>
* CyclicBarrier使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:32
*/
public class CyclicBarrierDemo {
private static int i = 0;
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
i++;
System.out.println("第" + i + "组执行");
});
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 4; i++) {
executorService.execute(() -> {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
可以看得到它是按照指定的数量分组进行执行,但是假设某一组并未达到指定的线程数量将会进入阻塞状态
/**
* <p>
* CyclicBarrier使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:32
*/
public class CyclicBarrierDemo {
private static int i = 0;
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
i++;
System.out.println("第" + i + "组执行");
});
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(() -> {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
可以看到程序没能结束,因为有一个线程进入了阻塞状态它在等待足够的数量才会被唤醒
实现
CyclicBarrier用cas的机制不好实现,而它的实现原理也并非是利用aqs
而是利用了可重入锁ReentrantLock里面的Condition机制
/**
* <p>
* 自定义CyclicBarrier
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:51
*/
public class MyCyclicBarrier {
/**
* 可重入锁
*/
private Lock lock = new ReentrantLock();
/**
* 等待/唤醒机制
*/
private Condition condition = lock.newCondition();
/**
* 版本号(被唤醒时需要判断是否属于这一组的)
*/
private Object generation = new Object();
/**
* 累加栅栏数
*/
private volatile int count = 0;
/**
* 固定栅栏数
*/
private final int parties;
/**
* 调用者自定义的Runnable方法,每当达到固定栅栏数时调用
*/
private final Runnable runnable;
public MyCyclicBarrier(int parties, Runnable runnable) {
this.parties = parties;
this.runnable = runnable;
}
public void await() throws InterruptedException {
Lock lock = this.lock;
lock.lock();
try {
count++;
final Object obj = generation;
if (count != parties) {
// 防止伪唤醒,循环处理
while (true) {
condition.await();
// condition.signalAll() 可能会唤醒到不同版本的线程,通过版本号进行区分
if (obj != generation) {
return;
}
}
} else {
count = 0;
generation = new Object();
runnable.run();
condition.signalAll();
}
} finally {
lock.unlock();
}
}
}
/**
* <p>
* CyclicBarrier使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:32
*/
public class CyclicBarrierDemo {
private static int i = 0;
private static MyCyclicBarrier cyclicBarrier = new MyCyclicBarrier(2, () -> {
i++;
System.out.println("第" + i + "组执行");
});
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 4; i++) {
executorService.execute(() -> {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
故意将执行数量设置为拼凑不成一组的看看会不会被阻塞
/**
* <p>
* CyclicBarrier使用
* </p>
*
* @author 千手修罗
* @date 2020/12/24 0024 18:32
*/
public class CyclicBarrierDemo {
private static int i = 0;
private static MyCyclicBarrier cyclicBarrier = new MyCyclicBarrier(2, () -> {
i++;
System.out.println("第" + i + "组执行");
});
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(() -> {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
通过使用我们实现MyCyclicBarrier同样可以达到CyclicBarrier的效果
总结
并发协同工具的使用和原理到这就结束了,现在回过头来再看aqs会发现已经不再那么神秘而陌生了
无非也就是将**(独占/共享)锁的实现进行封装起来**,然后再将上锁/解锁的方式交由到子类自己实现
从而达到子类不费吹灰之力就能够实现一把锁的特性,这也是为什么aqs常常会伴随着锁一起出现的原因