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

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

关于scanGuard:

  1. volatile int scanGuard;
  2.  
  3. private static final int SG_UNIT = 1 << 16;
  4.  
  5. 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。

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

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

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

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

  1. private boolean scan(ForkJoinWorkerThread w, int a) {
  2. int g = scanGuard;
  3. // parallelism表示并发数,一般等于CPU可以同时运行的线程数,
  4. // 默认值是Runtime类的availableProcessors方法返回值,表示
  5. // 处理器的数量,因此parallelism大于0。
  6. // a是活跃的Worker线程数,肯定大于等于0,因此
  7. // 条件parallelism == 1 - a满足意味着parallelism为1而a为0。
  8. // 也就是当前没有Worker线程在执行任务。blockedCount为0意味
  9. // 着没有线程因为join被阻塞。
  10. // 两个条件同时满足也就意味既没有任何线程在运行,那么也就
  11. // 意味着不可能有任务存放于worker线程,所以m=0,也就是没
  12. // 法偷任务。
  13. // g & SMASK返回的值scanGuard的0到15位的数值(一个2的N次方减去1的值)
  14. int m = (parallelism == 1 - a && blockedCount == 0) ? 0 : g & SMASK;
  15. ForkJoinWorkerThread[] ws = workers;
  16. if (ws == null || ws.length <= m)
  17. return false;
  18.  
  19. // 偷任务
  20. for (int r = w.seed, k = r, j = -(m + m); j <= m + m; ++j) {
  21. ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;
  22. // 从线程队列中随机获取一个worker线程
  23. ForkJoinWorkerThread v = ws[k & m];
  24. // v!=null表示随机索引的线程存在
  25. // queueBase不等于queueTop表示线程的任务队列不为空
  26. // v.queue不为null表示任务队列已经被初始化
  27. // (q.length - 1) 同样是2的N次方减一,和b相与得到一个
  28. // 在数组长度范围内的数组下标
  29. // 这一串判断是为了确认找到了一个有任务的线程来偷任务
  30. if (v != null && (b = v.queueBase) != v.queueTop &&
  31. (q = v.queue) != null && (i = (q.length - 1) & b) >= 0) {
  32. // u是计算Unsafe的索引,用以CAS操作
  33. long u = (i << ASHIFT) + ABASE;
  34.  
  35. // (t = q[i]) != null用以判断数组该位置存有任务
  36. // v.queueBase == b为了确认没有线程拿走任务
  37. // CAS操作把该数组元素设为null表示拿走任务
  38. if ((t = q[i]) != null && v.queueBase == b &&
  39. UNSAFE.compareAndSwapObject(q, u, t, null)) {
  40. //v.queueBase = b + 1更新队列头部位置
  41. int d = (v.queueBase = b + 1) - v.queueTop;
  42. v.stealHint = w.poolIndex;
  43. // d是偷走一个任务后任务队列的长度
  44. if (d != 0)
  45. signalWork();
  46. w.execTask(t);
  47. }
  48. r ^= r << 13; r ^= r >>> 17; w.seed = r ^ (r << 5);
  49. // false表示扫描到了任务
  50. return false;
  51. }
  52. // j < 0时随机选取Worker线程
  53. else if (j < 0) { // 异或移位,更新k
  54. r ^= r << 13; r ^= r >>> 17; k = r ^= r << 5;
  55. }
  56. // j >= 0后按个尝试线程
  57. else
  58. ++k;
  59. }
  60.  
  61. // 如果扫描不到任务,但是scanGuard被更新了,
  62. // 说明有新的Worker线程被添加进来
  63. if (scanGuard != g)
  64. return false;
  65. else {
  66. // 从线程池的任务队列中取出任务来执行
  67. // 逻辑和上面从其他线程的任务队列偷任务类似
  68. ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;
  69. if ((b = queueBase) != queueTop &&
  70. (q = submissionQueue) != null &&
  71. (i = (q.length - 1) & b) >= 0) {
  72. long u = (i << ASHIFT) + ABASE;
  73. if ((t = q[i]) != null && queueBase == b &&
  74. UNSAFE.compareAndSwapObject(q, u, t, null)) {
  75. queueBase = b + 1;
  76. w.execTask(t);
  77. }
  78. return false;
  79. }
  80. return true;
  81. }
  82. }

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

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

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

  1. private boolean tryAwaitWork(ForkJoinWorkerThread w, long c) {
  2. int v = w.eventCount;
  3. // ctl值的0-30位存储了等待线程的信息
  4. //(参考第23篇中work方法解析中关于ctl的解释)
  5. // 等待线程是按照栈的方式存储的,因此这里把原来排
  6. // 第一位的等待线程设为当前线程的下一个,当前线程
  7. // 变成排到第一位
  8. w.nextWait = (int)c;
  9. // 正在运行的线程数减少1,因此把48-63位的AC值减1
  10. long nc = (long)(v & E_MASK) | ((c - AC_UNIT) & (AC_MASK|TC_MASK));
  11.  
  12. // 两个条件等同于ctl发生了变化
  13. if (ctl != c || !UNSAFE.compareAndSwapLong(this, ctlOffset, c, nc)) {
  14. long d = ctl;
  15. // 第一个条件表示第一个等待线程已经发生变化(ctl值的0-30位)
  16. // 第二个条件表示增加了正在运行的线程数变少
  17. // 两个条件都满足时返回true,强制再扫描一次
  18. return (int)d != (int)c && ((d - c) & AC_MASK) >= 0L;
  19. }
  20.  
  21. //
  22. for (int sc = w.stealCount; sc != 0;) { // accumulate stealCount
  23. long s = stealCount;
  24. // 把线程w的stealCount加到线程池的stealCount上,然后再设置w
  25. // 的stealCount为0
  26. if (UNSAFE.compareAndSwapLong(this, stealCountOffset, s, s + sc))
  27. sc = w.stealCount = 0;
  28. // 线程自己的eventCount发生变化,则下次再更新stealCount
  29. else if (w.eventCount != v)
  30. return true;
  31. }
  32. // shutdown或者tryTerminate不为false表示当前的线程没有处于正在关闭状态
  33. // (int)c != 0表示有线程在等待
  34. // parallelism + (int)(nc >> AC_SHIFT)表示活跃线程数为0
  35. // blockedCount == 0表示正在join等待的线程数为0
  36. // quiescerCount == 0表示Quiesce线程池中的线程数为0
  37. // 关于Quiesce线程池后面会做介绍
  38. if ((!shutdown || !tryTerminate(false)) &&
  39. (int)c != 0 && parallelism + (int)(nc >> AC_SHIFT) == 0 &&
  40. blockedCount == 0 && quiescerCount == 0)
  41. // 满足上述条件说明当前线程池没有任何线程在工作(包括运行
  42. // 任务和join等待),这种情况下,这个线程就会等待一段时间
  43. // 然后如果还是没有任何事件发生,就会把这个线程关闭。
  44. idleAwaitWork(w, nc, c, v);
  45. for (boolean rescanned = false;;) {
  46. if (w.eventCount != v)
  47. return true;
  48.  
  49. // 尝试把当前线程从等待队列中移除,
  50. // 一旦移除,eventCount就会发生变化,然后返回
  51. if (!rescanned) {
  52. int g = scanGuard, m = g & SMASK;
  53. ForkJoinWorkerThread[] ws = workers;
  54. if (ws != null && m < ws.length) {
  55. rescanned = true;
  56. for (int i = 0; i <= m; ++i) {
  57. ForkJoinWorkerThread u = ws[i];
  58. if (u != null) {
  59. if (u.queueBase != u.queueTop &&
  60. !tryReleaseWaiter())
  61. rescanned = false;
  62. if (w.eventCount != v)
  63. return true;
  64. }
  65. }
  66. }
  67. if (scanGuard != g ||
  68. (queueBase != queueTop && !tryReleaseWaiter()))
  69. rescanned = false;
  70. if (!rescanned)
  71. // 让出控制权,减少冲突
  72. Thread.yield();
  73. else
  74. // 在Park之前清除中断状态
  75. Thread.interrupted();
  76. }
  77. else {
  78. w.parked = true;
  79. if (w.eventCount != v) {
  80. w.parked = false;
  81. return true;
  82. }
  83. LockSupport.park(this);
  84. rescanned = w.parked = false;
  85. }
  86. }
  87. }

零零碎碎说了关于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. vue-cli 自定义指令directive 添加验证滑块

    vue项目注册登录页面遇到了一个需要滑块的功能,网上看了很多插件发现都不太好用,于是自己写了一个插件供大家参考: 用的是vue的自定义指令direcive,只需要在需要的组件里放入对应的标签嵌套即可: ...

  2. win10 UWP Controls by function

    Windows的XAML UI框架提供了很多控件,支持用户界面开发库.其中一些有可视化,一些布局. 一些控件例子:https://github.com/Microsoft/Windows-univer ...

  3. C#控件基础

    在说控件之前,还是有必要说一下如何创建项目的. 现在我们就不用创建控制台应用程序了,而是文件>新建>C#>Windows窗体应用程序.名称,位置自己选择. 创建好了大致就是这样了,可 ...

  4. configparser模块(拷贝)

    该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值). 创建文件 来看一个好多软件的常见文档格式如下: [DEFAULT] ...

  5. Taffy自动化测试框架简介

    Taffy Taffy是基于nosetests的自动化测试框架. Taffy主要用来测试后台服务(包括且不限于Http, Dubbo/hessian, Webservice, Socket等类型接口) ...

  6. 赋值运算符函数__from <剑指Offer>

    前段时间忙于项目,难得偷得几日闲,为即将到来的就业季做准备.在面试时,应聘者要注意多和考官交流,只有具备良好的沟通能力,才能充分了解面试官的需求,从而有针对性地选择算法解决问题. 题目来源于<剑 ...

  7. linux组网笔记

    一直以为自己linux还说的过去,事实上已经跟不上日新月异的应用需要了. 现成文档都没法看,错太多.然而毕竟是多年积累,整理一个准确的文档还是能做到的. 本机ip设置: # static interf ...

  8. java分页算法,传入当前pageIndex,pageSise,dataTotal可计算出页面上显示的页码,和是否启动上一页下一页

    public class CalculationPage { private Boolean showStartPagerDot; private Boolean showEndPagerDot; p ...

  9. Linux系统EXT文件系统

    分区格式化(Linux创建文件系统):(假设需要格式化的分区为/dev/sdb1) 1. ext2文件系统和ext3文件系统的区别: ext2不支持日志文件系统,不会产生日志文件,ext3则会产生日志 ...

  10. PHP操作Memcached

    一.PHP连接Memcached: 一个简单的使用示例: $memcache = new Memcache; $memcache->connect("127.0.0.1",1 ...