java并发线程框架的变迁
jdk5之前 | jdk5 | jdk7 | jdk8 | |
---|---|---|---|---|
编程接口 | Runnable/Thread | Executor | ForkJoin | CompletableFuture |
jdk5之前
- green thread 绿色线程 维基百科解释:
- 绿色线程是由系统运行库或者JVM调度,不依赖原生操作系统的模拟多核环境的线程,绿色线程运行于用户空间,不需要原生线程的支持。
- 在一个多核的环境中,原生线程可以自动为任务分配多个核,然而绿色线程不可以。
- native thread 原生线程
编程模型(Runnable/Thread)
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
}).start();
new Thread的局限性如下:
- 实现的局限性
- 缺少线程原生支持
- 缺少锁api
- 缺少执行完成原生支持
- 执行结果获取困难
- Double checking lock 不确定
- 每次new Thread新建对象性能差
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
一下列出了这么多弊端,小心脏有点受不了,毕竟我启动线程还时不时的用Runnable和new一个Thread呢,感觉太low了?不要紧,重要的是感觉到了!
jdk5(编程模型:Executors)
相比于之前解决的问题
- 重用存在的线程,减少对象创建、消亡的开销,性能佳 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
- 提供定时执行、定期执行、单线程、并发数控制等功能
- 异步执行(Future接口,Future以及相关使用方法提供了异步执行任务的能力,但对于结果的获取却是不方便,只能通过阻塞或轮询的方式得到任务结果。阻塞的方式与我们理解的异步编程其实是相违背的,而轮询又会耗无谓的CPU资源。)
执行单位
- Callable 可返回值
- Runable 无返回值
四种线程池
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
Talk is cheap, show me the code :
package com.wenniuwuren.concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class newCachedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 1; i < 10000; i++) {
executorService.submit(new Task());
}
}
}
class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果创建的线程数量过多,可能会出现堆栈内存溢出,线程最大值初始化为 Integer.MAX_VALUE,如果线程不断被创建,可能会用光内存!
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
Talk is cheap, show me the code :
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
以上线程允许3个任务同时执行,多出来的线程会放在线程池中等待,最大线程池数量为Integer.MAX_VALUE。
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行,ScheduledExecutorService比Timer更安全,功能更强大
Talk is cheap, show me the code :
定时任务(3s之后执行):
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
周期性任务执行(延迟1s执行,每隔3s执行一次):
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
Talk is cheap, show me the code :
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
线程池的作用:
线程池作用就是管理系统中执行线程的数量,根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果,少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
- 减少了线程创建和销毁的次数,每个工作线程可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
- Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
重要的类
线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
类 | 类型 | 功能 |
---|---|---|
ExecutorService | interface | 真正的线程池接口 |
ScheduledExecutorService | interface | 继承ExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题 |
ThreadPoolExecutor | class | ExecutorService的默认实现 |
ScheduledThreadPoolExecutor | class | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现 |
jdk7(编程模型:Fork/Join)
Fork/Join框架
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架,
是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
理解Fork/Join框架的设计
任务的执行分为2个步骤:
1. 递归分割任务,直到任务不能再被分割,开始计算,注意特殊情况的处理。
2. 合并结果,分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。
子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据
Fork/Join框架的类
- ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
- RecursiveAction:用于没有返回结果的任务。
- RecursiveTask :用于有返回结果的任务。- ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务
Talk is cheap, show me the code:
下面展示两个累加的例子:
RecursiveAction:
public class Java7RecursiveAction {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
//累加数
List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
//定义一个用于线程安全的累加器
LongAdder longAdder = new LongAdder();
//自定义累加任务
AddTask addTask = new AddTask(nums,longAdder);
//forkJoinPool 调用累加任务
forkJoinPool.invoke(addTask);
//关闭线程池
forkJoinPool.shutdown();
System.out.println("1..10 RESULT="+ longAdder.longValue());
}
private static class AddTask extends RecursiveAction {
private final List<Integer> nums;
private final LongAdder longAdder;
public AddTask(List<Integer> nums, LongAdder longAdder) {
this.nums=nums;
this.longAdder=longAdder;
}
@Override
protected void compute() {
int size = nums.size();
if (size > 1){
//分割任务
int part = size/2;
List<Integer> left = nums.subList(0,part);
AddTask left_addTask = new AddTask(left,longAdder);
List<Integer> right = nums.subList(part,size);
AddTask right_addTask = new AddTask(right,longAdder);
//执行所有累加任务
invokeAll(left_addTask,right_addTask);
}else {
if (size == 0){
return;
}
//当任务最细粒度时执行加法操作
Integer value = nums.get(0);
System.out.printf(longAdder.longValue()+"+");
longAdder.add(value);
}
}
}
}
RecursiveTask:
public class Java7RecursiveTask {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
AddTask addTask = new AddTask(1,100);
Future<Integer> result = forkJoinPool.submit(addTask);
//调用异常捕获接口 捕获异常
if (addTask.isCompletedAbnormally()){
System.out.println(addTask.getException());
}
try {
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private static class AddTask extends RecursiveTask{
//阈值,最少2个数相加
private static final int THRESHOLD = 2;
private final int start;
private final int end;
public AddTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
if (end-start <= THRESHOLD){
for (int i = start; i <= end; i++) {
sum += i;
}
}else {
int mid = (end+start)/2;
//创建子任务
AddTask leftTask =new AddTask(start, mid);
leftTask.fork();
AddTask rightTask =new AddTask(mid+1, end);
rightTask.fork();
//等待子任务的执行
int leftResult= (int) leftTask.join();
int rightResult= (int) rightTask.join();
//累加
sum = leftResult + rightResult;
}
//执行结果
return sum;
}
}
}
Fork/Join并发框架的局限性
- 无法手动完成
- 阻塞式返回结果
- 无法链式多future
- 无法合并多个future的结果
- 缺少异常处理,异常接口的调用在外部只能获取状态,而不能及时处理
jdk8(编程模型:CompletionStage/CompletableFuture)
Future 接口的局限性
future接口可以构建异步应用,但依然有其局限性。它很难直接表述多个Future 结果之间的依赖性。实际开发中,我们经常需要达成以下目的:
- 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
- 等待 Future 集合中的所有任务都完成。
- 仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果。
- 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式)。
- 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)
操作方法
- 基本使用
CompletableFuture<Integer> completableFuture1 = CompletableFuture.completedFuture(1 + 1);
Integer value = completableFuture1.get();
注:若上述代码执行出现异常,执行get的时候可能会陷入无尽的等待,get()最好设置超市时间。
部分方法的作用:
2. 进行结果变换 :thenApply/thenApplyAsync
3. 结果的消耗 :thenAccept/thenAcceptAsync
4. 执行下一个操作:thenRun/thenRunAsync
5. 结合两个CompletionStage的结果,进行转化:thenCombine/thenCombineAsync
6. 结合两个CompletionStage的结果,进行消耗:thenAcceptBoth/thenAcceptBothAsync
7. 在两个CompletionStage都运行完执行:runAfterBoth/runAfterBothAsync
8. 在两个CompletionStage谁计算的快:applyToEither/applyToEitherAsync
9. 两个CompletionStage,谁计算的快,执行下一步:acceptEither/acceptEitherAsync
10. 两个CompletionStage,任何一个完成了都会执行下一步的操作:runAfterEither/runAfterEitherAsync
11. 当运行时出现了异常,可以通过exceptionally进行补偿
12. 当运行完成时,对结果的记录:whenComplete/whenCompleteAsync
13. 运行完成时,对结果的处理:handle/handleAsync
14. allOf 工厂方法接收一个由CompletableFuture 构成的数组,数组中的所有 Completable-Future 对象执行完成之后,它返回一个 CompletableFuture 对象。这意味着,如果你需要等待多个 CompletableFuture 对象执行完毕,对 allOf 方法返回的
CompletableFuture 执行 join 操作可以等待CompletableFuture执行完成。
15. CompletableFuture 对象数组中有任何一个执行完毕就不再等待,在这种情况下,你可以使用一个类似的工厂方法 :anyOf 。
16. thenCompose 方法允许你对两个异步操作进行流水线
代码示例
public class Java8CompletableFuture {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture1 = CompletableFuture.completedFuture(1/1);
Integer value = completableFuture1.get();
System.out.println(value);
CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
System.out.println("completableFuture2"+"thread --> "+ Thread.currentThread().getName());
}
});
CompletableFuture<String> completableFuture3 = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
return "hello word completableFuture3"+"thread --> "+ Thread.currentThread().getName();
}
});
System.out.println(completableFuture3.get());
//合并操作,第一个式自线程,之后的链都是主线程
CompletableFuture<Void> completableFuture4 = CompletableFuture.supplyAsync(() ->{
return "completableFuture4"+" thread 4-1 --> "+ Thread.currentThread().getName();
}).thenApply(value1 -> {
return value1 + " "+ LocalDateTime.now()+" thread 4-2 --> "+ Thread.currentThread().getName();
}).thenApply( v -> {
System.out.println(v+" thread 4-2--> "+ Thread.currentThread().getName());
return v;
}).thenRun(()->{});
}
}
参考:链接:https://www.jianshu.com/p/4897ccdcb278 作者:不迷失 來源:简书
参考:链接:https://www.jianshu.com/p/6f3ee90ab7d3 作者:数齐 來源:简书
参考:链接:http://ifeve.com/talk-concurrency-forkjoin/ 作者:方 腾飞 来源:并发编程网 - ifeve.com
参考:咕泡学院讲师小马哥的视频《java8并发异步编程》
Java高并发编程是同其他技术一样都是一门艺术,这里的java并发框架只是迈向艺术殿堂的第一步,实际编程的实践和经验的积累才是迈向艺术殿堂的光明大道。感谢参考各位技术达人的博客以及视频。