Java併發編程之多線程會導致的問題

1. 線程安全問題

1.1 什麼是線程安全?

  • 某權威作者定義:“當多個線程訪問同一個對象時,如果不用考慮這些線程的調度,也不需要額外的同步,調用這個對象的行爲都可以獲得正確的結果,那麼這個對象時線程安全的“。
  • 通俗易懂的定義:當多個線程訪問同一個對象時,不需要做額外的任何處理,像單線程編程一樣,程序可以正常運行,就可以稱爲線程安全。

1.2 線程安全問題有哪些?

  1. 運行結果錯誤:a++多線程下出現結果錯誤
  2. 活躍性問題:死鎖、活鎖、飢餓

1.2.1 運行結果錯誤

下面演示a++運行結果錯誤:

public class APlusPlus implements Runnable {

    static int a = 0;
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        APlusPlus aPlusPlus = new APlusPlus();
        Thread t1 = new Thread(aPlusPlus);
        Thread t2 = new Thread(aPlusPlus);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(a);
    }
}
11054

我們預計的結果是20000,最後結果比預計的少,因爲a++,看上去是一個操作,實際上包含了三個動作:
1. 讀取a
2. 將a加一
3. 將a的值寫入到內存中
說明a++操作是不具備原子性的,因此需要對a++操作進行同步,保證同一時刻只有一個線程執行a++操作。

1.2.2 活躍性問題:死鎖、活鎖、飢餓

下面演示死鎖問題:

public class DeadLock {
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "獲取到lockA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在嘗試獲取lockB...");
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "獲取到lockB");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "獲取到lockB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在嘗試獲取lockA...");
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + "獲取到lockA");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

由於t1獲取到lockA,t2獲取到lockB,t1和t2分別想獲取lockB和lockA,由於雙方都沒有釋放鎖,所以t1和t2都進入到了死鎖狀態。

1.3 各種需要考慮線程安全的情況

  1. 訪問共享變量,比如對象的屬性、靜態變量、共享緩存等;
  2. 所有依賴順序的操作,比如:讀改寫(原子性)、讀取-檢查-操作(內存可見性);
  3. 數據之間存在綁定關係;
  4. 使用其他類的時候,比如使用HashMap就不是線程安全的;

2. 性能問題

爲什麼多線程會帶來性能問題?一共分爲以下兩個原因:

  1. 上下文切換
  2. 內存同步

2.1 上下文切換

  • 什麼是上下文切換:進行一次上下文切換時,先掛起當前線程,然後把當前線程狀態存在某處,以便線程切換回來知道執行到哪裏了,這個線程狀態包含當前線程執行的指令和位置,這個狀態就是上下文。
  • 由於上下文切換也會使得CPU自身的一些緩存失效,相對而言,也降低了執行速度。
  • 何時會密集地導致上下文切換:搶鎖、IO

2.2 內存同步

  • 由於JMM規定了主內存和線程內存,而線程內存會緩存數據,可以增加計算速度。由於在多線程編程時使用了synchronized、volatile關鍵字等,禁止了線程緩存,也禁止了指令重排序,編譯器CPU等優化手段,使得是能只用主內存了,相對而言就降低了性能
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章