[英]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 方法可能是更容易理解。
如果您可以忍受偶爾的過度同步(即在不需要時按順序完成工作),請嘗試以下操作:
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);
}
}
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.