設計模式模塊答疑

話題:設計模式模塊熱點問題答疑

多線程設計模式是前人解決併發問題的經驗總結,當我們試圖解決一個併發問題時,首選方案往往是使用匹配的設計模式,這樣能避免走彎路。同時,由於大家都熟悉設計模式,所以使用設計模式還能提升方案和代碼的可理解性。

避免共享的設計模式

Immutability模式Copy-on-Write模式線程本地存儲模式本質上都是爲了避免共享,只是實現手段不同而已。這3種設計模式的實現都很簡單,但是實現過程中有些細節還是需要格外注意的。例如,使用Immutability模式需要注意對象屬性的不可變性,使用Copy-on-Write模式需要注意性能問題,使用線程本地存儲模式需要注意異步執行問題和內存泄漏問題。

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;
  }
}

在異步場景中,是否可以使用 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模式在實現的時候需要注意是否存在線程的頻繁創建、銷燬以及是否可能導致OOM。在 Thread-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();
  }
}

前面知識點:九種設計模式
在這裏插入圖片描述
後面文章僅有代碼demo演示

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章