Future
Java创建线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口,这两种方式有一个共同的缺陷,那就是在执行完任务之后,无法获取执行结果。
从Java1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果,Future模式的核心思想是能够让主线程将原来需要同步等待的这段时间用来做其他事情。(因为可以异步获得执行结果,所以不用一直同步等待去获得执行结果)
Callable与Runnable
先说一下java.lang.Runnable吧,它是一个接口,在里面只声明了一个run()方法,因为方法的返回类型是void,所以在执行完任务之后无法返回任何结果
public abstract void run();
Callable位于java.util.concurrent包下,它也是一个接口,在它里面只声明了一个方法,这个方法叫做call(),可以看到这个一个泛型接口,call()函数返回的类型就是传递进来的V类型
V call() throws Exception;
那么怎么使用Callable呢,一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个submit方法的重载
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
通常我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用,下面用代码来演示
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableTest1 {
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("this is call");
Thread.sleep(1000L);
return "done";
}
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> future = executorService.submit(new MyCallable());
System.out.println("do something in main1");
System.out.println("future.get()=" + future.get());
System.out.println("do something in main2");
executorService.shutdown();
// do something in main1
// this is call
// future.get()=done
// do something in main2
}
}
Future
future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
FutureTask
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("this is call");
Thread.sleep(1000L);
return "done";
}
});
new Thread(futureTask).start();
System.out.println("do something in main1");
System.out.println("future.get()=" + futureTask.get());
System.out.println("do something in main2");
// do something in main1
// this is call
// future.get()=done
// do something in main2
}
}
ForkJoin
Java7开始引入了一种新的 Fork/Join 线程池,它可以执行一种特殊的任务。把一个大任务拆分成多个小任务并行执行。
我们举一个例子,如果要计算一个超大数组的和,最简单的做法是在一个线程内用一个循环解决
还有一种方法,可以把数组拆分成两部分,用两个线程并行执行,分别计算然后相加。
这就是Fork/Join任务的原理,判断一个任务是否足够小,如果是,直接计算,否则就会拆分成多个子任务分别计算。
下面使用Fork/Join对大数据进行并行求和
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
public class ForkJoinTest1 {
public static void main(String[] args) {
// 创建2000个随机数组成的数组
Long[] array = new Long[2000];
Long expectedSum = 0L;
for (int i = 0; i < array.length; i++) {
array[i] = generateRandom();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork join
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
Long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
Long endTime = System.currentTimeMillis();
System.out.println("fork/join sum: " + result + " in" + (endTime - startTime) + "ms.");
}
static Random random = new Random();
static long generateRandom() {
return random.nextInt(10000);
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
Long[] array;
int start;
int end;
public SumTask(Long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢计算速度
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return sum;
}
// 任务太大,一分为二
int middle = (end + start) / 2;
System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
SumTask subTask1 = new SumTask(this.array, start, middle);
SumTask subTask2 = new SumTask(this.array, middle, end);
invokeAll(subTask1, subTask2);
Long subResult1 = subTask1.join();
Long subResult2 = subTask2.join();
Long result = subResult1 + subResult2;
System.out.println("result = " + subResult1 + " + " + subResult2 + " ==> " + result);
return result;
}
}
观察上述代码,一个大的任务0~2000首先分裂为两个小任务0~1000和1000~2000,这两个小任务仍然太大,继续分裂为更小的0~500,500~1000,1000~1500,1500~2000.最后计算结果并依次合并。
核心代码如下
// 分裂子任务
SumTask subTask1 = new SumTask(this.array, start, middle);
SumTask subTask2 = new SumTask(this.array, middle, end);
// 合并运行两个子任务
invokeAll(subTask1, subTask2);
// 子任务结果
Long subResult1 = subTask1.join();
Long subResult2 = subTask2.join();
// 汇总
Long result = subResult1 + subResult2;
BlockingQueue
阻塞队列是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满
支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用时,这两个附加操作提供了4种处理方式,如下所示:
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
抛出异常:当队列满时,如果再往队列里插入元素,会抛出lllegalArgumentException异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
Tips:如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer方法时,该方法永远返回true
ArrayBlockingQueue
它是一个用数组实现的有界阻塞队列。必须在初始化的时候指定容量,且容量不可变。
此队列按照先进先出的原则对元素进行排序
DelayQueue
它是一个支持延时获取元素的无界阻塞队列。队列使用PriorityBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素
缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
任务超时处理:比如下单后15分钟内未付款,自动关闭订单
LinkedBlockingQueue
它是用链表实现的阻塞队列。大部分和ArrayBlockingQueue一样
PriprityBlockingQueue
它是带有优先级的阻塞队列
允许null值
默认情况下元素采取自然升序排列。也可以自定义类实现compareTo()方法来制定元素排序规则,或者初始化对象时,指定构造参数Comparator来进行排序。需要注意的是不能保证同时优先级元素的顺序。
SynchronousQueue
它是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
它可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身不存储任何元素,非常适合传递性场景。
SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。