synchronized基礎用法
- synchronized可以用於修飾類的實例方法、靜態方法和代碼塊。它保護的是對象(包括類對象)而非代碼,只要訪問的是同一個對象的synchronized方法,即使是不同的代碼,也會被同步順序訪問。
- 每個對象有一個鎖(又叫監視器)和一個鎖等待隊列,鎖只能被一個線程持有,其他試圖獲得同樣鎖的線程需要等待,執行synchronized實例方法的過程大概如下:
- 嘗試獲得鎖,如果能夠獲得鎖,繼續下一步,否則加入鎖等待隊列,線程的狀態變為BLOCKED,阻塞並等待喚醒
- 執行被鎖住的方法或者代碼塊
- 釋放鎖,如果等待隊列上有等待的線程,從中取一個並喚醒,如果有多個等待的線程,喚醒哪一個是不一定的,不保證公平性
- 一般在保護變量時,需要在所有訪問該變量的方法上加上synchronized。
- 任何對象都可以作為synchronized鎖的對象。
理解synchronized
可重入性
可重入是指:對同一個執行線程,它在獲得了鎖之后,在調用其他需要同樣鎖的代碼時,可以直接調用。
可重入是通過記錄鎖的持有線程和持有數量來實現的,當調用被synchronized保護的代碼時,檢查對象是否已被鎖,如果是,再檢查是否被當前線程鎖定,如果是,增加持有數量,如果不是被當前線程鎖定,才加入等待隊列,當釋放鎖時,減少持有數量,當數量變為0時才釋放整個鎖。
內存可見性
除了保證原子操作外,synchronized還有一個重要的作用,就是保證內存可見性,在釋放鎖時,所有寫入都會寫回內存,而獲得鎖后,都會從內存中讀最新數據。
如果只是簡單地操作變量的話,可以用volatile修飾該變量,替代synchronized以減少成本。
加了volatile之后,Java會在操作對應變量時插入一個cpu指令(又叫內存柵欄),保證讀寫到內存最新值,而非緩存的值。
死鎖
死鎖就是類似這種現象,比如, 有a, b兩個線程,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A,a,b陷入了互相等待,最后誰都執行不下去。
避免死鎖的方案:
-
- 應該盡量避免在持有一個鎖的同時去申請另一個鎖,如果確實需要多個鎖,所有代碼都應該按照相同的順序去申請鎖。
- 使用顯式鎖接口Lock,它支持嘗試獲取鎖(tryLock)和帶時間限制的獲取鎖方法,使用這些方法可以在獲取不到鎖的時候釋放已經持有的鎖,然后再次嘗試獲取鎖或干脆放棄,以避免死鎖。
死鎖檢查工具:Java自帶的jstack命令
同步容器及其注意事項
同步容器
Collections類有一些方法,它們可以返回線程安全的同步容器,比如:
public static <T> Collection<T> synchronizedCollection(Collection<T> c) public static <T> List<T> synchronizedList(List<T> list) public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它們是給所有容器方法都加上synchronized來實現安全的。當多個線程並發訪問同一個容器對象時,不需要額外的同步操作,也不會出現錯誤的結果。
加了synchronized,所有方法調用變成了原子操作,但是也不是就絕對安全了,比如:
復合操作,比如先檢查再更新
例如:
public V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
假設map的每個方法都是安全的,但這個復合方法putIfAbsent是安全的嗎?顯然是否定的,這是一個檢查然后再更新的復合操作,在多線程的情況下,可能有多個線程都執行完了檢查這一步,都發現Map中沒有對應的鍵,然后就會都調用put,而這就破壞了putIfAbsent方法期望保持的語義。
偽同步,比如同步錯對象。
那給該方法加上synchronized就能實現安全嗎?如下所示:
public synchronized V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
答案是否定的!為什么呢?同步錯對象了。putIfAbsent同步鎖住的的是當前類的對象,如果該類還存在其他操作map的實例方法的話,那么它操作map時同步鎖住的是map,兩者是不同的對象。隨意要解決這個問題應該給map加鎖,如:
public V putIfAbsent(K key, V value){ synchronized(map){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; } }
迭代
對於同步容器對象,雖然單個操作是安全的,但迭代並不是。遍歷的同時容器如果發生了結構性變化,就會拋出ConcurrentModificationException異常,同步容器並沒有解決這個問題,如果要避免這個異常,需要在遍歷的時候給整個容器對象加鎖
並發容器
除了以上這些注意事項,同步容器的性能也是比較低的,當並發訪問量比較大的時候性能很差。所幸的是,Java中還有很多專為並發設計的容器類,比如:
-
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet