多執行緒安全問題的解決方案(多執行緒怎麼保證執行緒安全)

本文分享自華為雲社羣《多執行緒安全問題原理和解決辦法Synchronized和ReentrantLock使用與區別-雲社羣-華為雲》,作者:共飲一杯無。

執行緒安全問題概述

賣票問題分析

  • 單視窗賣票

一個視窗(單執行緒)賣100張票沒有問題

單執行緒程式是不會出現執行緒安全問題的

  • 多個視窗賣不同的票

3個視窗一起賣票,賣的票不同,也不會出現問題

多執行緒程式,沒有訪問共享資料,不會產生問題

  • 多個視窗賣相同的票

3個視窗賣的票是一樣的,就會出現安全問題

多執行緒訪問了共享的資料,會產生執行緒安全問題

執行緒安全問題程式碼實現

模擬賣票案例

建立3個執行緒,同時開啟,對共享的票進行出售

public class Demo01Ticket {     public static void main(String[] args) {         //建立Runnable介面的實現類物件         RunnableImpl run = new RunnableImpl();         //建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件         Thread t0 = new Thread(run);         Thread t1 = new Thread(run);         Thread t2 = new Thread(run);         //呼叫start方法開啟多執行緒         t0.start();         t1.start();         t2.start();     } } public class RunnableImpl implements Runnable{     //定義一個多個執行緒共享的票源     private  int ticket = 100;     //設定執行緒任務:賣票     @Override     public void run() {         //使用死迴圈,讓賣票操作重複執行         while(true){             //先判斷票是否存在             if(ticket>0){                 //提高安全問題出現的概率,讓程式睡眠                 try {                     Thread.sleep(10);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 //票存在,賣票 ticket--                 System.out.println(Thread.currentThread().getName() "-->正在賣第" ticket "張票");                 ticket--;             }         }     } }

執行緒安全問題原理分析

   

執行緒安全問題產生原理圖

分析:執行緒安全問題正常是不允許產生的,我們可以讓一個執行緒在訪問共享資料的時候,無論是否失去了cpu的執行權;讓其他的執行緒只能等待,等待當前執行緒賣完票,其他執行緒在進行賣票。

解決執行緒安全問題辦法1-synchronized同步程式碼塊

同步程式碼塊:synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

使用synchronized同步程式碼塊格式:

synchronized(鎖物件){
       可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
       }

程式碼實現如下:

public class Demo01Ticket {     public static void main(String[] args) {         //建立Runnable介面的實現類物件         RunnableImpl run = new RunnableImpl();         //建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件         Thread t0 = new Thread(run);         Thread t1 = new Thread(run);         Thread t2 = new Thread(run);         //呼叫start方法開啟多執行緒         t0.start();         t1.start();         t2.start();     } } public class RunnableImpl implements Runnable{     //定義一個多個執行緒共享的票源     private  int ticket = 100;     //建立一個鎖物件     Object obj = new Object();     //設定執行緒任務:賣票     @Override     public void run() {         //使用死迴圈,讓賣票操作重複執行         while(true){            //同步程式碼塊             synchronized (obj){                 //先判斷票是否存在                 if(ticket>0){                     //提高安全問題出現的概率,讓程式睡眠                     try {                         Thread.sleep(10);                     } catch (InterruptedException e) {                         e.printStackTrace();                     }                     //票存在,賣票 ticket--                     System.out.println(Thread.currentThread().getName() "-->正在賣第" ticket "張票");                     ticket--;                 }             }         }     } }

注意:

  1. 程式碼塊中的鎖物件,可以使用任意的物件。
  2. 但是必須保證多個執行緒使用的鎖物件是同一個。
  3. 鎖物件作用:把同步程式碼塊鎖住,只讓一個執行緒在同步程式碼塊中執行。

同步技術原理分析

同步技術原理:

使用了一個鎖物件,這個鎖物件叫同步鎖,也叫物件鎖,也叫物件監視器

3個執行緒一起搶奪cpu的執行權,誰搶到了誰執行run方法進行賣票。

  • t0搶到了cpu的執行權,執行run方法,遇到synchronized程式碼塊這時t0會檢查synchronized程式碼塊是否有鎖物件

發現有,就會獲取到鎖物件,進入到同步中執行

  • t1搶到了cpu的執行權,執行run方法,遇到synchronized程式碼塊這時t1會檢查synchronized程式碼塊是否有鎖物件

發現沒有,t1就會進入到阻塞狀態,會一直等待t0執行緒歸還鎖物件,t0執行緒執行完同步中的程式碼,會把鎖物件歸 還給同步程式碼塊t1才能獲取到鎖物件進入到同步中執行

總結:同步中的執行緒,沒有執行完畢不會釋放鎖,同步外的執行緒沒有鎖進不去同步。

解決執行緒安全問題辦法2-synchronized普通同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A執行緒執行該方法的時候,其他執行緒只能在方法外等著。

格式:

public synchronized void payTicket(){
       可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
       }

程式碼實現:

    public /**synchronized*/ void payTicket(){         synchronized (this){             //先判斷票是否存在             if(ticket>0){                 //提高安全問題出現的概率,讓程式睡眠                 try {                     Thread.sleep(10);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 //票存在,賣票 ticket--                 System.out.println(Thread.currentThread().getName() "-->正在賣第" ticket "張票");                 ticket--;             }         }     }

分析:定義一個同步方法,同步方法也會把方法內部的程式碼鎖住,只讓一個執行緒執行。

同步方法的鎖物件是誰?

就是實現類物件 new RunnableImpl(),也是就是this,所以同步方法是鎖定的this物件。

解決執行緒安全問題辦法3-synchronized靜態同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A執行緒執行該方法的時候,其他執行緒只能在方法外等著。對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)。

格式:

public static synchronized void payTicket(){
       可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
       }

程式碼實現:

    public static /**synchronized*/ void payTicketStatic(){         synchronized (RunnableImpl.class){             //先判斷票是否存在             if(ticket>0){                 //提高安全問題出現的概率,讓程式睡眠                 try {                     Thread.sleep(10);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 //票存在,賣票 ticket--                 System.out.println(Thread.currentThread().getName() "-->正在賣第" ticket "張票");                 ticket--;             }         }     }

分析:靜態的同步方法鎖物件是誰?

不能是this,this是建立物件之後產生的,靜態方法優先於物件

靜態方法的鎖物件是本類的class屬性–>class檔案物件(反射)。

解決執行緒安全問題辦法4-Lock鎖

Lock介面中的方法:

  • public void lock() :加同步鎖。
  • public void unlock() :釋放同步鎖

使用步驟:

  1. 在成員位置建立一個ReentrantLock物件
  2. 在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖
  3. 在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖

程式碼實現:

public class RunnableImpl implements Runnable{     //定義一個多個執行緒共享的票源     private  int ticket = 100;     //1.在成員位置建立一個ReentrantLock物件     Lock l = new ReentrantLock();     //設定執行緒任務:賣票     @Override     public void run() {         //使用死迴圈,讓賣票操作重複執行         while(true){             //2.在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖             l.lock();             try {                 //先判斷票是否存在                 if(ticket>0) {                     //提高安全問題出現的概率,讓程式睡眠                     Thread.sleep(10);                     //票存在,賣票 ticket--                     System.out.println(Thread.currentThread().getName()   "-->正在賣第"   ticket   "張票");                     ticket--;                 }             } catch (InterruptedException e) {                 e.printStackTrace();             }finally {                 l.unlock();                 //3.在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖                 //無論程式是否異常,都會把鎖釋放             }         }     }

分析
   java.util.concurrent.locks.Lock介面

Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。相比Synchronized,ReentrantLock類提供了一些高階功能,主要有以下3項:

  1. 等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。通過lock.lockInterruptibly()來實現這個機制。
  2. 公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock預設的建構函式是建立的非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的效能不是很好。

公平鎖、非公平鎖的建立方式:

//建立一個非公平鎖,預設是非公平鎖 Lock lock = new ReentrantLock(); Lock lock = new ReentrantLock(false);  //建立一個公平鎖,構造傳參true Lock lock = new ReentrantLock(true);

  1. 鎖繫結多個條件,一個ReentrantLock物件可以同時繫結多個物件。ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的執行緒們,而不是像synchronized要麼隨機喚醒一個執行緒要麼喚醒全部執行緒。

ReentrantLock和Synchronized的區別

相同點:

  1. 它們都是加鎖方式同步;
  2. 都是重入鎖;
  3. 阻塞式的同步;也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的(作業系統需要在使用者態與核心態之間來回切換,代價很高,不過可以通過對鎖優化進行改善);

點選下方,第一時間瞭解華為雲新鮮技術~

華為雲部落格_大資料部落格_AI部落格_雲端計算部落格_開發者中心-華為雲