本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml
上節咱們提到了多線程共享內存的兩個問題,一個是競態條件,另外一個是內存可見性,咱們提到,解決這兩個問題的一個方案是使用synchronized關鍵字,本節就來討論這個關鍵字。java
用法git
synchronized能夠用於修飾類的實例方法、靜態方法和代碼塊,咱們分別來看下。github
實例方法編程
上節咱們介紹了一個計數的例子,當多個線程併發執行counter++的時候,因爲該語句不是原子操做,出現了意料以外的結果,這個問題能夠用synchronized解決。swift
咱們來看代碼:緩存
public class Counter { private int count; public synchronized void incr(){ count ++; } public synchronized int getCount() { return count; } }
Counter是一個簡單的計數器類,incr方法和getCount方法都加了synchronized修飾。加了synchronized後,方法內的代碼就變成了原子操做,當多個線程併發更新同一個Counter對象的時候,也不會出現問題,咱們看使用的代碼:安全
public class CounterThread extends Thread { Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { try { Thread.sleep((int) (Math.random() * 10)); } catch (InterruptedException e) { } counter.incr(); } public static void main(String[] args) throws InterruptedException { int num = 100; Counter counter = new Counter(); Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { threads[i] = new CounterThread(counter); threads[i].start(); } for (int i = 0; i < num; i++) { threads[i].join(); } System.out.println(counter.getCount()); } }
與上節相似,咱們建立了100個線程,傳遞了相同的counter對象,每一個線程主要就是調用Counter的incr方法,main線程等待子線程結束後輸出counter的值,此次,不論運行多少次,結果都是正確的100。微信
這裏,synchronized到底作了什麼呢?看上去,synchronized使得同時只能有一個線程執行實例方法,但這個理解是不確切的。多個線程是能夠同時執行同一個synchronized實例方法的,只要它們訪問的對象是不一樣的,好比說:多線程
Counter counter1 = new Counter(); Counter counter2 = new Counter(); Thread t1 = new CounterThread(counter1); Thread t2 = new CounterThread(counter2); t1.start(); t2.start();
這裏,t1和t2兩個線程是能夠同時執行Counter的incr方法的,由於它們訪問的是不一樣的Counter對象,一個是counter1,另外一個是counter2。
因此,synchronized實例方法實際保護的是同一個對象的方法調用,確保同時只能有一個線程執行。再具體來講,synchronized實例方法保護的是當前實例對象,即this,this對象有一個鎖和一個等待隊列,鎖只能被一個線程持有,其餘試圖得到一樣鎖的線程須要等待,執行synchronized實例方法的過程大概以下:
synchronized的實際執行過程比這要複雜的多,並且Java虛擬機採用了多種優化方式以提升性能,但從概念上,咱們能夠這麼簡單理解。
當前線程不能得到鎖的時候,它會加入等待隊列等待,線程的狀態會變爲BLOCKED。
咱們再強調下,synchronized保護的是對象而非代碼,只要訪問的是同一個對象的synchronized方法,即便是不一樣的代碼,也會被同步順序訪問,好比,對於Counter中的兩個實例方法getCount和incr,對同一個Counter對象,一個線程執行getCount,另外一個執行incr,它們是不能同時執行的,會被synchronized同步順序執行。
此外,須要說明的,synchronized方法不能防止非synchronized方法被同時執行,好比,若是給Counter類增長一個非synchronized方法:
public void decr(){ count --; }
則該方法能夠和synchronized的incr方法同時執行,這一般會出現非指望的結果,因此,通常在保護變量時,須要在全部訪問該變量的方法上加上synchronized。
靜態方法
synchronized一樣能夠用於靜態方法,好比:
public class StaticCounter { private static int count = 0; public static synchronized void incr() { count++; } public static synchronized int getCount() { return count; } }
前面咱們說,synchronized保護的是對象,對實例方法,保護的是當前實例對象this,對靜態方法,保護的是哪一個對象呢?是類對象,這裏是StaticCounter.class,實際上,每一個對象都有一個鎖和一個等待隊列,類對象也不例外。
synchronized靜態方法和synchronized實例方法保護的是不一樣的對象,不一樣的兩個線程,能夠同時,一個執行synchronized靜態方法,另外一個執行synchronized實例方法。
代碼塊
除了用於修飾方法外,synchronized還能夠用於包裝代碼塊,好比對於前面的Counter類,等價的代碼能夠爲:
public class Counter { private int count; public void incr(){ synchronized(this){ count ++; } } public int getCount() { synchronized(this){ return count; } } }
synchronized括號裏面的就是保護的對象,對於實例方法,就是this,{}裏面是同步執行的代碼。
對於前面的StaticCounter類,等價的代碼爲:
public class StaticCounter { private static int count = 0; public static void incr() { synchronized(StaticCounter.class){ count++; } } public static int getCount() { synchronized(StaticCounter.class){ return count; } } }
synchronized同步的對象能夠是任意對象,任意對象都有一個鎖和等待隊列,或者說,任何對象均可以做爲鎖對象。好比說,Counter的等價代碼還能夠爲:
public class Counter { private int count; private Object lock = new Object(); public void incr(){ synchronized(lock){ count ++; } } public int getCount() { synchronized(lock){ return count; } } }
理解synchronized
介紹了synchronized的基本用法和原理,咱們再從下面幾個角度來進一步理解一下synchronized:
可重入性
synchronized有一個重要的特徵,它是可重入的,也就是說,對同一個執行線程,它在得到了鎖以後,在調用其餘須要一樣鎖的代碼時,能夠直接調用,好比說,在一個synchronized實例方法內,能夠直接調用其餘synchronized實例方法。可重入是一個很是天然的屬性,應該是很容易理解的,之因此強調,是由於並非全部鎖都是可重入的(後續章節介紹)。
可重入是經過記錄鎖的持有線程和持有數量來實現的,當調用被synchronized保護的代碼時,檢查對象是否已被鎖,若是是,再檢查是否被當前線程鎖定,若是是,增長持有數量,若是不是被當前線程鎖定,才加入等待隊列,當釋放鎖時,減小持有數量,當數量變爲0時才釋放整個鎖。
內存可見性
對於複雜一些的操做,synchronized能夠實現原子操做,避免出現競態條件,但對於明顯的原本就是原子的操做方法,也須要加synchronized嗎?好比說,對於下面的開關類Switcher,它只有一個boolean變量on和對應的setter/getter方法:
public class Switcher { private boolean on; public boolean isOn() { return on; } public void setOn(boolean on) { this.on = on; } }
當多線程同時訪問同一個Switcher對象時,會有問題嗎?沒有競態條件問題,但正如上節所說,有內存可見性問題,而加上synchronized能夠解決這個問題。
synchronized除了保證原子操做外,它還有一個重要的做用,就是保證內存可見性,在釋放鎖時,全部寫入都會寫回內存,而得到鎖後,都會從內存中讀最新數據。
不過,若是隻是爲了保證內存可見性,使用synchronzied的成本有點高,有一個更輕量級的方式,那就是給變量加修飾符volatile,以下所示:
public class Switcher { private volatile boolean on; public boolean isOn() { return on; } public void setOn(boolean on) { this.on = on; } }
加了volatile以後,Java會在操做對應變量時插入特殊的指令,保證讀寫到內存最新值,而非緩存的值。
死鎖
使用synchronized或者其餘鎖,要注意死鎖,所謂死鎖就是相似這種現象,好比, 有a, b兩個線程,a持有鎖A,在等待鎖B,而b持有鎖B,在等待鎖A,a,b陷入了互相等待,最後誰都執行不下去。示例代碼以下所示:
public class DeadLockDemo { private static Object lockA = new Object(); private static Object lockB = new Object(); private static void startThreadA() { Thread aThread = new Thread() { @Override public void run() { synchronized (lockA) { try { Thread.sleep(1000); } catch (InterruptedException e) { } synchronized (lockB) { } } } }; aThread.start(); } private static void startThreadB() { Thread bThread = new Thread() { @Override public void run() { synchronized (lockB) { try { Thread.sleep(1000); } catch (InterruptedException e) { } synchronized (lockA) { } } } }; bThread.start(); } public static void main(String[] args) { startThreadA(); startThreadB(); } }
運行後aThread和bThread陷入了相互等待。怎麼解決呢?首先,應該儘可能避免在持有一個鎖的同時去申請另外一個鎖,若是確實須要多個鎖,全部代碼都應該按照相同的順序去申請鎖,好比,對於上面的例子,能夠約定都先申請lockA,再申請lockB。
不過,在複雜的項目代碼中,這種約定可能難以作到。還有一種方法是使用後續章節介紹的顯式鎖接口Lock,它支持嘗試獲取鎖(tryLock)和帶時間限制的獲取鎖方法,使用這些方法能夠在獲取不到鎖的時候釋放已經持有的鎖,而後再次嘗試獲取鎖或乾脆放棄,以免死鎖。
若是仍是出現了死鎖,怎麼辦呢?Java不會主動處理,不過,藉助一些工具,咱們能夠發現運行中的死鎖,好比,Java自帶的jstack命令會報告發現的死鎖,對於上面的程序,在個人電腦上,jstack會有以下報告:
同步容器及其注意事項
同步容器
咱們在54節介紹過Collection的一些方法,它們能夠返回線程安全的同步容器,好比:
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來實現安全的,好比SynchronizedCollection,其部分代碼以下所示:
static class SynchronizedCollection<E> implements Collection<E> { final Collection<E> c; // Backing Collection final Object mutex; // Object on which to synchronize SynchronizedCollection(Collection<E> c) { if (c==null) throw new NullPointerException(); this.c = c; mutex = this; } public int size() { synchronized (mutex) {return c.size();} } public boolean add(E e) { synchronized (mutex) {return c.add(e);} } public boolean remove(Object o) { synchronized (mutex) {return c.remove(o);} } //.... }
這裏線程安全針對的是容器對象,指的是當多個線程併發訪問同一個容器對象時,不須要額外的同步操做,也不會出現錯誤的結果。
加了synchronized,全部方法調用變成了原子操做,客戶端在調用時,是否是就絕對安全了呢?不是的,至少有如下狀況須要注意:
複合操做
先來看複合操做,咱們看段代碼:
public class EnhancedMap <K, V> { Map<K, V> map; public EnhancedMap(Map<K,V> map){ this.map = Collections.synchronizedMap(map); } public V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; } public void put(K key, V value){ map.put(key, value); } //... 其餘代碼 }
EnhancedMap是一個裝飾類,接受一個Map對象,調用synchronizedMap轉換爲了同步容器對象map,增長了一個方法putIfAbsent,該方法只有在原Map中沒有對應鍵的時候才添加。
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同步使用的是EnhancedMap對象,而其餘方法(如代碼中的put方法)使用的是Collections.synchronizedMap返回的對象map,二者是不一樣的對象。要解決這個問題,全部方法必須使用相同的鎖,可使用EnhancedMap的對象鎖,也可使用map。使用EnhancedMap對象做爲鎖,則EnhancedMap中的全部方法都須要加上synchronized。使用map做爲鎖,putIfAbsent方法能夠改成:
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; } }
迭代
對於同步容器對象,雖然單個操做是安全的,但迭代並非。咱們看個例子,建立一個同步List對象,一個線程修改List,另外一個遍歷,看看會發生什麼,代碼爲:
private static void startModifyThread(final List<String> list) { Thread modifyThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { list.add("item " + i); try { Thread.sleep((int) (Math.random() * 10)); } catch (InterruptedException e) { } } } }); modifyThread.start(); } private static void startIteratorThread(final List<String> list) { Thread iteratorThread = new Thread(new Runnable() { @Override public void run() { while (true) { for (String str : list) { } } } }); iteratorThread.start(); } public static void main(String[] args) { final List<String> list = Collections .synchronizedList(new ArrayList<String>()); startIteratorThread(list); startModifyThread(list); }
運行該程序,程序拋出併發修改異常:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) at java.util.ArrayList$Itr.next(ArrayList.java:831)
咱們以前介紹過這個異常,若是在遍歷的同時容器發生告終構性變化,就會拋出該異常,同步容器並無解決這個問題,若是要避免這個異常,須要在遍歷的時候給整個容器對象加鎖,好比,上面的代碼,startIteratorThread能夠改成:
private static void startIteratorThread(final List<String> list) { Thread iteratorThread = new Thread(new Runnable() { @Override public void run() { while (true) { synchronized(list){ for (String str : list) { } } } } }); iteratorThread.start(); }
併發容器
除了以上這些注意事項,同步容器的性能也是比較低的,當併發訪問量比較大的時候性能不好。所幸的是,Java中還有不少專爲併發設計的容器類,好比:
這些容器類都是線程安全的,但都沒有使用synchronized、沒有迭代問題、直接支持一些複合操做、性能也高得多,它們能解決什麼問題?怎麼使用?實現原理是什麼?咱們留待後續章節介紹。
小結
本節詳細介紹了synchronized的用法和實現原理,爲進一步理解synchronized,介紹了可重入性、內存可見性、死鎖等,最後,介紹了同步容器及其注意事項如複合操做、僞同步、迭代異常、併發容器等。
多線程之間除了競爭訪問同一個資源外,也常常須要相互協做,怎麼協做呢?下節介紹協做的基本機制wait/notify。
(與其餘章節同樣,本節全部代碼位於 https://github.com/swiftma/program-logic)
----------------
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。