目录
5. ExecutorCompletionService(强烈推荐)
1.概念
1.1 线程和进程的区别
进程:是一个动态的过程,是一个活动的实体,简单来说,一个应用程序的运行就可以看作是一个进程。可以说,进程中包含了多个可以同时运行的线程。
线程(Thread):是运行中实际任务的执行者,线程是操作系统能够进行运算调度的最小单位。它被包装在进程中,是进程中的实际运行单位。每个线程都有自己的程序计数器、堆栈和局部变量,但它们共享进程的代码和内存。
1.2 线程的五种状态
-
新建(New):当线程对象对创建后,即进入了新建状态,如:
Thread t = new Thread()
。 -
就绪(Runnable):也被称为“可执行”状态,当线程对象调用
start()
方法(启动线程)后,线程即进入就绪状态。处于就绪状态的线程,只是说明了该线程可以运行,但还没有真正运行,等待CPU分配时间片。 -
运行(Running):当CPU开始调度处于就绪状态的线程时,线程进入运行状态,真正开始执行线程代码。
-
阻塞(Blocked):线程在运行过程中可能因为各种原因导致无法继续执行,比如等待I/O操作结果,或者尝试获得一个同步监视器而失败,这时它就会进入阻塞状态。
-
死亡(Dead):线程执行完毕或者因异常退出run()方法后,线程就进入死亡状态。
1.3 单线程,多线程,线程池
单线程: 顾名思义是只有一条线程在执行任务,在工作中很难遇到。
多线程: 是创建多条线程同时执行任务。
线程池(ThreadPool): 是一种管理线程的机制,它能够复用线程,避免因频繁创建和销毁线程导致的性能问题。通过设置线程池的大小,可以有效管理线程的运行。
1.4 异步与多线程的概念
异步和多线程并不是同一关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是调用者发送一个请求给被调用者,而调用者不用等待请求结果的返回,可以去做其他事。实现异步可以使用多线程或交给其他进程来处理。
2. 实现异步的方式
Util类:
public class Util {
/**
* sleep
*
* @param milliseconds
*/
public static void mySleep(int milliseconds) {
try {
TimeUnit.MILLISECONDS.sleep(milliseconds);
} catch (InterruptedException e) {
}
}
/**
* print log
* @param message
*/
public static void printfLog(String message) {
LocalDateTime localDateTime = LocalDateTime.now();
String dateString = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
System.out.println(String.format("%s - %s", dateString, message));
}
}
2.1 方式1 裸线程(Thread)
使用“原汁原味”的裸线程(Thread)。Java线程本质上被映射到操作系统线程,并且每个线程对象对应着一个计算机底层线程。
JVM管理着线程的生存期,而且只要你不需要线程间通讯,你也不需要关注线程调度。
每个线程有自己的栈空间,它占用了JVM进程空间的指定一部分。
线程的接口相当简明,你只需要提供一个Runnable,调用.start()开始计算。没有现成的API来结束线程,你需要自己来实现,通过类似boolean类型的标记来通讯。
private static void threadMethod(List<Integer> list) {
while (list.get(0) > 0) {
Util.mySleep(Double.valueOf(Math.random() * 100).intValue());
Integer result = list.get(0);
result--;
list.add(0, result);
}
}
public static void threadTest() {
Util.printfLog("主线程开始");
//线程外的变量只读,值类型只能显示的生命final
//这里正常应该会输出list result=0,但是这里因为时多个线程同时操作一个变量导致线程不安全,输出list result为负值
List<Integer> list = new ArrayList<>(1);
list.add(100);
for (int i = 0; i < 100; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
threadMethod(list);
Util.printfLog("子线程结束!currentName = " + Thread.currentThread().getName());
}
};
Thread thread = new Thread(runnable);
thread.start();
}
Util.printfLog("主线程结束");
Util.mySleep(10000);
Util.printfLog("list result=" + list.get(0));
}
这里暂时没考虑线程安全行,所以list result可能会出现负值。
2.1 方式2 线程池(Executor)
2.1.1 源码分析
Executor一个接口,它通过一系列抽象类,接口等最终生成了线程池ThreadPoolExecutor。
它的包在java.util.concurrent。线程池的底层实现也是通过一系列的操作,通过Thread创建单独的线程。
ExecutorService.submit->AbstractExecutorService.submit->ThreadPoolExecutor.execute->
ThreadPoolExecutor.addWorker->Worker构造函数->DefaultThreadFactory.newThread
也可以使用工具类Executors创建线程池,底层也是通过创建ThreadPoolExecutor创建线程池。
ThreadPoolExecutor的主要方法:
方法名 | 描述 |
---|---|
submit | Callable<T>的实现类,创建带返回值的线程 Runnable的实现类,创建不带返回值的线程 |
shutdown | 优雅的终止线程。线程池中的线程不会立即结束,等线程池中没有在运行的线程才终止后台的线程池。 |
shutdownNow | 直接终止线程。不管线程池中有没有在运行的线程,直接将后台的线程池终止。 |
2.1.2 线程池创建(Executors)
线程池Executors工具类的主要方法:
方法名 | 描述 |
---|---|
newCachedThreadPool | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 |
newFixedThreadPool | 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 |
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 |
newScheduledThreadPool | 创建一个定长线程池,支持定时及周期性任务执行。 ScheduledExecutorService的方法: schedule:定时到达时间间隔执行。 |
public static void main(String[] args) throws IOException {
System.out.println("主线程【开始】");
executorsNoWait();
System.out.println("主线程【结束】");
System.in.read();
}
/**
* 线程直接提交,提交完成之后直接直接回到主进程
*/
private static void executorsNoWait() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(() -> {
threadMethod(5, "线程--1");
return "第一线程";
});
executorService.submit(() -> {
threadMethod(2, "线程--2");
return "第二线程";
});
// 线程池没shutdwon,后台运行
executorService.shutdown();
}
private static void threadMethod(int num, String name) {
for (int i = 0; i < num; i++) {
Util.mySleep(1000);
Util.printfLog(String.format("%s", name));
}
}
这种方式创建只提交线程到线程池,不阻塞主线程,不获取线程的返回值。通过代码运行结果,主线程已经执行完成,但是线程池中的子线程还没结束所以没有获取结果就不会阻塞主线程的运行。 只有当线程池中的没有runing的线程,线程池才会shutdown。
2.1.3 阻塞主线程获取子线程返回值
这种方式就是主线程提交子线程后,子线程异步运行。然后通过阻塞主线程,等待子线程都运行结束,再获取子线程的结果。
1.线程池awaitTermination轮询
这种方式只能通过future.get方式获取返回值,只能等全部子线程执行完成才能获取,通过future.get返回值。
/**
* 线程池等待,阻塞回到主进程--awaitTermination轮训
*/
private static void executorsAwaitTermination() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<String> future = executorService.submit(() -> {
threadMethod(5, "线程--1");
return "第一线程";
});
Future<String> future2 = executorService.submit(() -> {
threadMethod(2, "线程--2");
return "第二线程";
});
executorService.shutdown();
try {
while (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
Util.printfLog("等待中");
}
} catch (Exception e) {
}
}
2.线程池future.get
直接使用future.get获取子线程的返回值,通过遍历集合中的future,以固定的顺序获取子线程返回值,只有当获取的子线程有返回值之后才继续循环获取下一个子线程的返回值。
/**
* 线程池等待,阻塞回到主进程--Future.get()
*/
private static void executorsFutureGet() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Future<String>> listTemp = new ArrayList<>();
Future<String> future = executorService.submit(() -> {
threadMethod(5, "线程--1");
return "第一线程";
});
listTemp.add(future);
Future<String> future2 = executorService.submit(() -> {
threadMethod(2, "线程--2");
return "第二线程";
});
listTemp.add(future2);
for (Future<String> item : listTemp) {
try {
System.out.println(item.get());
} catch (Exception e) {
}
}
executorService.shutdown();
}
3. CountDownLatch类的await(推荐)
通过await方法阻塞主线程等待全部子线程执行完成后,通过future.get获取子线程的返回值,这种方式比方式一更加优雅。
/**
* 线程池等待,阻塞回到主进程--CountDownLatch
*/
private static void executorsCountDownLatch() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(2);
Future<String> future = executorService.submit(() -> {
threadMethod(5, "线程--1");
countDownLatch.countDown();
return "第一线程";
});
Future<String> future2 = executorService.submit(() -> {
threadMethod(2, "线程--2");
countDownLatch.countDown();
return "第二线程";
});
executorService.shutdown();
try {
countDownLatch.await();
Util.printfLog(future2.get());
Util.printfLog(future.get());
} catch (Exception e) {
}
}
4.线程池invokeAll
这种方式和方式二类似,也是通过future.get获取子线程返回值,只能通过遍历集合中的future,以固定的顺序获取子线程返回值。
/**
* 线程池等待,阻塞回到主进程---使用invokeAll
*/
private static void executorsInvokeAll() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<String> callable = () -> {
threadMethod(5, "线程--1");
return "第一线程";
};
Callable<String> callable2 = () -> {
threadMethod(2, "线程--2");
return "第二线程";
};
List<Callable<String>> list = Arrays.asList(callable, callable2);
try {
List<Future<String>> futureList = executorService.invokeAll(list);
executorService.shutdown();
Util.printfLog("获取结果中---");
for (Future<String> item : futureList) {
Util.printfLog(item.get());
}
} catch (Exception e) {
}
}
5. ExecutorCompletionService(强烈推荐)
- 优雅的获取子线程返回值,只要任何子线程结束就有返回值。
- ExecutorCompletionService内部管理者一个已完成任务的阻塞队列
- ExecutorCompletionService引用了一个Executor, 用来执行任务
- submit()方法最终会委托给内部的executor去执行任务
- take/poll方法的工作都委托给内部的已完成任务阻塞队列
- 如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成。
/**
* 线程池等待,阻塞回到主进程---使用ExecutorCompletionService
*/
private static void executorsExecutorCompletionService() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
ExecutorCompletionService<String> executorCompletionService = new ExecutorCompletionService(executorService);
executorCompletionService.submit(() -> {
threadMethod(5, "线程--1");
return "第一线程返回";
});
executorCompletionService.submit(() -> {
threadMethod(2, "线程--2");
return "第二线程返回";
});
executorService.shutdown();
try {
Util.printfLog(executorCompletionService.take().get());
Util.printfLog(executorCompletionService.take().get());
} catch (Exception e) {
}
}
2.1.4 非阻塞主线程获取子线程返回值
1. Future接口:
JDK 5引入了Future模式。Future接口是Java多线程Future模式的实现,在java.util.concurrent包中,可以来进行异步计算。Future模式是多线程设计常用的一种设计模式。
Future虽然可以实现获取异步执行结果的需求,但它没有提供通知的机制,我们无法得知Future什么时候完成,不是真正意义上的异步。
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方式都会使主线程也会被迫等待,耗费CPU的资源。
private static void futureGet() {
try {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Util.printfLog("===task start===");
Util.mySleep(5000);
Util.printfLog("===task finish===");
return 3;
}
});
executor.shutdown();
//这里需要返回值时会阻塞主线程
Integer result = future.get();
Util.printfLog("线程返回值:" + result);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
2. CompletableFuture类:
CompletableFuture实现异步操作,加上对lambda的支持,可以说实现异步任务已经发挥到了极致。CompletableFuture弥补了Future模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过thenAccept、thenApply、thenCompose等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。
CompletableFuture的静态工厂方法:
方法名 | 描述 |
---|---|
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作无返回值 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool执行异步代码,异步操作无返回值 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作为它的线程池执行异步代码,异步操作有返回值 |
supplyAsync(Supplier<U> supplier, Executor executor) | 使用指定的thread pool执行异步代码,异步操作有返回值 |
/**
* 线程提交后,不阻塞主进程,
* completableFuture子线程执行完后回调--thread pool执行异步代码
*/
private static void completableFutureThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(10);
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
threadMethod(5, "线程--1");
return "第一线程";
}, executorService);
CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {
threadMethod(2, "线程--2");
return "第二线程";
}, executorService);
executorService.shutdown();
completableFuture.thenAccept((r) -> {
Util.printfLog(r);
});
completableFuture2.thenAccept((r) -> {
Util.printfLog(r);
});
}
2.3 方式3 ForkJoinPool
Java 8中加入了并行流,从此我们有了一个并行处理集合的简单方法。它和lambda一起,构成了并发计算的一个强大工具。默认情况下是通过ForkJoinPool.commonPool()实现并行的。这个通用池由JVM来管理,并且被JVM进程内的所有线程共享。
/**
* 使用ForkJoinPool,异步发处理
*/
private static void forkJoinPool() {
List<Supplier<String>> actionList = Arrays.asList(() -> {
threadMethod(5, "线程--1");
return "第一线程";
},
() -> {
threadMethod(2, "线程--2");
return "线程2返回";
});
List<String> threadResult = actionList.parallelStream().map(row -> row.get()).collect(Collectors.toList());
Util.printfLog(threadResult.stream().collect(Collectors.joining(",")));
}
2.4 方式4 Spring的Async注解
spring实现异步需要开启注解@EnableAsync,可以使用xml方式或者java code config的方式。
(1)@Async 异步的方法
4. 总结:
虽然Thread可以创建线程,但是线程的创建销毁不能很好的控制,就会导致资源耗尽的风险,所以线程资源尽量通过线程池提供,不在应用中自行显示的创建线程,一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。
线程池的创建尽量不使用Executors,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
参考:
Java 并发的四种风味:Thread、Executor、ForkJoin 和 Actor
java多线程并发之旅-28-Executor CompletionService ExecutorCompletionService 详解