JAVA并发编程-2-线程并发工具类

上一章:看这里JAVA并发编程-1-线程基础

本章主要介绍java.util.concurrent下给我们提供的线程并发工具类的作用和使用场景。

一、Fork/Join

1、分而治之与工作密取

Fork/Join框架体现了分而治之的思想,就是在必要的情况下,将一个大任务进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个小任务运算的结果进行join汇总。
在这里插入图片描述
二叉查找树、快速排序算法,Hadoop中的map-reduce都是典型的分而治之思想,相信大家也并不难理解。

分完之后的结果可以交给不同的线程去执行,并且平均分配。但是并不能保证每个线程分到的任务都同时执行完,也就是说有的线程执行的快,有的执行的慢,而我们期望最终同时得到的是一个join后的结果,所以让执行的快的线程去执行的慢的线程的任务队列的尾部“偷”一个任务来帮它执行,执行的结果还是交给原线程来join,这种机制就是工作密取

在这里插入图片描述

2、使用标准范式

abstract class ForkJoinTask< V> 是forkjoin的基础Task类,他有两个实现:用于同步方法的有返回值的RecursiveTask< V> 和用于异步方法的没有返回值的 RecursiveAction,我们自己的方法要继承自它们,实现对应的compute()方法。
使用流程如下:
在这里插入图片描述

3、Fork/Join的同步用法

下面我们用Fork/Join框架来将10000个数相加,做到每个最终任务只加1000个数,通过Fork/Join体会一下用法。

public class SumArray1 {

    public static class MakeArray {
        //数组长度
        public static final int ARRAY_LENGTH = 10000;

        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 static class SumTask extends RecursiveTask<Integer> {

        //定义我们最终拆分的任务的数组的最大长度
        private final static int THRESHOLD = MakeArray.ARRAY_LENGTH / 10;
        private int[] nums;
        private Integer fromIndex;
        private Integer endIndex;

        public SumTask(int[] nums, Integer fromIndex, Integer endIndex) {
            this.nums = nums;
            this.fromIndex = fromIndex;
            this.endIndex = endIndex;
        }

        @Override
        protected Integer compute() {
            //小于最大限制,说明已经是最终任务,就去执行相加逻辑
            if (endIndex - fromIndex <= THRESHOLD) {
                int count = 0;
                for (int i = fromIndex; i <= endIndex; i++) {
                    //模拟1ms,因为实际执行太快了
                    SleepTools.ms(1);
                    count = count + nums[i];
                }
                return count;
            } else {
                //如果没达到,就要继续拆分
                int mid = (endIndex - endIndex) / 2;
                SumTask left = new SumTask(nums, fromIndex, mid);
                SumTask right = new SumTask(nums, fromIndex, mid);
                invokeAll(left, right);
                return left.join() + right.join();
            }
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int[] nums = MakeArray.makeArray();
        SumTask sumTask = new SumTask(nums, 0, nums.length - 1);
        long start = System.currentTimeMillis();
        pool.invoke(sumTask);
        System.out.println("Task is Running.....");
        System.out.println("The count is " + sumTask.join()
                + " spend time:" + (System.currentTimeMillis() - start) + "ms");
    }
}

在计算的过程中,我们sleep了一秒,主要是为了模拟较复杂的计算过程。因为实际上你会发现,如果单纯的加10000个数,单线程for循环是非常的快的。这个很好理解,一直强调的是,多线程开发是有本身的线程上下文切换等的资源消耗要考虑的,甚至本身消耗的资源和时间是比执行的任务占用的资源和时间还要大,这个时候多线程的执行并不能带来性能上的提升。所以我们在使用多线程进行开发时一定要分析清楚实际的情况,再决定是否要使用多线程。

4、Fork/Join的异步用法

异步方法很好理解,就是不需要子线程的返回值或者子线程执行过程中需要主线程去完成其它的工作。

假设我们要寻找磁盘D上的所有.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("D:/"));

            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();//阻塞的方法
            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()) {
                //<T extends ForkJoinTask<?>> Collection<T> invokeAll(Collection<T> tasks)
                for (FindDirsFiles subTask : invokeAll(subTasks)) {
                    subTask.join();//等待子任务执行完成
                }
            }
        }
    }
}

在主线程中调用了join方法进行等待,如果主线程太快执行完就看不到子线程执行结果了。

二、CountDownLatch

作用:一组线程等待其他的线程完成工作以后再执行,加强版join。

怎么理解呢?
首先简单来说它就是一个计数器

最主要的就三个方法
构造方法:public CountDownLatch(int count) 创建一个计数器,参数是计数器初始值
减数值方法:countDown() 给计数器的值减1
等待方法:await() 调用后开始等待,直到计数器值减为0接着执行

看一个例子,略复杂:

/**
 * 类说明:演示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步,需要扣减两次
        Thread thread1 = 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();//每完成一步初始化工作,扣减一次
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new BusiThread());
        thread2.start();
        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(6)的初始值为6
thread2和main线程都调用了latch.await() 方法
for循环中创建了4个线程,每个线程调用一次countDown()方法
thread1线程中调用了2次countDown()方法
期望的执行流程:main和thread2等待其它5个线程执行6次countDown()将值减为0后接着执行

值得注意的几点:
1,一个线程中可以执行多次countDown()方法
2,所有的一组等待线程是靠另外的线程控制是否继续执行的
3,另外有await(long timeout, TimeUnit unit)方法可以等待某个时间后不再等待

三、CyclicBarrier

作用:让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行

同样作为一个计数器
只有两个方法:
构造方法:CyclicBarrier(int parties) 传入计数器初始值
等待方法:await() 执行到此对初始值减1并等待,直到初始值为0,接着执行

CyclicBarrier跟CountDownLatch最大的不同是,它是一组线程内的屏障或者计数器,不能被该组线程外的其他线程的执行影响到

构造方法CyclicBarrier(int parties, Runnable barrierAction),屏障开放或者说计数器减为0,barrierAction定义的任务会执行

来看例子:

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() {
            StringBuilder result = new StringBuilder();
            for (Map.Entry<String, Long> workResult : resultMap.entrySet()) {
                result.append("[" + workResult.getValue() + "]");
            }
            System.out.println(" the result = " + result);
            System.out.println("do other business........");
        }
    }

    //工作线程
    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();
                Thread.sleep(1000 + id);
                System.out.println("Thread_" + id + " ....do its business ");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

new CyclicBarrier(5, new CollectThread()创建了一个CyclicBarrier,for循环中创建了5个SubThread,每个线程执行到 barrier.await()时会将CyclicBarrier的值减1并等待,减到0后会执行CollectThread任务和各个线程后面的任务

四、Semaphore信号量

作用:控制同时访问某个特定资源的线程数量,用在流量控制

用一个获取线程池连接的例子来感受下用法:

ublic class DBPoolSemaphore {

    private final static int POOL_SIZE = 10;
    private final Semaphore useful;//useful表示可用的数据库连接,useless表示已用的数据库连接

    public DBPoolSemaphore() {
        this.useful = new Semaphore(POOL_SIZE);
    }

    //存放数据库连接的容器
    private static LinkedList<Connection> pool = new LinkedList<Connection>();

    //初始化池
    static {
        for (int i = 0; i < POOL_SIZE; i++) {
            pool.addLast(SqlConnectImpl.fetchConnection());
        }
    }

    /*归还连接*/
    public void returnConnect(Connection connection) throws InterruptedException {
        if (connection != null) {
            System.out.println("当前有" + useful.getQueueLength() + "个线程等待数据库连接!!"
                    + "可用连接数:" + useful.availablePermits());
            synchronized (pool) {
                pool.addLast(connection);
            }
            useful.release();
        }
    }

    /*从池子拿连接*/
    public Connection takeConnect() throws InterruptedException {
        //如果拿不到,会阻塞,直到拿到为止
        useful.acquire();
        Connection conn;
        synchronized (pool) {
            conn = pool.removeFirst();
        }
        return conn;
    }

    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();
        }
    }
}

初始化一个大小为10的Semaphore,takeConnect中用到acquire(),它会阻塞直到拿到连接,returnConnect中用到**release()**将一个数量归还给Semaphore
在这里插入图片描述
执行结果来看,最开始有10个线程马上拿到连接,后面的会阻塞等待Semaphore的有值

五、Exchanger

作用:用于两个线程间的数据交换

看一段伪代码:

    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 {
                	/*添加数据
                	 * set.add(.....)
                	 * */
                	setA = exchange.exchange(setA);//交换set
                	/*处理交换后的数据*/
                } catch (InterruptedException e) {
                }
            }
        }).start();

      //第二个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
            	Set<String> setB = new HashSet<String>();//存放数据的容器
                try {
                	/*添加数据
                	 * set.add(.....)
                	 * set.add(.....)
                	 * */
                	setB = exchange.exchange(setB);//交换set
                	/*处理交换后的数据*/
                } catch (InterruptedException e) {
                }
            }
        }).start();

    }

exchange(value)方法用于交换数据信息。

exchange使用较少。简单举个使用场景帮助理解,生产者线程生成了一个list用于给消费者线程使用,则可以在生产者生成数据结束后使用exchange做两个线程间的数据交换,消费者拿到数据,处理后再用exchange将结果交给生产者。

本帖介绍了java.util.concurrent下的线程并发工具类,关于它们的使用相信大家有了初步的了解,这些工具类可以通过自己的使用的来逐渐掌握它们,来解决我们的业务问题。
关于他们的源码和实现机制本章没有涉及到,因为它们的原理基本都是基于AQS,关于AQS这座java并发编程的高山会在后面单独开帖来讲解,到时候也会结合本章的工具类源码来理解。

发布了7 篇原创文章 · 获赞 12 · 访问量 2968

猜你喜欢

转载自blog.csdn.net/weixin_40292704/article/details/105379208