[Java併發-17-併發設計模式] Immutability模式:如何利用不變性解決併發問題?

解決併發問題,其實最簡單的辦法就是讓共享變量只有讀操做,而沒有寫操做。這個辦法如此重要,以致於被上升到了一種解決併發問題的設計模式:不變性(Immutability)模式。所謂不變性,簡單來說,就是對象一旦被建立以後,狀態就再也不發生變化。換句話說,就是變量一旦被賦值,就不容許修改了(沒有寫操做);沒有修改操做,也就是保持了不變性。設計模式

快速實現具有不可變性的類

實現一個具有不可變性的類,仍是挺簡單的。將一個類全部的屬性都設置成 final 的,而且只容許存在只讀方法,那麼這個類基本上就具有不可變性了。更嚴格的作法是這個類自己也是 final 的,也就是不容許繼承。由於子類能夠覆蓋父類的方法,有可能改變不可變性,因此推薦你在實際工做中,使用這種更嚴格的作法。緩存

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

咱們結合 String 的源代碼來解釋一下這個問題,下面的示例代碼源自 Java 1.8 SDK,我略作了修改,僅保留了關鍵屬性 value[] 和 replace() 方法,你會發現: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 的實現,你可能已經發現了,若是具有不可變性的類,須要提供相似修改的功能,具體該怎麼操做呢?作法很簡單,那就是建立一個新的不可變對象,這是與可變對象的一個重要區別,可變對象每每是修改本身的屬性。併發

全部的修改操做都建立一個新的不可變對象,你可能會有這種擔憂:是否是建立的對象太多了,有點太浪費內存呢?是的,這樣作的確有些浪費,那如何解決呢?分佈式

利用享元模式避免建立重複對象

若是你熟悉面向對象相關的設計模式,相信你必定能想到享元模式(Flyweight Pattern)。利用享元模式能夠減小建立對象的數量,從而減小內存佔用。 Java 語言裏面 Long、Integer、Short、Byte 等這些基本數據類型的包裝類都用到了享元模式。函數

享元模式本質上其實就是一個對象池,利用享元模式建立對象的邏輯也很簡單:建立以前,首先去對象池裏看看是否是存在;若是已經存在,就利用對象池裏的對象;若是不存在,就會新建立一個對象,而且把這個新建立出來的對象放進對象池裏。性能

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

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 模式的時候必定要確認保持不變性的邊界在哪裏,是否要求屬性對象也具有不可變性

下面咱們再看看如何正確地發佈不可變對象。不可變對象雖然是線程安全的,可是並不意味着引用這些不可變對象的對象就是線程安全的。例如在下面的代碼中,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;
      }
    }
  }
}

總結

利用 Immutability 模式解決併發問題,也許你以爲有點陌生,其實你每天都在享受它的戰果。Java 語言裏面的 String 和 Long、Integer、Double 等基礎類型的包裝類都具有不可變性,這些對象的線程安全性都是靠不可變性來保證的。Immutability 模式是最簡單的解決併發問題的方法,建議當你試圖解決一個併發問題時,能夠首先嚐試一下 Immutability 模式,看是否可以快速解決。

具有不變性的對象,只有一種狀態,這個狀態由對象內部全部的不變屬性共同決定。其實還有一種更簡單的不變性對象,那就是無狀態。無狀態對象內部沒有屬性,只有方法。在分佈式領域,無狀態意味着能夠無限地水平擴展,因此分佈式領域裏面性能的瓶頸必定不是出在無狀態的服務節點上。

相關文章
相關標籤/搜索