摘要:多執行緒訪問了共享的資料,會產生執行緒安全問題,
本文分享自華為云社區《多執行緒安全問題原理和解決辦法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--; } } } } }
??注意:
- 代碼塊中的鎖物件,可以使用任意的物件,
- 但是必須保證多個執行緒使用的鎖物件是同一個,
- 鎖物件作用:把同步代碼塊鎖住,只讓一個執行緒在同步代碼塊中執行,
同步技術原理分析
同步技術原理:
使用了一個鎖物件,這個鎖物件叫同步鎖,也叫物件鎖,也叫物件監視器
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() :釋放同步鎖
使用步驟:
- 在成員位置創建一個ReentrantLock物件
- 在可能會出現安全問題的代碼前呼叫Lock介面中的方法lock獲取鎖
- 在可能會出現安全問題的代碼后呼叫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);
3.鎖系結多個條件,一個ReentrantLock物件可以同時系結多個物件,ReenTrantLock提供了一個Condition(條件)類,用來實作分組喚醒需要喚醒的執行緒們,而不是像synchronized要么隨機喚醒一個執行緒要么喚醒全部執行緒,
ReentrantLock和Synchronized的區別
相同點:
- 它們都是加鎖方式同步;
- 都是重入鎖;
- 阻塞式的同步;也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的(作業系統需要在用戶態與內核態之間來回切換,代價很高,不過可以通過對鎖優化進行改善);
不同點:
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/539962.html
標籤:設計模式
上一篇:【深入淺出SpringCloud原理及實戰】「SpringCloud-Alibaba系列」微服務模式搭建系統基礎架構實戰指南及版本規劃踩坑分析
下一篇:【深入淺出SpringCloud原理及實戰】「SpringCloud-Alibaba系列」微服務模式搭建系統基礎架構實戰指南及版本規劃踩坑分析
