java線程安全問題以及使用synchronized解決線程安全問題的幾種方式

1、線程安全問題

1.產生緣由

  咱們使用java多線程的時候,最讓咱們頭疼的莫過於多線程引發的線程安全問題,那麼線程安全問題究竟是如何產生的呢?究其本質,是由於多條線程操做同一數據的過程當中,破壞了數據的原子性。所謂原子性,就是不可再分性。有物理常識的小夥伴可能要反駁了,誰說原子不可再分?原子裏邊還有質子和中子。咱們不在這裏探討物理問題,我確實也沒深究過爲何被稱爲原子性,也許是這個原則出現的時候尚未發現質子和中子,咱們只要記住在編程中所提到的原子性指的是不可再分性就行了。回到正題,爲何說破壞了數據的原子性就會產生的線程安全問題呢?咱們用一個很是簡單的例子來講明這個問題。java

  咱們來看下面這段很是簡單的代碼:面試

1 int i = 1;
2 int temp; 
3 
4 while(i < 10){
5 temp = i; //讀取i的值
6 i = temp + 1; //對i進行+1操做後再從新賦給i
7 };

  細心的小夥伴可能已經發現了,這不就是i++作的事情嗎。沒錯,其實i++就是作了上面的兩件事:編程

  1. 讀取i當前的值
  2. 對讀取到的值加1而後再賦給i

  咱們知道,在某一個時間點,系統中只會有一條線程去執行任務,下一時間點有可能又會切換爲其餘線程去執行任務,咱們沒法預測某一時刻到底是哪條線程被執行,這是由CPU來統一調度的。所以如今假設咱們有t一、t2兩條線程同時去執行這段代碼。假設t1執行完第5行代碼停住了(須要等待CPU下次調度才能繼續向下執行),此時t1讀到i的值是1。而後CPU讓t2執行,注意剛纔t1只執行完了第5行,也就是說t1並無對i進行加1操做而後再賦回給i,所以這是i的值仍是1,t2拿到i=1後一路向下執行直到結束,當執行到第6行的時候對i進行加1並賦回給i,完成後i的值變爲2。好了,此時CPU又調度t1讓其繼續執行,重點在這裏,還記不記得t1暫停前讀取到的i是幾?沒錯是1,此時t1執行第6行代碼,對i進行加1獲得的結果是2而後賦回給i。好了,問題出來了,咱們清楚的直到循環進行了兩次,按正常邏輯來講,對i進行兩次加1操做後,此時i應該等於3,可是兩條線程完成兩次加1操做後i的值居然是2,當進行第三次循環的時候,讀取到i的值將會是2,這樣的結果是否是很詭異,這就是線程安全問題的產生。那麼引起這個問題的緣由是什麼呢?其實就是將讀和寫進行了分割,當讀和寫分割開後,若是一條線程讀完但未寫時被CPU停掉,此時其餘線程就有可能趁虛而入致使最後產生奇怪的數據。安全

  那麼上面這段代碼怎麼修改才能不產生線程安全問題呢?咱們知道一條線程被CPU調度執行任務時,最少要執行一行代碼,因此解決辦法很簡單,只要將讀和寫合併到一塊兒就能夠了,下面的代碼是經過JUC中的原子操做來完成自增的操做(不熟悉的同窗能夠簡單的將其理解成讀寫是一塊兒執行的,不能夠被分開執行):多線程

1 private static AtomicInteger count = new AtomicInteger(0);
2 
3 while(count < 10){
4    count.incrementAndGet(); 
5 };

這樣,咱們將讀和寫用原子操做count.incrementAndGet()來替代,此時線程不管在哪行中止,其餘線程也不會對數據產生干擾,我畫一個圖來形象的說明這一點(圖有點醜,不要介意):ide

咱們能夠把左邊的圓當作是符合原子性(即一步執行)的代碼,而右邊的圓是被分割成了兩步執行的代碼。若是數據沒有破壞原子性,因爲線程被調度一次的最少要執行1行代碼,那麼t1只要執行了這行代碼,就會連讀帶寫所有完成,其餘線程再拿到的數據就是被寫過的最新數據,不會有任何安全隱患;而若是數據破壞了原子性,將讀寫進行了分割,那麼t1,讀取完數據若是停掉的話,t2執行的時候拿到的就是一個老數據(沒有被更新的數據),接下來t1,t2同時對相同的老數據進行更新勢必會所以數據的異常。函數

另外須要說明的是,爲何上面我要使用JUC的AtomicInteger類而不是count++?這裏涉及到了一個經典的面試題,count++操做是不是線程安全的?答案是否是,有興趣的同窗能夠參考http://www.javashuo.com/article/p-tdstvhyb-gw.htmlthis

2.注意

  對於線程安全問題,須要注意如下兩點:spa

  1. 只存在讀數據的時候,不會產生線程安全問題。
  2. 在java中,只有同時操做成員(全局)變量的時候纔會產生線程安全問題,局部變量不會(每一個線程執行時將會把局部變量放在各自棧幀的工做內存中,線程間不共享,故不存在線程安全問題,這裏不展開描述內存問題,有興趣可自行百度)。

3.代碼演示

  基於上面的分析,咱們經過最經典的賣票的例子來進行代碼演示。需求:使用兩個線程來模擬兩個窗口同時出售100張票:.net

 1 public class TicketThread implements Runnable{
 2 
 3     private int ticketCount = 100;
 4     
 5     @Override
 6     public void run() {
 7         while (ticketCount > 0) {
 8             try {
 9                 Thread.sleep(50);
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             sale();
14         }
15     }
16     
17     public void sale(){
18         if (ticketCount > 0) {
19             System.out.println(Thread.currentThread().getName() + "正在出售第" + (100-ticketCount+1) + "張票");
20             ticketCount --;
21         }
22     }
23 }

 

 

 1 public class Main {
 2     public static void main(String[] args) {
 3 
 4       TicketThread ticketThread = new TicketThread(); 
 5       Thread t1 = new Thread(ticketThread, "窗口1--");
 6       Thread t2 = new Thread(ticketThread, "窗口2--");
 7       t1.start();
 8       t2.start();
 9     }
10 }

運行結果:

結果分析:

從結果來看出現了不少詭異的數據,很明顯是發生了線程安全問題,根據上面的分析,相信你應該知道是哪裏致使的了。正式因爲TicketThread類中第 19,20行的代碼對成員變量ticketCount的讀和寫進行了分割才形成的,另外count--也是形成線程安全問題的緣由之一,上面已經提過,這裏不作詳述。至於線程安全問題的解決方法之一,經過synchronized關鍵字會在下面進行講解。

 

2、使用synchronized解決線程安全問題

1.synchronized的概念

  synchronized在英語中翻譯成同步,同步想必你們都不陌生。例如同步調用,有A,B兩個方法,必需要先調用A而且得到A的返回值才能去調用B,也就是說,想作下一步,必需要拿到上一步的返回值。一樣的道理,使用了synchronized的代碼,當線程t1進入的時候,另外一個線程若t2想進入,就必需要獲得返回值才能進入,怎麼獲得返回值呢?那就要等t1出來了纔會有返回值。這就是多線程中常說的加鎖,使用synchronized的代碼咱們能夠想象成將他們放到了一個房間,我前邊所說的返回值就至關於這個房間的鑰匙,進入這個房間的線程同時會把鑰匙帶進去,當它出來的時候會將鑰匙仍在地上(釋放資源),而後其餘線程過來搶鑰匙(爭奪CPU執行權),以此類推。

  被放到房間裏代碼,其實就是爲了讓其保持原子性,由於當線程t1進入被synchronized修飾的代碼當中的時候,其餘線程是被鎖在外邊進不來的,知道線程t1執行完裏邊的全部代碼(或拋出異常),纔會釋放資源。咱們換個角度想,這不就是讓房間(synchronized)裏面的代碼保持了原子性嗎,某一線程只要進去了,就必需要執行完畢裏邊的代碼別的線程再進去,期間不會有其餘線程趁虛而入來干擾它,就像我上面圖中左邊那個圓同樣,也就是至關於將原本分割的讀和寫的操做合併在了一塊兒,讓一個線程要麼不執行,只要執行就得把讀和寫所有執行完(且期間不會受干擾)。

  理解了我上邊所說的,就不再用糾結到底把什麼代碼放入synchronized中了,只要把讀和寫分割的代碼,而且分割後會引起線程安全問題的代碼放入讓其保持原子性就能夠了。很明顯在上面TicketThread類中,就是第19和20行。

2.synchronized的三種用法

(1)同步代碼塊

 1 public class SynchronizedBlockThread implements Runnable {
 2 
 3     private Object obj = new Object();
 4     private int ticketCount = 100;
 5     
 6     @Override
 7     public void run() {
 8         while (ticketCount > 0) {
 9             try {
10                 Thread.sleep(50);
11             } catch (InterruptedException e) {
12                 e.printStackTrace();
13             }
14             sale();
15         }
16     }
17     
18     public void sale(){
19         
20         synchronized (obj) { //使用同步代碼塊使線程間同步 21         if (ticketCount > 0) {
22             System.out.println(Thread.currentThread().getName() + "正在出售第" + (100-ticketCount+1) + "張票");
23             ticketCount --;
24             }
25         }
26     }
27 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         SynchronizedBlockThread blockThread = new SynchronizedBlockThread();
 5         Thread t1 = new Thread(blockThread, "窗口1--");
 6         Thread t2 = new Thread(blockThread, "窗口2--");
 7         t1.start();
 8         t2.start();
 9     }
10 }

 

代碼分析:

SynchronizedBlockThread類中須要注意一點,第20行,多個線程之間的同步代碼塊中必須使用相同的鎖(體如今代碼中就是同一個對象)才能保證同步,才能使其餘不進入干擾,兩條線程若是使用的不是同一把鎖,那麼一條線程進入synchronized中且未釋放資源前,另外一條線程依然能夠進入。同步代碼塊中使用的鎖要求必須是引用數據類型,最經常使用的就是傳入一個Object對象,或者使用當前類的對象,即this。

 

運行結果:使用synchronized是讀寫數據同步後沒有再出現線程安全問題

 

(2)同步函數

 1 public class SynchronizedMethodThread implements Runnable{
 2 
 3     private int ticketCount = 100;
 4     
 5     @Override
 6     public void run() {
 7         while (ticketCount > 0) {
 8             try {
 9                 Thread.sleep(50);
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             sale();
14         }
15     }
16     
17     public synchronized void sale(){ //使用同步函數使線程間同步 18         if (ticketCount > 0) {
19             System.out.println(Thread.currentThread().getName()
20                     + "正在出售第" + (100-ticketCount+1) + "張票");
21             ticketCount --;
22             }
23     }
24 }
 1 public class SellTicketMain {
 2 
 3     public static void main(String[] args) {
 4         SynchronizedMethodThread methodThread = new SynchronizedMethodThread();
 5         Thread t1 = new Thread(methodThread, "窗口1--");
6      Thread t2 = new Thread(methodThread, "窗口2--");
7
    t1.start();
8
    t2.start();
9   }
10 }

 

代碼分析:

在(1)同步代碼塊中,咱們建立了Object對象並將其當作鎖來使用,那麼在同步函數中,咱們沒法本身傳入鎖,那是否是同步函數中有默認的鎖呢?沒錯,同步函數中默認使用的鎖是當前類的對象,即this。下面代碼證實了同步函數中使用的鎖是this:

 1 public class VerifySynchronizedThread implements Runnable {
 2 
 3     private static int trainCount = 100;
 4     private Object obj = new Object();
 5     public boolean flag = true;
 6 
 7     @Override
 8     public void run() {
 9         if (flag) {
10             // 執行同步代碼塊this鎖
11             while (trainCount > 0) {
12                 synchronized (this) {
13                     if (trainCount > 0) {
14                         try {
15                             Thread.sleep(50);
16                         } catch (Exception e) {
17                             e.printStackTrace();
18                         }
19                         System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "票");
20                         trainCount--;
21                     }
22                 }
23 
24             }
25         } else {
26             // 執行同步函數
27             while (trainCount > 0) {
28                 sale();
29             }
30         }
31 
32     }
33 
34     public synchronized void sale() { // 同步函數 35         if (trainCount > 0) {
36             try {
37                 Thread.sleep(50);
38             } catch (Exception e) {
39                 e.printStackTrace();
40             }
41             System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "票");
42             trainCount--;
43         }
44     }
45 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {    
 4         VerifySynchronizedThread thread = new VerifySynchronizedThread();
 5 
 6         Thread t1 = new Thread(thread, "窗口1--");
 7         Thread t2 = new Thread(thread, "窗口2--");
 8         t1.start();
 9         try {
10             Thread.sleep(40);
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         thread.flag = false;
15         t2.start();
16     }
17 }

 

代碼分析:

咱們經過flag控制,讓t1執行同步代碼塊,讓t2執行同步函數,因爲兩條線程同時操做trainCount這個成員變量,所以可能會引起線程安全問題,按照咱們前邊的描述,使用synchronized讓線程同步,可是如今t1使用的是同步代碼塊,t2使用的是同步函數,按照前邊的分析若是他們倆使用的是通一把鎖,那麼當一個線程進入synchronized中的代碼時,另外一個線程是進不去的,從而解決線程安全問題。咱們既然是在驗證同步函數使用的是this鎖,所以咱們將同步代碼塊中也使用this,通過幾回反覆的運行,並無發現數據錯誤,也就說明了同步函數使用的是this鎖,爲了更加準確,咱們再將同步代碼塊中的鎖換成obj試一下,發現換成obj後出現了錯誤數據,所以咱們證實了同步函數使用的是this鎖。 

 

(3)靜態同步函數

 1 public class StaticSynchronizedThread implements Runnable {
 2     private static int ticketCount = 100;
 3     public boolean flag = true;
 4     @Override
 5     public void run() {
 6         if (flag) {
 7             while (ticketCount > 0) {
 8                 synchronized (StaticSynchronizedThread.class) { // 同步代碼塊
 9                     if (ticketCount > 0) {
10                         try {
11                             Thread.sleep(50);
12                         } catch (Exception e) {
13                             e.printStackTrace();
14                         }
15                         System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - ticketCount + 1) + "票");
16                         ticketCount--;
17                     }
18                 }
19 
20             }
21         } else {
22             // 執行靜態同步函數
23             while (ticketCount > 0) {
24                 sale();
25             }
26         }
27 
28     }
29     public static synchronized void sale() { //靜態同步函數
30         if (ticketCount > 0) {
31             try {
32                 Thread.sleep(50);
33             } catch (Exception e) {
34                 e.printStackTrace();
35             }
36             System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - ticketCount + 1) + "票");
37             ticketCount--;
38         }
39     }
40 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         StaticSynchronizedThread thread = new StaticSynchronizedThread();
 5 
 6         Thread t1 = new Thread(thread, "窗口1--");
 7         Thread t2 = new Thread(thread, "窗口2--");
 8         t1.start();
 9         try {
10             Thread.sleep(40);
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         thread.flag = false;
15         t2.start();
16     }
17 }

代碼分析:

靜態同步函數的形式也比較簡單,僅僅是將同步函數寫成靜態的形式。可是須要注意的是,靜態同步函數使用的鎖不是this,它也不可能使用this,由於咱們知道靜態函數要先於對象加載,也就是說當靜態同步函數被加載的時候,本類的對象即this在內存中還不存在,所以更不可能使用它。這裏靜態同步函數使用的鎖實際上是本類的字節碼文件,即StaticSynchronizedThread.class。一樣還使用以前的代碼,將同步代碼塊的鎖設爲StaticSynchronizedThread.class來驗證,運行發現不會出現錯誤數據,當換成其餘鎖時,便會出現錯誤數據。

 3.對於synchronized的總結

  • 要使用synchronized,必需要有兩個以上的線程。單線程使用沒有意義,還會使效率下降。
  • 要使用synchronized,線程之間須要發生同步,不須要同步的不必使用synchronized,例如只讀數據。
  • 使用synchronized的缺點是效率很是低,由於加鎖、釋放鎖和釋放鎖後爭搶CPU執行權的操做都很耗費資源。
相關文章
相關標籤/搜索