常用Fork-Join 与 CountDownLatch----并发工具类(同步异步基本实现原理)


人总是容易忘记敬畏,无论自然还是社会,总得时刻保持颗敬畏的心,鬼门关走一走,总能发现多的不足


Fork-Join

Fork 字面意思为拆分,将任务自定义拆分为小任务,然后每个小任务单独处理,再join汇总

分而治之

        Fork-Join适用于处理分而治之的问题。例如 快速排序、堆排序、归并排序、二分查找、线性查找、深度优先、广度优先、Dijkstra、动态规划、朴素贝叶斯分类,其中快速排序归并排序二分查找,还有大数据中 M/R 都属于分而治之。
        分治法的设计思想:将一个大问题,分割成一些规模较小的相同问题,以便各个结算,实现分而治之。


分治策略

        对于一个规模为 n 的问题,若该问题可以容易地解决(比如说规模 n 较小)则直接单线程解决,否则将其分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同(子问题相互之间有联系就会变为动态规范算法), 递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。


归并排序

        归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序,若将两个有序表合并成一个有序表,称为 2-路归并,同时还有多路归并。
        对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
        为了性能优化,在半子表的个数小于某个数的情况下,对半子表的排序采用其他排序算法,比如插入排序。

归并排序(降序)示例:
在这里插入图片描述
先将数组划分为左右两个子表:
在这里插入图片描述
然后继续左右两个子表拆分:
在这里插入图片描述
对最后的拆分的子表,两两进行排序
在这里插入图片描述
对有序的子表进行排序和比较合并
在这里插入图片描述
对合并后的子表继续比较合并
在这里插入图片描述


Fork-Join 原理

工作密取原理&&作用

        对于同时运行的线程数是有限的,任务数可能远大于线程数,每个线程有任务列表,当一个提前做完了任务,然后偷偷去别的线程的任务列表中偷一个任务到本地来处理,处理完毕又放回去。
        注意:
        自定义拆分的小任务中的任务量会不同,原因是拆分过程中是按照任务的个数进行拆分,并不会考虑单个任务的大小

例如:累加1000,拆分大小是10,那就会有最小的处理量:1到10,最大处理量是990到1000,同样是加10个数,但必然是990加到1000的计算量大于1到10

       所以会出现有些线程先完成任务,然后就可以利用工作密取充分调节线程的工作量,加快大任务的进度 ,即当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 池中取出 Task 继续执行。

       ForkJoinPool 中维护着多个线程(一般为 CPU 核数)在不断地执行 Task,每个线程除了执行自己职务内的 Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的 Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高 CPU 利用率。
在这里插入图片描述


Fork/Join实现

       我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 forkjoin 的操作机制,但是ForkjoinTask 是抽象类,jdk又抽象了一层,通常我们不直接继承 ForkjoinTask 类,只需要直接继承其子类RecursiveActionRecursiveTask,然后从这两个类中派生出我们自定义的任务:

  1. RecursiveAction :用于没有返回结果的任务
    Public abstract class RecursiveAction extends ForkJoinTask
  2. RecursiveTask :用于有返回值的任务
    Public abstract class RecursiveTask extends ForkJoinTask

! ! !要么派生到RecursiveTask要么派生到RecursiveAction

       Task 要通过 ForkJoinPool 来执行,使用 submitinvoke 提交到 pool 池中执行,两者的区别是:
       invoke 是同步执行,调用之后需要等待任务完成,才能执行后面的代码; submit 是异步执行。
       joinget 方法当任务完成的时候返回计算结果。

流程图:
在这里插入图片描述


(具体的业务逻辑):怎么拆分 怎么fork 怎么join 由自己决定

       Compute 里面先判断拆分到满足条件的时候,执行实际业务工作,如果不满足,继续拆分,再invokeAll提交到池里,继续判定是否满足,直到所有都满足,拿到每个任务的结果再join起来


Fork/Join 的同步用法和异步用法

  1. Fork/Join 的同步用法同时演示返回值结果:统计整形数组中所有元素的和
  2. Fork/Join 的异步用发同时演示不要求返回值:遍历指定目录(含子目录)

注意:
       同步与异步与返不返回值没关系,返回:Task,不返回:Action
       是同步还是异步是在提交任务的时候决定的,提交任务要求同步就要invoke,异步可以用submit / execturesubmitexecture 的区别:两个都是异步提交.

submit 允许有返回值:

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
    if (task == null)
        throw new NullPointerException();
    externalPush(task);
    return task;
}

execture 没有返回值:

public void execute(Runnable task) {
    if (task == null)
        throw new NullPointerException();
    ForkJoinTask<?> job;
    if (task instanceof ForkJoinTask<?>) // avoid re-wrap
        job = (ForkJoinTask<?>) task;
    else
        job = new ForkJoinTask.RunnableExecuteAction(task);
    externalPush(job);
}

Fork/Join 的同步用法

先看看单线程实现累加:

public class HYQ{
    //数组长度
    public static final int ARRAY_LENGTH  = 4000;

    public static int[] makeArray() {

        //new一个随机数发生器
        Random r = new Random();
        int[] result = new int[ARRAY_LENGTH];
        for(int i=0;i<ARRAY_LENGTH;i++){
            //用随机数填充数组
            result[i] =  r.nextInt(ARRAY_LENGTH*3);
        }
        return result;

    }
}

测试类:

/**
 * 单线程累加
 * */
public class SumNormal {
	
	public static void main(String[] args) {
	    int count = 0;
	    int[] src = MakeArray.makeArray();

	    long start = System.currentTimeMillis();
	    for(int i= 0;i<src.length;i++){
	    	//SleepTools.ms(1);
	    	count = count + src[i];
	    }
	    System.out.println("The count is "+count
	            +" spend time:"+(System.currentTimeMillis()-start)+"ms");		
	}

}

结果:
在这里插入图片描述

用Fork/Join 的同步用法实现:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class SumArray {
    private static class SumTask extends RecursiveTask<Integer>{

        //定义阈值,规定拆分到多少就不再拆分,可以执行计算
        private final static int THRESHOLD = MakeArray.ARRAY_LENGTH/10;
        private int[] src;  //需要统计的原始数组
        private int fromIndex;  //拆分任务的起始位置
        private int toIndex;    //拆分任务的终点位置

        public SumTask(int[] src, int fromIndex, int toIndex) {
            this.src = src;
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }

        //每个Task都需要重写compute方法
        @Override
        protected Integer compute() {
            //进来首先判定任务大小是否合适
            if (toIndex - fromIndex < THRESHOLD){
                //汇总统计
                System.out.println(" from index = "+fromIndex+" toIndex="+toIndex);
                int count = 0;
                for(int i= fromIndex;i<=toIndex;i++){
                	//SleepTools.ms(1);
                	count = count + src[i];
                }
                //返回compute本次计算结果
                return count;
            }else{
                //不满足就继续拆分(用折半拆分拆分)
                //fromIndex....mid.....toIndex
                int mid = (fromIndex + toIndex)/2;
                //左子任务
                SumTask left = new SumTask(src,fromIndex,mid);
                //右子任务
                SumTask right = new SumTask(src,mid+1,toIndex);
                //将左右两个子任务重新提交给Pool执行
                invokeAll(left,right);
                //归并子任务的结果,也就是拿到上面的count值
                return left.join()+right.join();
            }
        }
    }


    public static void main(String[] args) {

        //拿到ForkJoin的池实例,所有任务在这个池中执行
        ForkJoinPool pool = new ForkJoinPool();
        int[] src = MakeArray.makeArray();

        //new出Task的实例,Task就是交给池去执行的任务
        //因为需要返回值,所以SumTask扩展RecursiveTask
        SumTask innerFind = new SumTask(src,0,src.length-1);

        long start = System.currentTimeMillis();

        //同步执行
        pool.invoke(innerFind);
        //System.out.println("Task is Running.....");

        System.out.println("The count is "+innerFind.join()
                +" spend time:"+(System.currentTimeMillis()-start)+"ms");

    }
}

再来看看用分而治之的结果:
在这里插入图片描述
很明显的Fork-Join单线程 所消耗时间要长:

  • 因为单线程在执行的时候,是直接把全部的数组都累加了,但Fork-Join是递归,而递归调用方法需要不断压栈和入栈,时间就肯定要比没有方法调用要慢
  • 多线程会有上下文反复地切换,自然时间比单线程要慢

解决办法:
在单线程的主线程和Fork-Join的主要业务逻辑中加上阻塞方法SleepTools.ms(1); //休眠一秒钟

........
 protected Integer compute() {
            //进来首先判定任务大小是否合适
            if (toIndex - fromIndex < THRESHOLD){
                //汇总统计
                System.out.println(" from index = "+fromIndex+" toIndex="+toIndex);
                int count = 0;
                for(int i= fromIndex;i<=toIndex;i++){
                	-----------------
                	SleepTools.ms(1);
                	-----------------
                	count = count + src[i];
                }
                //返回compute本次计算结果
                return count;
            }else{
            ........

再来看看结果:
单线程:
在这里插入图片描述
Fork-Join:
在这里插入图片描述

       结果就是多线程一定比单线程要快,对单线程来说,每执行一次就要休眠一次,合起来就会很消耗时间,利用多线程拆分就会体现优势
       计算密集型的时候 Fork-Join 的优势:数据量越大优势就越大,但到底是使用单线程开发还是多线程开发还是要预判项目规模


Fork/Join 的异步用法
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

/**
 *类说明:遍历指定目录(含子目录)找寻指定类型文件
 */
public class FindDirsFiles extends RecursiveAction {

    private File path;

    public FindDirsFiles(File path) {
        this.path = path;
    }

    @Override
    protected void compute() {
        List<FindDirsFiles> subTasks = new ArrayList<>();

        File[] files = path.listFiles();
        if (files!=null){
            for (File file : files) {
                if (file.isDirectory()) {
                    // 对每个子目录都新建一个子任务。
                    subTasks.add(new FindDirsFiles(file));
                } else {
                    // 遇到文件,检查。
                    if (file.getAbsolutePath().endsWith("txt")){
                        System.out.println("文件:" + file.getAbsolutePath());
                    }
                }
            }
            if (!subTasks.isEmpty()) {
                // 在当前的 ForkJoinPool 上调度所有的子任务。
                for (FindDirsFiles subTask : invokeAll(subTasks)) {
                    subTask.join();
                }
            }
        }
    }

    public static void main(String [] args){
        try {
            // 用一个 ForkJoinPool 实例调度总任务
            ForkJoinPool pool = new ForkJoinPool();
            //不要求返回结果便扩展至Action
            FindDirsFiles task = new FindDirsFiles(new File("F:/"));

            /**异步提交*/
            pool.execute(task);

            //主线程做自己的业务工作
            System.out.println("Task is Running......");
            Thread.sleep(1);
            int otherWork = 0;
            for(int i=0;i<100;i++){
                otherWork = otherWork+i;
            }
            System.out.println("Main Thread done sth......,otherWork="+otherWork);

            task.join();    //阻塞方法
            //必须等到task.join()返回以后才会执行打印
            System.out.println("Task end");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}


CountDownLatch

       CountDownLatch 称为 闭锁, 类似发令枪作用,目的是让线程同时进行,让一个或多个线程等待其他线程完成各自的工作以后再执行
常用:

启动框架的时候,框架中启动一个主线程,但是框架会存在一个或多个初始化工作,初始化工作如果放到主线程中必定会影响性能,所以一般放在初始化线程中执行,常见就有连接数据库,读取配置文件或者到keep上读取相关配置,主线程则专门做业务逻辑的事情
这时候主线程就必须要等待初始化线程将所有初始化工作完成后才能继续执行其他相关工作

例如下图:
在这里插入图片描述

调用Await() 方法就会处于等待状态,直到 Ta,Tb,Tc,Td 完成所有初始化工作以后,每个初始化线程完成一个工作后就会调用countDown() ,执行减一操作。
如图所示,Td 完成一个初始化工作后调用countDown() ,计数器减一,当Tb 完成第二个初始化工作后继续调用countDown() ,计数器继续执行减一,这时候有可能 Td 需要执行两个初始化工作,所以Td 又结束一个工作就又调用一次countDown() ,当所有的初始化工作都做完的时候,计数器为0,这时候主线程被唤醒,继续执行。

需要注意两个点:

  1. 对于countDown计数器,图示计数器为5, 但是工作线程只有4个,所以计数器和线程数没有直接关系,计数器可以远大于工作线程数,即计数器和初始化工作个数相关
  2. 工作线程当调用了countDown()后可以继续进行,不一定要关闭线程,甚至可以创建线程池,当做完初始化工作后继续用来做其他工作也是可以的

基本实现:

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

/**
 *类说明:遍历指定目录(含子目录)找寻指定类型文件
 */
public class FindDirsFiles extends RecursiveAction {

    private File path;

    public FindDirsFiles(File path) {
        this.path = path;
    }

    @Override
    protected void compute() {
        List<FindDirsFiles> subTasks = new ArrayList<>();

        File[] files = path.listFiles();
        if (files!=null){
            for (File file : files) {
                if (file.isDirectory()) {
                    // 对每个子目录都新建一个子任务。
                    subTasks.add(new FindDirsFiles(file));
                } else {
                    // 遇到文件,检查。
                    if (file.getAbsolutePath().endsWith("txt")){
                        System.out.println("文件:" + file.getAbsolutePath());
                    }
                }
            }
            if (!subTasks.isEmpty()) {
                // 在当前的 ForkJoinPool 上调度所有的子任务。
                for (FindDirsFiles subTask : invokeAll(subTasks)) {
                    subTask.join();
                }
            }
        }
    }

    public static void main(String [] args){
        try {
            // 用一个 ForkJoinPool 实例调度总任务
            ForkJoinPool pool = new ForkJoinPool();
            //不要求返回结果便扩展至Action
            FindDirsFiles task = new FindDirsFiles(new File("F:/"));

            /**异步提交*/
            pool.execute(task);

            //主线程做自己的业务工作
            System.out.println("Task is Running......");
            Thread.sleep(1);
            int otherWork = 0;
            for(int i=0;i<100;i++){
                otherWork = otherWork+i;
            }
            System.out.println("Main Thread done sth......,otherWork="+otherWork);

            task.join();    //阻塞方法
            //必须等到task.join()返回以后才会执行打印
            System.out.println("Task end");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}


在这里插入图片描述
       ——Before Im gone,in gratitude and love I end;

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值