并发编程从入门到放弃系列开始和结束(五)
Fork/Join
Fork/Join 是一个并行执行任务的框架,利用的分而治之的思想。
Fork 是把一个大的任务拆分成若干个小任务并行执行,Join 则是合并拆分的子任务的结果集,最终计算出大任务的结果。
所以整个 Fork/Join 的流程可以认为就是两步:
- Fork 拆分任务,直到拆分到最小粒度不可拆分为止
- Join 计算结果,把每个子任务的结果进行合并
fork:join
这里我们需要介绍一下主要的几个类:
ForkJoinTask:就是我们的分治任务的抽象类
RecursiveTask:继承于 ForkJoinTask,用于计算有返回结果的任务
RecursiveAction: 继承于 ForkJoinTask,用于计算没有返回结果的任务
ForkJoinPool:用于执行 ForkJoinTask 任务的线程池,通常我们可以用 ForkJoinPool.commonPool() 去创建一个 Fork/Join 的线程池,然后用 submit 或者 invoke 去提交执行任务。
这里我们写一个测试程序,用于计算[0,999]的求和结果,所以我们写一个类继承 RecursiveTask ,并且实现他的 compute 方法。
invokeAll() 相当于每个任务都执行 fork,fork 之后会再次执行 compute 判断是否要继续拆分,如果无需拆分那么则使用 join 方法计算汇总结果。
public class ForkJoinTest {
public static void main(String[] args) throws Exception {
List<Integer> list = new LinkedList<>();
Integer sum = 0;
for (int i = 0; i < 1000; i++) {
list.add(i);
sum += i;
}
CalculateTask task = new CalculateTask(0, list.size(), list);
Future<Integer> future = ForkJoinPool.commonPool().submit(task);
System.out.println("sum=" + sum + ",Fork/Join result=" + future.get());
}
@Data
static class CalculateTask extends RecursiveTask<Integer> {
private Integer start;
private Integer end;
private List<Integer> list;
public CalculateTask(Integer start, Integer end, List<Integer> list) {
this.start = start;
this.end = end;
this.list = list;
}
@Override
protected Integer compute() {
Integer sum = 0;
if (end - start < 200) {
for (int i = start; i < end; i++) {
sum += list.get(i);
}
} else {
int middle = (start + end) / 2;
System.out.println(String.format("从[%d,%d]拆分为:[%d,%d],[%d,%d]", start, end, start, middle, middle, end));
CalculateTask task1 = new CalculateTask(start, middle, list);
CalculateTask task2 = new CalculateTask(middle, end, list);
invokeAll(task1, task2);
sum = task1.join() + task2.join();
}
return sum;
}
}
}
//输出
从[0,1000]拆分为:[0,500],[500,1000]
从[0,500]拆分为:[0,250],[250,500]
从[500,1000]拆分为:[500,750],[750,1000]
从[0,250]拆分为:[0,125],[125,250]
从[250,500]拆分为:[250,375],[375,500]
从[500,750]拆分为:[500,625],[625,750]
从[750,1000]拆分为:[750,875],[875,1000]
sum=499500,Fork/Join result=499500
使用完成之后,我们再来谈一下 Fork/Join 的原理。
先看 fork 的代码,调用 fork 之后,使用workQueue.push() 把任务添加到队列中,注意 push 之后调用 signalWork 唤醒一个线程去执行任务。
public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
final ForkJoinPool.WorkQueue workQueue; // 工作窃取
final void push(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) { // ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
U.putOrderedInt(this, QTOP, s + 1);
if ((n = s - b) <= 1) {
if ((p = pool) != null)
p.signalWork(p.workQueues, this);
}
else if (n >= m)
growArray();
}
}
上面我们看到了 workQueue,这个其实就是我们说的工作队列,它是一个双端队列,并且有一个工作线程和他对应。
@sun.misc.Contended
static final class WorkQueue {
volatile int base; // 下一个出队列索引
int top; // 下一个入队列索引
ForkJoinTask<?>[] array; // 队列中的 task
final ForkJoinPool pool;
final ForkJoinWorkerThread owner; // 工作队列中的工作线程
volatile Thread parker; // == owner during call to park; else null
volatile ForkJoinTask<?> currentJoin; // 当前join的任务
volatile ForkJoinTask<?> currentSteal; // 当前偷到的任务
}
那如果工作线程自己队列的做完了怎么办?只能傻傻地等待吗?并不是,这时候有一个叫做工作窃取的机制,所以他就会去其他线程的队列里偷一个任务来执行。
为了避免偷任务线程和自己的线程产生竞争,所以自己的工作线程是从队列头部获取任务执行,而偷任务线程则从队列尾部偷任务。
工作窃取
Executor
Executor是并发编程中重要的一环,任务创建后提交到Executor执行并最终返回结果。
Executor
任务
线程两种创建方式:Runnable和Callable。
Runnable是最初创建线程的方式,在JDK1.1的版本就已经存在,Callable则在JDK1.5版本之后加入,他们的主要区别在于Callable可以返回任务的执行结果。
任务执行
任务的执行主要靠Executor,ExecutorService继承自Executor,ThreadPoolExecutor和ScheduledThreadPoolExecutor分别实现了ExecutorService。
那说到线程池之前,我们肯定要提及到线程池的几个核心参数和原理,这个之前的文章也写到过,属于基础中的基础部分。
首先线程池有几个核心的参数概念:
- 最大线程数maximumPoolSize
- 核心线程数corePoolSize
- 活跃时间keepAliveTime
- 阻塞队列workQueue
- 拒绝策略RejectedExecutionHandler
当提交一个新任务到线程池时,具体的执行流程如下:
- 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
- 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
- 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
- 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
拒绝策略主要有四种:
- AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
- CallerRunsPolicy:使用调用者所在的线程来处理任务
- DiscardOldestPolicy:丢弃等待队列中最老的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务,也不抛出异常
ThreadPoolExecutor
通常为了快捷我们会用Executors工具类提供的创建线程池的方法快速地创建一个线程池出来,主要有几个方法,但是一般我们不推荐这样使用,非常容易导致出现问题,生产环境中我们一般推荐自己实现,参数自己定义,而不要使用这些方法。
创建
//创建固定线程数大小的线程池,核心线程数=最大线程数,阻塞队列长度=Integer.MAX_VALUE
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//创建只有一个线程的线程池,阻塞队列长度=Integer.MAX_VALUE
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//创建核心线程数为0,最大线程数=Integer.MAX_VALUE的线程池,阻塞队列为同步队列
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
最好的办法就是自己创建,并且指定线程名称:
new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors()*2,
1000L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("thread-name").build());
文章转自公众号:艾小仙