並發設計模式:Immutability模式


多個線程同時讀寫同一共享變量存在並發問題,其中的必要條件之一就是 讀寫 ,如果沒有寫,只存在讀,是不會存在並發問題的。

如果讓一個共享變量只有讀操作,沒有寫操作,如此則可以解決並發問題。該理論的具體實現就是 不變性(Immutability)模式。所謂不變性,簡單來講,就是對象一旦被創建之后,狀態就不再發生變化。換句話說,就是變量一旦被賦值,就不允許修改了(沒有寫操作);沒有修改操作,也就是保持了不變性。

實現具備不可變性的類

將一個類所有的屬性都設置成 final 的,並且只允許存在只讀方法,那么這個類基本上就具備不可變性了。更嚴格的做法是這個類本身也是 final 的,也就是不允許繼承。因為子類可以覆蓋父類的方法,有可能改變不可變性。

Java SDK 里很多類都具備不可變性,只是由於它們的使用太簡單,最后反而被忽略了。例如經常用到的 String 和 Long、Integer、Double 等基礎類型的包裝類都具備不可變性,這些對象的線程安全性都是靠不可變性來保證的。如果你仔細翻看這些類的聲明、屬性和方法,你會發現它們都嚴格遵守不可變類的三點要求:類和屬性都是 final 的,所有方法均是只讀的

看到這里你可能會疑惑,Java 的 String 方法也有類似字符替換操作,怎么能說所有方法都是只讀的呢?下面通過String 的源代碼來看一哈。

下面的示例代碼源自 Java 1.8 SDK。String 這個類以及它的屬性 value[] 都是 final 的;而 replace() 方法的實現,就的確沒有修改 value[],而是將替換后的字符串作為返回值返回了。

public final class String {
  private final char value[];
  // 字符替換
  String replace(char oldChar, 
      char newChar) {
    // 無需替換,直接返回 this  
    if (oldChar == newChar){
      return this;
    }

    int len = value.length;
    int i = -1;
    /* avoid getfield opcode */
    char[] val = value; 
    // 定位到需要替換的字符位置
    while (++i < len) {
      if (val[i] == oldChar) {
        break;
      }
    }
    // 未找到 oldChar,無需替換
    if (i >= len) {
      return this;
    } 
    // 創建一個 buf[],這是關鍵
    // 用來保存替換后的字符串
    char buf[] = new char[len];
    for (int j = 0; j < i; j++) {
      buf[j] = val[j];
    }
    while (i < len) {
      char c = val[i];
      buf[i] = (c == oldChar) ? 
        newChar : c;
      i++;
    }
    // 創建一個新的字符串返回
    // 原字符串不會發生任何變化
    return new String(buf, true);
  }
}

由上面的代碼可以發現,String 是通過創建一個新的不可變對象 來實現 修改 的功能。如果 所有的修改操作都創建一個新的不可變對象,你可能會有這種擔心:是不是創建的對象太多了,有點太浪費內存呢?是的,這樣做的確有些浪費,那如何解決呢?

利用享元模式避免創建重復對象

利用享元模式可以減少創建對象的數量,從而減少內存占用。Java 語言里面 Long、Integer、Short、Byte 等這些基本數據類型的包裝類都用到了享元模式。

下面以 Long 這個類作為例子,看看它是如何利用享元模式來優化對象的創建的。

享元模式本質上其實就是一個對象池,利用享元模式創建對象的邏輯也很簡單:創建之前,首先去對象池里看看是不是存在;如果已經存在,就利用對象池里的對象;如果不存在,就會新創建一個對象,並且把這個新創建出來的對象放進對象池里。

Long 這個類並沒有照搬享元模式,Long 內部維護了一個靜態的對象池,僅緩存了 [-128,127] 之間的數字,這個對象池在 JVM 啟動的時候就創建好了,而且這個對象池一直都不會變化,也就是說它是靜態的。之所以采用這樣的設計,是因為 Long 這個對象的狀態共有 2的64次方 種,實在太多,並不適合全部緩存,而 [-128,127] 之間的數字利用率最高。下面的示例代碼出自 Java 1.8,valueOf() 方法就用到了 LongCache 這個緩存。

Long valueOf(long l) {
  final int offset = 128;
  // [-128,127] 直接的數字做了緩存
  if (l >= -128 && l <= 127) { 
    return LongCache
      .cache[(int)l + offset];
  }
  return new Long(l);
}
// 緩存,等價於對象池
// 僅緩存 [-128,127] 直接的數字
static class LongCache {
  static final Long cache[] 
    = new Long[-(-128) + 127 + 1];

  static {
    for(int i=0; i<cache.length; i++)
      cache[i] = new Long(i-128);
  }
}

注意: “Integer 和 String 類型的對象不適合做鎖”,其實基本上所有的基礎類型的包裝類都不適合做鎖,因為它們內部用到了享元模式,這會導致看上去私有的鎖,其實是共有的。例如在下面代碼中,本意是 A 用鎖 al,B 用鎖 bl,各自管理各自的,互不影響。但實際上 al 和 bl 是一個對象,結果 A 和 B 共用的是一把鎖。

class A {
  Long al=Long.valueOf(1);
  public void setAX(){
    synchronized (al) {
      // 省略代碼無數
    }
  }
}
class B {
  Long bl=Long.valueOf(1);
  public void setBY(){
    synchronized (bl) {
      // 省略代碼無數
    }
  }
}

使用 Immutability 模式的注意事項

在使用 Immutability 模式的時候,需要注意以下兩點:

  1. 對象的所有屬性都是 final 的,並不能保證不可變性;
  2. 不可變對象也需要正確發布。

在 Java 語言中,final 修飾的屬性一旦被賦值,就不可以再修改,但是如果屬性的類型是普通對象,那么這個普通對象的屬性是可以被修改的。例如下面的代碼中,Bar 的屬性 foo 雖然是 final 的,依然可以通過 setAge() 方法來設置 foo 的屬性 age。所以,在使用 Immutability 模式的時候一定要確認保持不變性的邊界在哪里,是否要求屬性對象也具備不可變性

class Foo{
  int age=0;
  int name="abc";
}
final class Bar {
  final Foo foo;
  void setAge(int a){
    foo.age=a;
  }
}

下面我們再看看如何正確地發布不可變對象。不可變對象雖然是線程安全的,但是並不意味着引用這些不可變對象的對象就是線程安全的。例如在下面的代碼中,Foo 具備不可變性,線程安全,但是類 Bar 並不是線程安全的,類 Bar 中持有對 Foo 的引用 foo,對 foo 這個引用的修改在多線程中並不能保證可見性和原子性。

//Foo 線程安全
final class Foo{
  final int age=0;
  final int name="abc";
}
//Bar 線程不安全
class Bar {
  Foo foo;
  void setFoo(Foo f){
    this.foo=f;
  }
}

如果你的程序僅僅需要 foo 保持可見性,無需保證原子性,那么可以將 foo 聲明為 volatile 變量,這樣就能保證可見性。如果你的程序需要保證原子性,那么可以通過原子類來實現。下面的示例代碼是合理庫存的原子化實現,你應該很熟悉了,其中就是用原子類解決了不可變對象引用的原子性問題。

public class SafeWM {
  class WMRange{
    final int upper;
    final int lower;
    WMRange(int upper,int lower){
    // 省略構造函數實現
    }
  }
  final AtomicReference<WMRange>
    rf = new AtomicReference<>(
      new WMRange(0,0)
    );
  // 設置庫存上限
  void setUpper(int v){
    while(true){
      WMRange or = rf.get();
      // 檢查參數合法性
      if(v < or.lower){
        throw new IllegalArgumentException();
      }
      WMRange nr = new
          WMRange(v, or.lower);
      if(rf.compareAndSet(or, nr)){
        return;
      }
    }
  }
}	

總結

具備不變性的對象,只有一種狀態,這個狀態由對象內部所有的不變屬性共同決定。其實還有一種更簡單的不變性對象,那就是無狀態。無狀態對象內部沒有屬性,只有方法。除了無狀態的對象,你可能還聽說過無狀態的服務、無狀態的協議等等。無狀態有很多好處,最核心的一點就是性能。在多線程領域,無狀態對象沒有線程安全問題,無需同步處理,自然性能很好;在分布式領域,無狀態意味着可以無限地水平擴展,所以分布式領域里面性能的瓶頸一定不是出在無狀態的服務節點上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM