1、任务性质类型
1.1、CPU密集型(CPU-bound)
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
线程数一般设置为:线程数 = CPU核数+1 (现代CPU支持超线程)
1.2、IO密集型
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
线程数一般设置为:线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
1.3、CPU密集型和IO密集型
我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
2、什么是Fork/Join框架
Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork 就是把一个大任务切分为若干子任务并行的执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+.....+10000,可以分割成 10 个子任务,每个子任务分别对 1000 个数进行求和,最终汇总这 10 个子任务的结果。如下图所示:
ForkJoinPool :ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
5、示例
定义fork/join任务,如下示例,随机生成2000w条数据在数组当中,然后求和。
package com.ch03.juc.tools;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
* desc: fork join 框架使用例子
*
* @author [email protected]
* @version 1.0
* @date 2020/07/09 17:09
*/
public class ForkJoinTest {
// 获取逻辑处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
static final long NPS = (1000L * 1000 * 1000);
static final boolean reportSteals = true;
public static void main(String[] args) throws ExecutionException, InterruptedException {
int[] array = buildRandomIntArray(20000000);
System.out.println("CPU-NUM: " + NCPU);
// 单线程下计算数组的和
long calcNum = seqNum(array);
System.out.println("单线程计算结果: "+calcNum);
//采用fork/join方式将数组求和任务进行拆分执行,最后合并结果
LongNum ls = new LongNum(0, array.length, array);
// 使用4个线程
ForkJoinPool fjp = new ForkJoinPool(4);
ForkJoinTask forkJoinTask = fjp.submit(ls);
System.out.println("fork计算后结果:"+forkJoinTask.get());
// 获取异常信息
if (forkJoinTask.isCompletedAbnormally()){
System.out.println(forkJoinTask.getException());
}
fjp.shutdown();
}
private static long seqNum(int[] array) {
long num = 0;
for (int i = 0; i < array.length; i++){
num += array[i];
}
return num;
}
private static int[] buildRandomIntArray(int num) {
int[] array = new int[num];
for (int i = 0; i < num; i++){
array[i] = new Random().nextInt(100);
}
return array;
}
}
class LongNum extends RecursiveTask {
static final int SEQUENTIAL_THRESHOLD = 1000;
static final long NPS = (1000L * 1000 * 1000);
static final boolean extraWork = true;
int low;
int high;
int[] array;
public LongNum(int low, int high, int[] array) {
this.low = low;
this.high = high;
this.array = array;
}
/**
* fork()方法:将任务放入队列并安排异步执行,一个任务应该只调用一次fork()函数,
* 除非已经执行 完毕并重新初始化。
* tryUnfork()方法:尝试把任务从队列中拿出单独处理,但不一定成功。
* join()方法:等待计算完成并返回计算结果。
* isCompletedAbnormally()方法:用于判断任务计算是否发生异常。
**/
@Override
protected Long compute() {
if (high - low <= SEQUENTIAL_THRESHOLD){
// 当任务拆分的足够小时,则进行求和
long num = 0;
for (int i = low; i < high; i++){
num += array[i];
}
return num;
}else {
// 任务不够小,继续拆分
int mid = low + (high - low)/2;
LongNum left = new LongNum(low, mid, array);
LongNum right = new LongNum(mid, high, array);
left.fork();
right.fork();
return left.join() + right.join();
}
}
}
6、Fork/Join原理
6.1、异常处理
ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的 getException 方法获取异常。示例如下
// 获取异常信息
if (forkJoinTask.isCompletedAbnormally()){
System.out.println(forkJoinTask.getException());
}
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
int mode,
String workerNamePrefix) {
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
public final ForkJoinTask fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
6.4、ForkJoinTask join 方法
join() 的工作则复杂得多,也是 join() 可以使得线程免于被阻塞的原因——不像同名的 Thread.join()。
1. 检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。
2. 查看任务的完成状态,如果已经完成,直接返回结果。3. 如果任务尚未完成,但处于自己的工作队列内,则完成它。
4. 如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式),执行,以期帮助它早日完成欲 join 的任务。
5. 如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务。
6. 递归地执行第5步。
6.5、ForkJoinPool.submit 方法
public ForkJoinTask submit(ForkJoinTask task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task;
}
final void externalPush(ForkJoinTask> task) {
// 任务队列
WorkQueue[] ws; WorkQueue q; int m;
int r = ThreadLocalRandom.getProbe();
int rs = runState;
// U: Unsafe魔术类
if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
(q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
U.compareAndSwapInt(q, QLOCK, 0, 1)) {
ForkJoinTask>[] a; int am, n, s;
if ((a = q.array) != null &&
(am = a.length - 1) > (n = (s = q.top) - q.base)) {
int j = ((am & s) << ASHIFT) + ABASE;
U.putOrderedObject(a, j, task);
U.putOrderedInt(q, QTOP, s + 1);
U.putIntVolatile(q, QLOCK, 0);
if (n <= 1)
signalWork(ws, q);
return;
}
U.compareAndSwapInt(q, QLOCK, 1, 0);
}
externalSubmit(task);
}
ForkJoinPool 自身拥有工作队列,这些工作队列的作用是用来接收由外部线程(非 ForkJoinThread 线程)提交过来的任务,而这些工作队列被称为 submitting queue 。
submit() 和 fork() 其实没有本质区别,只是提交对象变成了 submitting queue而已(还有一些同步,初始化的操作)。submitting queue 和其他 workqueue 一样,是工作线程”窃取“的对象,因此当其中的任务被一个工作线程成功窃取时,就意味着提交的任务真正开始进入执行阶段。
6.6、Fork/Join框架执行流程