简介

ForkJoinPool管理着ForkJoinWorkerThread线程,ForkJoinWorkerThread线程内部有一个双端队列,这个双端队列主要由一个数组queue、数组下标queueBase、数组上标queueTop三个值保证。

ForkJoinTask<?>[] queue:数组的大小必须是2的n次方,方便将取模转换为移位运算;

int queueTop:标识下一个被push或者pop的位置,这个值只会被当前线程修改,因些没有加volatile修饰;

volatile int queueBase:下一个可以被其他线程steal的位置,由于其他线程会修改这个值,所以用volatile修饰保证可见性。

初始化

在线程的run方法启动时,会调用线程的onStart()方法,在这个方法中对queue进行了初始化,长度为1 << 13,这个方法并没有对queueTop,queueBase进行赋值,采用默认值0。

扩容

当向线程中添加任务时,有可能会导致数组满的情况,如下代码所示:

final void pushTask(ForkJoinTask<?> t) {
ForkJoinTask<?>[] q; int s, m;
if ((q = queue) != null) { // ignore if queue removed
long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
UNSAFE.putOrderedObject(q, u, t);
queueTop = s + 1; // or use putOrderedInt
if ((s -= queueBase) <= 2)
pool.signalWork();
else if (s == m)
growQueue();
}
}

其中s代表queueTop的值,m为数组长度-1,当s == m时,也就是queue数组中都放满任务了,这时需要对数组进行扩容。

private void growQueue() {
ForkJoinTask<?>[] oldQ = queue;
int size = oldQ != null ? oldQ.length << 1 : INITIAL_QUEUE_CAPACITY;
if (size > MAXIMUM_QUEUE_CAPACITY)
throw new RejectedExecutionException("Queue capacity exceeded");
if (size < INITIAL_QUEUE_CAPACITY)
size = INITIAL_QUEUE_CAPACITY;
ForkJoinTask<?>[] q = queue = new ForkJoinTask<?>[size];
int mask = size - 1;
int top = queueTop;
int oldMask;
if (oldQ != null && (oldMask = oldQ.length - 1) >= 0) {
for (int b = queueBase; b != top; ++b) {
long u = ((b & oldMask) << ASHIFT) + ABASE;
Object x = UNSAFE.getObjectVolatile(oldQ, u);
if (x != null && UNSAFE.compareAndSwapObject(oldQ, u, x, null))
UNSAFE.putObjectVolatile
(q, ((b & mask) << ASHIFT) + ABASE, x);
}
}
}

从以上扩容代码可以看出,最大容量不能超过MAXIMUM_QUEUE_CAPACITY(1 << 24),最小不能小于初始值。每次扩容为先前大小的2倍,将原始数组复制到新数组中,同时将旧数组置null。扩容的过程中,queueBase和queueTop并不需要变化。

入队列

向线程队列中添加一个任务,或者向线程池添加一个任务时,如果这个任务是一个ForkJoinTask实例,就会做入队列的操作。前面已有这段代码,这里简要分析一下

long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
UNSAFE.putOrderedObject(q, u, t);
queueTop = s + 1;

第一行,找到queueTop在数组中的位置

第二行,用新任务填充queueTop所在位置

第三行,queueTop加1.

出队列

本地线程需要执行一个任务

final void execTask(ForkJoinTask<?> t) {
currentSteal = t;
for (;;) {
if (t != null)
t.doExec();
if (queueTop == queueBase)
break;
t = locallyFifo ? locallyDeqTask() : popTask();
}
++stealCount;
currentSteal = null;
}

注意locallyFifo 这个属性,是否对自己的队列采用FIFO策略,默认为false,即默认从queueTop一端取任务。如果这个值为false,则从queueBase一端取数据。这个值可以通过ForkJoinPool类的asyncMode属性加以修改。

final ForkJoinTask<?> locallyDeqTask() {
ForkJoinTask<?> t; int m, b, i;
ForkJoinTask<?>[] q = queue;
if (q != null && (m = q.length - 1) >= 0) {
while (queueTop != (b = queueBase)) {
if ((t = q[i = m & b]) != null &&
queueBase == b &&
UNSAFE.compareAndSwapObject(q, (i << ASHIFT) + ABASE,
t, null)) {
queueBase = b + 1;
return t;
}
}
}
return null;
} private ForkJoinTask<?> popTask() {
int m;
ForkJoinTask<?>[] q = queue;
if (q != null && (m = q.length - 1) >= 0) {
for (int s; (s = queueTop) != queueBase;) {
int i = m & --s;
long u = (i << ASHIFT) + ABASE; // raw offset
ForkJoinTask<?> t = q[i];
if (t == null) // lost to stealer
break;
if (UNSAFE.compareAndSwapObject(q, u, t, null)) {
queueTop = s; // or putOrderedInt
return t;
}
}
}
return null;
}

关键两句话:

queueBase = b + 1:FIFO策略每次从queueBase取任务,每取一个,queueBase增加1;
--s,queueTop = s:LIFO策略每次从queueTop取任务,每取一个,queueTop减1。

其他线程需要偷一个任务执行

以下是work-stealing的核心代码

for (;;) {
ForkJoinTask<?>[] q; int b, i;
if (joinMe.status < 0)
break outer;
if ((b = v.queueBase) == v.queueTop ||
(q = v.queue) == null ||
(i = (q.length-1) & b) < 0)
break; // empty
long u = (i << ASHIFT) + ABASE;
ForkJoinTask<?> t = q[i];
if (task.status < 0)
break outer; // stale
if (t != null && v.queueBase == b &&
UNSAFE.compareAndSwapObject(q, u, t, null)) {
v.queueBase = b + 1;
v.stealHint = poolIndex;
ForkJoinTask<?> ps = currentSteal;
currentSteal = t;
t.doExec();
currentSteal = ps;
helped = true;
}
}

1、瞄到第i个位置这个任务,i = (q.length-1) & b,i其实就是queueBase在数组中所在的位置;
2、将这个位置上的任务设置为null,并增加queueBase的值,设置stealHint表示你的东西被我偷了;

3、保存先前的currentSteal值,设置currentSteal为这个偷来的task,然后执行这个task,执行完后,恢复currentSteal的值。

Fork/Join框架之双端队列的更多相关文章

  1. Fork/Join 框架-设计与实现(翻译自论文《A Java Fork/Join Framework》原作者 Doug Lea)

    作者简介 Dong Lea任职于纽约州立大学奥斯威戈分校(State University of New York at Oswego),他发布了第一个广泛使用的java collections框架实 ...

  2. java多线程8:阻塞队列与Fork/Join框架

    队列(Queue),是一种数据结构.除了优先级队列和LIFO队列外,队列都是以FIFO(先进先出)的方式对各个元素进行排序的. BlockingQueue 而阻塞队列BlockingQueue除了继承 ...

  3. 聊聊并发(八)——Fork/Join框架介绍

      作者 方腾飞 发布于 2013年12月23日 | 被首富的“一个亿”刷屏?不如定个小目标,先把握住QCon上海的优惠吧!2 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件分享 ...

  4. Java并发——Fork/Join框架

    为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/4631466. ...

  5. 转:聊聊并发(八)——Fork/Join框架介绍

    1. 什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过 ...

  6. Java并发编程--Fork/Join框架使用

    上篇博客我们介绍了通过CyclicBarrier使线程同步,可是上述方法存在一个问题,那就是假设一个大任务跑了2个线程去完毕.假设线程2耗时比线程1多2倍.线程1完毕后必须等待线程2完毕.等待的过程线 ...

  7. 多线程(五) Fork/Join框架介绍及实例讲解

    什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过For ...

  8. Java Fork/Join 框架

    简介 从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果. 这种思想和MapReduce很像 ...

  9. Fork/Join框架详解

    Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架.Fork/Join框架要完成两件事情: 1.任务分 ...

随机推荐

  1. jsoup入门

    官网地址:http://jsoup.org/ Jsoup是一个开源的Java库,它可以用于处理实际应用中的HTML.它提供了非常便利的API来进行数据的提取及修改,充分利用了 DOM,CSS以及jqu ...

  2. Wiki动画回顾系列序&&目录

    嘛,前前后后看了太多动画,我自己一直想做的事也是喜欢能做一款acg相关的应用,但一直没有好的点子,当然纠结到最后还是需要一个比较好的社区来让大家加入进来.一直有人让我给他们推番,而我也慢慢懂得“人心” ...

  3. 为什么要Cssreset

    1.CssReset是什么?由于html标签在各浏览器的默认样式解析出来的有所不同.例如<ul>标签在IE.firefox.chrome浏览器下默认边距是不一样的.各浏览器的默认样,会给我 ...

  4. STL源码分析读书笔记--第三章--迭代器(iterator)概念与traits编程技法

    1.准备知识 typename用法 用法1:等效于模板编程中的class 用法2:用于显式地告诉编译器接下来的名称是类型名,对于这个区分,下面的参考链接中说得好,如果编译器不知道 T::bar 是类型 ...

  5. linux中ctrl+z和ctrl+c的区别

    ctrl+c和ctrl+z都是中断命令,但是他们的作用却不一样.ctrl+c是强制中断程序的执行,而ctrl+z的是将任务中断,但是此任务并没有结束,他仍然在进程中他只是维持挂起的状态,用户可以使用f ...

  6. SharePoint咨询师之路:设计之前的那些事二:规模

    提示:本系列只是一个学习笔记系列,大部分内容都可以从微软官方网站找到,本人只是按照自己的学习路径来学习和呈现这些知识. 有些内容是自己的经验和积 累,如果有不当之处,请指正. 咨询师更多的时候是解决方 ...

  7. LINQ标准查询操作符(四) —AsEnumerable,Cast,OfType,ToArray,ToDictionary,ToList,ToLookup,First,Last,ElementAt

    十.转换操作符 转换操作符是用来实现将输入对象的类型转变为序列的功能.名称以“As”开头的转换方法可更改源集合的静态类型但不枚举(延迟加载)此源集合.名称以“To”开头的方法可枚举(即时加载)源集合并 ...

  8. C语言中返回字符串函数的四种实现方法 2015-05-17 15:00 23人阅读 评论(0) 收藏

    C语言中返回字符串函数的四种实现方法 分类: UNIX/LINUX C/C++ 2010-12-29 02:54 11954人阅读 评论(1) 收藏 举报 语言func存储 有四种方式: 1.使用堆空 ...

  9. OSPF虚链路配置.示例2

    先看一个拓扑图 黄色区域是area0,即骨干区域,如果如图示RT1与RT6之间的链路断了,那么会出现骨干区域被“分裂”的情况,很明显骨干区域是不能被分割开的,出现这种状况的时候可能会影响到整个自制系统 ...

  10. AJAX的工作原理及其优缺点

    1.什么是AJAX?AJAX全称为“Asynchronous JavaScript and XML”(异步JavaScript和XML),是一种创建交互式网页应用的网页开发技术.它使用:使用XHTML ...