• 温故知新-多线程-forkjoin、CountDownLatch、CyclicBarrier、Semaphore用法




    摘要

    本文主要简单介绍forkjoin、CountDownLatch、CyclicBarrier、Semaphore的常见用法;

    forkjoin

    从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

    这种思想和MapReduce很像(input --> split --> map --> reduce --> output)

    主要有两步:

    • 第一、任务切分;
    • 第二、结果合并

    它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

    假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

    工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

    public class ForkJoinDemo {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            long start = System.currentTimeMillis();
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            ForkJoinTask<Long> task = new MyForkJoinTask(0L, 10_0000_0000L);
            ForkJoinTask<Long> submit = forkJoinPool.submit(task);
            Long sum = submit.get();
            long end = System.currentTimeMillis();
            System.out.println("sum=" + sum + " 时间:" + (end - start));
        }
    
    
    }
    
    class MyForkJoinTask extends RecursiveTask<Long> {
    
        private Long start;
        private Long end;
        // 临界值
        private Long temp = 10000L;
    
        public MyForkJoinTask(Long start, Long end) {
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Long compute() {
            if ((end - start) < temp) {
                Long sum = 0L;
                for (Long i = start; i <= end; i++) {
                    sum += i;
                }
                return sum;
            } else { // forkjoin 递归
                long middle = (start + end) / 2; 
                MyForkJoinTask task1 = new MyForkJoinTask(start, middle);
                task1.fork(); 
                MyForkJoinTask task2 = new MyForkJoinTask(middle + 1, end);
                task2.fork(); 
                return task1.join() + task2.join();
            }
        }
    }
    

    CountDownLatch

    CountDownLatch 的方法不是很多,将它们一个个列举出来:

    1. await() throws InterruptedException:调用该方法的线程等到构造方法传入的 N 减到 0 的时候,才能继续往下执行;
    2. await(long timeout, TimeUnit unit):与上面的 await 方法功能一致,只不过这里有了时间限制,调用该方法的线程等到指定的 timeout 时间后,不管 N 是否减至为 0,都会继续往下执行;
    3. countDown():使 CountDownLatch 初始值 N 减 1;
    4. long getCount():获取当前 CountDownLatch 维护的值
    public class CountDownLatchDemo {
        private static CountDownLatch startSignal = new CountDownLatch(1);
        //用来表示裁判员需要维护的是6个运动员
        private static CountDownLatch endSignal = new CountDownLatch(6);
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(6);
            for (int i = 0; i < 6; i++) {
                executorService.execute(() -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 运动员等待裁判员响哨!!!");
                        startSignal.await();
                        System.out.println(Thread.currentThread().getName() + "正在全力冲刺");
                        endSignal.countDown();// 数量-1
                        System.out.println(Thread.currentThread().getName() + "  到达终点");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
            System.out.println("裁判员发号施令啦!!!");
            startSignal.countDown(); // 数量-1
            endSignal.await();
            System.out.println("所有运动员到达终点,比赛结束!");
            executorService.shutdown();
        }
    
        @SneakyThrows
        public static void test0() {
            // 总数是6,必须要执行任务的时候,再使用!
            CountDownLatch countDownLatch = new CountDownLatch(6);
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + " Go out");
                    countDownLatch.countDown();
                }, String.valueOf(i)).start();
            }
            countDownLatch.await(); // 等待计数器归零,然后再向下执行
            System.out.println("Close Door");
    
        }
    }
    

    CyclicBarrier

    当多个线程都达到了指定点后,才能继续往下继续执行。这就有点像报数的感觉,假设 6 个线程就相当于 6 个运动员,到赛道起点时会报数进行统计,如果刚好是 6 的话,这一波就凑齐了,才能往下执行。**CyclicBarrier 在使用一次后,下面依然有效,可以继续当做计数器使用,这是与 CountDownLatch 的区别之一。**这里的 6 个线程,也就是计数器的初始值 6,是通过 CyclicBarrier 的构造方法传入的。

    下面来看下 CyclicBarrier 的主要方法:

    // 等到所有的线程都到达指定的临界点 await() throws InterruptedException, BrokenBarrierException

    // 与上面的await方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止 await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

    //获取当前有多少个线程阻塞等待在临界点上 int getNumberWaiting()

    //用于查询阻塞等待的线程是否被中断 boolean isBroken()

    public class CyclicBarrierDemo {
    
    
        public static void main(String[] args) {
            test();
        }
        public static void test() {
    
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> {
                System.out.println("召唤神龙成功!");
            });
            for (int i = 1; i <=7 ; i++) {
                final int temp = i;
                // lambda能操作到 i 吗
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
                    try {
                        cyclicBarrier.await(); // 等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    }
    

    Semaphore

    Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

    Semaphore 类中比较重要的几个方法:

    1. public void acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许
      可。
    2. public void acquire(int permits):获取 permits 个许可
    3. public void release() { } :释放许可。注意,在释放许可之前,必须先获获得许可。
    4. public void release(int permits) { }:释放 permits 个许可
      上面 4 个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法13/04/2018 Page 86 of 283
    5. public boolean tryAcquire():尝试获取一个许可,若获取成功,则立即返回 true,若获取失
      败,则立即返回 false
    6. public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取一个许可,若在指定的
      时间内获取成功,则立即返回 true,否则则立即返回 false
    7. public boolean tryAcquire(int permits):尝试获取 permits 个许可,若获取成功,则立即返
      回 true,若获取失败,则立即返回 false
    8. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits
      个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
    9. 还可以通过 availablePermits()方法得到可用的许可数目。

    应用场景

    Semaphore可以用于做流量控制,特别公用资源有限的应用场景;

    public class SemaphoreDemo {
        public static void main(String[] args) {
            // 线程数量:停车位! 限流!
            Semaphore semaphore = new Semaphore(3);
            for (int i = 1; i <=6 ; i++) {
                new Thread(()->{
                    try {
                     		 // acquire() 得到
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName()+"抢到车位");
                                TimeUnit.SECONDS.sleep(2);
                        System.out.println(Thread.currentThread().getName()+"离开车位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release(); // release() 释放
                    }
                },String.valueOf(i)).start();
            }
        }
    }
    

    预告:下一篇会分析一下AQS的实现原理,因为CountDownLatch、CyclicBarrier、Semaphore都是基于AQS实现的;

    参考

    JDK 7 中的 Fork/Join 模式
    一文秒懂 Java Fork/Join
    并发工具类(三)控制并发线程数的Semaphore


    你的鼓励也是我创作的动力

    打赏地址

  • 相关阅读:
    一个简易邮件群发软件设计与实现
    一种公众号回复关键词机制
    Oracle 异常 ORA-01861: literal does not match format string(字符串格式不匹配)
    Linux使用命令
    IDEA在引入Maven项目后Dependencies中在出现红色波浪线
    MySQL安装Write configuration file 提示:configuration file template my.ini Error code-1
    redis批量删除键的操作
    在WINDOWS服务器下设置MARIADB自动备份的方法
    xampp3.2下mysql中文乱码终极解决方案
    CentOS 7虚拟机下模拟实现nginx负载均衡
  • 原文地址:https://www.cnblogs.com/yangsanchao/p/13062897.html
Copyright © 2020-2023  润新知