勿對不可變物件做同步/加鎖

sorra發表於2019-02-16

另載於 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

相關文章