线程的并发框架和并发工具类

为了平常开发并发业务方便,Java为我们提供了很多并发工具类,看并发工具类之前我们可以先了解一个java的并行执行框架Fork-Join

Fork-Join

分治思想

Fork-Join主要用到的就是“分而治之”的思想,对于一个规模为N的任务,如果N<阈值(我们自己定义)就直接解决,但是N>阈值,就将N  Fork成几个子任务,然后递归继续看子任务是否还是>阀值,继续Fork直到最后得到规模是阀值内的子任务

子任务的特点是互相独立而且与上层任务形式相同(只是规模小而已),子任务可以并行执行以提高效率,最后将子任务的解值一层一层向上Join合并就完成了整个大任务。

Fork的意思就是拆分并行处理,Join的意思就理解成等待拿结果合并。

如果任务拆分的多用到的线程岂不是很多无法控制?,其实Fork-Join实际使用过程中采用的是任务(ForkJoinTask)+池(ForkJoinPool)的配合,向池中提交任务,并异步地等待结果。ForkJoinPool做到线程复用。

ForkJoinTask有两个主要的子类:

  • RecursiveTask  有返回值的任务
  • RecursiveAction  无返回值的任务

Recursive:递归的意思。这两个子类主要的区别就是有无返回值,类似于Rannable 和 Callable的区别。根据是否需要返回值选择合适的子类进行继承编写我们自己的任务类即可。实际开发流程也有一套标准范式。

使用Fork-Join开发的标准范式:

 

主要的还是定义我们的Task,继承自RecuriveTask/RecursiveAction.在compute方法中做好阈值判断,满足阈值的就写我们的实际任务逻辑,不满足的代码分支就写好我们的递归拆分任务并调用InvokeAll拆分任务并最后调用子任务.join方法等待值返回合并返回。

我们根据上面的标准范式,实战下Fork-Join的用法

第一个例子,同步用法(提交给池任务后会不会阻塞,只相当于主线程来说),并且需要返回值

1、Fork/Join的同步用法同时演示返回结果值:统计整形数组中所有元素的和

定义一个产生固定长度数组并填充一定范围的整形数字,数组供后面演示使用

/**
 *产生整型数组
 */
public class BuildArray {
    //数组长度
    public static final int ARRAY_LENGTH  = 10000;
    public static int[] buildArray() {
        //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;
    }
}

 主要看我们的任务类的定义和使用Fork-Join 池+任务的配合

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

        private final static int THRESHOLD = BuildArray.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;
        }

        @Override
        protected Integer compute() {
            if(toIndex-fromIndex < THRESHOLD) {
                int count = 0;
                for(int i=fromIndex;i<=toIndex;i++) {
                     SleepTools.ms(1);
                    //实际的任务工作 累加和
                    count = count + src[i];
                }
                return count;
            }else {
                //fromIndex....mid....toIndex
                //1...................70....100
                int mid = (fromIndex+toIndex)/2;
                SumTask left = new SumTask(src,fromIndex,mid);
                SumTask right = new SumTask(src,mid+1,toIndex);
                //调用所有拆分好的子任务 子任务还是SumTask类型 相当于递归调用了compute方法
                invokeAll(left,right);
                //等待返子任务执行结束获取返回值相加
                return left.join()+right.join();
            }
        }
    }


    public static void main(String[] args) {
        //准备好执行任务的池 
        ForkJoinPool pool = new ForkJoinPool();
        int[] src = BuildArray.buildArray();
        //创建任务
        SumTask innerFind = new SumTask(src,0,src.length-1);

        long start = System.currentTimeMillis();
        pool.invoke(innerFind);//同步调用  把任务提交给了池处理
        System.out.println("Task is Running.....");
         //innerFind.join()  获取结果
        System.out.println("The count is "+innerFind.join()
                +" spend time:"+(System.currentTimeMillis()-start)+"ms");

    }
}

流程很简单,就是创建池和任务,然后 pool.invoke(task) 把任务提交给池去处理(invoke是同步调用方法 主线程会阻塞),如果想要获取结果就调用task.join即可。主要的还是我们自己的任务类实现的compute方法,需要拆分子任务,并invokeAll递归调用子任务。最后合并返回。

测试结果:

Task is Running.....
The count is 150354625 spend time:3455ms

Process finished with exit code 0

 我们在正常单线程的方式累加对比下结果:

public class SumNormal {
    public static void main(String[] args) {
        int count = 0;
        int[] src = BuildArray.buildArray();
        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");        
    }

}
The count is 150452241 spend time:17166ms

Process finished with exit code 0

 对比看确实Fork-Join的方式快很多。  但是如果注意看我们的代码会发现两种方式其中都有Sleep操作,这是因为为了演示效果加了一些耗时,如果我们都去掉sleep操作,在对比两种方式的耗时,结果发现,Fork-Join的操作(耗时3ms)反而比单线程操作(耗时1ms)还慢了一些,这是因为什么呢?

主要的原因就是我们这个任务只是单纯的cpu计算的任务,cpu计算很快,而Fork-Join的方式多线程就存在上下文切换也需要耗时,切换的时间比多线程并行计算带来的速度提升还多,就显得有点“得不偿失”,但是如果把数组规模在加大点计算,慢慢的Fork-Join的优势又慢慢凸显,所以Fork-Join并不是

一定都适用,也不一定比单线程不拆分运行的就快,受规模受任务类型本身耗时甚至受阀值设定的影响。

来看第二个示例,异步用法:

2、Fork/Join的异步用法 同时演示不要求返回值:遍历指定目录(含子目录)寻找指定类型文件

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

    private File path;//当前任务需要搜寻的目录

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

    public static void main(String [] args){
        try {
            // 用一个 ForkJoinPool 实例调度总任务
            ForkJoinPool pool = new ForkJoinPool();
            FindDirsFiles task = new FindDirsFiles(new File("F:/"));

            pool.execute(task);//execute 异步调用 主线程不是阻塞在这

            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();//阻塞方法等待任务执行完  这力只是防止任务没执行完主线程就结束了
            System.out.println("Task end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @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()) {
                //invokeAll(任务集合) 提交执行子任务集合
                for(FindDirsFiles subTask:invokeAll(subTasks)) {
                    subTask.join();//等待子任务执行完成
                }
            }
        }

    }
}

代码上都有注释,这里只说一点19行,这次调用的是池的execute方法提交的任务,就是异步调用,主线程不会阻塞到这,上一个实例调用的是invoke方法是同步方法,主线程会阻塞。

测试结果:

Task is Running......
Main Thread done sth......,otherWork=4950
文件:F:\资源网站建站内容储备\Public\plugins\My97DatePicker\┐к╖в░№\readme.txt
文件:F:\资源网站建站内容储备\runtime\log\pt_20181229.txt
文件:F:\资源网站建站内容储备\runtime\log\pt_20181230.txt
文件:F:\资源网站建站内容储备\runtime\log\pt_20190331.txt
文件:F:\资源网站建站内容储备\runtime\log\pt_20190914.txt
文件:F:\资源网站建站内容储备\public\plugin\datepicker\skin\ext\readme.txt
Task end

Process finished with exit code 0

 可以看到先打印了一些主线程的其他工作,确实没在提交任务时阻塞。

常用的并发工具类

  • CountDownLatch的作用、应用场景和实战
  • CyclicBarrier的作用、应用场景和实战
  •     CountDownLatch和CyclicBarrier辨析
  • Semaphore的作用、应用场景和实战
  • Exchange的作用、应用场景和实战

CountDownLatch 

作用:也叫闭锁,用于一组线程等待其他的线程完成工作以后再执行的控制。

常用方法:await用来等待,countDown负责计数器的减一。

比如我们的业务线程要等待一些初始化资源的线程执行完工作后再执行就可以用CountDownLatch完成。

直接看代码使用示例:

/**
 *类说明:演示CountDownLatch,有5个初始化的线程,6个扣除点,
 *扣除完毕以后,主线程和业务线程才能继续自己的工作
 */
public class UseCountDownLatch {
    
    static CountDownLatch latch = new CountDownLatch(6);

    //初始化线程(只有一步,有4个)
    private static class InitThread implements Runnable{

        @Override
        public void run() {
            System.out.println("Thread_"+Thread.currentThread().getId()
                    +" ready init work......");
            latch.countDown();//初始化线程完成工作了,countDown方法只扣减一次;
            for(int i =0;i<2;i++) {
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ........continue do its work");
            }
        }
    }
    
    //业务线程
    private static class BusiThread implements Runnable{

        @Override
        public void run() {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i =0;i<3;i++) {
                System.out.println("BusiThread_"+Thread.currentThread().getId()
                        +" do business-----");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //一个单独的初始化线程,初始化分为2步,需要扣减两次
        new Thread(new Runnable() {
            @Override
            public void run() {
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ready init work step 1st......");
                latch.countDown();//每完成一步初始化工作,扣减一次
                System.out.println("begin step 2nd.......");
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +" ready init work step 2nd......");
                latch.countDown();//每完成一步初始化工作,扣减一次
            }
        }).start();
        //开启一个业务线程 这个业务线程里也等待初始化线程都完成
        new Thread(new BusiThread()).start();
        //开启4个初始化线程
        for(int i=0;i<=3;i++){
            Thread thread = new Thread(new InitThread());
            thread.start();
        }
        //主线程也等待
        latch.await();
        System.out.println("Main do ites work........");
    }
}

主要就是CountDownLatch的主要应用场景,以及主要的方法await()进行阻塞等待扣减到0,countDown扣减一次,并且调用await等待的线程可以有多个,一个线程也可以多次调用countDown扣减多次。

打印结果:

Thread_15 ready init work......
Thread_16 ready init work......
Thread_16 ........continue do its work
Thread_16 ........continue do its work
Thread_14 ready init work......
Thread_17 ready init work......
Thread_15 ........continue do its work
Thread_17 ........continue do its work
Thread_14 ........continue do its work
Thread_17 ........continue do its work
Thread_15 ........continue do its work
Thread_14 ........continue do its work
Thread_12 ready init work step 1st......
begin step 2nd.......
Thread_12 ready init work step 2nd......
Main do ites work........
BusiThread_13 do business-----
BusiThread_13 do business-----
BusiThread_13 do business-----

Process finished with exit code 0

主线程和业务线程都等待初始化线程执行完初始化操作之后再同时继续执行。

CyclicBarrier 

  • /ˈsaɪklɪkˌˈsɪklɪk/  /ˈbæriər/ 

作用:也称"栅栏"或“屏障”,让一组线程达到某个屏障(约定点),会被阻塞,一直到组内最后一个线程达到屏障时,屏障才开放,所有被阻塞的线程同时继续运行。因为它可以复用,所以叫CyclicBarrier(“循环的屏障”)。

它和countDownLatch感觉功能很像,也是阻塞住线程,达到条件再继续运行,但countDownLatch是一个或多个线程等待其他线程扣减(外部线程做扣减),而CyclicBarrier主要用于组内线程之间的等待,最后一个到了才全都继续执行(像所有朋友都到了我们再吃饭,也像组队游戏匹配到两队人齐才能开局)。

同时CyclicBarrier有两个构造器:

  • CyclicBarrier(int parties)  parties需要拦截的线程数。parties个线程执行到这个cyclicBarrier的await方法后屏障开放(内部也有一个计数器,调用到await后自减1再等待),参与的线程继续运行。
  • CyclicBarrier(int parties, Runnable barrierAction)  屏障开放后, barrierAction定义的任务会优先执行,执行完,拦截的线程再都放开。(意思就是屏障开放前会插队一个barrierAction的线程任务)。

参与线程的执行流程图:

我们看例子:

/**
 *
 *类说明:CyclicBarrier的使用
 */
public class UseCyclicBarrier {
    
    private static CyclicBarrier barrier 
        = new CyclicBarrier(5,new CollectThread());
    
    private static ConcurrentHashMap<String,Long> resultMap
            = new ConcurrentHashMap<>();//存放子线程工作结果的容器

    public static void main(String[] args) {
        for(int i=0;i<=4;i++){
            Thread thread = new Thread(new SubThread());
            thread.start();
        }

    }

    //负责屏障开放优先进行的工作  汇总结果
    private static class CollectThread implements Runnable{

        @Override
        public void run() {

            try {
                StringBuilder result = new StringBuilder();
                for(Map.Entry<String,Long> workResult:resultMap.entrySet()){
                    result.append("["+workResult.getValue()+"]");
                }
                //Thread.sleep(3000);
                System.out.println(" the result = "+ result);
                System.out.println("do other business........");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //工作线程
    private static class SubThread implements Runnable{

        @Override
        public void run() {
            long id = Thread.currentThread().getId();//当做是线程本身的工作处理结果
            resultMap.put(Thread.currentThread().getId()+"",id);
            Random r = new Random();//随机决定工作线程的是否睡眠
            try {
                //随机选择一些线程比其他线程多休眠一会,表示处理比较慢的任务
                if(r.nextBoolean()) {
                    Thread.sleep(2000+id);
                    System.out.println("Thread_"+id+" ....do something ");
                }
                System.out.println(id+"....is await");
                barrier.await();
                System.out.println("Thread_"+id+" ....do its business ");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

 打印结果:

13....is await
16....is await
Thread_12 ....do something 
12....is await
Thread_14 ....do something 
14....is await
Thread_15 ....do something 
15....is await
 the result = [12][13][14][15][16]
do other business........
Thread_15 ....do its business 
Thread_13 ....do its business 
Thread_16 ....do its business 
Thread_14 ....do its business 
Thread_12 ....do its business 

Process finished with exit code 0

模拟开启了5个工作线程,工作处理结果放到一个map中,随机选工作线程休眠2秒,可以看出13号线程先完成任务到达约定点await,15号线程最后完成结果到达约定点后屏障开启,这5个线程将要继续他们的工作,但在之前优先运行了汇总5个线程结果的线程CollectThread,因为我们8行实例化这个CyclicBarrier时用的是带优先任务的构造器,5个线程到达屏障后被加了塞优先执行汇总线程。(如果不带第二个参数,就是最后一个线程15号到达后,5个线程立即同时放开做其他事。不再演示)。

 CountDownLatch和CrclicBarrier区别

1、CountDownLatch是一次性的,扣减到0放开后这个CountDownLatch不能再次使用。而CyclicBarrier是可以反复使用的,等屏障打破后内部计数器会有一个复位机制,可以再次使用。

2、使用场景上,CountDownLatch是一个或多个线程等待await外部一组线程扣减countDown,CyclicBarrier是一组线程内部相互等待,CyclicBarrier的await是内部计数器先减1再等待。

Semaphore 

  •  /ˈseməfɔːr/ 

semaphore 是一个计数信号量(许可证集),一般做流控,用作限制共享资源的同时访问线程数量。

最常用的方法就两个,acquire获取一个许可证,release归还一个许可证。基本的用法如图:

 ①初始化了一个一定数量许可证(笑脸)的Semaphore,②使用acquire()获取一个许可证,③使用release归还一个许可证

④假设多个线程去获取许可证,⑤许可证被获取完之后没有获取到许可证的线程会一直阻塞在acquire方法处等待获取,⑥直到有获取到许可证的线程调用release方法归还了许可证之后才会通知等待获取许可证的正在阻塞的线程去获取(内部有等待通知机制 release有类似像notify一样的唤醒功能)。

我们之前用wait和natify实现模拟实现过数据库连接池,我们现在用Semaphore实现一下,当做一个Semaphore使用的例子。

直接看代码和演示:

/**
 *类说明:演示Semaphore用法,一个数据库连接池的实现
 */
public class DBPoolSemaphore {
    
    private final static int POOL_SIZE = 10;
    //两个指示器,分别表示池子还有可用连接useful 和 取走正在使用的连接数useless
    private final Semaphore useful,useless;
    //存放数据库连接的容器
    private static LinkedList<Connection> pool = new LinkedList<Connection>();
    //初始化池
    static {
        for (int i = 0; i < POOL_SIZE; i++) {
            pool.addLast(SqlConnectImpl.fetchConnection());
        }
    }
    public DBPoolSemaphore() {
        this.useful = new Semaphore(10);
        this.useless = new Semaphore(0);
    }
    
    /*归还连接*/
    public void returnConnect(Connection connection) throws InterruptedException {
        if(connection!=null) {
            //getQueueLength获取正在acquire方法处正在阻塞的队列长度   availablePermits剩余的可用许可证数
            System.out.println("当前有"+useful.getQueueLength()+"个线程等待数据库连接!!"
                    +"可用连接数:"+useful.availablePermits());
            useless.acquire();
            synchronized (pool) {
                pool.addLast(connection);
            }//释放许可证 内部通知其他线程获取
            useful.release();
        }
    }
    
    /*从池子拿连接*/
    public Connection takeConnect() throws InterruptedException {
        //许可证发完 获取不到的线程在这里阻塞
        useful.acquire();
        Connection connection;
        synchronized (pool) {
            connection = pool.removeFirst();
        }
        useless.release();
        return connection;
    }
    

 除了acquire和release方法外,例子中还用到了getQueueLength获取正在acquire方法处正在阻塞的队列长度,availablePermits剩余的可用许可证数,其他一些方法可以再去了解。

测试一下,开50个线程并发获取一下:

/**
 *类说明:测试数据库连接池
 */
public class AppTest {

    private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
    
    private static class BusiThread extends Thread{
        @Override
        public void run() {
            Random r = new Random();//让每个线程持有连接的时间不一样
            long start = System.currentTimeMillis();
            try {
                Connection connect = dbPool.takeConnect();
                System.out.println("Thread_"+Thread.currentThread().getId()
                        +"_获取数据库连接共耗时【"+(System.currentTimeMillis()-start)+"】ms.");
                SleepTools.ms(100+r.nextInt(100));//模拟业务操作,线程持有连接查询数据
                System.out.println("查询数据完成,归还连接!");
                dbPool.returnConnect(connect);
            } catch (InterruptedException e) {
            }
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            Thread thread = new BusiThread();
            thread.start();
        }
    }
    
}

 打印效果:

Thread_18_获取数据库连接共耗时【0】ms.
Thread_15_获取数据库连接共耗时【0】ms.
Thread_12_获取数据库连接共耗时【0】ms.
Thread_14_获取数据库连接共耗时【0】ms.
Thread_19_获取数据库连接共耗时【0】ms.
Thread_13_获取数据库连接共耗时【0】ms.
Thread_16_获取数据库连接共耗时【0】ms.
Thread_17_获取数据库连接共耗时【0】ms.
Thread_20_获取数据库连接共耗时【0】ms.
Thread_21_获取数据库连接共耗时【0】ms.
查询数据完成,归还连接!
当前有40个线程等待数据库连接!!可用连接数:0
Thread_25_获取数据库连接共耗时【107】ms.
查询数据完成,归还连接!
当前有39个线程等待数据库连接!!可用连接数:0......
查询数据完成,归还连接!
当前有19个线程等待数据库连接!!可用连接数:0
当前有19个线程等待数据库连接!!可用连接数:0
Thread_30_获取数据库连接共耗时【419】ms.
Thread_27_获取数据库连接共耗时【418】ms.
查询数据完成,归还连接!
当前有17个线程等待数据库连接!!可用连接数:0
Thread_31_获取数据库连接共耗时【445】ms.
查询数据完成,归还连接!
......
Thread_60_获取数据库连接共耗时【615】ms.
查询数据完成,归还连接!
当前有1个线程等待数据库连接!!可用连接数:0
Thread_61_获取数据库连接共耗时【627】ms.
查询数据完成,归还连接!
当前有0个线程等待数据库连接!!可用连接数:0
查询数据完成,归还连接!
......
查询数据完成,归还连接!
当前有0个线程等待数据库连接!!可用连接数:7
查询数据完成,归还连接!
当前有0个线程等待数据库连接!!可用连接数:8
查询数据完成,归还连接!
当前有0个线程等待数据库连接!!可用连接数:9

前10个线程获取到连接后,后面的线程就得等待前面线程归还才能获取连接,并且可用连接数最多也不超过10。达到了控流的效果。

Exchanger

用作两个线程进行线程安全的数据交换,使用场景比较狭窄,直接用一个例子演示一下效果:

/**
 *类说明:演示Exchanger用法
 */
public class UseExchange {
    private static final Exchanger<Set<String>> exchange = new Exchanger<Set<String>>();

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                Set<String> setA = new HashSet<String>();//存放数据的容器
                try {
                    setA.add("原本线程A的数据1");
                    setA.add("原本线程A的数据2");
                    setA.add("原本线程A的数据3");
                    System.out.println("A线程做完了数据 等待交换...");
                    //回阻塞在这直到有另外的线程和它数据交换
                    setA = exchange.exchange(setA);//交换set
                    /*处理交换后的数据*/
                    System.out.println("setA交换后:"+setA.toString());
                } catch (InterruptedException e) {
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Set<String> setB = new HashSet<String>();//存放数据的容器
                try {
                    setB.add("原本线程B的数据1");
                    setB.add("原本线程B的数据2");
                    Thread.sleep(3000);
                    System.out.println("B模拟做完了数据处理 将要和A交换数据......");
                    setB = exchange.exchange(setB);//交换set
                    /*处理交换后的数据*/
                    System.out.println("setB交换后:"+setB.toString());
                } catch (InterruptedException e) {
                }
            }
        }).start();

    }
}

 打印结果:

A线程做完了数据 等待交换...
B模拟做完了数据处理 将要和A交换数据......
setB交换后:[原本线程A的数据3, 原本线程A的数据2, 原本线程A的数据1]
setA交换后:[原本线程B的数据2, 原本线程B的数据1]

Callable、Future 和FutureTask

这是一个重点,实际工作开发中经常用到。

我们知道开启线程有一种实现Runnable接口并重写它的run方法,把创建的实例当构造参数传给Thread类的方式构建线程类。但是有一个缺点是Runnable的run方法是没有返回值的。意味着假如我们有一个任务递交给Runnable实现并且希望拿到任务的执行结果。Runnable这种方式是做不到的。而Jdk中有另外一个和Runable很相似的接口Callable,它的call方法类似Runnable接口的run方法,区别是call方法是带返回值的。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

但是我们再看一下Thread类的构造函数列表,发现大多都是接收Runable接口的构造函数,并不支持直接接收Callable接口类型创建线程:

也就是如果我们想通过传参创建线程几乎都是需要和Runnable有关系,比如是Runnable的实现类,而且我们现在又想创建有返回结果的线程,意味着Callable接口也得有关系,比如持有Callable接口的对象,JDK中是否有同时满足条件的类呢?有,那就是FutureTask。

FutureTask实现了RunableFutrue接口,而RunableFuture接口是Runable和Future的子接口

public class FutureTask<V> implements RunnableFuture<V> 
public interface RunnableFuture<V> extends Runnable, Future<V>

 也就意味着FutureTask可以当成Runnable使用,把它当参数传给Thread创建线程。而Future接口提供了一些线程任务管理的一些方法(cancel打取消标记、get得到返回值、isCancelled判断是否已取消、isDone判断任务是否已执行完成)。

主要的是FutureTask持有Callable接口的实例当成员属性,并且支持传Callable接口实现类构造参数创建FutureTask。

 因此,Callable、Runnable、FutureTask关系图如下:

这样我们就可以创建一个实现类实现Callable接口(泛型就是我们要返回结果的类型),把我们线程主要做的工作重写在call方法中并返回结果。然后我们把实例包装成(当成构造参数创建FutreTask)FutureTask对象,再把FutureTask以Runnable接口的身份当参数创建Thread对象开启线程即可。

因此FurureTask既持有Callable可以定义带返回值的的线程任务类,又可以当成Runnable交给线程去处理。并且因为扩展了Futrue接口还具有了终止线程任务等等线程管理的一些能力。

Callable->FutureTask(Rannable)->Thread

实际使用关系伪代码表示:

new Thread(new FutureTask(new Callable<返回结果类型>)).start();

我们直接看一个示例:

/**
 * @author Pency
 * @version 1.0.0
 * @Description TODO  演示使用Callable 包装成 FutureTask 开启一个计算的线程任务返回结果 并且演示取消任务处理
 *
 */
public class UseCallable {

      //定义任务类 返回Integer类型结果
       public static class MyCallable implements Callable<Integer>{
           @Override
           public Integer call() throws Exception {
               Integer sum=0;
               System.out.println("call方法线程任务开始 计算累加和....");
               for(int i=0;i<=1000;i++){
                   if(Thread.currentThread().isInterrupted()){
                       System.out.println("判断到了中断任务的请求,取消任务....");
                       return null;
                   }
                    System.out.println("sum="+sum);

                   sum=sum+i;
               }
               System.out.println("call任务结束,返回结果:"+sum);
               return sum;
           }
       }

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        MyCallable myCallable = new MyCallable();
        //包装成FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        new Thread(futureTask).start();

            Random random = new Random();
            if(random.nextBoolean()){
                //等待拿结果
                System.out.println("进入到了模拟调用get方法拿结果分支...");
                Integer integer = futureTask.get();
                System.out.println("调用get方法拿到的执行结果为:"+integer);
            }else{
                //等待一下 确保线程更大几率已经启动之后再做取消操作
                Thread.sleep(10);
                //另外一半的几率模拟取消任务(这里只是打取消标记,真正的取消还是需要在任务中判断取消标记后代码处理)
                System.out.println("进入到了模拟取消的分支...");
                //里面调用的还是interrupt()
                futureTask.cancel(true);
            }

    }
}

可以使用get()方法阻塞拿到返回结果,可以使用cancel(true)方法打取消标记(48行),需要注意的是内部就是调了interrupt方法打中断线程标记,所以需要和16行判断中断标记配合使用自己写中断逻辑才会生效,不然只打标记不理会并不会中断任务。

FutureTask还有其他的方法 入isDone()、isCancal()等等比较简单,不再演示。

并发编程的相关工具类就到此结束了。用的比较多的就是FutureTask、CountDownLatch、Fork-Join等。

posted @ 2021-12-31 11:28  PencyNoBug  阅读(55)  评论(0)    收藏  举报