Day856.多线程设计模式一些列问题 -Java 并发编程实战

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day856.多线程设计模式一些列问题 -Java 并发编程实战相关的知识,希望对你有一定的参考价值。

多线程设计模式一些列问题

Hi,我是阿昌,今天学习记录的是关于多线程设计模式一些列问题的内容。

多线程设计模式 是前人解决并发问题的经验总结,当试图解决一个并发问题时,首选方案往往是使用匹配的设计模式,这样能避免走弯路。

同时,由于都熟悉设计模式,所以使用设计模式还能提升方案和代码的可理解性。


一、避免共享的设计模式

Immutability 模式、Copy-on-Write 模式和线程本地存储模式本质上都是为了避免共享,只是实现手段不同而已。这 3 种设计模式的实现都很简单,但是实现过程中有些细节还是需要格外注意的。


  • 使用 Immutability 模式需要注意对象属性的不可变性
  • 使用 Copy-on-Write 模式需要注意性能问题
  • 使用线程本地存储模式需要注意异步执行问题

Immutability 模式 的思考题是讨论 Account 这个类是不是具备不可变性。

这个类初看上去属于不可变对象的中规中矩实现,而实质上这个实现是有问题的,原因在于 StringBuffer 不同于 String,StringBuffer 不具备不可变性,通过 getUser() 方法获取 user 之后,是可以修改 user 的。

一个简单的解决方案是让 getUser() 方法返回 String 对象。

public final class Account
  private final 
    StringBuffer user;
  public Account(String user)
    this.user = 
      new StringBuffer(user);
  
  //返回的StringBuffer并不具备不可变性
  public StringBuffer getUser()
    return this.user;
  
  public String toString()
    return "user"+user;
  


Copy-on-Write 模式的思考题是讨论 Java SDK 中为什么没有提供 CopyOnWriteLinkedList。

这是一个开放性的问题,没有标准答案,但是性能问题一定是其中一个很重要的原因,毕竟完整地复制 LinkedList 性能开销太大了。

数组存储在连续内存,连续内存更有利于CPU加载和缓存,特点是增删慢,读取快;

链表数据结构存储在分散内存,特点是增删快,读取慢; 链表结构的设计初衷就是用于增删频繁,读取少的场景;

CopyOnWrite使用场景:要求读取性能高,读取多,修改少; 二者设计理念相违背,所以存在CopyOnWriteArrayList,而不存在CopyOnWriteLinkedList


线程本地存储模式的思考题是在异步场景中,是否可以使用 Spring 的事务管理器。答案显然是不能的,Spring 使用 ThreadLocal 来传递事务信息,因此这个事务信息是不能跨线程共享的。

实际工作中有很多类库都是用 ThreadLocal 传递上下文信息的,这种场景下如果有异步操作,一定要注意上下文信息是不能跨线程共享的。


二、多线程版本 IF 的设计模式

Guarded Suspension 模式和 Balking 模式都可以简单地理解为“多线程版本的 if”,但它们的区别在于前者会等待 if 条件变为真,而后者则不需要等待。


Guarded Suspension 模式的经典实现是使用管程,很多初学者会简单地用线程 sleep 的方式实现,比如Guarded Suspension 模式的思考题就是用线程 sleep 方式实现的。

但不推荐你使用这种方式,最重要的原因是性能,如果 sleep 的时间太长,会影响响应时间;sleep 的时间太短,会导致线程频繁地被唤醒,消耗系统资源。

同时,示例代码的实现也有问题:由于 obj 不是 volatile 变量,所以即便 obj 被设置了正确的值,执行 while(!p.test(obj)) 的线程也有可能看不到,从而导致更长时间的 sleep。

//获取受保护对象  
T get(Predicate<T> p) 
  try 
    //obj的可见性无法保证
    while(!p.test(obj))
      TimeUnit.SECONDS
        .sleep(timeout);
    
  catch(InterruptedException e)
    throw new RuntimeException(e);
  
  //返回非空的受保护对象
  return obj;

//事件通知方法
void onChanged(T obj) 
  this.obj = obj;


实现 Balking 模式最容易忽视的就是竞态条件问题。

Balking 模式的思考题就存在竞态条件问题。

因此,在多线程场景中使用 if 语句时,一定要多问自己一遍:是否存在竞态条件。

class Test
  volatile boolean inited = false;
  int count = 0;
  void init()
    //存在竞态条件
    if(inited)
      return;
    
    //有可能多个线程执行到这里
    inited = true;
    //计算count的值
    count = calc();
  
  

三、三种最简单的分工模式

Thread-Per-Message 模式、Worker Thread 模式和生产者 - 消费者模式是三种最简单实用的多线程分工方法。

虽说简单,但也还是有许多细节需要你多加小心和注意。


Thread-Per-Message 模式 在实现的时候需要注意是否存在线程的频繁创建、销毁以及是否可能导致 OOMThread-Per-Message 模式的思考题就是关于如何快速解决 OOM 问题的。在高并发场景中,最简单的办法其实是限流

当然,限流方案也并不局限于解决 Thread-Per-Message 模式中的 OOM 问题。


Worker Thread 模式的实现,需要注意潜在的线程死锁问题。Worker Thread 模式的思考题中的示例代码就存在线程死锁。注意线程池之间的隔离问题

“工厂里只有一个工人,他的工作就是同步地等待工厂里其他人给他提供东西,然而并没有其他人,他将等到天荒地老,海枯石烂!”因此,共享线程池虽然能够提供线程池的使用效率,但一定要保证一个前提,那就是:任务之间没有依赖关系。

ExecutorService pool = Executors
  .newSingleThreadExecutor();
//提交主任务
pool.submit(() -> 
  try 
    //提交子任务并等待其完成,
    //会导致线程死锁
    String qq=pool.submit(()->"QQ").get();
    System.out.println(qq);
   catch (Exception e) 
  
);

Java 线程池本身就是一种生产者 - 消费者模式的实现,所以大部分场景你都不需要自己实现,直接使用 Java 的线程池就可以了。

但若能自己灵活地实现生产者 - 消费者模式会更好,比如可以实现批量执行和分阶段提交,不过这过程中还需要注意如何优雅地终止线程,生产者 - 消费者模式的思考题就是关于此的。


如何优雅地终止线程两阶段终止模式是一种通用的解决方案。但其实终止生产者 - 消费者服务还有一种更简单的方案,叫做“毒丸”对象。

Java 并发编程实战》第 7 章的 7.2.3 节对“毒丸”对象有过详细的介绍。简单来讲,“毒丸”对象是生产者生产的一条特殊任务,然后当消费者线程读到“毒丸”对象时,会立即终止自身的执行。

下面是用“毒丸”对象终止写日志线程的具体实现,整体的实现过程还是很简单的:类 Logger 中声明了一个“毒丸”对象 poisonPill ,当消费者线程从阻塞队列 bq 中取出一条 LogMsg 后,先判断是否是“毒丸”对象,如果是,则 break while 循环,从而终止自己的执行。

class Logger 
  //用于终止日志执行的“毒丸”
  final LogMsg poisonPill = new LogMsg(LEVEL.ERROR, "");
  //任务队列  
  final BlockingQueue<LogMsg> bq = new BlockingQueue<>();
  //只需要一个线程写日志
  ExecutorService es = Executors.newFixedThreadPool(1);
  //启动写日志线程
  void start()
    File file=File.createTempFile("foo", ".log");
    final FileWriter writer= new FileWriter(file);
    this.es.execute(()->
      try 
        while (true) 
          LogMsg log = bq.poll(
            5, TimeUnit.SECONDS);
          //如果是“毒丸”,终止执行  
          if(poisonPill.equals(logMsg))
            break;
            
          //省略执行逻辑
        
       catch(Exception e)
       finally 
        try 
          writer.flush();
          writer.close();
        catch(IOException e)
      
    );  
  
  //终止写日志线程
  public void stop() 
    //将“毒丸”对象加入阻塞队列
    bq.add(poisonPill);
    es.shutdown();
  


以上是关于Day856.多线程设计模式一些列问题 -Java 并发编程实战的主要内容,如果未能解决你的问题,请参考以下文章

Leetcode刷题100天—856. 括号的分数(栈)—day03

Day-12: 进程和线程

day-3 聊聊python多线程编程那些事

死锁Lock锁等待唤醒机制线程组线程池定时器单例设计模式_DAY24

day 7-5 生产者消费者模型

day35 python多线程