控制并发流程
第七章 控制并发流程
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并出发门闩打开后,就不能再次使用了,除非新建新的实例。