控制并发流程

第七章 控制并发流程

1、控制并发流程的工具类,作用就是帮助我们程序员更容易得让线程之间合作;

2、让线程之间相互配合,来满足业务逻辑;

3、比如让线程A等待线程B执行完毕后再执行等合作策略。

 

1、 CountDownLatch

1、CountDownLatch 类的作用(并发流程控制的工具类)

  1)倒数门闩(shuan);

  2)流程:倒数结束之前,一致处于等待状态,直到倒计时结束了,此线程才继续工作。

 

2、类的主要方法介绍

  1)CountDownLatch(int count):仅有一个构造函数,参数 count 为需要倒数的数值;

  2)await():调用 await() 方法的线程会被挂起,他会等待直到 count 值为0才继续执行;

  3)countDown():将 count 值减1,直到为0时,等待的线程会被唤起。

 

3、两个典型用法

  1)用法一:一个线程等待多个线程都执行完毕之后,再继续自己的工作;(一等多)

示例:

public class CountDownLathDemo1 {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            service.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "开始任务");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //减一
                    latch.countDown();
                    System.out.println(Thread.currentThread().getName() + "完成任务");
                }
            });
        }
        System.out.println("main 发配任务");
        try {
            //阻塞,直到 CountDownLatch 的参数被减为 0 后,才会被唤醒
            latch.await();
            System.out.println("所有任务完成");
            service.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

 

  2)用法二:多个线程等待一个线程执行完毕后再执行(多等一)

示例:

public class CountDownLathDemo2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "准备起跑!");
                    try {
                        latch.await();
                        System.out.println(Thread.currentThread().getName() + "开始出发");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
        }
        System.out.println("正在等待裁判");
        Thread.sleep(3000);
        System.out.println("出发!");
        latch.countDown();
        service.shutdown();
    }
}

 

 

  3)注意点:

    1、扩展用法:多个线程等多个线程完成执行后,再同时执行

示例:

public class CountDownLathDemo2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "准备起跑!");
                    try {
                        latch.await();
                        System.out.println(Thread.currentThread().getName() + "开始出发");
                        Thread.sleep((long)(Math.random() * 10000));
                        System.out.println(Thread.currentThread().getName() + "到达终点");
                        end.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
        }
        System.out.println("正在等待裁判");
        Thread.sleep(3000);
        System.out.println("出发!");
        latch.countDown();
        end.await();
        System.out.println("结束");
        service.shutdown();
    }
}

 

    2、CountDownLatch 时不能够重用的,如果需要重新计数,可以考虑使用 CyclicBarrier 或者创建新的 CountDownLatch 实例。

 

4、总结

  1)两个典型用法:一等多和多等一;

  2)CountDownLatch 类在创建实例的时候,需要传递倒数次数。倒数到0的时候,之前等待的线程会继续运行。

 

2、Semaphore 信号量

1、Semaphore  可以用来限制或管理数量有限的资源的使用情况;

2、例如:有十家污染工厂,每天只能发放五张许可证,也就是说每天只能允许其中五家工厂运行,那么这十家工厂,今天运行五家,明天运行另外五家;

3、信号量的作用是维护一个“许可证”的计数,线程可以“获取”许可证,那信号量剩余的许可证就减一,线程也可以“释放”一个许可证,那么信号量剩余的许可证就加一,当信号量所拥有的许可证数量为0,那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证。

 

4、信号量使用流程

  1)初始化 Semaphore   并指定许可证的数量;

  2)在需要被现在的代码前加 acquire() 或者 acquireUninterruptibly();

  3)在任务执行结束后,调用 release() 来释放许可证。

 

5、信号量主要方法介绍

  1)new Semaphore (int permits,boolean fair):这里可以设置是否要使用公平策略,如果传入 true,那么 Semaphore   会把之前等待的线程放到 FIFO 的队列里,以便于当有了新的许可证,可以分给之前等了最长时间的线程。

  2)tryAcquire():看看现在有没有空闲的许可证,如果有的话就获取,如果没有的话也没关系,我不必陷入阻塞,我可以去做别的事,过一会再来查看许可证的空闲情况。

  3)tryAcquire(timeout):和 tryAcquire() 一样,但是多了一个超时时间,比如“在3秒内获取不到许可证,我就去做别的事”。

  4)acquire():获取许可,如果获取不到,则阻塞;

  5)acquireUninterruptibly():的作用是使等待进入 acquire() 的线程,不允许被中断;

  6)release():释放许可证。

acquire() 与 release() 示例:

public class SemaphroeDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5,true);
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {
            service.execute(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "拿到了许可证");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName() + "释放了许可证");
                    semaphore.release();
                }
            });
        }
        service.shutdown();
    }
}

 

 

6、acquire() 和 release() 特殊用法:

  1)调用 acquire() 和 release() 只能获取和释放一个许可证;

  2)调用 acquire(n) 和 release(m) 可以获取 n 个许可证和释放 m 个许可证。

 

7、注意点:

  1)获取和释放的许可证数量必须一致,否则比如每次都获取2个但是只释放1个,甚至不是放,随着时间的推移,到最后许可证数量不够用,会导致程序卡死。

  2)注意在初始化 Semaphore 的时候设置公平性,一般设置为 true 会更合理;

  3)并不是必须有获取许可证的线程释放那个许可证,事实上,获取和释放许可证对线程并无要求,也许事A获取了,然后由B释放,只要逻辑合理即可;

  4)信号量的作用,除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现“条件等待”,例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1 acquire(),而线程2完成任务后 release(),这样的话,相当于是轻量级的 CountDownLatch。

 

3、Condition 

1、Condition 作用

  1)当线程一需要等待某个条件的时候,他就执行 condition.await() 方法,一旦执行了 await() 方法,线程就会进入阻塞状态;

  2)然后通常会有另外一个线程,假设是线程而,去执行对应的条件,知道这个条件达成的时候,线程二就会去执行 condition.signal() 方法,这时 JVM 就会从被阻塞的线程里找,找到那些等待该 condition 的线程,当线程一收到可执行信号的时候,他的线程状态就会变成 Runnable 可执行状态。

 

2、signalAll() 和 signal() 区别

  1)signalAll 会唤起所有的正在等待的线程;

  2)但是 signal() 是公平的,只会唤起那个等待时间最长的线程。

示例:

public class ConditionDemo {
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    static void methods1(){
        lock.lock();
        try{
            System.out.println("条件不满足,进入await()");
            condition.await();
            System.out.println("条件满足,退出await()");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    static void methods2(){
        lock.lock();
        try{
            Thread.sleep(1000);
            System.out.println("开始唤醒methods1");
            Thread.sleep(1000);
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            methods2();
        }).start();
        methods1();
    }
}

 

3、Condition 注意点

  1)实际上,如果说 Lock 用来代替 synchronized,那么 Condition 就是用来代替相对应的 Object.wait/notify 的,所以在用法和性质上,几乎都一样;

  2)await() 会自动释放持有的 Lock 锁,和 Object.wait() 一样,不需要自己手动先释放锁;

  3)调用 await() 的时候,必须持有锁,否则会抛出异常,和 Object.wait() 一样。

消费者生产者示例:

public class ConditionDemo1 {
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmtity = lock.newCondition();
    private PriorityQueue<Integer> queue = new PriorityQueue(10);

    //消费者
    public void Customer() {
        while (true) {
            lock.lock();
            try {
                if (queue.size() == 0) {
                    System.out.println("队列为空,开始生产");
                    notEmtity.await();
                }
                queue.poll();
                System.out.println("从队列里拿走了一个元素,剩余:" + queue.size());
                notFull.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public void Factor(){
        while(true){
            lock.lock();
            try{
                if(queue.size() == 10){
                    System.out.println("队列已满,停止生产");
                    notFull.await();
                }
                queue.add(1);
                System.out.println("队列剩余:" + (10 - queue.size()) + "个空间");
                notEmtity.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ConditionDemo1 demo1 = new ConditionDemo1();
        new Thread(() -> {
            demo1.Customer();
        }).start();
        new Thread(() -> {
            demo1.Factor();
        }).start();
    }
}

 

 

4、CyclicBarrier 循环栅栏

1、CyclicBarrier 循环栅栏和 CountDownLatch 很类似,都能阻塞一组线程。

2、当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用 CyclicBarrier 。CycliBarrier 可以构造一个集结点,当某一个线程执行完毕,他就会到集合点等待,直到所有线程都到了集结点,那么该栅栏就被撤销,所有线程统一出发,继续执行剩下的任务。

示例:

public class CyclicBarrierDemo1 {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("这一车已满,出发");

            }
        });
        for (int i = 0; i < 10; i++) {
            new Thread(new People(barrier)).start();
        }
    }
    static class People implements Runnable{
        CyclicBarrier barrier;
        public People(CyclicBarrier barrier){
            this.barrier = barrier;
        }
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName()+"正前往集合点");
                Thread.sleep((long) (Math.random()*10000));
                System.out.println(Thread.currentThread().getName()+"已到达集合点,准备出发");
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

 

运行结果:

 

3、CyclicBarrier 和 CountDownLatch 的区别

  1)作用不同:CyclicBarrier 要等固定数量的线程都到达了栅栏位置才能继续执行,而 CountDownLatch 只需等待数字到0,也就是说,CountDownLatch 用于事件,但是 CycliBarrier 是用于线程的。

  2)可重用性不同:CyclicBarrier 可以重复使用。CountDownLatch 在倒数到0并出发门闩打开后,就不能再次使用了,除非新建新的实例。

 

posted @ 2021-09-21 19:46  nicechen  阅读(133)  评论(0编辑  收藏  举报