在写前面两篇文章23和24的时候自己有很多细节搞得不是很明白,这篇文章把Fork和Work-Stealing相关的源代码重新梳理一下。

首先来看一些线程池定义的成员变量:

关于scanGuard:

volatile int scanGuard;

private static final int SG_UNIT = 1 << 16;

private static final int  SMASK      = 0xffff;

scanGuard低位16位数值(0到15位)始终等于2的N次方减去1,代表的是大于Worker线程数的最小的2的N次方减去1。因此每次要取低16位数据时都要用到SMASK。

scanGuard的第16位是一个标志位,被当成是一个更新worker线程数组的锁使用。当该位的数据是1时,表示worker线程数组被锁住,其他线程无法更新worker线程。

要更新第16位的数值,就需要用到SG_UNIT。

再来说说与任务队列有关的三个变量:

// 存储任务的数组,长度是2的N次方
ForkJoinTask<?>[] queue; // 最后一个元素数组下标+1
// 如果把数组看成是队列,那么该位置就是队列尾部(FIFO添加元素)
// 如果看成是栈,那么该位置就栈顶(LIFO拿走元素)
// 只能当前线程会使用这个数值,不存在多线程问题,因此不用volatile
int queueTop; // 第一个元素的数组下标
// 也就是队列的头部的位置,从队列中拿走元素时,该数值加1
// 其他线程偷任务(FIFO方式)时会更新这个变量,因此需要volatile
volatile int queueBase;

任务队列的设计和Work-Stealing要求的一致(支持LIFO和FIFO)。

下面是scan方法源代码解析(补充了一些细节):

    private boolean scan(ForkJoinWorkerThread w, int a) {
int g = scanGuard;
// parallelism表示并发数,一般等于CPU可以同时运行的线程数,
// 默认值是Runtime类的availableProcessors方法返回值,表示
// 处理器的数量,因此parallelism大于0。
// a是活跃的Worker线程数,肯定大于等于0,因此
// 条件parallelism == 1 - a满足意味着parallelism为1而a为0。
// 也就是当前没有Worker线程在执行任务。blockedCount为0意味
// 着没有线程因为join被阻塞。
// 两个条件同时满足也就意味既没有任何线程在运行,那么也就
// 意味着不可能有任务存放于worker线程,所以m=0,也就是没
// 法偷任务。
// g & SMASK返回的值scanGuard的0到15位的数值(一个2的N次方减去1的值)
int m = (parallelism == 1 - a && blockedCount == 0) ? 0 : g & SMASK;
ForkJoinWorkerThread[] ws = workers;
if (ws == null || ws.length <= m)
return false; // 偷任务
for (int r = w.seed, k = r, j = -(m + m); j <= m + m; ++j) {
ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;
// 从线程队列中随机获取一个worker线程
ForkJoinWorkerThread v = ws[k & m];
// v!=null表示随机索引的线程存在
// queueBase不等于queueTop表示线程的任务队列不为空
// v.queue不为null表示任务队列已经被初始化
// (q.length - 1) 同样是2的N次方减一,和b相与得到一个
// 在数组长度范围内的数组下标
// 这一串判断是为了确认找到了一个有任务的线程来偷任务
if (v != null && (b = v.queueBase) != v.queueTop &&
(q = v.queue) != null && (i = (q.length - 1) & b) >= 0) {
// u是计算Unsafe的索引,用以CAS操作
long u = (i << ASHIFT) + ABASE; // (t = q[i]) != null用以判断数组该位置存有任务
// v.queueBase == b为了确认没有线程拿走任务
// CAS操作把该数组元素设为null表示拿走任务
if ((t = q[i]) != null && v.queueBase == b &&
UNSAFE.compareAndSwapObject(q, u, t, null)) {
//v.queueBase = b + 1更新队列头部位置
int d = (v.queueBase = b + 1) - v.queueTop;
v.stealHint = w.poolIndex;
// d是偷走一个任务后任务队列的长度
if (d != 0)
signalWork();
w.execTask(t);
}
r ^= r << 13; r ^= r >>> 17; w.seed = r ^ (r << 5);
// false表示扫描到了任务
return false;
}
// j < 0时随机选取Worker线程
else if (j < 0) { // 异或移位,更新k
r ^= r << 13; r ^= r >>> 17; k = r ^= r << 5;
}
// j >= 0后按个尝试线程
else
++k;
} // 如果扫描不到任务,但是scanGuard被更新了,
// 说明有新的Worker线程被添加进来
if (scanGuard != g)
return false;
else {
// 从线程池的任务队列中取出任务来执行
// 逻辑和上面从其他线程的任务队列偷任务类似
ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;
if ((b = queueBase) != queueTop &&
(q = submissionQueue) != null &&
(i = (q.length - 1) & b) >= 0) {
long u = (i << ASHIFT) + ABASE;
if ((t = q[i]) != null && queueBase == b &&
UNSAFE.compareAndSwapObject(q, u, t, null)) {
queueBase = b + 1;
w.execTask(t);
}
return false;
}
return true;
}
}

Worker线程一上来就直接偷其他线程的任务,自己的任务不管吗?来看execTask就知道了:

    final void execTask(ForkJoinTask<?> t) {
currentSteal = t;
for (;;) {
// 首先执行偷来的任务
if (t != null)
t.doExec();
// 先把自己的任务全部执行,再返回去偷别的线程去执行
if (queueTop == queueBase)
break;
// locallyFifo一般来自线程池的设置
// 为true使用FIFO的方式从队列中取任务执行
// 为false使用LIFO的方式(栈的方式)取任务
t = locallyFifo ? locallyDeqTask() : popTask();
}
// 更新偷任务的计数
++stealCount;
currentSteal = null;
}

在线程池的work方法(见第23篇)中还涉及到一个tryAwaitWork方法,以下是该方法的解析:

    private boolean tryAwaitWork(ForkJoinWorkerThread w, long c) {
int v = w.eventCount;
// ctl值的0-30位存储了等待线程的信息
//(参考第23篇中work方法解析中关于ctl的解释)
// 等待线程是按照栈的方式存储的,因此这里把原来排
// 第一位的等待线程设为当前线程的下一个,当前线程
// 变成排到第一位
w.nextWait = (int)c;
// 正在运行的线程数减少1,因此把48-63位的AC值减1
long nc = (long)(v & E_MASK) | ((c - AC_UNIT) & (AC_MASK|TC_MASK)); // 两个条件等同于ctl发生了变化
if (ctl != c || !UNSAFE.compareAndSwapLong(this, ctlOffset, c, nc)) {
long d = ctl;
// 第一个条件表示第一个等待线程已经发生变化(ctl值的0-30位)
// 第二个条件表示增加了正在运行的线程数变少
// 两个条件都满足时返回true,强制再扫描一次
return (int)d != (int)c && ((d - c) & AC_MASK) >= 0L;
} //
for (int sc = w.stealCount; sc != 0;) { // accumulate stealCount
long s = stealCount;
// 把线程w的stealCount加到线程池的stealCount上,然后再设置w
// 的stealCount为0
if (UNSAFE.compareAndSwapLong(this, stealCountOffset, s, s + sc))
sc = w.stealCount = 0;
// 线程自己的eventCount发生变化,则下次再更新stealCount
else if (w.eventCount != v)
return true;
}
// shutdown或者tryTerminate不为false表示当前的线程没有处于正在关闭状态
// (int)c != 0表示有线程在等待
// parallelism + (int)(nc >> AC_SHIFT)表示活跃线程数为0
// blockedCount == 0表示正在join等待的线程数为0
// quiescerCount == 0表示Quiesce线程池中的线程数为0
// 关于Quiesce线程池后面会做介绍
if ((!shutdown || !tryTerminate(false)) &&
(int)c != 0 && parallelism + (int)(nc >> AC_SHIFT) == 0 &&
blockedCount == 0 && quiescerCount == 0)
// 满足上述条件说明当前线程池没有任何线程在工作(包括运行
// 任务和join等待),这种情况下,这个线程就会等待一段时间
// 然后如果还是没有任何事件发生,就会把这个线程关闭。
idleAwaitWork(w, nc, c, v);
for (boolean rescanned = false;;) {
if (w.eventCount != v)
return true; // 尝试把当前线程从等待队列中移除,
// 一旦移除,eventCount就会发生变化,然后返回
if (!rescanned) {
int g = scanGuard, m = g & SMASK;
ForkJoinWorkerThread[] ws = workers;
if (ws != null && m < ws.length) {
rescanned = true;
for (int i = 0; i <= m; ++i) {
ForkJoinWorkerThread u = ws[i];
if (u != null) {
if (u.queueBase != u.queueTop &&
!tryReleaseWaiter())
rescanned = false;
if (w.eventCount != v)
return true;
}
}
}
if (scanGuard != g ||
(queueBase != queueTop && !tryReleaseWaiter()))
rescanned = false;
if (!rescanned)
// 让出控制权,减少冲突
Thread.yield();
else
// 在Park之前清除中断状态
Thread.interrupted();
}
else {
w.parked = true;
if (w.eventCount != v) {
w.parked = false;
return true;
}
LockSupport.park(this);
rescanned = w.parked = false;
}
}
}

零零碎碎说了关于Fork的部分,后面会继续说关于Join的过程。

《java.util.concurrent 包源码阅读》25 Fork/Join框架之Fork与Work-Stealing(重写23,24)的更多相关文章

  1. 《java.util.concurrent 包源码阅读》 结束语

    <java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...

  2. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  3. 《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包

    Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的A ...

  4. 《java.util.concurrent 包源码阅读》04 ConcurrentMap

    Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:Concu ...

  5. 《java.util.concurrent 包源码阅读》17 信号量 Semaphore

    学过操作系统的朋友都知道信号量,在java.util.concurrent包中也有一个关于信号量的实现:Semaphore. 从代码实现的角度来说,信号量与锁很类似,可以看成是一个有限的共享锁,即只能 ...

  6. 《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue

    对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元 ...

  7. 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验

    JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...

  8. 《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇

    concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void ...

  9. 《java.util.concurrent 包源码阅读》24 Fork/Join框架之Work-Stealing

    仔细看了Doug Lea的那篇文章:A Java Fork/Join Framework 中关于Work-Stealing的部分,下面列出该算法的要点(基本是原文的翻译): 1. 每个Worker线程 ...

随机推荐

  1. windows下LINUX模拟终端Cypwin以及Vim的配置使用

    Cypwin的安装 从官网下载相应版本后,直接安装. 官网地址:Cypwin 安装过程中可以选择相应的Packages,我们需要安装的Vim就需要在这一步中选择相应的包. Cypwin的使用 纯命令行 ...

  2. WPF DelegateCommand 出现Specified cast is not valid

    使用 DelegateCommand 出现 Specified cast is not valid 最近写快捷键需要 DelegateCommand ,于是用了 DelegateCommand< ...

  3. mybatis逆向工程

    一.背景 在实际开发中我们会自己去写mapper映射文件,接口,数据库表对应的实体类,如果需求任务比较少,咱们还可以慢慢的一个一个去写,但是这是不现实的,因为在工作中我们的任务是很多的,这时mybat ...

  4. Android基础知识05—活动的生命周期

    ------ 活动的生命周期 ------ Android是使用任务Task来管理活动的,一个任务就是一组存放在栈里的活动的集合.每当启动一个活动 ,他就会在返回栈中入栈,并处于栈顶位置.而每当我们按 ...

  5. ASP 文件内部访问数据库的通常途径

    创建至数据库的 ADO 连接(ADO connection) 打开数据库连接 创建 ADO 记录集(ADO recordset) 打开记录集(recordset) 从数据集中提取你所需要的数据 关闭数 ...

  6. LeetCode 33. Search in Rotated Sorted Array(在旋转有序序列中搜索)

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...

  7. Call From master/192.168.128.135 to master:8485 failed on connection exception: java.net.ConnectException: Connection refused

    hadoop集群搭建了ha,初次启动正常,最近几天启动时偶尔发现,namenode1节点启动后一段时间(大约10几秒-半分钟左右),namenode1上namenode进程停掉,查看日志: -- :: ...

  8. C# 8.0的三个令人兴奋的新特性

    C# 语言是在2000发布的,至今已正式发布了7个版本,每个版本都包含了许多令人兴奋的新特性和功能更新.同时,C# 每个版本的发布都与同时期的 Visual Studio 以及 .NET 运行时版本高 ...

  9. Android_简易的短信发送器

    这个随笔将介绍如何完成一个简单的第三方的短信发送器(不打开短信界面,调用android的api完成功能) 1.首先,我们来做布局 由于我这里写的是一个简易的,,短信发送,所以只是一个LinearLay ...

  10. BZOJ-2463

    2463: [中山市选2009]谁能赢呢? Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 2321  Solved: 1711[Submit][Sta ...