另載於 http://www.qingjingjie.com/blogs/10
概念
不可變物件(Immutable Object),就是狀態始終不會改變的物件,例如值物件(Value Object),無狀態的服務物件(Stateless Service Object)。
Java和Scala都是JVM語言,都經常用synchronized
來做同步。本文以Java為例,Scala同理。
先重溫一下synchronized
的知識:指定了一個同步範圍,進出範圍重新整理變數,並阻止其他執行緒進入該範圍。synchronized method的範圍是this,synchronized static method的範圍是class,也可顯式指定一個物件作為範圍。
synchronized(object) {
...
}
同步範圍是作用於物件的,任何物件都含有一個隱藏的鎖狀態,JVM把它置為鎖態,就加上了當前執行緒獨佔的鎖。
分析
從物件導向程式設計來看,鎖狀態不應視為不可變物件的一部分,如果對它做同步,就是把鎖狀態視為它的一部分了,破壞了該物件的設計抽象。
從併發程式設計來看,不可變的物件被設計為允許多執行緒自由共享,不引起競爭。然而如果對它做同步,就會引起多執行緒競爭,違反了設計目的。
一般沒人會對值物件做同步,但可能有人會誤對無狀態的服務物件做同步。(牛人也可能有失誤)
我們來看個反面例子:
// UserService is singleton
public class UserService {
// 修改資料庫中的使用者資訊
public synchronized User changeName(Long id, String name) {
User user = UserRepo.get(id);
user.setName(name);
UserRepo.merge(user);
return user;
}
}
通過資料庫的事務隔離,能保證user從取出來到存回去之間不被別的執行緒修改。
但是NoSQL沒有事務,怎麼辦?NoSQL使用者可能會用synchronized
,這就使得changeName同時只能被一個執行緒調,網站扛不住併發。
考慮到不同使用者的資料可以同時修改,可以給每個使用者單獨上鎖,以提高併發度:
// UserService is singleton
public class UserService {
private Map<Long, Object> userLocks = new ConcurrentHashMap<>();
// 修改資料庫中的使用者資訊
public synchronized User changeName(Long id, String name) {
// 獲取鎖
Object lock = new Object();
Object prevLock = userLocks.putIfAbsent(id, lock);
if (prevLock != null) {
lock = prevLock;
}
synchronized (lock) {
try {
User user = UserRepo.get(id);
user.setName(name);
UserRepo.merge(user);
return user;
} finally {
// 防止太多空閒的鎖佔用記憶體
userLocks.remove(id);
}
}
}
}
玩玩而已,這麼複雜的程式碼,我覺得產品裡還是不寫為好。
況且,在叢集環境中,這種單機同步是沒用的。
附:JDK也有類似的併發優化,見我的舊文 http://www.cnblogs.com/sorra/p/3653951.html