天天看點

多線程安全問題原理和4種解決辦法

作者:華為雲開發者聯盟

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

線程安全問題概述

賣票問題分析

  • 單視窗賣票
多線程安全問題原理和4種解決辦法

一個視窗(單線程)賣100張票沒有問題

單線程程式是不會出現線程安全問題的

  • 多個視窗賣不同的票
多線程安全問題原理和4種解決辦法

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

多線程程式,沒有通路共享資料,不會産生問題

  • 多個視窗賣相同的票
多線程安全問題原理和4種解決辦法

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--;
            }
        }
    }
}
           

線程安全問題原理分析

多線程安全問題原理和4種解決辦法

線程安全問題産生原理圖

分析:線程安全問題正常是不允許産生的,我們可以讓一個線程在通路共享資料的時候,無論是否失去了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. 阻塞式的同步;也就是說當如果一個線程獲得了對象鎖,進入了同步塊,其他通路該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的(作業系統需要在使用者态與核心态之間來回切換,代價很高,不過可以通過對鎖優化進行改善);
多線程安全問題原理和4種解決辦法

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

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

繼續閱讀