一、前言
Java中的多线程无疑是编程中的非常重要的一环、可以并行执行任务,达到提高效率、节省时间的目的。我们可以直接通过继承Thread类或者实现Runnable接口创建多线程。当然线程少的话是可以直接创建线程的,但是当线程多时可能造成系统内存OOM等,因此有必要使用线程池、线程池和连接池的概念类似、是通过维护一定数量的线程来达到多个线程复用的目的。
使用多线程时、有时候我们需要保证前面的多线程全部执行完毕再执行后面的任务,如何实现让我们一起来看看
二、多线程创建
1、 实现thread类,重写run()方法,start()方法开启线程
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("实现Thread类创建多线程");
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
2、实现Runnable接口、实现run()方法,同样可以直接创建一个new Thread(xxxRunnable).start()开启线程。该方式使用较频繁、因为Java是单继承、多实现、实现Runnable接口还可以实现其他的接口,而继承了Thread类之后就不能再继承其他类了,有很大的局限性。实际上jdk8之后lamda表达式可以简化这种多线程创建的方式,下面可见示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我是直接实现Runnable接口的多线程实现方式!");
}
public static void main(String[] args) {
Thread t2 = new Thread(new MyRunnable());
t2.start();
// lamda表达式简化简化书写
new Thread(() -> System.out.println("我实质也是实现Runnable接口的匿名对象--lamda表达式实现!")).start();
}
}
3、使用线程池可以提高线程的使用效率、避免反复的创建线程、销毁线程带来的开销。创建线程池之后以后如果需要使用线程池的时候,可以直接从线程池里取出即可。
public class MyThreadExecutor {
int corePoolSize = 10; // 线程池的基本大小
int maximumPoolSize = 30; // 线程池最大大小
int keepAliveTime = 10; // 线程存活保持时间
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
// 创建一个固定长的线程池,更简单一些
ExecutorService executorService = Executors.newFixedThreadPool(30);
public void submit(Runnable task){
executor.submit(task);
}
/**
* 多线程-获取返回结果
* @param task 需要运行的任务
* @param <T> Future<T>用于取得任务的返回结果
* @return 可以用 future.get() 方法读取
*/
public static <T> Future<T> submit(Callable<T> task){
return executor.submit(task);
}
}
比如我们前面创建了4个多线程,我们想统计这4个多线程全部执行完之后所需要的时间、这时候就必须等待全部线程执行完毕再计算耗时,这里使用到CountDownLatch,每个线程执行完毕之后,调用其countDown()方法,数量就会减少1,最后调用await()方法,当计数器为0时就表征全部的多线程执行完毕,否则线程将一直悬挂等待计数器的值为0. 这里给出测试方法实例。
public class AsyncService {
public String testMethod1(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是异步方法1");
return "1";
}
public String testMethod2(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是异步方法2");
return "2";
}
public String testMethod3(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是异步方法3");
return "3";
}
public String testMethod4(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是异步方法4");
return "4";
}
public String testMethod5(String n){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是异步方法"+n);
return n;
}
}
接下来我们调用这四个方法,并统计全部时间执行完毕所需时间:
public class MultiThreadCsdnTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
CountDownLatch countDownLatch = testMyExecutorPool();
try {
// 判断计数器是否为0,如果不为0,线程将一直处于悬挂状态直到计数器的值为0
countDownLatch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("四个线程全部执行完毕");
long endTime = System.currentTimeMillis();
System.out.println("4个线程全部执行完所需时间为:" + (endTime - startTime));
}
public static CountDownLatch testMyExecutorPool(){
AsyncService service = new AsyncService();
CountDownLatch countDownLatch = new CountDownLatch(4);
MyThreadExecutor myThreadExecutor = new MyThreadExecutor();
myThreadExecutor.submit(() -> {
service.testMethod1();
countDownLatch.countDown();
});
myThreadExecutor.submit(() -> {
service.testMethod2();
countDownLatch.countDown();
});
myThreadExecutor.submit(() -> {
service.testMethod3();
countDownLatch.countDown();
});
myThreadExecutor.submit(() -> {
service.testMethod4();
countDownLatch.countDown();
});
return countDownLatch;
}
}
result:
我是异步方法1
我是异步方法3
我是异步方法4
我是异步方法2
四个线程全部执行完毕
方法执行的时间为:2067
可以发现,四个线程全部执行完毕之后,才统计到方法总执行时间,总用时2067ms,符合预期。如果不使用多线程,依次顺序执行上述4个方法那么总耗时就是8000ms.
@Test
public void testOneThread() {
AsyncService service = new AsyncService();
long startTime = System.currentTimeMillis();
service.testMethod1();
service.testMethod2();
service.testMethod3();
service.testMethod4();
long endTime = System.currentTimeMillis();
System.out.println("总耗时:"+(endTime-startTime));
}
result:
我是异步方法1
我是异步方法2
我是异步方法3
我是异步方法4
总耗时:8000
用jdk8之后的方法CompletableFuture实现多线程,这里是实现了Future接口的一种设计模式。Future模式可以理解为:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何自己想做的事情。一段时间之后,我就可以从Future那里取出结果。Java代码如下:
@Service
public class AsyncServiceImpl {
@Test
public void asyncCompletableFuture(){
// 自定义一个线程池
ExecutorService executor = Executors.newFixedThreadPool(30);
// 这个是线程执行不带有返回值的
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println("我是自定义线程池的方法,没有返回值!");
}, executor);
// 这个是带有返回值的
CompletableFuture<String> futureResult = CompletableFuture.supplyAsync(() -> {
System.out.println("我是带有返回值的异步方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello result";
}, executor);
// 通过future.get()获取异步线程池的执行结果,如果暂时获取不到结果就会一直等待
try {
String s = futureResult.get();
System.out.println("result========="+s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 如果不显示指定线程池,将使用默认线程池 ForkJoinPool.commonPool()
CompletableFuture.runAsync(()->{
System.out.println("我就是一个直接的异步方法111");
});
CompletableFuture.runAsync(()->{
System.out.println("我就是一个直接的异步方法222");
});
}
}
三、获取线程执行结果
项目中我们比如有一个统计任务需要汇总商品表、人员表、订单表等信息做展示,如果使用单线程执行可能就比较耗时,这时候最好使用多线程并发去执行,比如可以开三个线程分别去统计商品表、人员表、订单表等的数据,各线程统计完毕后再汇总包装VO,返回给前端展示即可。此时我们就要获取多线程的执行结果,并且只能等3个线程全部执行完毕后,才能包装最后的结果,因为只有这样才能确保统计项不会丢失。这时候可以使用实现了Callable接口的任务借助Future接口来获取:
/**
* 多线程-获取返回结果
* @param task 需要运行的任务
* @param <T> Future<T>用于取得任务的返回结果
* @return 可以用 future.get() 方法读取
*/
public static <T> Future<T> submit(Callable<T> task){
return executor.submit(task);
}
该方法已添加在上文MyThreadExecutor自定义线程池的方法里面了,我们修改AsyncService测试方法返回值类型为string,见上文,接下来我们写个小例子测试下:
@Test
public void testCallable() throws Exception {
AsyncService service = new AsyncService();
Future<String> f1 = MyThreadExecutor.submit(() -> {
String result = service.testMethod1();
return result;
});
Future<String> f2 = MyThreadExecutor.submit(() -> {
String result = service.testMethod2();
return result;
});
Future<String> f3 = MyThreadExecutor.submit(() -> {
String result = service.testMethod3();
return result;
});
Future<String> f4 = MyThreadExecutor.submit(() -> {
String result = service.testMethod4();
return result;
});
Future<String> f5 = MyThreadExecutor.submit(() -> {
String result = service.testMethod5(n);
return result;
});
/**
* 这个方法非常实用啊,这个异步获取结果可以保证执行的顺序以及并发获取结果
*/
long start = System.currentTimeMillis();
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
System.out.println(f5.get());
long end = System.currentTimeMillis();
System.out.println("=============总耗时==========" + (end - start));
}
result:
我是异步方法1
1
我是异步方法4
我是异步方法3
我是异步方法2
2
3
4
我是异步方法5
5
=============总耗时==========2000
特别说明的是,future.get()方法如果没有获取到结果就一直处于悬挂等待状态,或者抛出异常,实际上这正是我们想要的,因为最后汇总数据的时候我们要确保所有的多线程执行完毕拿到结果才行。可以看到:5个异步方法的执行顺序是不固定的,取决于抢夺CPU的速度,但是最后future.get()方法获取结果的时候是有顺序的,如果第一个线程没有执行完毕就一直处于等待状态中。
四、小结
我们在项目中很多场景都要使用多线程,当创建的线程不多时可以直接创建而不必使用线程池、但是当线程较多时为了提高效率和方便管理最好是首先创建线程池,然后再使用。创建线程池除了上文提到的基本创建方式外,也可以创建一个固定数目的线程池(Executors.newFixedThreadPool(threadCount))或者其他方式等。最后很多场景下我们需要等所有的多线程执行完毕之后再执行后面的任务,这时候可以选用一个计数器,每个线程执行完毕后计数器减少1,只有当计数器为0时才执行后面的内容,否则一直处于悬挂状态。