話題: Immutability模式:如何利用不變性解決併發問題?
“多個線程同時讀寫同一共享變量存在併發問題”,這裏的必要條件之一是讀寫,如果只有讀,而沒有寫,是沒有併發問題的。
解決併發問題,其實最簡單的辦法就是讓共享變量只有讀操作,而沒有寫操作。這個辦法如此重要,以至於被上升到了一種解決併發問題的設計模式:不變性(Immutability)模式。**所謂不變性,簡單來講,就是對象一旦被創建之後,狀態就不再發生變化。**換句話說,就是變量一旦被賦值,就不允許修改了(沒有寫操作);沒有修改操作,也就是保持了不變性。
快速實現具備不可變性的類
實現一個具備不可變性的類,還是挺簡單的。**將一個類所有的屬性都設置成final的,並且只允許存在只讀方法,那麼這個類基本上就具備不可變性了。**更嚴格的做法是這個類本身也是final的,也就是不允許繼承。因爲子類可以覆蓋父類的方法,有可能改變不可變性,所以推薦使用這種更嚴格的做法。
Java SDK裏很多類都具備不可變性,只是由於它們的使用太簡單,最後反而被忽略了。例如經常用到的String
和Long
、Integer
、Double
等基礎類型的包裝類都具備不可變性,這些對象的線程安全性都是靠不可變性來保證的。如果你仔細翻看這些類的聲明、屬性和方法,你會發現它們都嚴格遵守不可變類的三點要求:類和屬性都是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);
}
}
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模式的時候,需要注意以下兩點:
- 對象的所有屬性都是final的,並不能保證不可變性;
- 不可變對象也需要正確發佈。
在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 + "]";
}
}