Java中模拟并发请求,自然是很方便的,只要多开几个线程,发起请求就好了。但是,这种请求,一般会存在启动的先后顺序了,算不得真正的同时并发!怎么样才能做到真正的同时并发呢?是本文想说的点,java中提供了闭锁 CountDownLatch, 刚好就用来做这种事就最合适了。
只需要:
1. 开启n个线程,加一个闭锁,开启所有线程;
2. 待所有线程都准备好后,按下开启按钮,就可以真正的发起并发请求了。
1.CountDownLatch
CountDownLatch 是 java.util.concurrent 包下的一个同步辅助类,它能使一个或多个线程在其他的线程的一系列操作完成之前一直等待。
CountDownLatch
是一种通用的同步工具,可用于多种用途。
(1) 一个CountDownLatch
为一个计数的CountDownLatch用作一个简单的开/关锁存器,或者门:所有线程调用await
在门口等待,直到被调用countDown()
的线程打开。
(2) 一个CountDownLatch
初始化N可以用来做一个线程等待,直到N个线程完成某项操作,或某些动作已经完成N次
常用方法API
(1)await()方法
public void await() throws InterruptedException
线程阻塞,直到计数器为0,才会启动。
(2)countDown()方法
public void countDown()
减少锁存器的计数,如果计数达到零,释放所有等待的线程。
(3)构造方法
public CountDownLatch(int count)
构造一个以给定计数 CountDownLatch
CountDownLatch。
2.使用CountDownLatch实现模拟多线程并发请求
2.1 方法1
测试线程类:
package org.jeecg.modules.test;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
* @Author lucky
* @Date 2021/11/18 14:48
*/
public class MyTestThread implements Runnable{
private final CountDownLatch startSignal;
public MyTestThread(CountDownLatch startSignal) {
super();
this.startSignal = startSignal;
}
@Override
public void run() {
try {
startSignal.await(); //一直阻塞当前线程,直到计时器的值为0
} catch (InterruptedException e) {
e.printStackTrace();
}
doTask();
}
private void doTask() {
// TODO Auto-generated method stub
long startTime = System.currentTimeMillis();
//1. 模拟调用Flep SDK请求操作(让线程睡眠随机毫秒数)
Random random = new Random();
int timeduration=100+random.nextInt(300); // 返回给定返回的随机秒数
try {
Thread.sleep(timeduration);
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " ended at: " + endTime + ", cost: " + (endTime - startTime) + " ms.");
}
}
测试主函数类
package org.jeecg.modules.test;
import java.util.concurrent.CountDownLatch;
/**
* @Author lucky
* @Date 2021/11/18 15:04
*/
public class MyMainTest {
public void concurrentInit(int threadNum){
CountDownLatch start=new CountDownLatch(1);// 初始化计数器为1
for(int i=0;i<threadNum;i++){//模拟用户设置的给定数量的线程
MyTestThread tt =new MyTestThread(start);
Thread t = new Thread(tt);
t.start();
}
Thread.sleep(1000);//等1s,让所有子线程创建完毕,都进入run方法,执行await()方法
start.countDown();//计数器減1 所有线程释放 同时跑
}
public static void main(String[] args) {
int threadNum=20;
MyMainTest myMainTest=new MyMainTest();
myMainTest.concurrentInit(threadNum);
}
}
注意:这种方法有一个坑,由于主线程和子线程存在争夺cpu时间片,若不让主线程睡眠1s,则主线程和子线程会同时争抢CPU时间片,导致start.countDown()这句代码在所有子线程阻塞住之前执行,并发效果无法实现。
原因分析:Thread类下的start()和run()方法的区别
多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。
调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。
1) start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
2) run: run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。
记住:多线程就是分时利用CPU,宏观上让所有线程一起执行 ,也叫并发。
2.2 方法2(推荐)
测试主函数类
@RestController
@RequestMapping("/test/current")
@Slf4j
public class TestConcurrentController {
@PostMapping("/uploadFileInner")
public void uploadFileInner(@RequestParam String contentId,@RequestParam String filePath,@RequestParam int threadNum){
log.info("--------------------------------------------------");
//01 创建指定threadNum个数的线程
final CountDownLatch latch =new CountDownLatch(threadNum);
for (int i = 0; i <threadNum ; i++) {
String fileFullPath=filePath+i+".doc";
MyConcurrentUploadThread uploadThread=new MyConcurrentUploadThread(latch, contentId, fileFullPath);
Thread t=new Thread(uploadThread);
t.start();
}
}
}
测试线程类:
@Slf4j
public class MyConcurrentUploadThread implements Runnable {
private final CountDownLatch latch;
private final String contentId;
private final String fileFullPath;
public MyConcurrentUploadThread(CountDownLatch latch, String contentId, String fileFullPath) {
this.latch = latch;
this.contentId = contentId;
this.fileFullPath = fileFullPath;
}
@Override
public void run() {
//01 当子线程争抢到CPU时间片时,latch中计数减一,直至计数减至0,放开阻塞,所有线程一起跑
latch.countDown();
log.info(Thread.currentThread().getName()+",prepare at: "+System.currentTimeMillis());
try {
//02 阻塞线程
latch.await();
doTask();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doTask() {
long startTime = System.currentTimeMillis();
//1. 模拟调用Flep SDK请求操作(让线程睡眠随机毫秒数)
Random random = new Random();
int timeduration=100+random.nextInt(600); // 返回给定返回的随机秒数
try {
Thread.sleep(timeduration);
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
log.info(Thread.currentThread().getName() + ", start at: "+startTime+", end at: " + endTime + ", cost: " + (endTime - startTime) + " ms.");
}
}
测试截图: