「JAVA」Java 線程不安全分析,同步鎖和Lock機制,哪一個解決方案更好

Java 線程不安全分析及其解決方法

線程不安全

線程不安全的問題分析:在小朋友搶氣球的案例中模擬網絡延遲來將問題暴露出來;示例代碼以下:java

public class ImplementsDemo {
    public static void main(String []args) {
        Balloon balloon = new Balloon();
        new Thread(balloon, "小紅").start();
        new Thread(balloon, "小強").start();
        new Thread(balloon, "小明").start();
    }
}
// 氣球
class Balloon extends Thread {
    
    private int num = 50;
    
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
            }
                  System.out.println(Thread.currentThread().getName() + "搶到了" + (num--) + "號氣球")
            }
        }
    }
}

在線程中的run方法上不能使用throws來聲明拋出異常,因此在run方法中調用有可能出現異常的代碼時,只能使用try-catch將其捕獲來處理。算法

緣由是:子類覆蓋父類方法時不能拋出新的異常,父類的run方法都沒有拋出異常,子類就更加不能拋出異常了。詳情可查看個人另外一篇文章 「JAVA」運行時異常、編譯時異常、自定義異常,經過案例實踐轉譯和異常鏈
)編程

在上述案例中,經過引入Thread.sleep();來模擬網絡延遲,該方法的做用是讓當前線程進入睡眠狀態10毫秒,此時其餘線程就能夠去搶佔資源了,方法的參數是睡眠時間,以毫秒爲單位緩存

經過觀察運行結果,發現了問題:安全

小紅、小強兩個小朋友都搶到了14號氣球

在運行結果中,小紅、小強兩個小朋友都搶到了14號氣球,也就是14號氣球被搶到了2次。咱們來梳理線程的運行過程來看看發生了什麼:網絡

  1. 小強和小紅兩個線程都拿到了14號氣球,因爲線程調度,小強得到了CPU時間片,打印出了搶到的氣球,而小紅則進入睡眠;小強在打印後對num作了減一操做,此時num13
  2. 小明線程開始運行,搶到了13號氣球,並對num作了減一操做,此時num12
  3. 小紅線程醒來,打印出搶到的14號氣球;此時的num12,減一後結果爲11
  4. 因爲多個線程是併發操做,因此對num作判斷時可能上一個線程還未對num減一,故都能經過(num > 0)的判斷;

而後再來運行上述代碼,得出以下的結果:多線程

運行結果中出現了本不應出現的0 和 -1

運行結果中出現了本不應出現的0-1,由於按照正常邏輯,氣球數量到1以後就不該該被打印和減一了。出現這樣的結果是由於出現瞭如下的執行步驟:併發

  1. 小紅、小強、小明都同時搶到了1號氣球,因爲線程調度,小強獲取了cpu時間片,得以執行,而小明和小紅則進入睡眠;小強打印出結果後,對num減一,此時num0
  2. 小明醒來,得到的num0,而後小明將num打印出來,再對num減一,此時num-1
  3. 小紅醒來,得到的num-1,隨後小紅將num打印出來,再對num減一,此時怒木爲-2
  4. 因爲多個線程是併發操做,因此對num作判斷時可能上一個線程還未對num減一,故都能經過(num > 0)的判斷;

解決方案:ide

在案例中的搶氣球實際上是兩步操做:先搶到氣球,再對氣球總數減一;既然是兩步操做,在併發中就徹底有可能會被分開執行,且執行順序沒法獲得控制;性能

想要解決上述的線程不安全的問題,就必需要將這兩步操做做爲一個原子操做,保證其同步運行;也就是當一個線程A進入操做的時候,其餘線程只能在操做外等待,只有當線程A執行完畢,其餘線程纔能有機會進入操做。

原子操做:不能被分割的操做,必須保證其從一而終徹底執行,要麼都執行,要麼都不執行。

爲解決多線程併發訪問同一個資源的安全性問題,Java 提供以下了幾種不一樣的同步機制:

  1. 同步代碼塊;
  2. 同步方法;
  3. Lock 鎖機制;

同步代碼塊

同步代碼塊: 爲了保證線程可以正常執行原子操做,Java 引入了線程同步機制,其語法以下:

synchronized (同步鎖) {        
        // 須要同步操做的代碼      
        ... ...
}

上述中同步鎖,又稱同步監聽對象、同步監聽器、互斥鎖,同步鎖是一個抽象概念,能夠理解爲在對象上標記了一把鎖;

Java 中可使用任何對象做爲同步監聽對象,但在項目開發中,咱們會把當前併發訪問的共享資源對象做爲同步監聽對象在任什麼時候候,最多隻能運行一個線程擁有同步鎖

衛生間的使用就是一個很好的例子,一個衛生間在一段時間內只能被一我的使用,當一我的進入衛生間後,衛生間會被上鎖,其餘只能等待;只有當使用衛生間的人使用完畢,開鎖後才能被下一我的使用。

而後就可使用同步代碼塊來改寫搶氣球案例,示例代碼以下:

public class ImplementsDemo {
    public static void main(String []args) {
        Balloon balloon = new Balloon();
        new Thread(balloon, "小紅").start();
        new Thread(balloon, "小強").start();
        new Thread(balloon, "小明").start();
    }
}

// 氣球
class Balloon implements Runnable {
    
    private int num = 500;
    
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            synchronized (this) {
                if (num > 0) {
                      System.out.println(Thread.currentThread().getName() + "搶到了" 
                     + num + "號氣球");
                      num--;
                }
            }
        
        }
    }
}

經過查看運行結果,線程同步的問題已經獲得解決。

同步方法

同步方法: 使用synchronized修飾的方法稱爲同步方法,可以保證當一個線程進入該方法的時候,其餘線程在方法外等待。好比:

public synchronized void doSomething() {        
        // 方法邏輯    
}
PS:方法修飾符不分前後順序。

使用同步方法來改寫搶氣球案例,代碼以下:

public class ImplementsDemo {
    public static void main(String []args) {
        Balloon balloon = new Balloon();
        new Thread(balloon, "小紅").start();
        new Thread(balloon, "小強").start();
        new Thread(balloon, "小明").start();
    }
}

// 氣球
class Balloon implements Runnable {
    
    private int num = 500;
    
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            grabBalloon();
        }
    }
    // 搶氣球
    private synchronized void grabBalloon() {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "搶到了" 
                               + num + "號氣球");
            num--;
        }
    }
}

注意:不能使用synchronized修改線程類中的run方法,由於使用以後,就會出現一個線程執行完了全部功能,多個線程出現串行;本來是多行道,使用synchronized修改線程類中的run方法,多行道變成了單行道。

synchronized 的好與壞

好:synchronized 保證了併發訪問時的同步操做,避免了線程的安全性問題。

壞:使用synchronized 的方法、代碼塊的性能會比不用要低一些。

StringBuilder和StringBuffer

StringBuilderStringBuffer 區別就在於StringBuffer中的方法都使用了synchronized修飾,StringBuilder中的方法沒有使用synchronized修飾;這也是StringBuilder性能比StringBuffer高的主要緣由。

Vector和ArrayList

二者都有一樣的方法,有一樣的實現算法,惟一不一樣就是Vector中的方法使用了synchronized修飾,因此Vector的性能要比ArrayList低。

Hashtable和HashMap

二者都有一樣的方法,有一樣的實現算法,惟一不一樣就是Hashtable中的方法使用了synchronized修飾,因此Hashtable的性能要比HashMap低。

volatile關鍵字

volatile 關鍵字的做用在於:被volatile 關鍵字修飾的變量的值,將不會被本地線程緩存,全部對該變量的讀寫都是直接操做共享內存,從而能夠確保多個線程能正確處理該變量。

須要注意的是,volatile關鍵字可能會屏蔽虛擬機中的一些必要的優化操做,因此運行效率不是很高,所以,沒有特別的須要,不要使用;即使使用,也要避免大量使用。

單例模式

單例模式--餓漢模式

代碼以下:

public class SlackerDemo {

    private SlackerDemo() {}
    
    private static SlackerDemo instance = null;
    
    public static SlackerDemo getInstance() {
        if (instance == null) {
            instance = new SlackerDemo();
        }
        return instance;
    }
    
}

單例模式--懶漢模式

代碼以下:

public class SlackerDemo {

    private SlackerDemo() {}
    
    private static SlackerDemo instance = null;
    
    public static SlackerDemo getInstance() {
        if (instance == null) {
            instance = new SlackerDemo();
        }
        return instance;
    }
    
}

懶漢模式存在線程不安全問題,在對instance對象作判斷時因爲併發致使出現和搶氣球案例同樣的問題。爲了解決這個問題,使用雙重檢查加鎖機制來解決。

雙重檢查加鎖機制

使用「雙重檢查加鎖」機制實現的程序,既能實現線程安全,有可以使性能不受較大的影響。那麼何謂「雙重檢查加鎖」機制?其指的是:

    • 並非每次進入getInstance方法都須要同步,而是先不一樣步,進入方法後,先檢查實例是否存在,若是不存在才執行同步代碼塊,這是**第一重檢查;
    • 進入同步塊後,再次檢查實例是否存在,若是不存在,就在同步塊中建立一個實例,這是第二重檢查

    這樣,就只須要同步一次,減小了屢次在同步狀況判斷所浪費的時間。

    「雙重檢查加鎖」機制的實現須要volatile關鍵字的配合使用,且Java 版本須要在Java 5及以上,雖然該機制可實現線程安全的單例模式,也要根據實際狀況酌情使用,不宜大量推廣使用。

    使用「雙重檢查加鎖」機制改寫後的懶漢模式,代碼以下:

    public class SlackerDemo {
    
        private SlackerDemo() {}
        
        private static SlackerDemo instance = null;
        
        public static SlackerDemo getInstance() {
            if (instance == null) {
                synchronized (SlackerDemo.class) {
                    if (instance == null) {
                        instance = new SlackerDemo();
                    }
                }
            }
            return instance;
        }
    }

    Lock 鎖機制

    Lock 接口

    java.util.concurrent.locks包提供了Lock接口,Lock鎖機制提供了比synchronized代碼塊和synchronized方法更普遍的鎖定操做,並且功能比synchronized代碼塊和synchronized方法更增強大。

    官方的提供了參考價值很大的demo,可以很好的提現Lock機制的功能:

    Lock 機制的官方demo

    使用Lock 機制改寫的搶氣球案例代碼以下所示:

    import java.util.concurrent.locks.*;
    
    public class LockDemo {
        public static void main(String []args) {
            Balloon balloon = new Balloon();
            new Thread(balloon, "小紅").start();
            new Thread(balloon, "小強").start();
            new Thread(balloon, "小明").start();
        }
    }
    
    // 氣球
    class Balloon implements Runnable {
        
        private int num = 500;
        private final Lock lock = new ReentrantLock(); // 建立鎖對象
        
        @Override
        public void run() {
            for (int i = 0; i < 500; i++) {
                grabBalloon();
            }
        }
        // 搶氣球
        private void grabBalloon() {
            lock.lock(); // 獲取鎖對象
            if (num > 0) {
                try {
                    System.out.println(Thread.currentThread().getName() + "搶到了" 
                                   + num + "號氣球");
                    num--;
                } catch (Exception e) {
                    
                } finally {
                    lock.unlock(); // 釋放鎖
                }
            }
        }
    }

    案例運行正常。

    完結。老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。

    相關文章
    相關標籤/搜索