并发编程 Future、ForkJoin框架学习总结

任务性质类型

CPU密集型(CPU-bound)

CPU密集型也叫计算密集型,这种大部分时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound程序。这种程序一般而言CPU占用率会很高。
线程数一般设置为:
线程数 = CPU核数+1 (现代CPU支持超线程)

IO密集型(I/O bound)

IO密集型指的是IO操作较多,如读写DB、Redis等,这种大部分时间是CPU在等I/O (硬盘/内存) 的读/写操作称之为I/O bound程序,这种程序达到性能极限时,CPU占用率仍然较低。
线程数一般设置为:
线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

Future类

在并发编程中,经常会用到非阻塞的模型,在多线程的三种实现中,继承thread类、实现runnable接口,这两种都无法获取到任务的执行结果。但通过实现Callback接口,并用Future可以来接收多线程的执行结果,Future表示一个可能还没有完成的异步任务的结果。

例如:去饭店打包菜,热菜需要5分钟,凉菜需要2分钟,如果串行执行的话,打包好需要7分钟,但如果用Future这种模式的话就可以在做热菜的同时去打包凉菜,等热菜好了就可以立刻打包给我们,这样就只需要5分钟。

代码示例:

public class FutureTaskTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long start = System.currentTimeMillis();
        //等凉菜
        Callable ca1 = new Callable(){
            @Override
            public String call() throws Exception {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "凉菜准备完毕";
            }
        };
        //用FutureTask包装凉菜任务
        FutureTask<String> ft1 = new FutureTask<String>(ca1);
        new Thread(ft1).start();

        //等热菜
        Callable ca2 = new Callable(){
            @Override
            public Object call() throws Exception {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "热菜准备完毕";
            }
        };
        //用FutureTask包装热菜任务
        FutureTask<String> ft2 = new FutureTask<String>(ca2);
        new Thread(ft2).start();
		//都接收到返回值后执行后续代码
        System.out.println(ft1.get());
        System.out.println(ft2.get());
        
        long end = System.currentTimeMillis();
        System.out.println("准备完毕时间:"+(end-start));
    }
}

第二种写法:

public class FutureTaskTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        long start = System.currentTimeMillis();
		//等凉菜
        Future<?> submit1 = executorService.submit(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "凉菜准备完毕";
        });
		//等热菜	
        Future<?> submit2 = executorService.submit(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "热菜准备完毕";
        });

		//get可以传入超时等待时间,如果任务在时间内未完成,则抛出异常
		/**try {
            System.out.println(submit1.get(1,TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }**/
        System.out.println(submit1.get());
        System.out.println(submit2.get());


        long end = System.currentTimeMillis();
        System.out.println("准备完毕时间:"+(end-start));
    }
}

源码分析:

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //判断任务是否完成
        if (s <= COMPLETING)
        	//没有完成,就等待
            s = awaitDone(false, 0L);
        //任务完成后返回结果
        return report(s);
    }
private int awaitDone(boolean timed, long nanos) throws InterruptedException {
		//判断是否超时处理
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        //设置存放任务节点
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
			//
            if (Thread.interrupted()) {
            	//如果主线程中断了,就移除节点
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //判断任务是否完成
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //如果完成了
            else if (s == COMPLETING) 
                Thread.yield();
            //如果节点为空,表示该节点还没存放任何任务
            else if (q == null)
            	//构建链表存储任务节点
                q = new WaitNode();
            //判断是否需要排队
            else if (!queued)
            	//CAS入队
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
            //判断是否需要超时销毁                                        
            else if (timed) {
            	//判断主线程是否到达超时时间(不是任务线程)
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                	//如果到达超时时间就移除节点
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
            	//如果没到,就阻塞线程
                LockSupport.park(this);
        }
    }

Fork/Join框架

Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架,ForkJoinPool 不是为了替代 ExecutorService,而是它的补充。通过“分而治之”的算法,它可以把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果。ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker使用。

**应用场景:**数据清洗、数据排序、数据查找、数据计算等数据量大的场景。

工作窃取算法:

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。

1、在做一个比较大的任务时,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,可以把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务。
2、但是有的线程会先把自己队列里的任务做完,此时其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
3、而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的栈顶拿任务执行,而窃取任务的线程永远从双端队列的栈尾拿任务执行。
4、工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些特殊情况下还是会存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

ForkJoinPool工作原理:

1、ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)
2、双端队列本质上是一个栈结构队列,线程工作最开始会用comit方法提交到双端队列中,双端队列分为奇数位和偶数位,线程只会执行奇数位队列中的任务,偶数位队列用来存放工作期间外部线程提交进来的任务,线程空闲时会扫描全部队列,扫描到的任务会拿到奇数队列中来执行。
3、线程工作时,如果发现没有达到设置的任务粒度,会使用fork方法把任务继续拆分到自己想要的粒度,然后再push把任务压栈,这样就保证了粒度最小的任务永远都在栈顶。
4、线程处理自己的工作队列是通过pop方法从栈顶取任务来执行的,执行完后会join回上个任务。
5、线程如果没任务了就会去扫描其他线程中的队列,然后通过poll方法从其他队列的栈尾拿任务到自己的队列进行压栈,再通过pop方法取出任务来执行。
6、在 join时,如果需要 join 的任务尚未完成,则会先处理其他任务,等待其完成。
7、在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

ForkJoinPool构造方法:
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); 
    this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}

参数解释:
parallelism:并行度,默认情况下跟我们机器的cpu个数保持一致,使用Runtime.getRuntime().availableProcessors()可以得到我们机器运行时可用的CPU个数。

factory:创建新线程的工厂,也就是工作队列,默认情况下使用ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory。

handler:线程异常情况下的处理器,该处理器在线程执行任务时由于某些无法预料到的错误而导致任务线程中断时进行一些处理,默认情况为null。

mode:表示工作线程内的任务队列是采用何种方式进行调度,可以是先进先出FIFO,也可以是先入后出LIFO。如果为true,则线程池中的工作线程则使用先进先出方式进行任务调度,默认情况下是false。

核心类:

ForkJoinTask:所有实现类的父类,是abstract的,所以我们使用时需要继承它的子类
子类:
RecursiveAction:用于没有返回结果的任务,比如写数据到磁盘,然后就退出了。它可以把自己的工作分割为若干更小任务, 每个小任务可以由独立的线程或者CPU执行,可以通过继承来实现一个RecursiveAction。
RecursiveTask :用于有返回结果的任务。他可以把自己的工作分割为若干更小任务,并将这些子任务的执行合并到一个集体结果,可以通过继承来实现一个RecursiveTask。 。
CountedCompleter: 在任务完成执行后会触发执行一个自定义的钩子函数

RecursiveTask使用示例:
//拆分任务类
class LongSum extends RecursiveTask<Long> {
    //任务拆分的最小阈值
    static final int SEQUENTIAL_THRESHOLD = 1000;
    int low;
    int high;
    int[] array;
    LongSum(int[] arr, int lo, int hi) {
        array = arr;
        low = lo;
        high = hi;
    }

    protected Long compute() {
        //任务被拆分到足够小时,开始求和
        if (high - low <= SEQUENTIAL_THRESHOLD) {
            long sum = 0;
            for (int i = low; i < high; ++i) {
                sum += array[i];
            }
            return sum;
        } else {//如果任务任然过大,则继续拆分任务,本质就是递归拆分
            int mid = low + (high - low) / 2;
            LongSum left = new LongSum(array, low, mid);
            LongSum right = new LongSum(array, mid, high);
            //fork()将任务放入队列并安排异步执行
            left.fork();
            right.fork();
            //join()等待计算完成并返回计算结果
            long rightAns = right.join();
            long leftAns = left.join();
            return leftAns + rightAns;
        }
    }
}
//执行fork/join任务类
public class LongSumMain {
    //获取逻辑处理器数量
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    static long calcSum;

	//定义fork/join任务,随机生成2000w条数据在数组当中,然后求和
    public static void main(String[] args) throws Exception {
        int[] array = Utils.buildRandomIntArray(20000000);
        System.out.println("cpu核数:" + NCPU);
        //单线程下计算数组数据总和
        long start = System.currentTimeMillis();
        calcSum = seqSum(array);
        long end = System.currentTimeMillis();
        System.out.println("单线程计算结果:" + calcSum);
        System.out.println("单线程计算时间:" + (end - start));
        //采用fork/join方式将数组求和任务进行拆分执行,最后合并结果
        long start1 = System.currentTimeMillis();
        LongSum ls = new LongSum(array, 0, array.length);
        ForkJoinPool fjp = new ForkJoinPool(4); //使用的线程数
        ForkJoinTask<Long> result = fjp.submit(ls);
        long end1 = System.currentTimeMillis();
        System.out.println("forkJoin计算结果:" + result.get());
        System.out.println("forkJoin计算时间:" + (end1 - start1));
        fjp.shutdown();

    }
    
    static long seqSum(int[] array) {
        long sum = 0;
        for (int i = 0; i < array.length; ++i)
            sum += array[i];
        return sum;
    }
}

你可能感兴趣的:(学习,java)