Immutability模式: 不可變對象設計模式

話題: Immutability模式:如何利用不變性解決併發問題?

“多個線程同時讀寫同一共享變量存在併發問題”,這裏的必要條件之一是讀寫,如果只有讀,而沒有寫,是沒有併發問題的。

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

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

實現一個具備不可變性的類,還是挺簡單的。**將一個類所有的屬性都設置成final的,並且只允許存在只讀方法,那麼這個類基本上就具備不可變性了。**更嚴格的做法是這個類本身也是final的,也就是不允許繼承。因爲子類可以覆蓋父類的方法,有可能改變不可變性,所以推薦使用這種更嚴格的做法。

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

看到這裏你可能會疑惑,Java的String方法也有類似字符替換操作,怎麼能說所有方法都是隻讀的呢?我們結合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這個類並沒有照搬享元模式,Long內部維護了一個靜態的對象池,僅緩存了[-128,127]之間的數字,這個對象池在JVM啓動的時候就創建好了,而且這個對象池一直都不會變化,也就是說它是靜態的。之所以採用這樣的設計,是因爲Long這個對象的狀態共有 264 種,實在太多,不宜全部緩存,而[-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);
  }
}

IntegerString 類型的對象不適合做鎖”,其實基本上所有的基礎類型的包裝類都不適合做鎖,因爲它們內部用到了享元模式,這會導致看上去私有的鎖,其實是共有的。例如在下面代碼中,本意是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 {
 
  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;
      }
    }
  }
  
   class WMRange{
    final int upper;
    final int lower;
    WMRange(int upper,int lower){
    //省略構造函數實現
    }
  }
}

總結

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

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

Demo

final public class Person {//final
    private final String name;//final
    private final String address;//final

    public Person(final String name, final String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
    
    private final List<String> list;
    public List<String> getList() {
        return Collections.unmodifiableList(list);//返回不可變集合
    }
}
client
public class ImmutableClient {
    public static void main(String[] args) {

        //Share data
        Person person = new Person("Alex", "GuanSu");
        IntStream.range(0, 5).forEach(i ->
                new UsePersonThread(person).start()
        );
    }
}

public class UsePersonThread extends Thread {
    private Person person;

    public UsePersonThread(Person person) {
        this.person = person;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " print " + person.toString());
        }
    }
}

不可變和正常的對比
public class ImmutablePerformance {
    public static void main(String[] args) throws InterruptedException {

        //36470
        //35857 immutable
        long startTimestamp = System.currentTimeMillis();
        SyncObj synObj = new SyncObj();
        synObj.setName("Alex");

//        ImmutableObj synObj = new ImmutableObj("Alex");


        //10000 times
        //22856 sync
        //11856 immutable

        //100000 times
        //230175 sync
        //122096 immutable
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (long l = 0L; l < 100000; l++) {
                    System.out.println(Thread.currentThread().getName() + "=" + synObj.toString());
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (long l = 0L; l < 100000; l++) {
                    System.out.println(Thread.currentThread().getName() + "=" + synObj.toString());
                }
            }
        };
        t2.start();
        t1.join();
        t2.join();


        long endTimestamp = System.currentTimeMillis();
        System.out.println("Elapsed time " + (endTimestamp - startTimestamp));
    }
}

//不可變
final class ImmutableObj {
    private final String name;

    ImmutableObj(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        try {
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "[" + name + "]";
    }
}
//可變
class SyncObj {

    private String name;

    public synchronized void setName(String name) {
        this.name = name;
    }

    @Override
    public synchronized String toString() {//synchronized同步
        try {
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "[" + name + "]";
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章