1. 程式人生 > >併發程式設計-(2)執行緒安全問題

併發程式設計-(2)執行緒安全問題

目錄

1、什麼叫執行緒安全

當多個執行緒訪問某個類(物件或者方法)時,這個類始終能表現出正確的行為,那麼這個類(物件或方法)是安全的。

2、多視窗買票案例

package com.fly.thread_demo.demo_2;

/**
 *  模擬火車站買票:一共庫存 100張票 ,兩個視窗同時賣票
 */
public class ThreadTrain {
    /**
     * 車票庫存100張
     */
    private int count  = 100;

    /**
     * 買票方法
     */
    public  void sell(){
        if (count > 0 ){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count -- ;
            System.out.println(Thread.currentThread().getName()+ ":正在出售第"+(100-count)+"張火車票,賣完還剩"+count+"張!");
        }
    }

    public static void main(String[] args) {
        ThreadTrain threadTrain = new ThreadTrain();
        /**
         * 視窗一
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (threadTrain.count > 0 ){
                    threadTrain.sell();
                }
            }
        },"視窗一").start();
        /**
         * 視窗二
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (threadTrain.count > 0 ){
                    threadTrain.sell();
                }
            }
        },"視窗二").start();
    }
}

我們一共有100張火車票,但是最後我們賣了101張,這就是執行緒安全問題

3、如何解決執行緒安全問題

3.1、鎖的特徵

只能同時被一個執行緒持有。

3.2、內建鎖(synchronized)

保證執行緒的原子性,當執行緒進入方法時,自動獲取鎖,一旦鎖被獲取,其他執行緒在執行該方法時候會等待,當程式執行完畢後就會釋放鎖,後面排隊的執行緒會搶這把鎖,沒搶到的執行緒會繼續等待。

3.3、解決火車票問題

3.3.1、同步程式碼塊

    
    private Object obj = new Object();
    /**
     * 買票方法 使用靜態程式碼快的方式 
     */
    public void sell(){
        
        //讓一個obj物件成為一把鎖 
        synchronized (obj){

            if (count > 0 ){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count -- ;
                System.out.println(Thread.currentThread().getName()+ ":正在出售第"+(100-count)+"張                火車票,賣完還剩"+count+"張!");
            }
        }

    }

public  void sell(){
    //這裡我們每次使用不同的物件做為鎖
    synchronized (new Object()){

        if (count > 0 ){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count -- ;
            System.out.println(Thread.currentThread().getName()+ ":正在出售第"+(100-count)+"張火車票,賣完還剩"+count+"張!");
        }
    }

}

結論一: 在多執行緒中,要使用同步,必須使用同一把鎖。

另:

​ 如果使用String 型別的鎖,我們改變了String物件的值,就會讓鎖發生改變

​ 如果我們用物件鎖,我們改變物件屬性的值,不會改變鎖

3.3.2、同步方法

/**
 * 買票方法  方法加    synchronized  關鍵字
 */
public synchronized void sell(){
    if (count > 0 ){
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count -- ;
        System.out.println(Thread.currentThread().getName()+ ":正在出售第"+(100-count)+"張火車票,賣完還剩"+count+"張!");
    }
}

4、同步方法鎖的進階

這裡我們就只演示結論,不演示不成功的來對比了。

package com.fly.thread_demo.demo_2;

/**
 * 非靜態同步方法  和  靜態同步方法
 */
public class ThreadSafe {
    private Object obj = new Object();

    public void printNum_1(){
        synchronized (obj){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
        }
    }
    public void printNum_2(){
        synchronized (obj){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
        }
    }

    /**
     * 現在有兩個執行緒,分別呼叫printNum_1和printNum_2方法
     * 如果我們給printNum_1  和  printNum_2  加鎖
     * 我們使用前面的結論:如果使用同一把鎖  兩個方法將會同步  如果不是同一把鎖 將不會同步
     */
    public static void main(String[] args) {

        ThreadSafe threadSafe = new ThreadSafe();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_2();
            }
        }).start();

    }
}

線上程Thread-0執行完畢後再執行執行緒Thread-1,說明前面的結論沒有錯

4.1、非靜態同步方法

package com.fly.thread_demo.demo_2;

/**
 * 非靜態同步方法  和  靜態同步方法
 */
public class ThreadSafe {
    private Object obj = new Object();

    /**
     * 在非靜態方法上加synchronized鎖
     */
    public synchronized void printNum_1(){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
    }

    /**
     * 使用當前物件作為鎖
     */
    public void printNum_2(){
        synchronized (this){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
        }
    }

    /**
     * 現在有兩個執行緒,分別呼叫printNum_1和printNum_2方法
     * 如果我們給printNum_1  和  printNum_2  加鎖
     * 如果使用同一把鎖  他們將會同步  如果不是同一把鎖 將不會同步
     */
    public static void main(String[] args) {

        ThreadSafe threadSafe = new ThreadSafe();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_2();
            }
        }).start();

    }
}

結論二: 非靜態方法上加synchronize鎖,其實使用的是this鎖,也就是把當前物件作為鎖

4.2、靜態同步方法

package com.fly.thread_demo.demo_2;

/**
 * 非靜態同步方法  和  靜態同步方法
 */
public class ThreadSafe {
    private Object obj = new Object();

    /**
     * 在靜態方法上加synchronized鎖
     */
    public synchronized static void printNum_1(){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
    }

    /**
     * 使用當前類的 位元組碼檔案 作為鎖
     */
    public void printNum_2(){
        synchronized (ThreadSafe.class){
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ ": i = " + i);
            }
        }
    }

    /**
     * 現在有兩個執行緒,分別呼叫printNum_1和printNum_2方法
     * 如果我們給printNum_1  和  printNum_2  加鎖
     * 如果使用同一把鎖  他們將會同步  如果不是同一把鎖 將不會同步
     */
    public static void main(String[] args) {

        ThreadSafe threadSafe = new ThreadSafe();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafe.printNum_2();
            }
        }).start();

    }
}

結論三:在靜態方法上加synchronize鎖,其實是把當前類的位元組碼檔案當做鎖。

5、死鎖

5.1、死鎖案例

package com.fly.thread_demo.demo_2;

/**
 *  死鎖  一般是由鎖的巢狀引起的
 *  小明和小紅打架,小明揪著小紅的頭髮,小紅揪著小明的頭髮,
 *  小明要等小紅放手自己才會放手,小紅也要等著小明放手自己才會放手
 *  這樣他們永遠也不會鬆手
 */
public class DeathThread implements Runnable{
    private String tag;
    public void setTag(String tag){
        this.tag = tag;
    }
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    @Override
    public void run() {

        if ("小明怒了".equals(tag)) {
            synchronized (obj1) {
                System.out.println("小明揪住了小紅的頭髮...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2) {
                    System.out.println("因為小紅放開了小明,所以小明放開了小紅...");
                }
            }
        }

        if ("小紅怒了".equals(tag)) {
            synchronized (obj2) {
                System.out.println("小紅揪住了小明的頭髮...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1) {
                    System.out.println("因為小明放開了小紅,所以小紅放開了小明...");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeathThread dt1 = new DeathThread();
        dt1.setTag("小明怒了");
        DeathThread dt2 = new DeathThread();
        dt2.setTag("小紅怒了");

        Thread t1 = new Thread(dt1);
        Thread t2 = new Thread(dt2);

        t1.start();

        t2.start();

    }
}

程式一直沒結束,但是小明和小紅都不放手,這是因為小明拿到先拿到了obj1鎖,然後休眠了1秒鐘,與此同時,小紅拿到了obj2鎖,也休眠了1秒鐘,一秒鐘過後,小明要obj2鎖才能放手,但是obj2鎖在小紅手上,要等小紅執行完畢才能釋放obj2鎖,但是小紅想要執行完畢要先獲得obj1鎖,同樣obj1鎖在小明手上,這樣就造成了死鎖問題

5.2、快速定位死鎖位置

我們一般使用jdk工具

  1. 開啟控制檯輸入jconsole

  2. 這個時候會開啟Java簡直和管理控制檯

  1. 開啟對應的類

  1. 選擇不安全連結

  2. 找到對應的執行緒名稱,檢視詳情