其實就是多個線程操做同一個資源,但動做不一樣。
示例:在某個數據庫中,Input輸入人的姓名,性別,Output輸出,兩個線程同時做用。
思考:1.明確哪些代碼是多線程操做的?2.明確共享數據。3.明確多線程代碼中哪些是共享數據的。
思考後發現,Input和Output類中的run方法對Res類的Field數據同時操做。故須要考慮使用同步。
同步前提:1.是多線程。2.必須是多個線程使用同一個鎖
惟一的鎖有:類字節碼文件(非靜態同步函數不推薦),資源對象rjava
class Res //共同處理的資源庫,包含兩個屬性 { String name; String sex; } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized (r) { if (x==0) { r.name="mike"; r.sex="male"; x=1; } else { r.name="莉莉"; r.sex="女女女"; x=0; } } } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { synchronized (r) { System.out.println(r.name+"————"+r.sex); } } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
觀察結果:
因爲輸入線程一直搶奪資源,致使輸出線程長時間屬於阻塞狀態。爲了使其達到輸入-輸出的行爲,考慮等待喚醒機制。數據庫
注意:如下三種方法使用時要求必須有監視器(鎖),所以必須使用在同步裏。須要標示他們所操做線程持有的鎖。等待和喚醒必須是同一個鎖。
-wait();將該線程載入線程池,等待喚醒。(該方法拋出異常,故須要配合try catch使用)
-notify();隨機喚醒線程池中一線程。
-notifyAll();喚醒線程池中全部線程。
代碼以下:數據結構
class Res //共同處理的資源庫 { String name; String sex; boolean flag = false; //標識位來表示和判斷已輸入or已輸出 } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized (r) { if (r.flag) //若是標識位爲真,說明已經輸入,此時關閉輸入,等待輸出 { try { r.wait();//wait配合try catch使用,且要標識鎖。 } catch (Exception e) { } } else //不然輸入數據,置標識位爲真並喚醒輸出。 { if (x==0) { r.name="mike"; r.sex="male"; x=1; } else { r.name="莉莉"; r.sex="女女女"; x=0; } r.flag = true; r.notify(); //喚醒輸出 } } } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { synchronized (r) { if (r.flag) //若是標識位爲真,則有數據等待輸出,此時取出數據後置標識位爲假,喚醒輸入 { System.out.println(r.name+"————"+r.sex); r.flag = false; r.notify(); } else //不然關閉輸出。等待輸入 try { r.wait(); } catch (Exception e) { } } } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
最後考慮到設計慣例,封裝數據和操做方法,優化後代碼以下(參考設計思路和設計慣例)多線程
class Res //共同處理的資源庫 { private String name; private String sex; private boolean flag = false; //標識位來表示和判斷已輸入or已輸出 public synchronized void set(String name,String sex) { if (flag) try { this.wait(); //非靜態同步函數的鎖爲this } catch (Exception e) { } this.name = name; this.sex = sex; flag = true; this.notify(); } public synchronized void out() { if (!flag) try { this.wait(); } catch (Exception e) { } System.out.println(name+"......."+sex); flag = false; this.notify(); } } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { if (x==0) r.set("mike","male"); else r.set("莉莉","女女女女"); x = (x+1)%2; } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { r.out(); } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); new Thread(new Input(r)).start(); //匿名對象,簡化代碼 new Thread(new Output(r)).start(); /* Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); */ } }
1.加鎖順序
當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。
若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:函數
Thread 1: lock A lock B Thread 2: wait for A lock C (when A locked) Thread 3: wait for A wait for B wait for C
若是一個線程(好比線程3)須要一些鎖,那麼它必須按照肯定的順序獲取鎖。它只有得到了從順序上排在前面的鎖以後,才能獲取後面的鎖。按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖,並對這些鎖作適當的排序,但總有些時候是沒法預知的。工具
2.加鎖時限
另一個能夠避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行。此外,若是有很是多的線程同一時間去競爭同一批資源,就算有超時和回退機制,仍是可能會致使這些線程重複地嘗試但卻始終得不到鎖,由於這些線程等待相等的重試時間的機率就高的多。
這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。你須要建立一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。寫一個自定義鎖類不復雜,但超出了本文的內容。優化
3.死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。this
每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。spa
當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,可是鎖7這個時候被線程B持有,這時線程A就能夠檢查一下線程B是否已經請求了線程A當前所持有的鎖。若是線程B確實有這樣的請求,那麼就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。線程
固然,死鎖通常要比兩個線程互相持有對方的鎖這種狀況要複雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A爲了檢測死鎖,它須要遞進地檢測全部被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,而後又找到了線程D,發現線程D請求的鎖被線程A本身持有着。這是它就知道發生了死鎖。
下面是一幅關於四個線程(A,B,C和D)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。
那麼當檢測出死鎖時,這些線程該作些什麼呢?
一個可行的作法是釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖,緣由同超時相似,不能從根本上減輕競爭。
一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。