JAVA并发编程与高并发解决方案 - 并发编程 五
版本 | 作者 | 内容 |
---|---|---|
2018.7.4 | chuIllusions | J.U.C组件拓展 |
J.U.C 组件拓展
FutureTask
Introduction
FutureTask
这个组件是J.U.C里面的,但不是AQS的子类,但是这个类对线程处理的结果很值得我们学习和在项目中使用。
在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,在Java 1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果。
Callable 与 Runnable
Callable
接口定义,运行Callable
任务可以拿到一个Future对象,表示异步计算的结果。
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果或失败时扔出异常
* @since 1.5
* @return 计算结果
* @throws 计算失败扔出异常
*/
V call() throws Exception;
}
Runnable
接口定义,由于run()
方法返回值为void
类型,所以在执行完任务之后无法返回任何结果。
@FunctionalInterface
public interface Runnable {
/**
* 当一个对象实现<code>Runnable</code>接口创建一个线程,这个对象通过覆写
* run方法处理线程逻辑,并且Thread类启动该线程,执行Runnable处理线程逻辑
* @since 1.0
*/
public abstract void run();
}
&emsp可以看到Callable
是个泛型接口,泛型V就是要call()方法返回的类型。Callable
接口和Runnable
接口很像,都可以被另外一个线程执行,Callable
功能更强大些,正如前面所说的,Runnable
不会返回数据也不能抛出异常,而Callable
可以有返回值与可以抛出异常。
Future
Future
接口代表异步计算的结果,通过Future接口提供的方法可以查看异步计算是否执行完成,或者等待执行结果并获取执行结果,同时还可以取消执行。也就是说Future
就是对于具体的Runnable
或者Callable
任务的执行结果进行取消、查询是否完成、获取结果。通常不能从线程中获得方法的返回值,这时Future
就出场了,Future
可以监控目标线程调用call()
的情况。总结来说,Future
可以得到线程任务方法的返回值。
public interface Future<V> {
/*
* 取消异步任务的执行。
* 如果异步任务已经完成或者已经被取消,或者由于某些原因不能取消,则会返回false;
* 如果任务还没有被执行,则会返回true并且异步任务不会被执行;
* 如果任务已经开始执行了但是还没有执行完成:
* 若mayInterruptIfRunning为true,则会立即中断执行任务的线程并返回true
* 若mayInterruptIfRunning为false,则会返回true且不会中断任务执行线程
*/
boolean cancel(boolean mayInterruptIfRunning);
/*
* 判断任务是否被取消,如果任务在结束(正常执行结束或者执行异常结束)前被取消则返回true,否则返回false。
*/
boolean isCancelled();
/*
* 判断任务是否已经完成,如果完成则返回true,否则返回false。需要注意的是:任务执行过程中发生异常、任务被取消也属于任务已完成,也会返回true。
*/
boolean isDone();
/*
* 获取任务执行结果:
* 如果任务还没完成则会阻塞等待直到任务执行完成
* 如果任务被取消则会抛出CancellationException异常
* 如果任务执行过程发生异常则会抛出ExecutionException异常
* 如果阻塞等待过程中被中断则会抛出InterruptedException异常
*/
V get() throws InterruptedException, ExecutionException;
/*
* 带超时时间的get()版本,如果阻塞等待过程中超时则会抛出TimeoutException异常。
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask
。
FutureTask
Future
只是一个接口,不能直接用来创建对象,FutureTask
是Future
的实现类。
public interface RunnableFuture<V> extends Runnable, Future<V> {}
public class FutureTask<V> implements RunnableFuture<V> {
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
}
从上面两个类结构,可以得知FutureTask
最终还是执行Callable
类型的任务。如果在FutureTask
构造函数中传入Runnable
,会转换成Callable
类型。
FutureTask
实际上实现了Runnable
与Future
接口,所以它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值。好处:假设有个很费时的逻辑需要计算,并且返回这个计算值,同时这个值又不是马上需要,那么就可以使用这个组合,用另外一个线程计算返回值,而当前线程在使用这个返回值之前,可以做其他的操作,等到需要这个返回值时,才通过Future
得到。
案例
@Slf4j
public class FutureExample {
/**
* Callable任务
*/
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
}
public static void main(String[] args) throws Exception {
//1.生成线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//线程池提交Callable任务,并且得到Future
Future<String> future = executorService.submit(new MyCallable());
log.info("do something in main");
Thread.sleep(1000);
//调用Future.get()时,如果任务线程还未执行完毕,则会一直阻塞在此,等待线程任务完成,然后拿到结果
String result = future.get();
log.info("result:{}", result);
}
}
以上Future
与以下FutureTask
要实现的效果是一样的。
@Slf4j
public class FutureTaskExample {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "Done";
}
});
new Thread(futureTask).start();
log.info("do something in main");
// 1. 调用isDone()判断任务是否结束
if(!futureTask.isDone()) {
System.out.println("Task is not done");
try {
//阻塞主线程一秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String result = futureTask.get();
log.info("result:{}", result);
}
}
Fork/Join
Introduction
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。它的思想与MapReduce
类似,从字面上理解,Fork即把一个大任务,切割成若干个子任务并行执行,Join即把若干个子任务结果进行合并,最后得到大任务的结果,主要采取工作窃取算法。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程,寻找其他并未被执行的任务,并开始执行,通过这种方式,线程充分利用它们的运行时间,来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性:
1. 任务只能使用Fork、Join操作来作为同步机制,如果使用了其他同步机制,那他们在同步操作时,工作线程则不能执行其他任务。如:在框架的操作中,使任务进入睡眠,那么在这个睡眠期间内,正在执行这个任务的工作线程,将不会执行其他任务
2. 所执行的任务,不应该执行IO操作,如读和写数据文件
3. 任务不能抛出检查型异常,必须通过必要的代码处理它们
核心是两个类:ForkJoinTask
与ForkJoinPool
。Pool主要负责实现,包括上面所介绍的工作窃取算法,管理工作线程和提供关于任务的状态以及它们的执行信息;Task主要提供在任务中,执行Fork与Join操作的机制。
引用[并行流与串行流 Fork/Join框架的一张图来说明过程
Example
我们先来看一下Fork/Join框架的演示: