Java基于线程池和AQS模拟高并发
概述
《手写高并发下线程安全的单例模式》主要介绍使用枚举类实现JAVA单例模式,以及在高并发环境下验证此单例模式是线程安全的。本文借助ReentrantLock、CountDownLatch和Semaphore等,基于线程池验证如何创建线程安全和不安全的方法。
实际项目中,我们有很多高并发的场景需要考虑、设计,在高并发领域有个耳熟能详的名词叫惊群效应。以喂鸽子为例进行说明,当你往一群鸽子中间扔一块食物时,虽然最终只有一只鸽子抢到食物,但所有鸽子都会被惊动从而飞过来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。也就是说,虽然只扔了一块食物,但是惊动了整个鸽群,最后却只有一只鸽子抢到食物,浪费了其它鸽子的能量。
对于操作系统来说,多个进程/线程在等待同一资源时,也会产生类似的效果,其结果就是每当有可用资源时,所有的进程/线程都来竞争资源,造成了资源的浪费[2]:
- 系统对用户进程/线程频繁做无效的调度和上下文切换,导致系统性能大打折扣。
- 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
下面基于ReentrantLock、CountDownLatch和Semaphore创建一个并发模拟工具,当被调用函数不出现惊群效应时,说明是线程安全的。
CountDownLatch是一个能阻塞主线程,让其它线程满足特定条件下再继续执行的工具。比如倒计时3000,每当一个线程完成一次操作就让它执行countDown一次,直到count为0之后输出结果,这样就保证了其它线程一定是满足了特定条件(执行某操作5000次),模拟了并发执行次数。
Semaphore信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。
模拟工具
创建模拟工具,首先创建一个线程安全地售票的任务类:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程安全地售票
*/
public class SafeSale implements Runnable {
//定义车票的数量
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
boolean a = true;
while (a) {
try {
//对操作共享数据的代码进行加锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + "出售第" + ticket + "张车票");
//车票数量减一
ticket--;
//线程休眠,增加其他线程调用的机会
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
a = false;
}
} finally {
lock.unlock(); //进行解锁
}
}
}
}
紧接着,创建测试用例所需要的非线程安全函数和main函数:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 模拟高并发
*/
public class ConcurrencyCheck {
// 执行次数
private static final int THREAD_COUNT = 3000;
// 并发数
private static final int CONCURRENT_COUNT = 200;
// 全局变量,容易出幺蛾子
private static int count = 10000;
public static void main(String[] args) throws InterruptedException {
lockSale();
checkUtil();
}
/**
* 基于lock方法售票
*/
private static void lockSale() {
ExecutorService executorService = Executors.newCachedThreadPool();
SafeSale sale = new SafeSale();
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.execute(sale);
}
executorService.shutdown();
}
/**
* 模拟线程非安全,模拟工具主题
* @throws InterruptedException
*/
private static void checkUtil() throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(CONCURRENT_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
subtraction();
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("计数结果:" + count);
}
/**
* 售票,非线程安全,被验证对象
*/
private static void subtraction() {
count--;
}
}
如果多运行几次如上main函数就会发现,方法subtraction()并不是线程安全的,即其执行结果几乎都是大于7000,很少等于7000。