很多的同學很少使用、或者干脆不了解不可變類(Immutable Class)。直觀上很容易認為Immutable類效率不高,或者難以理解他的使用場景。其實不可變類是非常有用的,可以提高並行編程的效率和優化設計。讓我們跳過一些寬泛的介紹,從一個常見的並行編程場景說起:
假設系統需要實時地處理大量的訂單,這些訂單的處理依賴於用戶的配置,例如用戶的會員級別、支付方式等。程序需要通過這些配置的參數來計算訂單的價格。而用戶配置同時被另外一些線程更新。顯然,我們在訂單計算的過程中保持配置的一致性。
上面的例子是我虛擬出來的,但是類似的場景非常常見--線程A實時地大量地處理請求;線程B偶爾地修改線程A依賴的配置信息。我們陷入這樣的兩難:
1,為了保持配置的一致性,我們不得不在線程A和線程B上,對配置的讀和寫都加鎖,才能保障配置的一致性。這樣才能保證請求處理過程中,不會出現某些配置項被更新了,而另外一些沒有;或者處理中開始使用的是舊配置,而後又使用新的配置。(聽起來類似於數據庫的髒讀問題)
2,另一方面,線程A明顯比線程B更繁忙,為了偶爾一次的配置更新,為每秒數以萬次的請求處理加鎖,顯然代價太高了。
解決方案有兩種:
第一種是,采用ReadWriteLock。這是最常見的方式。
對讀操作加讀鎖,對寫操作加寫鎖。如果沒有正在發生的寫操作,讀鎖的代價很低。
第二種是,采用不可變對象來保存配置信息,用替換配置對象的方式,而不是修改配置對象的方式,來更新配置信息。讓我們來思考一下這麼做的利弊:
1)對於訂單處理線程A來說,它不再需要加鎖了!因為用於保存配置的對象是不可變對象。我們要麼讀取的是一個舊的配置對象,要麼是一個新的配置對象(新的配置對象覆蓋了舊的配置對象)。不會出現“髒讀”的情況。
2)對於用於更新配置的線程B,它的負擔加重了 -- 更新任何一項配置,都必須重新創建一個新的不可變對象,然後把更新的新的屬性和其他舊屬性賦給新的對象,最後覆蓋舊的對象,被拋棄的舊對象還增加了GC的負擔。而原本,這一切只要一個set操作就能完成。
我們如何衡量利弊呢?經常,這是非常劃算的,線程A和線程B的工作量可能相差幾個數量級。用線程B壓力的增加(其實不值一提)來換取線程A可以不用鎖,效率應該會有很大提升。
讓我們用代碼來測試一下哪個解決方案更好。
方案一:采用ReentrantReadWriteLock來加讀寫鎖:
一個普通的配置類,保存了用戶的優惠信息,包括會員優惠和特殊節日優惠,在計算訂單總價的時候用到:
public class AccountConfig {
private double membershipDiscount;
private double specialEventDiscount;
public AccountConfig(double membershipDiscount, double specialEventDiscount)
{
this.membershipDiscount = membershipDiscount;
this.specialEventDiscount = specialEventDiscount;
}
public double getMembershipDiscount() {
return membershipDiscount;
}
public void setMembershipDiscount(double membershipDiscount) {
this.membershipDiscount = membershipDiscount;
}
public double getSpecialEventDiscount() {
return specialEventDiscount;
}
public void setSpecialEventDiscount(double specialEventDiscount) {
this.specialEventDiscount = specialEventDiscount;
}
}
程序包括2個工作線程,一個負責處理訂單,計算訂單的總價,它在讀取配置信息時采取讀鎖。另一個負責更新配置信息,采用寫鎖。
public static void main(String[] args) throws Exception {
final ConcurrentHashMap<String, AccountConfig> accountConfigMap =
new ConcurrentHashMap<String, AccountConfig>();
AccountConfig accountConfig1 = new AccountConfig(0.02, 0.05);
accountConfigMap.put("user1", accountConfig1);
AccountConfig accountConfig2 = new AccountConfig(0.03, 0.04);
accountConfigMap.put("user2", accountConfig2);
final ReadWriteLock lock = new ReentrantReadWriteLock();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Runnable() {
Random r = new Random();
@Override
public void run() {
Long t1 = System.nanoTime();
for (int i = 0; i < 100000000; i++) {
Order order = MockOrder();
lock.readLock().lock();
AccountConfig accountConfig = accountConfigMap.get(order.getUser());
double price = order.getPrice() * order.getCount()
* (1 - accountConfig.getMembershipDiscount())
* (1 - accountConfig.getSpecialEventDiscount());
lock.readLock().unlock();
}
Long t2 = System.nanoTime();
System.out.println("ReadWriteLock:" + (t2 - t1));
}
private Order MockOrder() {
Order order = new Order();
order.setUser("user1");
order.setPrice(r.nextDouble() * 1000);
order.setCount(r.nextInt(10));
return order;
}
});
executor.execute(new Runnable() {
Random r = new Random();
@Override
public void run() {
while (true) { lock.writeLock().lock();
AccountConfig accountConfig = accountConfigMap.get("user1");
accountConfig.setMembershipDiscount(r.nextInt(10) / 100.0);
lock.writeLock().unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
}
方案二:采用不可變對象:
創建一個不可變的配置類ImmutableAccountConfig:
public final class ImmutableAccountConfig {
private final double membershipDiscount;
private final double specialEventDiscount;
public ImmutableAccountConfig(double membershipDiscount, double specialEventDiscount)
{
this.membershipDiscount = membershipDiscount;
this.specialEventDiscount = specialEventDiscount;
}
public double getMembershipDiscount() {
return membershipDiscount;
}
public double getSpecialEventDiscount() {
return specialEventDiscount;
}
}
還是創建2個線程。訂單線程不必加鎖。而配置更新的線程由於采用了不可變類,采用替換對象的方式來更新配置:
public static void main(String[] args) throws Exception {
final ConcurrentHashMap<String, ImmutableAccountConfig> immutableAccountConfigMap
= new ConcurrentHashMap<String, ImmutableAccountConfig>();
ImmutableAccountConfig accountConfig1 = new ImmutableAccountConfig(0.02, 0.05);
immutableAccountConfigMap.put("user1", accountConfig1);
ImmutableAccountConfig accountConfig2 = new ImmutableAccountConfig(0.03, 0.04);
immutableAccountConfigMap.put("user2", accountConfig2);
//final ReadWriteLock lock = new ReentrantReadWriteLock();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Runnable() {
Random r = new Random();
@Override
public void run() {
Long t1 = System.nanoTime();
for (int i = 0; i < 100000000; i++) {
Order order = MockOrder();
ImmutableAccountConfig immutableAccountConfig =
immutableAccountConfigMap.get(order.getUser());
double price = order.getPrice() * order.getCount()
* (1 - immutableAccountConfig.getMembershipDiscount())
* (1 - immutableAccountConfig.getSpecialEventDiscount());
}
Long t2 = System.nanoTime();
System.out.println("Immutable:" + (t2 - t1));
}
private Order MockOrder() {
Order order = new Order();
order.setUser("user1");
order.setPrice(r.nextDouble() * 1000);
order.setCount(r.nextInt(10));
return order;
}
});
executor.execute(new Runnable() {
Random r = new Random();
@Override
public void run() {
while (true) {
//lock.writeLock().lock();
ImmutableAccountConfig oldImmutableAccountConfig =
immutableAccountConfigMap.get("user1");
Double membershipDiscount = r.nextInt(10) / 100.0;
Double specialEventDiscount =
oldImmutableAccountConfig.getSpecialEventDiscount();
ImmutableAccountConfig newImmutableAccountConfig =
new ImmutableAccountConfig(membershipDiscount,
specialEventDiscount);
immutableAccountConfigMap.put("user1", newImmutableAccountConfig);
//lock.writeLock().unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
(注:如果有多個寫進程,我們還是需要對他們加寫鎖,否則不同線程的配置信息修改會被相互覆蓋。而讀線程是不要加鎖的。)
結果:
ReadWriteLock:5289501171 Immutable :3599621120
測試結果表明,采用不可變對象的方式要比采用讀寫鎖的方式快很多。但是,並沒有數量級的差距。
真實的項目環境的性能差別,還要以實際的項目測試為准。因為不同項目,讀寫線程的個數,負載和使用方式都是不一樣的,得到的結果也會不一樣。
采用不可變對象方式,相比讀寫鎖的好處還有就是在設計上的 -- 由於不可變對象的特性,我們不必擔心項目組的程序員會錯誤的使用配置類: 讀進程不用加鎖,所以不用擔心在需要被加讀鎖的地方沒有合理的加鎖,導致數據不一致性(但如果是多進程寫,還是要非常注意加寫鎖);也不用擔心配置在不被預期的地方被任意修改。
我們不能簡單地說,在任何場景下采用Immutable對象就一定比采用讀寫鎖的方式好, 還取決於讀寫的頻率、Immutable對象更新的代價等因素。但是我們可以通過這個例子,更清楚的理解采用Immutable對象的好處,並認真地在項目中考慮它,因為有可能為效率和設計帶來很大的好處。
如果我們采用集合或者Map來保存不可變信息,我們可以采用google的不可變集合類庫(屬於Guava項目)。(JDK並沒有實現原生的不可變集合類庫)
http://mvnrepository.com/artifact/com.google.collections/google-collections/1.0
下面寫一些代碼示例一下:
public static void main(String[] args) throws Exception {
//創建ImmutableMap
ImmutableMap<String,Double> immutableMap = ImmutableMap.<String,Double>builder()
.put("SpecialEventDiscount", 0.01)
.put("MembershipDiscount", 0.02)
.build();
//基於原ImmutableMap生成新的更新的ImmutableMap
Map<String,Double> tempMap = Maps.newHashMap(immutableMap);
tempMap.put("MembershipDiscount", 0.03);
ImmutableMap<String,Double> newImmutableMap = ImmutableMap.<String,Double>builder()
.putAll(tempMap)
.build();
}
Binhua Liu原創文章,轉載請注明原地址http://www.cnblogs.com/Binhua-Liu/p/5573444.html