3 回答

TA貢獻1863條經驗 獲得超2個贊
我想知道僅在方法上同步是否線程安全
add
?contains
如果不同步,是否有任何可能的問題?
簡短回答:否和是。
有兩種解釋方式:
直觀的解釋
Java 同步(以其各種形式)防止許多事情,包括:
兩個線程同時更新共享狀態。
一個線程試圖讀取狀態,而另一個正在更新它。
線程看到過時的值,因為內存緩存尚未寫入主內存。
在您的示例中,同步add
就足以確保兩個線程不能同時更新HashSet
,并且兩個調用都將在最新HashSet
狀態下運行。
但是,如果contains
也不同步,則contains
調用可能會與調用同時發生add
。這可能導致contains
調用看到 的中間狀態HashSet
,從而導致不正確的結果,或者更糟。如果調用不是同時發生的,這也會發生,因為更改沒有立即刷新到主內存和/或讀取線程沒有從主內存讀取。
內存模型解釋
JLS 指定了 Java 內存模型,它規定了多線程應用程序必須滿足的條件,以保證一個線程可以看到另一個線程所做的內存更新。該模型是用數學語言表達的,并不容易理解,但要點是當且僅當從寫入到后續讀取之間存在一系列 happen before關系時,才能保證可見性。如果寫入和讀取在不同的線程中,那么線程之間的同步是這些關系的主要來源。例如在
// thread one
synchronized (sharedLock) {
sharedVariable = 42;
}
// thread two
synchronized (sharedLock) {
other = sharedVariable;
}
假設線程一的代碼在線程二的代碼之前運行,則線程一釋放鎖和線程二獲取鎖之間存在happens before關系。有了這個和“程序順序”的關系,我們就可以建立一個從寫入42到賦值到的鏈條other。這足以保證other將被分配42(或可能是變量的以后值)并且sharedVariable之前沒有任何值42被寫入它。
如果synchronized塊不在同一個鎖上同步,第二個線程可能會看到一個過時的值sharedVariable;即之前寫入的一些值42被分配給它。

TA貢獻1810條經驗 獲得超5個贊
該代碼對于該 synchronized (pSet) { }部分是線程安全的:
if (!pSet.contains(x)) {
synchronized (pSet) {
// Here you are sure to have the updated value of pSet
if (!pSet.contains(x)) {
// do some exclusive work with x.
pSet.add(x);
}
}
因為在synchronized對象的聲明中pSet:
一個且只有一個線程可能在這個塊中。
在其中,pSet它的更新狀態也由與 synchronized 關鍵字的 happens-before 關系保證。
因此,無論等待線程的第一個語句返回的值是什么if (!pSet.contains(x)),當這個被等待的線程醒來并進入語句時 synchronized,它都會設置最后更新的值pSet。因此,即使前一個線程添加了相同的元素,第二個線程 if (!pSet.contains(x))也會返回false。
但是這段代碼對于if (!pSet.contains(x))在寫入Set.
根據經驗,不應該使用未設計為線程安全的集合來執行并發的寫入和讀取操作,因為集合的內部狀態可能處于正在進行/不一致的狀態,以進行同時發生的讀取操作一個寫操作。
雖然一些非線程安全的集合實現在事實中接受了這樣的用法,但這根本不能保證它總是正確的。
所以你應該使用線程安全的Set實現來保證整個線程安全。
例如:
Set<String> pSet = ConcurrentHashMap.newKeySet();
這在引擎蓋下使用 a ConcurrentHashMap,因此沒有讀取鎖和最小的寫入鎖(僅在要修改的條目上而不是整個結構上)。

TA貢獻1982條經驗 獲得超2個贊
不,
您不知道在另一個線程添加期間哈希集可能處于什么狀態。可能正在進行根本性的更改,例如存儲桶的拆分,因此在另一個線程添加期間包含可能會返回false ,即使該元素將存在于單線程 HashSet 中。在那種情況下,您將嘗試第二次添加元素。
更糟糕的情況:由于兩個線程同時使用的內存中的 HashSet 處于臨時無效狀態,contains可能會陷入死循環或拋出異常。
添加回答
舉報