Fork/Join框架之双端队列

来源:互联网 发布:手机怎么设置4g网络 编辑:程序博客网 时间:2024/05/16 08:31

简介

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。


扩容

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

[java] view plain copy
 print?
  1. final void pushTask(ForkJoinTask<?> t) {  
  2.     ForkJoinTask<?>[] q; int s, m;  
  3.     if ((q = queue) != null) {    // ignore if queue removed  
  4.         long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;  
  5.         UNSAFE.putOrderedObject(q, u, t);  
  6.         queueTop = s + 1;         // or use putOrderedInt  
  7.         if ((s -= queueBase) <= 2)  
  8.             pool.signalWork();  
  9.         else if (s == m)  
  10.             growQueue();  
  11.     }  
  12. }  

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

[java] view plain copy
 print?
  1. private void growQueue() {  
  2.     ForkJoinTask<?>[] oldQ = queue;  
  3.     int size = oldQ != null ? oldQ.length << 1 : INITIAL_QUEUE_CAPACITY;  
  4.     if (size > MAXIMUM_QUEUE_CAPACITY)  
  5.         throw new RejectedExecutionException("Queue capacity exceeded");  
  6.     if (size < INITIAL_QUEUE_CAPACITY)  
  7.         size = INITIAL_QUEUE_CAPACITY;  
  8.     ForkJoinTask<?>[] q = queue = new ForkJoinTask<?>[size];  
  9.     int mask = size - 1;  
  10.     int top = queueTop;  
  11.     int oldMask;  
  12.     if (oldQ != null && (oldMask = oldQ.length - 1) >= 0) {  
  13.         for (int b = queueBase; b != top; ++b) {  
  14.             long u = ((b & oldMask) << ASHIFT) + ABASE;  
  15.             Object x = UNSAFE.getObjectVolatile(oldQ, u);  
  16.             if (x != null && UNSAFE.compareAndSwapObject(oldQ, u, x, null))  
  17.                 UNSAFE.putObjectVolatile  
  18.                     (q, ((b & mask) << ASHIFT) + ABASE, x);  
  19.         }  
  20.     }  
  21. }  

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


入队列

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

[java] view plain copy
 print?
  1. long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;  
  2. UNSAFE.putOrderedObject(q, u, t);  
  3. queueTop = s + 1;       
第一行,找到queueTop在数组中的位置

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

第三行,queueTop加1.


出队列

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

[java] view plain copy
 print?
  1. final void execTask(ForkJoinTask<?> t) {  
  2.     currentSteal = t;  
  3.     for (;;) {  
  4.         if (t != null)  
  5.             t.doExec();  
  6.         if (queueTop == queueBase)  
  7.             break;  
  8.         t = locallyFifo ? locallyDeqTask() : popTask();  
  9.     }  
  10.     ++stealCount;  
  11.     currentSteal = null;  
  12. }  
注意locallyFifo 这个属性,是否对自己的队列采用FIFO策略,默认为false,即默认从queueTop一端取任务。如果这个值为false,则从queueBase一端取数据。这个值可以通过ForkJoinPool类的asyncMode属性加以修改。
[java] view plain copy
 print?
  1. final ForkJoinTask<?> locallyDeqTask() {  
  2.     ForkJoinTask<?> t; int m, b, i;  
  3.     ForkJoinTask<?>[] q = queue;  
  4.     if (q != null && (m = q.length - 1) >= 0) {  
  5.         while (queueTop != (b = queueBase)) {  
  6.             if ((t = q[i = m & b]) != null &&  
  7.                 queueBase == b &&  
  8.                 UNSAFE.compareAndSwapObject(q, (i << ASHIFT) + ABASE,  
  9.                                             t, null)) {  
  10.                 queueBase = b + 1;  
  11.                 return t;  
  12.             }  
  13.         }  
  14.     }  
  15.     return null;  
  16. }  
  17.   
  18. private ForkJoinTask<?> popTask() {  
  19.     int m;  
  20.     ForkJoinTask<?>[] q = queue;  
  21.     if (q != null && (m = q.length - 1) >= 0) {  
  22.         for (int s; (s = queueTop) != queueBase;) {  
  23.             int i = m & --s;  
  24.             long u = (i << ASHIFT) + ABASE; // raw offset  
  25.             ForkJoinTask<?> t = q[i];  
  26.             if (t == null)   // lost to stealer  
  27.                 break;  
  28.             if (UNSAFE.compareAndSwapObject(q, u, t, null)) {  
  29.                 queueTop = s; // or putOrderedInt  
  30.                 return t;  
  31.             }  
  32.         }  
  33.     }  
  34.     return null;  
  35. }  
关键两句话:

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


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

以下是work-stealing的核心代码

[java] view plain copy
 print?
  1. for (;;) {  
  2.     ForkJoinTask<?>[] q; int b, i;  
  3.     if (joinMe.status < 0)  
  4.         break outer;  
  5.     if ((b = v.queueBase) == v.queueTop ||  
  6.         (q = v.queue) == null ||  
  7.         (i = (q.length-1) & b) < 0)  
  8.         break;                  // empty  
  9.     long u = (i << ASHIFT) + ABASE;  
  10.     ForkJoinTask<?> t = q[i];  
  11.     if (task.status < 0)  
  12.         break outer;            // stale  
  13.     if (t != null && v.queueBase == b &&  
  14.         UNSAFE.compareAndSwapObject(q, u, t, null)) {  
  15.         v.queueBase = b + 1;  
  16.         v.stealHint = poolIndex;  
  17.         ForkJoinTask<?> ps = currentSteal;  
  18.         currentSteal = t;  
  19.         t.doExec();  
  20.         currentSteal = ps;  
  21.         helped = true;  
  22.     }  
  23. }  
1、瞄到第i个位置这个任务,i = (q.length-1) & b,i其实就是queueBase在数组中所在的位置;
2、将这个位置上的任务设置为null,并增加queueBase的值,设置stealHint表示你的东西被我偷了;

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

0 0
原创粉丝点击