線程不安全的問題分析:在小朋友搶氣球的案例中模擬網絡延遲來將問題暴露出來;示例代碼以下:java
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小紅").start(); new Thread(balloon, "小強").start(); new Thread(balloon, "小明").start(); } } // 氣球 class Balloon extends Thread { private int num = 50; @Override public void run() { for (int i = 0; i < 50; i++) { if (num > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "搶到了" + (num--) + "號氣球") } } } }
在線程中的run
方法上不能使用throws
來聲明拋出異常,因此在run
方法中調用有可能出現異常的代碼時,只能使用try-catch
將其捕獲來處理。算法
緣由是:子類覆蓋父類方法時不能拋出新的異常,父類的run
方法都沒有拋出異常,子類就更加不能拋出異常了。詳情可查看個人另外一篇文章 「JAVA」運行時異常、編譯時異常、自定義異常,經過案例實踐轉譯和異常鏈
)編程
在上述案例中,經過引入Thread.sleep();
來模擬網絡延遲,該方法的做用是讓當前線程進入睡眠狀態10
毫秒,此時其餘線程就能夠去搶佔資源了,方法的參數是睡眠時間,以毫秒爲單位。緩存
經過觀察運行結果,發現了問題:安全
在運行結果中,小紅、小強兩個小朋友都搶到了14
號氣球,也就是14
號氣球被搶到了2
次。咱們來梳理線程的運行過程來看看發生了什麼:網絡
14
號氣球,因爲線程調度,小強得到了CPU
時間片,打印出了搶到的氣球,而小紅則進入睡眠;小強在打印後對num
作了減一操做,此時num
爲13
;13
號氣球,並對num
作了減一操做,此時num
爲12
;14
號氣球;此時的num
爲12
,減一後結果爲11
;num
作判斷時可能上一個線程還未對num
減一,故都能經過(num > 0
)的判斷;而後再來運行上述代碼,得出以下的結果:多線程
運行結果中出現了本不應出現的0
和-1
,由於按照正常邏輯,氣球數量到1
以後就不該該被打印和減一了。出現這樣的結果是由於出現瞭如下的執行步驟:併發
1
號氣球,因爲線程調度,小強獲取了cpu
時間片,得以執行,而小明和小紅則進入睡眠;小強打印出結果後,對num
減一,此時num
爲0
;num
爲0
,而後小明將num
打印出來,再對num
減一,此時num
爲-1
;num
爲-1
,隨後小紅將num
打印出來,再對num
減一,此時怒木爲-2
;num
作判斷時可能上一個線程還未對num
減一,故都能經過(num > 0
)的判斷;解決方案:ide
在案例中的搶氣球實際上是兩步操做:先搶到氣球,再對氣球總數減一;既然是兩步操做,在併發中就徹底有可能會被分開執行,且執行順序沒法獲得控制;性能
想要解決上述的線程不安全的問題,就必需要將這兩步操做做爲一個原子操做,保證其同步運行;也就是當一個線程A
進入操做的時候,其餘線程只能在操做外等待,只有當線程A
執行完畢,其餘線程纔能有機會進入操做。
原子操做:不能被分割的操做,必須保證其從一而終徹底執行,要麼都執行,要麼都不執行。
爲解決多線程併發訪問同一個資源的安全性問題,Java
提供以下了幾種不一樣的同步機制:
Lock
鎖機制;同步代碼塊: 爲了保證線程可以正常執行原子操做,Java
引入了線程同步機制,其語法以下:
synchronized (同步鎖) { // 須要同步操做的代碼 ... ... }
上述中同步鎖,又稱同步監聽對象、同步監聽器、互斥鎖,同步鎖是一個抽象概念,能夠理解爲在對象上標記了一把鎖;
Java
中可使用任何對象做爲同步監聽對象,但在項目開發中,咱們會把當前併發訪問的共享資源對象做爲同步監聽對象,在任什麼時候候,最多隻能運行一個線程擁有同步鎖。
衛生間的使用就是一個很好的例子,一個衛生間在一段時間內只能被一我的使用,當一我的進入衛生間後,衛生間會被上鎖,其餘只能等待;只有當使用衛生間的人使用完畢,開鎖後才能被下一我的使用。
而後就可使用同步代碼塊來改寫搶氣球案例,示例代碼以下:
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小紅").start(); new Thread(balloon, "小強").start(); new Thread(balloon, "小明").start(); } } // 氣球 class Balloon implements Runnable { private int num = 500; @Override public void run() { for (int i = 0; i < 500; i++) { synchronized (this) { if (num > 0) { System.out.println(Thread.currentThread().getName() + "搶到了" + num + "號氣球"); num--; } } } } }
經過查看運行結果,線程同步的問題已經獲得解決。
同步方法: 使用synchronized
修飾的方法稱爲同步方法,可以保證當一個線程進入該方法的時候,其餘線程在方法外等待。好比:
public synchronized void doSomething() { // 方法邏輯 }
PS:方法修飾符不分前後順序。
使用同步方法來改寫搶氣球案例,代碼以下:
public class ImplementsDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小紅").start(); new Thread(balloon, "小強").start(); new Thread(balloon, "小明").start(); } } // 氣球 class Balloon implements Runnable { private int num = 500; @Override public void run() { for (int i = 0; i < 500; i++) { grabBalloon(); } } // 搶氣球 private synchronized void grabBalloon() { if (num > 0) { System.out.println(Thread.currentThread().getName() + "搶到了" + num + "號氣球"); num--; } } }
注意:不能使用synchronized
修改線程類中的run
方法,由於使用以後,就會出現一個線程執行完了全部功能,多個線程出現串行;本來是多行道,使用synchronized
修改線程類中的run
方法,多行道變成了單行道。
好:synchronized
保證了併發訪問時的同步操做,避免了線程的安全性問題。
壞:使用synchronized
的方法、代碼塊的性能會比不用要低一些。
StringBuilder和StringBuffer
StringBuilder
和StringBuffer
區別就在於StringBuffer
中的方法都使用了synchronized
修飾,StringBuilder
中的方法沒有使用synchronized
修飾;這也是StringBuilder
性能比StringBuffer
高的主要緣由。
Vector和ArrayList
二者都有一樣的方法,有一樣的實現算法,惟一不一樣就是Vector
中的方法使用了synchronized
修飾,因此Vector
的性能要比ArrayList
低。
Hashtable和HashMap
二者都有一樣的方法,有一樣的實現算法,惟一不一樣就是Hashtable
中的方法使用了synchronized
修飾,因此Hashtable
的性能要比HashMap
低。
volatile
關鍵字的做用在於:被volatile
關鍵字修飾的變量的值,將不會被本地線程緩存,全部對該變量的讀寫都是直接操做共享內存,從而能夠確保多個線程能正確處理該變量。
須要注意的是,volatile
關鍵字可能會屏蔽虛擬機中的一些必要的優化操做,因此運行效率不是很高,所以,沒有特別的須要,不要使用;即使使用,也要避免大量使用。
單例模式--餓漢模式
代碼以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { instance = new SlackerDemo(); } return instance; } }
單例模式--懶漢模式
代碼以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { instance = new SlackerDemo(); } return instance; } }
懶漢模式存在線程不安全問題,在對instance
對象作判斷時因爲併發致使出現和搶氣球案例同樣的問題。爲了解決這個問題,使用雙重檢查加鎖機制來解決。
雙重檢查加鎖機制
使用「雙重檢查加鎖」機制實現的程序,既能實現線程安全,有可以使性能不受較大的影響。那麼何謂「雙重檢查加鎖」機制?其指的是:
getInstance
方法都須要同步,而是先不一樣步,進入方法後,先檢查實例是否存在,若是不存在才執行同步代碼塊,這是**第一重檢查;這樣,就只須要同步一次,減小了屢次在同步狀況判斷所浪費的時間。
「雙重檢查加鎖」機制的實現須要volatile
關鍵字的配合使用,且Java
版本須要在Java 5
及以上,雖然該機制可實現線程安全的單例模式,也要根據實際狀況酌情使用,不宜大量推廣使用。
使用「雙重檢查加鎖」機制改寫後的懶漢模式,代碼以下:
public class SlackerDemo { private SlackerDemo() {} private static SlackerDemo instance = null; public static SlackerDemo getInstance() { if (instance == null) { synchronized (SlackerDemo.class) { if (instance == null) { instance = new SlackerDemo(); } } } return instance; } }
java.util.concurrent.locks
包提供了Lock
接口,Lock
鎖機制提供了比synchronized
代碼塊和synchronized
方法更普遍的鎖定操做,並且功能比synchronized
代碼塊和synchronized
方法更增強大。
官方的提供了參考價值很大的demo
,可以很好的提現Lock
機制的功能:
使用Lock
機制改寫的搶氣球案例代碼以下所示:
import java.util.concurrent.locks.*; public class LockDemo { public static void main(String []args) { Balloon balloon = new Balloon(); new Thread(balloon, "小紅").start(); new Thread(balloon, "小強").start(); new Thread(balloon, "小明").start(); } } // 氣球 class Balloon implements Runnable { private int num = 500; private final Lock lock = new ReentrantLock(); // 建立鎖對象 @Override public void run() { for (int i = 0; i < 500; i++) { grabBalloon(); } } // 搶氣球 private void grabBalloon() { lock.lock(); // 獲取鎖對象 if (num > 0) { try { System.out.println(Thread.currentThread().getName() + "搶到了" + num + "號氣球"); num--; } catch (Exception e) { } finally { lock.unlock(); // 釋放鎖 } } } }
案例運行正常。
完結。老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。