曾宪杰的《大型网站系统与Java中间件实践》第一章第1.2.2.3小节给出以下代码示例:
使用HashMap
数据被进行统计;
public class TestClass
{
private HashMap<String, Integer> map = new HashMap<>();
public synchronized void add(String key)
{
Integer value = map.get(key);
if(value == null)
{
map.put(key, 1);
}
else
{
map.put(key, value + 1);
}
}
}
使用ConcurrentHashMap
保存数据并进行统计;
public class TestClass
{
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void add(String key)
{
Integer value = map.get(key);
if(value == null)
{
map.put(key, 1);
}
else
{
map.put(key, value + 1);
}
}
}
使用HashMap时,对add方法加锁,此时该方法是线程安全的,为何换为ConcurrentHashMap之后,原书中说存在并发陷阱???
沒加鎖啊。
EDIT:
確實是存在並發陷阱。考慮一下這種情況:
執行緒A執行map.get(key);if(value == null)得到結果true, 然後交出cpu時間。
此時, 執行緒B也執行到同一個地方, 得到結果也為true, 因為執行緒A還沒執行map.put(key, 1), 執行緒B執行map.put(key, 1),此時map中已經有key的值了。
線程A得到CPU時間繼續執行, 因為之前判斷結果是true, 所以線程A又put了一次。最後的結果就是兩個執行緒都執行了一次map.put(key, 1), 此時key的值依舊為1, 但實際上應該是2。
存在這個問題是原因是, ConcurrentHashMap的單一操作是原子性的, 但是你外部呼叫並不是原子性的, map.get 和map.put 是兩個操作, 相互獨立的操作, 所以你如果要保證線程安全依舊需要在你的程式碼上加鎖, 保證get和put兩個操作的原子性。
ConcurrentHashMap只保證在並發情況下其內部資料能夠保持一致,而這一點HashMap是做不到的。
但是add方法並不是線程安全,因為這是典型的Check-Then-Act或Read-Modify-Write。
你可以思考這樣一個問題,如果一個類別裡的所有field都是線程安全的類,那麼這個類是否線程安全。答案顯然是否定的。