相关文章
怎样理解阻塞非阻塞与同步异步的区别?
IO - 同步,异步,阻塞,非阻塞(亡羊补牢篇)
服务化基石之远程通信系列三:I/O模型
java异步的几个例子
同步调用
public class SyncDemo {
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
int i = syncCalculate();//<1>
System.out.println("计算结果:" + i);
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
}
//最常用的同步调用
static int syncCalculate() {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
执行耗时操作...
计算结果:100
主线程运算耗时:3001 ms
同步阻塞程序,大部门的程序逻辑,在<1>处执行耗时操作,程序必须等到耗时方法执行完毕之后,才能执行下面的语句。
Future 模式--回调模式1
上面的例子在耗时操作的时候,程序必须有等待耗时操作完成之后才能继续下面的程序,我们可以使用future模式,这个程序使用一个future占位符,主程序并不需要等待future函数完成耗时任务,而是初始化future对象即可进行下一步程序的操作,callback耗时任务会同步进行计算,需要耗时任务的返回值的时候,只需要调用初始化好的future占位符来获取callback的结果。Future代表一个占位符,代表未来的的结果;Future 模式可以细分为将来式和回调式两种模式。
public class TestFuture1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long l = System.currentTimeMillis();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
});//<1>
//其他耗时操作..
System.out.println("计算结果:" + future.get());//<2>
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
执行耗时操作...
计算结果:100
主线程运算耗时:3057 ms
- 结果分析:<1> 将回调接口放给线程池执行,但是并不是立马执行任务,而是立即先返回一个future占位符,让主程序可以继续往下执行不需要阻塞,这一步是非阻塞的 <2>future.get()是获取future占位符回调接口的结果,如果回调接口执行完毕,会立即返回结果,如果没有执行完毕,这一步主程序就会阻塞等待回调接口返回结果。
从执行结果来看,异步的future模式主线程的运算耗时比同步的模式还要长,并没有比同步的模式快,因为把耗时操作提交任务给线程池(非阻塞)和获取结果(阻塞)需要一些额外的开销,但是他们之间可以进行多个耗时操作的并行计算,消耗比提交任务给线程池的消耗大的多。所以Future模式适合多个相对独立的耗时任务并行执行。
用2个耗时任务来看一下结果:
public class TestFuture2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long l = System.currentTimeMillis();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
});//<1>
//其他耗时操作..<3>
timeConsumingOperation2();
System.out.println("计算结果:" + future.get());//<2>
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
static void timeConsumingOperation2() {
try {
Thread.sleep(4000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
执行耗时操作...
计算结果:100
主线程运算耗时:4008 ms
- 结果分析:由上面的例子可以看出,2个耗时操作,如果串行执行的话,总时间应该大于7s,如果其中一个使用future模式,另一个串行,最后的时间4008ms,其实2个任务是并行执行的,要比串行执行更节省时间。
Future 模式--回调式2
写过前端 ajax 代码的朋友对 callback 的写法并不会陌生,而 Future 模式的第二种用法便是回调。很不幸的事,jdk 实现的 Future 并没有实现 callback,addListener 这样的方法,想要在 JAVA 中体验到 callback 的特性,得引入一些额外的框架。
回调模式实现 --netty
Netty 除了是一个高性能的网络通信框架之外,还对 jdk 的Future 做了扩展,翻看其文档 netty.io/wiki/using-… 可以发现其扩展了一个 listener 接口。 maven 依赖:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.22.Final</version>
</dependency>
public class NettyFuture {
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
DefaultEventExecutorGroup executors = new DefaultEventExecutorGroup(4);
Future<Integer> f = executors.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
});
f.addListener(new FutureListener<Object>(){
@Override
public void operationComplete(Future<Object> objectFuture) throws Exception {
System.out.println("计算结果::"+objectFuture.get());
}
});
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
new CountDownLatch(1).await();//不让守护线程退出
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
主线程运算耗时:183 ms
执行耗时操作...
计算结果::100
- 结果分析:把耗时操作提交给另一个线程去完成,并且通过addListener给这个线程添加了一个监听器回调函数,监听这个线程执行结束之后触发其他的操作,从打印结果来看,主线程耗时183ms,说明主线程完全没有被阻塞,耗时操作的执行和结果都没有阻塞主线程执行。这才是真正的异步编程,回调。
回调式实现二-Guava
maven依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
public class GuavaFuture {
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
ListenableFuture<Integer> future = service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
}
});
Futures.addCallback(future, new FutureCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
System.out.println("计算结果:" + result);
}
@Override
public void onFailure(Throwable t) {
System.out.println("异步处理失败,e=" + t);
}
});//<2>
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
new CountDownLatch(1).await();//不让守护线程退出
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
执行耗时操作...
主线程运算耗时:61 ms
计算结果:100
- 结果分析:这种写法和最后的结果和netty差不多。
callback hell 和 promise模式
同样的如果你对 ES6 有所接触,就不会对 Promise 这个模式感到陌生,如果你对前端不熟悉,也不要紧,我们先来看看回调地狱(Callback Hell)是个什么概念。
回调是一种我们推崇的异步调用方式,但也会遇到问题,也就是回调的嵌套。当需要多个异步回调一起书写时,就会出现下面的代码(以 js 为例):
asyncFunc1(opt, (...args1) => {
asyncFunc2(opt, (...args2) => {
asyncFunc3(opt, (...args3) => {
asyncFunc4(opt, (...args4) => {
// some operation
});
});
});
});
java的业务代码很少出现这样的多层嵌套,这种场景出现在前端编程中比较多。多层嵌套的代码晦涩难懂,为了让代码更易读可维护,提出了promise模式,让代码变得更扁薄一些。
前面提到了 Netty 和 Guava 的扩展都提供了 addListener 这样的接口,用于处理 Callback 调用,但其实 jdk1.8 已经提供了一种更为高级的回调方式:CompletableFuture。首先尝试用 CompletableFuture 来解决回调的问题。
public class CompleteFuture {
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("执行耗时操作...");
timeConsumingOperation();
return 100;
});
completableFuture.whenComplete((result, e) -> {
System.out.println("结果:" + result);
});
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l)+" ms");
new CountDownLatch(1).await();
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
主线程运算耗时:132 ms
执行耗时操作...
结果:100
- 结果分析: 从打印结果来看,耗时操作并没有阻塞主线程的运行,主线程运行耗时132ms,然后并行执行耗时操作,最后耗时操作执行完成的时候,返回结果。不需要引入第三方类库,仅依靠jdk自己提供的java.util.concurrent.CompletableFuture就可以实现。
解决回调地狱的问题:
public class CompleteFuture2 {
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("在回调中执行耗时操作...");
timeConsumingOperation();
return 100;
});
completableFuture = completableFuture.thenCompose(i -> {
return CompletableFuture.supplyAsync(() -> {
System.out.println("在回调的回调中执行耗时操作...");
timeConsumingOperation();
return i + 100;
});
});
completableFuture.whenComplete((result, e) -> {
System.out.println("计算结果:" + result);
});
System.out.println("主线程运算耗时:" + (System.currentTimeMillis() - l) + " ms");
new CountDownLatch(1).await();
}
static void timeConsumingOperation() {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
在回调中执行耗时操作...
主线程运算耗时:56 ms
在回调的回调中执行耗时操作...
计算结果:200
- 结果分析: 使用 thenCompose 或者 thenComposeAsync 等方法可以实现回调的回调,且写出来的方法易于维护。