Java 併發學習筆記(一)——原子性、可見性、有序性問題

計算機的 CPU、內存、I/O 設備的速度一直存在較大的差別,依次是 CPU > 內存 > I/O 設備,爲了權衡這三者的速度差別,主要提出了三種解決辦法:java

  • CPU 增長了緩存,均衡和內存的速度差別
  • 發明了進程、線程,分時複用 CPU,提升 CPU 的使用效率
  • 編譯指令優化,更好的利用緩存

三種解決辦法雖然有效,可是也帶來了另外的三個問題,分別就是併發 bug 產生的源頭。緩存

1.可見性問題併發

若是是單核 CPU,多個線程操做的都是同一個 CPU 緩存,那麼一個線程修改了共享變量,另外一個線程確定能立刻看到。優化

若是是多核 CPU ,每一個 CPU 都有本身的緩存,這樣線程對共享變量的修改便對其餘線程不可見了。spa

在這裏插入圖片描述

2.原子性問題線程

爲何會有線程切換?一個線程在執行的過程當中,可能會進行耗時的 I/O 操做,這時線程須要等待 I/O 操做完成。線程在等待的過程當中,能夠釋放 CPU 的使用權,讓另外一個線程執行,這樣可以提升 CPU 的使用率。code

在這裏插入圖片描述

例如上圖,兩個線程同時對變量 count 加 1,線程 A 在執行的過程當中切換到了線程 B,最後致使寫入到內存的值都是 1,與預期不符。對象

3.有序性問題blog

首先看一段很經典的獲取單例對象的代碼:進程

public class Singleton {
    
    private static Singleton instance;
     //Java 獲取單例對象
    public Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

程序的邏輯是:首先判斷 instance 是否爲空,若是爲空,對其加鎖,而後再判斷是否爲空,此時爲空的話則初始化 instance 對象。

若是線程 A 和 B 同時執行方法,在 synchronized 處,一個線程會被阻塞,假設被阻塞的是線程 B,此時線程 A 進入並初始化 instance,而後喚醒線程 B,線程 B 進入的時候,發現 instance 不爲空了,因此不會建立對象。

可是由於有序性問題的存在,這段代碼也不是想象的那麼完美,咱們指望的初始化對象的過程是這樣的:1.分配內存;2.初始化對象;3.將內存地址賦給 instance。可是通過編譯優化以後,倒是這樣的:

  • 1.分配內存
  • 2.將內存地址賦給 instance
  • 3.在內存上面初始化對象

這樣,若是線程 A 執行到了第二步,而後切換到 線程 B,線程 B 就會認爲 instance 不爲空而後直接返回了,實際上 instance 並無初始化。

最後,總結一下,致使併發問題的三個源頭分別是

  • 原子性:一個線程在執行的過程中不被中斷。
  • 可見性:一個線程修改了共享變量,另外一個線程可以立刻看到,就叫作可見性。
  • 有序性:編譯指令重排致使的問題。
相關文章
相關標籤/搜索