Java多執行緒-----執行緒安全及解決機制
1.什麼是執行緒安全問題?
從某個執行緒開始訪問到訪問結束的整個過程,如果有一個訪問物件被其他執行緒修改,那麼對於當前執行緒而言就發生了執行緒安全問題;
如果在整個訪問過程中,無一物件被其他執行緒修改,就是執行緒安全的,即存在兩個或者兩個以上的執行緒物件共享同一個資源
2.執行緒安全問題產生的根本原因
首先是多執行緒環境,即同時存在有多個操作者,單執行緒環境不存線上程安全問題。在單執行緒環境下,任何操作包括修改操作都是操作者自己發出的,
操作者發出操作時不僅有明確的目的,而且意識到操作的影響。
多個操作者(執行緒)必須操作同一個物件,只有多個操作者同時操作一個物件,行為的影響才能立即傳遞到其他操作者。
多個操作者(執行緒)對同一物件的操作必須包含修改操作,共同讀取不存線上程安全問題,因為物件不被修改,未發生變化,不能產生影響。
綜上可知,執行緒安全問題產生的根本原因是共享資料存在被併發修改的可能,即一個執行緒讀取時,允許另一個執行緒修改
3.有執行緒安全的例項
模擬火車站售票視窗,開啟三個視窗售票,總票數為20張
例項一:
package com.practise.threadsafe;//模擬火車站售票視窗,開啟三個視窗售票,總票數為100張 //存線上程的安全問題 class Window extends Thread { static int ticket = 20; public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "售票,票號為:" + ticket--); } else { break; } } } } public class TestWindow { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("視窗1"); w2.setName("視窗2"); w3.setName("視窗3"); w1.start(); w2.start(); w3.start(); } }
執行結果的一種:出現重複售票及負數票
視窗3售票,票號為:20
視窗2售票,票號為:18
視窗1售票,票號為:19
視窗1售票,票號為:17
視窗3售票,票號為:16
視窗2售票,票號為:17
視窗1售票,票號為:15
視窗3售票,票號為:14
視窗2售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗1售票,票號為:10
視窗3售票,票號為:8
視窗2售票,票號為:9
視窗1售票,票號為:7
視窗1售票,票號為:6
視窗2售票,票號為:6
視窗3售票,票號為:5
視窗1售票,票號為:4
視窗3售票,票號為:4
視窗2售票,票號為:3
視窗2售票,票號為:2
視窗1售票,票號為:2
視窗3售票,票號為:2
視窗2售票,票號為:1
視窗1售票,票號為:-1
視窗3售票,票號為:0
例項二:
package com.practise.threadsafe; //使用實現Runnable介面的方式,售票 /* * 此程式存線上程的安全問題 */ class Window1 implements Runnable { int ticket = 20; public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--); } else { break; } } } } public class TestWindow1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("視窗1"); t2.setName("視窗2"); t3.setName("視窗3"); t1.start(); t2.start(); t3.start(); } }
執行結果的一種:出現重複售票
視窗2售票,票號為:20
視窗1售票,票號為:20
視窗3售票,票號為:20
視窗1售票,票號為:19
視窗3售票,票號為:18
視窗2售票,票號為:17
視窗2售票,票號為:16
視窗3售票,票號為:14
視窗1售票,票號為:15
視窗1售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗2售票,票號為:10
視窗1售票,票號為:10
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗3售票,票號為:7
視窗2售票,票號為:8
視窗2售票,票號為:6
視窗3售票,票號為:4
視窗1售票,票號為:5
視窗3售票,票號為:3
視窗1售票,票號為:3
視窗2售票,票號為:3
視窗1售票,票號為:2
視窗3售票,票號為:0
視窗2售票,票號為:1
4.執行緒安全解決機制Lock和synchronized
4.1 同步程式碼塊synchronized
package com.practise.threadsafe; /* 同步程式碼塊 * synchronized(同步監視器){ * //需要被同步的程式碼塊(即為操作共享資料的程式碼) * } * 1.共享資料:多個執行緒共同操作的同一個資料(變數) * 2.同步監視器:由一個類的物件來充當。哪個執行緒獲取此監視器,誰就執行大括號裡被同步的程式碼。俗稱:鎖 * 要求:所有的執行緒必須共用同一把鎖! * 注:在實現的方式中,考慮同步的話,可以使用this來充當鎖。但是在繼承的方式中,慎用this! */ class Window implements Runnable { int ticket = 20;// 共享資料 public void run() { while (true) { // this表示當前物件,本題中即為w synchronized (this) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--); } } } } } public class TestWindow { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("視窗1"); t2.setName("視窗2"); t3.setName("視窗3"); t1.start(); t2.start(); t3.start(); } }
執行結果的一種:
視窗1售票,票號為:20
視窗3售票,票號為:19
視窗3售票,票號為:18
視窗2售票,票號為:17
視窗2售票,票號為:16
視窗2售票,票號為:15
視窗2售票,票號為:14
視窗2售票,票號為:13
視窗2售票,票號為:12
視窗3售票,票號為:11
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗1售票,票號為:8
視窗3售票,票號為:7
視窗2售票,票號為:6
視窗2售票,票號為:5
視窗3售票,票號為:4
視窗3售票,票號為:3
視窗3售票,票號為:2
視窗3售票,票號為:1
4.2 同步方法synchronized
package com.practise.threadsafe; /* * 同步方法 * 將操作共享資料的方法宣告為synchronized。即此方法為同步方法,能夠保證當其中一個執行緒執行 * 此方法時,其它執行緒在外等待直至此執行緒執行完此方法 */ class Window1 implements Runnable { int ticket = 20;// 共享資料 public void run() { while (true) { show(); } } public synchronized void show() { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--); } } } public class TestWindow1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("視窗1"); t2.setName("視窗2"); t3.setName("視窗3"); t1.start(); t2.start(); t3.start(); } }
執行結果的一種:
視窗2售票,票號為:20
視窗1售票,票號為:19
視窗3售票,票號為:18
視窗1售票,票號為:17
視窗2售票,票號為:16
視窗1售票,票號為:15
視窗3售票,票號為:14
視窗1售票,票號為:13
視窗2售票,票號為:12
視窗1售票,票號為:11
視窗3售票,票號為:10
視窗1售票,票號為:9
視窗2售票,票號為:8
視窗1售票,票號為:7
視窗3售票,票號為:6
視窗3售票,票號為:5
視窗1售票,票號為:4
視窗2售票,票號為:3
視窗1售票,票號為:2
視窗3售票,票號為:1
4.3 同步鎖Lock
package com.practise.threadsafe; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Window2 implements Runnable { int ticket = 20;// 共享資料 Lock lock = new ReentrantLock(); public void run() { while (true) { lock.lock(); // 獲取鎖 try { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票號為:" + ticket--); } } finally { lock.unlock(); // 釋放鎖 } } } } public class TestWindow2 { public static void main(String[] args) { Window2 w = new Window2(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("視窗1"); t2.setName("視窗2"); t3.setName("視窗3"); t1.start(); t2.start(); t3.start(); } }
執行結果的一種:
視窗3售票,票號為:20
視窗3售票,票號為:19
視窗3售票,票號為:18
視窗3售票,票號為:17
視窗3售票,票號為:16
視窗3售票,票號為:15
視窗3售票,票號為:14
視窗3售票,票號為:13
視窗3售票,票號為:12
視窗1售票,票號為:11
視窗2售票,票號為:10
視窗3售票,票號為:9
視窗3售票,票號為:8
視窗3售票,票號為:7
視窗3售票,票號為:6
視窗3售票,票號為:5
視窗3售票,票號為:4
視窗3售票,票號為:3
視窗3售票,票號為:2
視窗1售票,票號為:1
5.synchronized 的侷限性 與 Lock 的優點
如果一個程式碼塊被synchronized關鍵字修飾,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待直至佔有鎖的執行緒釋放鎖。事實上,佔有鎖的執行緒釋放鎖一般會是以下三種情況之一:
- 佔有鎖的執行緒執行完了該程式碼塊,然後釋放對鎖的佔有;
- 佔有鎖執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖;
- 佔有鎖執行緒進入 WAITING 狀態從而釋放鎖,例如在該執行緒中呼叫wait()方法等。
synchronized 是Java語言的內建特性,可以輕鬆實現對臨界資源的同步互斥訪問。那麼,為什麼還會出現Lock呢?試考慮以下三種情況:
Case 1 :
在使用synchronized關鍵字的情形下,假如佔有鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,那麼其他執行緒就只能一直等待,別無他法。這會極大影響程式執行效率。因此,就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。
Case 2 :
我們知道,當多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作也會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是如果採用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個執行緒都只是進行讀操作時,也只有一個執行緒在可以進行讀操作,其他執行緒只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個執行緒都只是進行讀操作時,執行緒之間不會發生衝突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。
Case 3 :
我們可以通過Lock得知執行緒有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。
上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。事實上,Lock 是 java.util.concurrent.locks包 下的介面,Lock 實現提供了比 synchronized 關鍵字 更廣泛的鎖操作,它能以更優雅的方式處理執行緒同步問題。也就是說,Lock提供了比synchronized更多的功能。但是要注意以下幾點:
- 1)synchronized是Java的關鍵字,因此是Java的內建特性,是基於JVM層面實現的。而Lock是一個Java介面,是基於JDK層面實現的,通過這個介面可以實現同步訪問;
- 2)採用synchronized方式不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而 Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致死鎖現象
6.Lock和synchronized的選擇
總結來說,Lock和synchronized有以下幾點不同:
- 1)Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現
- 2)synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖
- 3)Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷
- 4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到
- 5)Lock可以提高多個執行緒進行讀操作的效率
在效能上來說,如果競爭資源不激烈,兩者的效能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇