并发编程(二十六):并发设计模式之分工

并发编程领域的三个核心问题:分工、同步和互斥。而解决并发编程问题的首要问题就是解决宏观的分工问题

而解决分工问题也有一系列的设计模式,比较常用的有Thread-Per-Message模式。Worker Thread模式、生产者-消费者模式

1.Thread-Per-Message模式

简单来说,该模式的思想就是为每一个任务分配一个线程去执行,例如Http Server端的设计,如果主线程又处理连接,又处理请求,那么就相当于编程了串行,因此可以创建子线程去处理具体的请求,例如Socket模型,也是采取类似这种思想。

但是Java的线程太重了,会占用很多资源,因此业界有了一种解决方案,叫做轻量级线程。这个方案在 Java 领域知名度并不高,但是在其他编程语言里却叫得很响,例如 Go 语言、Lua 语言里的协程,本质上就是一种轻量级的线程。轻量级的线程,创建的成本很低,基本上和创建一个普通对象的成本相似;并且创建的速度和内存占用相比操作系统线程至少有一个数量级的提升,所以基于轻量级线程实现 Thread-Per-Message 模式就完全没有问题了。

2.Worker Thread模式

Thread-Per-Message模式会频繁的创建和销毁线程,并不适用于Java,而Worker Thread模式能够有效避免重复创建线程

其实Worker Thread模式在Java中的体现就是线程池ThreadExecutorPool,将任务提交到线程池中,线程池中的线程去取线程去执行,这样就能有效避免线程的重复创建

2.1.正确的创建线程池

  1. 用有界队列来接受任务
  2. 创建线程池,清晰地指明拒绝策略
  3. 给线程赋予一个业务相关的名称

2.2.避免将有依赖的任务交给同一个线程池去执行,因为这样可能会产生死锁:

例如有一个任务分为了两个阶段执行,第一个阶段依赖于第二个阶段的执行结果,如果两个阶段的任务共用了一个线程池p1,那么当p1执行第一阶段的任务,而第一阶段执行的任务内部又去将第二阶段的任务提交到线程池,此时可能会出现线程池满了(可能线程池的线程数正好等于第一阶段的任务数),第二阶段的任务阻塞在队列中,而第一阶段的任务分配到了线程执行并且阻塞在等待第二阶段的结果,就会出现第一阶段和第二阶段互相等待的场景,此时就产生了死锁

解决方法就是第一阶段和第二阶段用不同的线程池。因为这样只会出现第一阶段等待第二阶段,而不会出现第二阶段等待线程池中空闲线程的场景,减少了互相等待的概率

代码描述如下:

//L1、L2 阶段共用的线程池
ExecutorService es = Executors.
  newFixedThreadPool(2);
//L1 阶段的闭锁    
CountDownLatch l1=new CountDownLatch(2);
for (int i=0; i<2; i++){
  System.out.println("L1");
  // 执行 L1 阶段任务
  es.execute(()->{
    //L2 阶段的闭锁 
    CountDownLatch l2=new CountDownLatch(2);
    // 执行 L2 阶段子任务
    for (int j=0; j<2; j++){
      es.execute(()->{
        System.out.println("L2");
        l2.countDown();
      });
    }
    // 等待 L2 阶段任务执行完
    l2.await();
    l1.countDown();
  });
}
// 等着 L1 阶段任务执行完
l1.await();
System.out.println("end");

3.生产者-消费者模式

生产者 - 消费者模式的优点

  1. 生产者 - 消费者模式的核心是一个任务队列,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行
  2. 生产者 - 消费者模式有一个很重要的优点,就是解耦
  3. 支持异步,并且能够平衡生产者和消费者的速度差异
  4. 支持批量执行以提升性能

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×