簡體   English   中英

Java 線程鎖定在特定的 object

[英]Java threads locking on a specific object

我有一個 web 應用程序,我正在使用 Oracle 數據庫,我有一個基本上像這樣的方法:

public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
     }
     // Have to do a lot other saving stuff, because it either saves everything or nothing
     commit() // pseudo code to actually commit all my changes to the database.
}

現在沒有任何同步,所以n個線程當然可以自由訪問這個方法,當2個線程進入這個方法時出現問題,同時檢查當然還沒有任何東西,然后他們都可以提交事務,創建一個重復的 object。

我不想用我的數據庫中的唯一鍵標識符來解決這個問題,因為我認為我不應該捕獲那個SQLException

我也不能在提交之前檢查,因為不僅有幾個檢查1 ,這將花費相當多的時間。

我對鎖和線程的經驗是有限的,但我的想法基本上是將這段代碼鎖定在它正在接收的 object 上。 I don't know if for example say I receive an Integer Object, and I lock on my Integer with value 1, would that only prevent threads with another Integer with value 1 from entering, and all the other threads with value != 1 can自由進入?,這是它的工作原理嗎?

另外,如果這就是它的工作原理,那么鎖 object 比較如何? 如何確定它們實際上是相同的 object? 一篇關於這方面的好文章也將不勝感激。

你會如何解決這個問題?

你的想法很好。 這是簡單/幼稚的版本,但不太可能起作用:

public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    synchronized (theObjectIwantToSave) {
        if (!methodThatChecksThatObjectAlreadyExists) {
            storemyObject() //pseudo code
        }
        // Have to do a lot other saving stuff, because it either saves everything or nothing
        commit() // pseudo code to actually commit all my changes to the database.
    }
}

此代碼使用 object 本身作為鎖。 但它必須是相同的 object(即 objectInThreadA == objectInThreadB)才能工作。 如果兩個線程在 object 上運行,該 object 是彼此的副本- 例如,具有相同的“id”,那么您需要同步整個方法:

    public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) ...

這當然會大大降低並發性(使用該方法的吞吐量將一次下降到一個線程 - 應避免)。

或者在保存object的基礎上,想辦法得到同樣的鎖object,像這樣的做法:

private static final ConcurrentHashMap<Object, Object> LOCKS = new ConcurrentHashMap<Object, Object>();
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    synchronized (LOCKS.putIfAbsent(theObjectIwantToSave.getId(), new Object())) {
        ....    
    }
    LOCKS.remove(theObjectIwantToSave.getId()); // Clean up lock object to stop memory leak
}

最后一個版本是推薦的版本:它將確保共享相同“id”的兩個保存對象被同一個鎖ConcurrentHashMap.putIfAbsent()只有那個objectInThreadA.getId().equals(objectInThreadB.getId())才能正常工作。 此外,getId() 的數據類型可以是任何東西,包括由於 java 的autoboxing而導致的原語(例如int )。

如果您為 object 覆蓋equals()hashcode() ,那么您可以使用 object 本身而不是object.getId() ,這將是一個改進(感謝@TheCapn)

此解決方案僅適用於一個 JVM。 如果您的服務器是集群的,那么完全不同的球賽和 java 的鎖定機制將無濟於事。 您必須使用集群鎖定解決方案,該解決方案超出了此答案的 scope。

這是一個改編自 And360 對 Bohemian 的回答的評論的選項,它試圖避免競爭條件等。雖然我更喜歡我對這個問題的其他回答而不是這個問題:

import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;

// it is no advantage of using ConcurrentHashMap, since we synchronize access to it
// (we need to in order to "get" the lock and increment/decrement it safely)
// AtomicInteger is just a mutable int value holder
// we don't actually need it to be atomic
static final HashMap<Object, AtomicInteger> locks = new HashMap<Integer, AtomicInteger>();

public static void saveSomethingImportantToDataBase(Object objectToSave) {
    AtomicInteger lock;
    synchronized (locks) {
        lock = locks.get(objectToSave.getId());
        if (lock == null) {
            lock = new AtomicInteger(1);
            locks.put(objectToSave.getId(), lock);
        }
        else 
          lock.incrementAndGet();
    }
    try {
        synchronized (lock) {
            // do synchronized work here (synchronized by objectToSave's id)
        }
    } finally {
        synchronized (locks) {
            lock.decrementAndGet();
            if (lock.get() == 0)  
              locks.remove(id);
        }
    }
}

您可以將它們拆分為輔助方法“獲取鎖定對象”和“釋放鎖定”或其他方法,以清理代碼。 這種方式比我的其他答案感覺更笨拙。

如果一個線程在同步部分中,而另一個線程從 Map 等中刪除同步對象,則 Bohemian 的答案似乎存在競爭條件問題。所以這里有一個利用 WeakRef 的替代方案。

// there is no synchronized weak hash map, apparently
// and Collections.synchronizedMap has no putIfAbsent method, so we use synchronized(locks) down below

WeakHashMap<Integer, Integer> locks = new WeakHashMap<>(); 

public void saveSomethingImportantToDataBase(DatabaseObject objectToSave) {
  Integer lock;
  synchronized (locks) {
    lock = locks.get(objectToSave.getId());
    if (lock == null) {
      lock = new Integer(objectToSave.getId());
      locks.put(lock, lock);
    }
  }
  synchronized (lock) {
    // synchronized work here (synchronized by objectToSave's id)
  }
  // no releasing needed, weakref does that for us, we're done!
}

以及如何使用上述樣式系統的更具體示例:

static WeakHashMap<Integer, Integer> locks = new WeakHashMap<>(); 

static Object getSyncObjectForId(int id) {
  synchronized (locks) {
    Integer lock = locks.get(id);
    if (lock == null) {
      lock = new Integer(id);
      locks.put(lock, lock);
    }
    return lock;
  }
}

然后像這樣在其他地方使用它:

...
  synchronized (getSyncObjectForId(id)) {
    // synchronized work here
  }
...

這樣做的原因基本上是,如果兩個具有匹配鍵的對象進入關鍵塊,第二個對象將檢索第一個對象已經使用的鎖(或留下的尚未被 GC 處理的鎖)。 但是,如果它未被使用,則兩者都會留下該方法並刪除對鎖 object 的引用,因此可以安全地收集它。

如果您想要使用的同步點的“已知大小”有限(最終不必減小大小),您可能可以避免使用 HashMap 並改用 ConcurrentHashMap ,其 putIfAbsent 方法可能是更容易理解。

如果您可以忍受偶爾的過度同步(即在不需要時按順序完成工作),請嘗試以下操作:

  1. 創建一個帶有鎖對象的表。 表越大,過度同步的機會就越少。
  2. 將一些散列 function 應用於您的 id 以計算表索引。 如果你的 id 是數字,你可以只使用余數(模)function,如果它是一個字符串,使用 hashCode() 和一個余數。
  3. 從表中獲取鎖並對其進行同步。

IdLock class:

public class IdLock {

private Object[] locks = new Object[10000];

public IdLock() {
  for (int i = 0; i < locks.length; i++) {
    locks[i] = new Object();
  }
}

public Object getLock(int id) {
  int index = id % locks.length;
  return locks[index];
}

}

及其用途:

private idLock = new IdLock();

public void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
  synchronized (idLock.getLock(theObjectIwantToSave.getId())) {
    // synchronized work here
  }
}

我的觀點是你並沒有在真正的線程問題上掙扎。

您最好讓 DBMS 自動分配一個不沖突的行 ID。

如果您需要使用現有的行 ID,請將它們存儲為線程局部變量。 如果不需要共享數據,請不要在線程之間共享數據。

http://download.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html

當應用程序服務器或 web 容器時,Oracle dbms 在保持數據一致性方面要好得多。

“許多數據庫系統在插入行時會自動生成唯一的鍵字段。Oracle 數據庫借助序列和觸發器提供相同的功能。JDBC 3.0 引入了自動生成鍵的檢索功能,使您能夠檢索此類生成的值。在 JDBC 3.0 中,增強了以下接口以支持檢索自動生成的密鑰功能...."

http://download.oracle.com/docs/cd/B19306_01/java.102/b14355/jdbcvers.htm#CHDEGDHJ

private static final Set<Object> lockedObjects = new HashSet<>();

private void lockObject(Object dbObject) throws InterruptedException {
    synchronized (lockedObjects) {
        while (!lockedObjects.add(dbObject)) {
            lockedObjects.wait();
        }
    }
}

private void unlockObject(Object dbObject) {
    synchronized (lockedObjects) {
        lockedObjects.remove(dbObject);
        lockedObjects.notifyAll();
    }
}

public void saveSomethingImportantToDatabase(Object theObjectIwantToSave) throws InterruptedException {
    try {
        lockObject(theObjectIwantToSave);

        if (!methodThatChecksThatObjectAlreadyExists(theObjectIwantToSave)) {
            storeMyObject(theObjectIwantToSave);
        }
        commit();
    } finally {
        unlockObject(theObjectIwantToSave);
    }
}
  • 您必須正確覆蓋對象類的方法“equals”“hashCode” 如果您在 object 中有唯一的id (字符串或數字),那么您只需檢查此 id 而不是整個 object 並且無需覆蓋“equals”和“hashCode”。
  • try-finally - 非常重要 - 即使您的操作拋出異常,您也必須保證在操作后解鎖等待線程。
  • 如果您的后端分布在多個服務器上,這種方法將不起作用。
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
  synchronized (theObjectIwantToSave) {

      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
      }
 // Have to do a lot other saving stuff, because it either saves everything or nothing
      commit() // pseudo code to actually commit all my changes to the database.
  }
}

synchronized 關鍵字會鎖定您想要的 object,這樣其他方法都無法訪問它。

我認為您別無選擇,只能采取您似乎不想做的解決方案之一。

在您的情況下,我認為 objectYouWantToSave 上的任何類型的同步都不會起作用,因為它們基於 web 請求。 因此,每個請求(在它自己的線程上)很可能有它自己的 object 實例。 即使它們在邏輯上可能被認為是相等的,但這對於同步並不重要。

synchronized 關鍵字(或其他同步操作)是必須的,但不足以解決您的問題。 您應該使用數據結構來存儲使用了哪些 integer 值。 在我們的示例中使用了 HashSet。 不要忘記從哈希集中清理太舊的記錄。

private static HashSet <Integer>isUsed= new HashSet <Integer>();

public synchronized static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {

      if(isUsed.contains(theObjectIwantToSave.your_integer_value) != null) {

      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
      }
 // Have to do a lot other saving stuff, because it either saves everything or nothing
      commit() // pseudo code to actually commit all my changes to the database.
      isUsed.add(theObjectIwantToSave.your_integer_value);

  }
}

要回答有關鎖定 Integer 的問題,簡短的回答是否定的 - 它不會阻止具有相同值的另一個 Integer 實例的線程進入。 長答案:取決於您如何獲得 Integer - 通過構造函數,通過重用某些實例或通過 valueOf(使用一些緩存)。 無論如何,我不會依賴它。

一個可行的解決方案是使方法同步:

public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    if (!methodThatChecksThatObjectAlreadyExists) {
        storemyObject() //pseudo code
    }
    // Have to do a lot other saving stuff, because it either saves everything or nothing
    commit() // pseudo code to actually commit all my changes to the database.
}

這在性能方面可能不是最好的解決方案,但它可以保證工作(注意,如果您不在集群環境中),直到您找到更好的解決方案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM