並行設計模式--immutable模式

線程不安全的原因是共享了變量且對該共享變量的操作存在原子性、可見性等問題,因此一種解決思路就是構造不可變的對象,沒有修改操作也就不存在併發競爭,自然也不需要額外的鎖,同步等操作,這種設計叫做immutable object模式,本文主要理解這種模式,並學習如何實現一個較好的immutable類。

immutable設計原則

一個比較嚴格的immutable模式,有如下幾種設計原則(來自Java多線程編程實戰指南

  1. 類本身是final修飾,防止其子類改變其定義的行爲
  2. 所有字段都是用final修飾,是用final修飾不僅可以從語義上說明被修飾字段的引用不可改變,更重要的是這個語義在多線程環境下由JMM(Java內存模型)保證了被引用字段的初始化安全。即final修飾字段在其他線程可見時,其必須初始化完成。
  3. 在對象的創建過程中,this指針沒有泄露給其他對象。防止其他對象在創建過程中對其進行修改。
  4. 任何字段,若其引用了其他可改變字段,其必須使用private修飾,並且該字段不能向外暴露,如有相關方法返回該值,則使用防禦性拷貝。

immutable設計陷阱

不可變類經常會遇到以下陷阱,他是不可變的嗎?答案當然不是,該類本身是不可變的,但是其內部引用的Date對象可變,調用方可以獲取Date之後調用其set方法改變其指向的時間,最終導致該類變化,這種設計過程中經常遇到的一個問題。

public final class Interval {

  private final Date start;

  private final Date end;
  // 傳入的是引用,因此共享了內存,導致Date可變。
  public Interval(Date start, Date end) {
    this.start = start;
    this.end = end;
  }

  public Date getStart() {
    return start;
  }

  public Date getEnd() {
    return end;
  }
  
}

一般解決思路是使用防禦性拷貝,也就是要賦值的地方都重新創建對象,如下所示。

public final class Interval {

  private final Date start;

  private final Date end;

    // 進行防禦性拷貝
  public Interval(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
  }
    // 對外接口仍然需要拷貝
  public Date getStart() {
    return new Date(start.getTime());
  }

  public Date getEnd() {
    return new Date(end.getTime());
  }

}

immutable設計舉例

JDK8日期類

在JDK8中新增關於時間日期的一批API ,其設計均採用immutable模式,主要體現在一旦該實例被創建之後,不會提供修改的方法,每次需要進行操作時返回的總是一個新的類,也因此該類是線程安全的。

public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
    /**
     * The year.
     */
    private final int year;
    /**
     * The month-of-year.
     */
    private final short month;
    /**
     * The day-of-month.
     */
    private final short day;
    
    ....
    // 對其的操作總會返回一個新的實例
  public LocalDate plusDays(long daysToAdd) {
        if (daysToAdd == 0) {
            return this;
        }
        long mjDay = Math.addExact(toEpochDay(), daysToAdd);
        return LocalDate.ofEpochDay(mjDay);
    }
}

immutable與享元模式

immutable模式最大的弊端是產生了很多對象,比如上述JDK8的日期類,每一步修改操作都要產生一箇中間對象,在很多情況下是可以利用享元模式來較少對象創建次數,事實上享元模式並沒有要求所共享的實例一定是不可變的,只是在大多數情況不可變會使得享元模式更加簡單純粹。比如系統中有表示用戶一次下單購買商品數量的類Quantity,那麼考慮到用戶一次性購買數量很少大於10,因此這個類設計成immutable並且應用享元模式就可以很好地提高性能。 其本身就是類似Integer類,因此設計的具體做法就非常類似Integer的實現(之所以在實現一遍,是爲了更好的語義描述),對外提供兩個創建入口,1是構造函數,構造函數直接創建出該類。2是valueOf靜態方法,該方法會先去緩存中查詢是否包含,包含則直接返回。當然也可以在該類中加一些關於數量本身限制判斷的業務方法。

public class Quantity {

  private final int value;

  /**
   * 提供創建不可變類
   */
  public Quantity(int value) {
    this.value = value;
  }

  /**
   * 提供享元模式複用類
   */
  public static Quantity valueOf(int value) {
    if (null != QuantityCache.cache[value-1]) {
      return QuantityCache.cache[value-1];
    }
    return new Quantity(value);
  }

  private static class QuantityCache {
    static final int low = 1;
    static final int high = 10;
    static final Quantity cache[];

    static {

      cache = new Quantity[(high - low) + 1];
      int j = low;
      for(int k = 0; k < cache.length; k++)
        cache[k] = new Quantity(j++);

    }

    private QuantityCache() {}
  }
}

immutable與Builder模式

immutable有可能面臨創建所需要過多的參數以及步驟,導致設計該類時需要提供很多與類本身沒必要的方法,因此比較好的解決方案是利用Builder模式創建實例,Builder模式的本質是把對象本身提供的操作與對象的創建分離開,爲客戶端提供一個較易操作的方式去得到類的實例。

Builder模式配合中,對應的目標類往往只需要提供私有的構造函數,以及屬性的get方法,構造過程則交給內部的Builder類來完成,這是一種對於過多參數或者構造之後很少變動的類所採取的一種比較好的方式。

public final class AsyncLoadConfig {
  /**
   * 模板執行任務所需要的線程池
   */
  @Getter
  private ExecutorService executorService;
  /**
   * 單個方法默認超時時間
   */
  @Getter
  private Long defaultTimeout;

  private AsyncLoadConfig(ExecutorService executorService, Long defaultTimeout) {
    this.executorService = executorService;
    this.defaultTimeout = defaultTimeout;
  }

  /**
   * 相關配置的建造器
   * @return
   */
  public static AsyncLoadConfigBuilder builder() {
    return new AsyncLoadConfigBuilder();
  }

  public static class AsyncLoadConfigBuilder {

    private Long defaultTimeout;

    private ExecutorService executorService;

    public AsyncLoadConfigBuilder defaultTimeout(Long defaultTimeout) {
      this.defaultTimeout = defaultTimeout;
      return this;
    }

    public AsyncLoadConfigBuilder executorService(ExecutorService executorService) {
      this.executorService = executorService;
      return this;
    }

    public AsyncLoadConfig build() {
      Assert.notNull(executorService, "executorService can't be null");
      Assert.notNull(defaultTimeout, "defaultTimeout can't be null");
      return new AsyncLoadConfig(executorService, defaultTimeout);
    }
  }
}

JDK中的CopyOnWrite容器

在JDK1.5之後提供了CopyOnWriteArrayListCopyOnWriteArraySet容器,這類容器並不是嚴格意義上的不可變,但是其是immutable思想的一種應用,其本質是每次添加都重新創建一個底層數組,把之前的數據拷貝過來,然後把要添加的數據添加到尾部,最後更新這個數組的引用,實現關鍵點時更新數組引用是一個原子性操作,因此所有讀線程將始終看到數組處於一致性狀態,那麼這個數組就可以理解爲immutable的一種實現,一旦創建後不再改變。

public boolean add(E e) {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
      Object[] elements = getArray();
      int len = elements.length;
      // 創建新數組,並拷貝舊元素
      Object[] newElements = Arrays.copyOf(elements, len + 1);
      // 設置新增的元素
      newElements[len] = e;
      // 設置新數組
      setArray(newElements);
      return true;
  } finally {
      lock.unlock();
  }
}

參考

Java多線程編程實戰指南

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