解決併發問題,其實最簡單的辦法就是讓共享變量只有讀操做,而沒有寫操做。這個辦法如此重要,以致於被上升到了一種解決併發問題的設計模式:不變性(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 模式的時候,須要注意如下兩點:
在 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 模式,看是否可以快速解決。
具有不變性的對象,只有一種狀態,這個狀態由對象內部全部的不變屬性共同決定。其實還有一種更簡單的不變性對象,那就是無狀態
。無狀態對象內部沒有屬性,只有方法。在分佈式領域,無狀態意味着能夠無限地水平擴展,因此分佈式領域裏面性能的瓶頸必定不是出在無狀態的服務節點上。