java 執行緒安全問題,解決執行緒安全問題——同步程式碼塊,同步方法,Lock鎖,Object類中wait方法,notify方法。等待喚醒案例。

Gaoju12138發表於2020-11-06

1、執行緒安全問題

執行緒出現安全問題的程式碼:

/*
    實現賣票案例
 */
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--;
            }
        }
    }
}

主類:

/*
    模擬賣票案例
    建立三個執行緒,同時開啟,對共享的票進行出售
 */
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();
    }
}

執行截圖:
在這裡插入圖片描述
三個執行緒賣了同一張票,即安全問題。

2、解決執行緒安全問題—同步程式碼塊

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

synchronized(同步鎖){
	需要同步操作的程式碼
}

使用程式碼:

/*
    實現賣票案例出現了執行緒安全問題,賣出了不存在的票和重複的票
    解決執行緒安全問題的第一種方案:使用同步程式碼塊
    格式:
    synchronized(同步鎖){
   需要同步操作的程式碼
        }

    注意:
        1、通過程式碼塊中的鎖物件,可以使用任意的物件
        2、但是必須保證多個執行緒使用的鎖物件是同一個
        3、鎖物件作用:
            把同步程式碼塊鎖住,只讓一個執行緒在同步程式碼塊中執行
 */
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、解決執行緒安全問題——同步方法

解決執行緒安全的第二種方案:使用同步方法
使用步驟:
1、把訪問了共享資料的程式碼抽取出來,放到一個方法中
2、在方法上新增synchronized修飾符
格式:定義方法的格式
修飾符 synchronized 返回值型別 方法民(引數列表) {
可能出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
}

程式碼:

/*
    實現賣票案例出現了執行緒安全問題

    解決執行緒安全的第二種方案:使用同步方法
 */
public class RunnableImpl implements Runnable{
    //定義一個多個執行緒共享的票源
    private int ticket = 100;

    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死迴圈,讓買票操作重複執行
        while (true) {
            payTicket();
        }
    }
    /*
        定義同步方法
     */
    public synchronized void payTicket() {
        //先判斷票是否存在
        if (ticket > 0) {
            //提高安全問題出現的概率,讓程式睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //票存在,賣票ticket
            System.out.println(Thread.currentThread().getName() + "正在賣第"+ticket+"張票");
            ticket--;
        }
    }
}

4、解決執行緒安全問題——Lock鎖

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
public void lock( ) :加同步鎖
public void unlock( ) :釋放同步鎖
程式碼及使用步驟如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
    實現賣票案例
    出現了執行緒安全問題

    解決執行緒安全問題的第三種方案:使用Lock鎖
    java.util.concurrent.Locks.Lock介面
    Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。
    Lock介面中的方法:
        public void lock( ) :加同步鎖
        public void unlock( ) :釋放同步鎖

    java.util.concurrent.locks.ReentrantLock implements Lock介面

    使用步驟:
        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();

            //先判斷票是否存在
            if (ticket > 0) {
                //提高安全問題出現的概率,讓程式睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,賣票ticket
                System.out.println(Thread.currentThread().getName() + "正在賣第"+ticket+"張票");
                ticket--;
            }

            // 3、可能會出現安全問題的程式碼後呼叫lock介面中的方法unlock釋放鎖
            l.unlock();
        }
    }
}

5、幾種執行緒的狀態

Timed Waiting 計時等待
Blocked 鎖阻塞狀態
Waiting 無限等待

6、等待喚醒案例

程式碼:

/*
    等待喚醒案例:執行緒之間的通訊
        建立一個顧客執行緒,告知老闆要的包子的種類和數量,呼叫wait方法,放棄cpu的執行,進入到
        waiting狀態(無限等待)
        建立一個老闆執行緒(生產者):花了5秒做包子,做好包子之後,呼叫notify方法,喚醒顧客吃包子

    注意:
        1、顧客和老闆執行緒必須使用同步程式碼塊包裹起來,保證等待和喚醒只能有一個在執行
        2、同步使用的鎖物件必須保證是唯一的
        3、只有鎖物件才能呼叫wait和notify方法

    Object類中的方法
    void wait()
        在其他執行緒呼叫此物件的notify()方法或者notifyAll()方法前,導致當前執行緒等待
    void notify()
        喚醒在此物件監視器上等待的單個執行緒。
        會繼續執行wait方法之後的程式碼
 */
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        //建立鎖物件,保證唯一
        Object obj = new Object();
        //建立一個顧客執行緒
        new Thread(){
            @Override
            public void run() {
                while (true) { //一直有顧客買包子
                    //保證等待和喚醒的執行緒只能有一個在執行,同步技術
                    synchronized (obj) {
                        System.out.println("告訴老闆要的包子的種類和數量");
                        //呼叫wait方法,放棄cpu的執行,進入到waiting狀態(無限等待)
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //喚醒之後執行的程式碼
                        System.out.println("包子已經做好了,開吃");
                        System.out.println("——————————————————————");
                    }
                }
            }
        }.start();
        //建立一個老闆執行緒(生產者)
        new Thread(){
            @Override
            public void run() {
                //一直做包子
                while (true) {
                    //花了5秒做包子
                    try {
                        Thread.sleep(5000); //5秒做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println("老闆5秒之後做好包子,告知顧客可以吃了");
                        //呼叫notify方法,喚醒顧客吃包子
                        obj.notify();
                    }
                }
            }
        }.start();

    }
}

執行截圖:

在這裡插入圖片描述

7、Object類中wait帶參方法和notifyAll方法

進入到TimeWaiting(計時等待)有兩種方式
1、使用sleep(long m)方法,在毫秒值結束之後,執行緒睡醒進入到Runnable/Blocked狀態
2、使用wait(long m)方法,wait方法如果在毫秒值結束之後,還沒有被notify喚醒,就會自動醒來,執行緒睡醒進入到Runnable/Blocked狀態

喚醒的方法:
void notify( )喚醒單個執行緒
void notifyAll( )喚醒所有執行緒。

相關文章