线程协作工具类:控制并发流程

1 什么是控制并发流程

  • 控制并发流程的工具类,作用就是帮助我们程序员更容易的让线程之间合作
  • 让线程之间相互配合,来满足业务逻辑
  • 比如让线程A等待线程B执行完毕后再执行等合作策略

 有哪些控制并发流程的工具类?

线程协作工具类:控制并发流程_第1张图片


2 CountDownLatch倒计时门闩

线程协作工具类:控制并发流程_第2张图片

2.1 CountDownLatch类的作用

并发流程控制的工具

  • 倒数门闩
  • 例子:购物拼团;大巴(游乐园坐过山车排队),人满发车。
  • 流程:倒数结束之前,一直处于等待状态,直到倒计时结束了,此线程才继续工作。

  • CountDownLatch(int count):仅有一个构造函数,参数count为需要倒数的数值。
  • await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行。
  • countDown():将count值减1,直到为0时,等待的线程会被唤起。

线程协作工具类:控制并发流程_第3张图片

2.2 两个典型用法

  • 两个典型用法:一等多多等一
  • CountDownLatch类在创建实例的时候,需要传递倒数次数。倒数到0的时候,之前等待的线程会继续运行
  • CountDownLatch不能回滚重置

一等多

  • 用法一:一个线程等待多个线程都执行完毕,再继续自己的工作。
/**
 * 工厂中,质检,5个工人检查,所有人都认为通过,才通过
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = () -> {
                try {
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println("No." + no + "完成了检查。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //谁减一谁调用countDown
                    latch.countDown();
                }
            };
            threadPool.submit(runnable);
        }
        System.out.println("等待5个人检查完.....");
        //谁等待谁调用await
        latch.await();
        System.out.println("所有人都完成了工作,进入下一个环节。");
        threadPool.shutdown();
    }
}

public boolean await(long timeout, TimeUnit unit)  还可以设置等待的时间,超时就不在等待了

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = () -> {
                try {
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println("No." + no + "完成了检查。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //谁减一谁调用countDown
                    latch.countDown();
                }
            };
            threadPool.submit(runnable);
        }
        System.out.println("等待了5秒我就不等了,管你检查没检查完.....");
        //谁等待谁调用await
        latch.await(5, TimeUnit.SECONDS);
        System.out.println("溜了溜了.....");
        threadPool.shutdown();
    }
}

多等一

  • 用法二:多个线程等待某一个线程的信号,同时开始执行。
/**
 * 模拟100米跑步,5名选手都准备好了,只等裁判员一声令下,所有人同时开始跑步。
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = () -> {
                System.out.println("No." + no + "准备完毕,等待发令枪");
                try {
                    begin.await();
                    System.out.println("No." + no + "开始跑步了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            threadPool.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        //裁判发枪
        begin.countDown();
        threadPool.shutdown();
    }
}

 综合用法一和用法二:运动员跑步

/**
 * 模拟100米跑步,5名选手都准备好了,只等裁判员一声令下,所有人同时开始跑步。当所有人都到终点后,比赛结束。
 */
public class CountDownLatchDemo1And2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = () -> {
                System.out.println("No." + no + "准备完毕,等待发令枪");
                try {
                    begin.await();
                    System.out.println("No." + no + "开始跑步了");
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println("No." + no + "跑到终点了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    end.countDown();
                }
            };
            service.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        //发枪
        begin.countDown();
        //等待所有运动员到达终点
        end.await();
        System.out.println("所有人到达终点,比赛结束");
        service.shutdown();
    }
}

2.3 注意点

  • 扩展用法:多个线程等多个线程完成执行后,再同时执行
  • CountDownLatch是能够重用的,如果需要重新计数,可以考虑使用CyclicBarrier或者创建新的CountDownLatch实例。

3 Semaphore信号量

  • Semaphore可以用来限制或管理数量有限的资源的使用情况。
  • 污染不能太多,污染许可证只能发3张
  • 信号量的作用是维护一个 “许可证” 的计数,线程可以 “获取” 许可证,那信号量剩余的许可证就减一,线程也可以 “释放” 一个许可证,那信号量剩余的许可证就加一,当信号量所拥有的许可证数量为0,那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证

线程协作工具类:控制并发流程_第4张图片

 

线程协作工具类:控制并发流程_第5张图片

没有使用信号量的时候,慢服务会拖垮整个程序的 

线程协作工具类:控制并发流程_第6张图片

3.1 使用信号量 

信号量使用流程

  • 初始化Semaphore并指定许可证的数量
  • 在需要被现在的代码前加acquire()或者acquireUninterruptibly()方法
  • 在任务执行结束后,调用release()来释放许可证

线程协作工具类:控制并发流程_第7张图片

线程协作工具类:控制并发流程_第8张图片

线程协作工具类:控制并发流程_第9张图片

 线程协作工具类:控制并发流程_第10张图片

线程协作工具类:控制并发流程_第11张图片

线程协作工具类:控制并发流程_第12张图片

线程协作工具类:控制并发流程_第13张图片

线程协作工具类:控制并发流程_第14张图片

 线程协作工具类:控制并发流程_第15张图片

3.2 信号量主要方法介绍

  • new Semaphore(int permits, boolean fair):这里可以设置是否要使用公平策略,如果传入true,那么Semaphore会把之前等待的线程放到FIFO的队列里,以便于当有了新的许可证,可以分发给之前等了最长时间的线程。
  • acquire():能够响应中断
  • acquireUninterruptibly():不能够响应中断
  • tryAcquire():看看现在有没有空闲的许可证,如果有的话就获取,如果没有的话也没关系,我不必陷入阻塞,我可以去做别的事,过一会再来查看许可证的空闲情况。
  • ◆tryAcquire(long timeout, TimeUnit unit):和tryAcquire()一样,但是多了一个超时时间,比如 “在3秒内获取不到许可证,我就去做别的事”
  • release():归还许可证

3.3 信号量代码演示

/**
 * 演示Semaphore用法
 */
public class SemaphoreDemo {
    static Semaphore semaphore = new Semaphore(5, true);
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 10; i++) {
            service.submit(new Task());
        }
        service.shutdown();
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                //!!!获取和释放需要一致
                semaphore.acquire(3);//semaphore.release(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了许可证");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "释放了许可证");
            semaphore.release(3);
        }
    }
}
/**
 * 限制最大的并发送为3,超过就丢弃
 */
@Slf4j
public class SemaphoreExample3 {
    private final static int threadCount = 20;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            if(i > threadCount / 2){
                Thread.sleep(500);
            }
            exec.execute(() -> {
                try {
                    //semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)
                    if (semaphore.tryAcquire()) { // 尝试获取一个许可
                        test(threadNum);
                        semaphore.release(); // 释放一个许可
                    }
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }
    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}

3.4 信号量特殊用法

一次性获取或释放多个许可证

  • 比如TaskA会调用很消耗资源的method1(),而TaskB调用的是不太消耗资源的method2(),假设我们一共有5个许可证。那么我们就可以要求TaskA获取5个许可证才能执行,而 TaskB只需要获取到一个许可证就能执行,这样就避免了A和B同时运行的情况,我们可以根据自己的需求合理分配资源。

3.5 注意点

  1. 获取释放的许可证数量必须一致,否则比如每次都获取2个但是只释放1个甚至不释放,随着时间的推移,到最后许可证数量不够用,会导致程序卡死。(虽然信号量类并不对是否和获取的数量做规定,但是这是我们的编程规范,否则容易出错)。
  2. 注意在初始化Semaphore的时候设置公平性,一般设置为true会更合理。
  3. 并不是必须由获取许可证的线程释放那个许可证,事实上,获取和释放许可证对线程并无要求,也许是A获取了,然后由B释放,只要逻辑合理即可。
  4. 信号量的作用,除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现 “条件等待” ,例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1 acquire(),而线程2(先执行)完成任务后release(),这样的话,相当于是轻量级的CountDownLatch

4 Condition接口(又称条件对象)

线程协作工具类:控制并发流程_第16张图片

4.1 作用

  • 当线程1需要等待某个条件的时候,它就去执行condition.await()方法,一旦执行了await()方法,线程就会进入阻塞状态。
  • 然后通常会有另外—个线程,假设是线程2,去执行对应的条件,直到这个条件达成的时候,线程2就会去执行condition.signal()方法,这时JVM就会从被阻塞的线程里找,找到那些等待该condition的线程,当线程1收到可执行信号的时候,它的线程状态就会变成Runnable可执行状态。

线程协作工具类:控制并发流程_第17张图片

signalAll()和signal()区别

  • signalAll()会唤起所有的正在等待的线程
  • 但是signal()是公平的,只会唤起那个等待时间最长的线程

4.2 代码演示

普通示例

/**
 * 演示Condition的基本用法
 */
public class ConditionDemo1 {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    void method1() throws InterruptedException {
        lock.lock();
        try{
            System.out.println("条件不满足,开始await");
            condition.await();
            System.out.println("条件满足了,开始执行后续的任务");
        }finally {
            lock.unlock();
        }
    }
    void method2() {
        lock.lock();
        try{
            System.out.println("准备工作完成,唤醒其他的线程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ConditionDemo1 conditionDemo1 = new ConditionDemo1();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                conditionDemo1.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        conditionDemo1.method1();
    }
}

用Condition实现生产者消费者模式 

  • 用wait/notify来实现生产者消费者模式:https://blog.csdn.net/qq_40794973/article/details/103869882#t1
/**
 * 演示用Condition实现生产者消费者模式
 */
public class ConditionDemo2 {
    private final int QUEUE_SIZE = 10;
    private PriorityQueue queue = new PriorityQueue<>(QUEUE_SIZE);
    private Lock lock = new ReentrantLock();
    //没有满(生产者用)
    private Condition notFull = lock.newCondition();
    //没有空(消费者)
    private Condition notEmpty = lock.newCondition();
    public static void main(String[] args) {
        ConditionDemo2 conditionDemo2 = new ConditionDemo2();
        Producer producer = conditionDemo2.new Producer();
        Consumer consumer = conditionDemo2.new Consumer();
        new Thread(producer).start();
        new Thread(consumer).start();
    }
    //消费者
    class Consumer implements Runnable {
        @Override
        public void run() {
            consume();
        }
        private void consume() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == 0) {
                        System.out.println("队列空,等待数据");
                        try {
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    //唤醒生产者
                    notFull.signalAll();
                    System.out.println("从队列里取走了一个数据,队列剩余" + queue.size() + "个元素");
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    //生产者
    class Producer implements Runnable {
        @Override
        public void run() {
            produce();
        }
        private void produce() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == QUEUE_SIZE) {
                        System.out.println("队列满,等待有空余");
                        try {
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    //唤醒消费者
                    notEmpty.signalAll();
                    System.out.println("向队列插入了一个元素,队列剩余空间" + (QUEUE_SIZE - queue.size()));
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

 

@Slf4j
public class LockExample {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();
        new Thread(() -> {
            try {
                reentrantLock.lock();
                log.info("wait signal"); // 1
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("get signal"); // 4
            reentrantLock.unlock();
        }).start();
        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock"); // 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal ~ "); // 3
            reentrantLock.unlock();
        }).start();
    }
}

4.3 Condition注意点

  • 实际上,如果说Lock用来代替synchronized,那么Condition就是用来代替相对应的Object.wait/notify的,所以在用法和性质上,几乎都一样。
  • await方法会自动释放持有的Lock锁,和Object.wait一样,不需要自己手动先释放锁。
  • 调用await的时候,必须持有锁,否则会抛出异常,和Object.wait一样。

5 CyclicBarrier循环栅栏

线程协作工具类:控制并发流程_第18张图片

5.1 使用CyclicBarrier

  • CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞一组线程
  • 当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier。CyclicBarrier可以构造一个集结点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集结点,那么该栅栏就被撤销,所有线程再统一出发,继续执行剩下的任务。
  • 生活中的例子:“咱们3个人明天中午在学校碰面,都到齐后,一起讨论下学期的计划。”
/**
 * 演示CyclicBarrier(集体活动,坐车场景,坐满就发车)
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        //5个一发车
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("车满,发车!");
            }
        });
        //10:发两趟车
        //5:发一趟车
        for (int i = 0; i < 10; i++) {
            new Thread(new Task(i, cyclicBarrier)).start();
        }
    }
    static class Task implements Runnable{
        private int id;
        private CyclicBarrier cyclicBarrier;

        public Task(int id, CyclicBarrier cyclicBarrier) {
            this.id = id;
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程" + id + "现在前往集合地点");
            try {
                Thread.sleep((long) (Math.random()*10000));
                System.out.println("线程"+id+"到了集合地点,开始等待其他人到达");
                cyclicBarrier.await();
                System.out.println("线程"+id+"出发了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}
@Slf4j
public class CyclicBarrierExample {
    private static CyclicBarrier barrier = new CyclicBarrier(5);
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        threadPool.shutdown();
    }
    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}
@Slf4j
public class CyclicBarrierExample {
    private static CyclicBarrier barrier = new CyclicBarrier(5);
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }
    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        try {
            barrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            //log.warn("BarrierException", e);
            System.out.println("不等了");
        }
        log.info("{} continue", threadNum);
    }
}
@Slf4j
public class CyclicBarrierExample {
    //到达屏障的时候,优先执行这个里面的Runnable
    private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        log.info("callback is running");
    });
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }
    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}

5.2 CyclicBarrier和CountDownlatch的区别

  • 作用不同:CyclicBarrier要等固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需等待数字到0,也就是说,CountDownLatch用于事件,但是CyclicBarrier是用于线程的。
  • 可重用性不同:CountDownLatch在倒数到0并触发门闩打开后,就不能再次使用了,除非新建新的实例。而 CyclicBarrier可以重复使用。

 

你可能感兴趣的:(多线程)