多線程的目的 為什麼要使用多線程?可以簡單的分兩個方面來說: 在多個cpu核心下,多線程的好處是顯而易見的,不然多個cpu核心只跑一個線程其他的核心就都浪費了; 即便不考慮多核心,在單核下,多線程也是有意義的,因為在一些操作,比如IO操作阻塞的時候,是不需要cpu參與的,這時候cpu就可以另開一個線 ...
多線程的目的
為什麼要使用多線程?可以簡單的分兩個方面來說:
- 在多個cpu核心下,多線程的好處是顯而易見的,不然多個cpu核心只跑一個線程其他的核心就都浪費了;
- 即便不考慮多核心,在單核下,多線程也是有意義的,因為在一些操作,比如IO操作阻塞的時候,是不需要cpu參與的,這時候cpu就可以另開一個線程去做別的事情,等待IO操作完成再回到之前的線程繼續執行即可。
多線程帶來的問題其實多線程根本的問題只有一個:線程間變數的共用
java里的變數可以分3類:
1類變數(類裡面static修飾的變數)
2實例變數(類裡面的普通變數)
3局部變數(方法里聲明的變數)
根據各個區域的定義,我們可以知道:
- 類變數 保存在“方法區”
- 實例變數 保存在“堆”
- 局部變數 保存在 “虛擬機棧”
“方法區”和“堆”都屬於線程共用數據區,“虛擬機棧”屬於線程私有數據區。
因此,局部變數是不能多個線程共用的,而類變數和實例變數是可以多個線程共用的。事實上,在java中,多線程間進行通信的唯一途徑就是通過類變數和實例變數。
也就是說,如果一段多線程程式中如果沒有類變數和實例變數,那麼這段多線程程式就一定是線程安全的。
以Web開發的Servlet為例,一般我們開發的時候,自己的類繼承HttpServlet之後,重寫doPost()、doGet()處理請求,不管我們在這兩個方法里寫什麼代碼,只要沒有操作類變數或實例變數,最後寫出來的代碼就是線程安全的。如果在Servlet類裡面加了實例變數,就很可能出現線程安全性問題,解決方法就是把實例變數改為ThreadLocal變數,而ThreadLocal實現的含義就是讓實例變數變成了“線程私有”的,即給每一個線程分配一個自己的值。
現在我們知道: 其實多線程根本的問題只有一個:線程間變數的共用, 這裡的變數,指的就是類變數和實例變數,後續的一切,都是為瞭解決類變數和實例變數共用的安全問題。
如何安全的共用變數
現在唯一的問題就是要讓多個線程安全的共用變數(下文中的變數一般特指類變數和實例變數),上文提到了一種ThreadLocal的方式,其實這種方式並不是真正的共用,而是為每個線程分配一個自己的值。
比如現在有一個特別簡單的需求,有一個類變數a=0,現在啟動5個線程,每個線程執行a++;如果用ThreadLocal的方式,最後的結果就是5個線程都擁有一份自己的a值,最終結果都是1,這顯然不符合我們的預期。
那麼如果不使用ThreadLocal呢?直接聲明一個類變數a=0,然後讓5個線程分別去執行a++;這樣結果依舊不對,而且結果是不確定的,可能是1,2,3,4,5中的任一個。這種情況叫做競態條件(Race Condition),要理解競態條件先要理解Java記憶體模型:
要理解java的記憶體模型,可以類比電腦硬體訪問記憶體的模型。由於電腦的cpu運算速度和記憶體io速度有幾個數量級的差距,因此現代電腦都不得不加入一層儘可能接近處理器運算速度的高速緩存來做緩衝:將記憶體中運算需要使用的數據先複製到緩存中,當運算結束後再同步回記憶體。如下圖:
因為jvm要實現跨硬體平臺,因此jvm定義了自己的記憶體模型,但是因為jvm的記憶體模型最終還是要映射到硬體上,因此jvm記憶體模型幾乎與硬體的模型一樣:
每個java線程都有一份自己的工作記憶體,線程訪問變數的時候,不能直接訪問主記憶體中的變數,而是先把主記憶體的變數複製到自己的工作記憶體,然後操作自己工作記憶體里的變數,最後再同步給主記憶體。
現在就可以解釋為什麼5個線程執行a++最後結果不一定是5了,因為a++可以分解為3步操作:
- 把主記憶體里的a複製到線程的工作記憶體
- 線程對工作記憶體里的a執行a=a+1
- 把線程工作記憶體里的a同步回主記憶體
而5個線程併發執行的時候完全有可能5個線程都先執行了第一步,這樣5個線程的工作記憶體里a的初始值都是0,然後執行a=a+1後在工作記憶體里的運算結果都是1,最後同步回主記憶體的值肯定也是1。
而避免這種情況的方法就是:在多個線程併發訪問a的時候,保證a在同一個時刻只被一個線程使用。
同步(synchronized)就是:在多個線程併發訪問共用數據的時候,保證共用數據在同一個時刻只被一個線程使用。
同步基本思想
為了保證共用數據在同一時刻只被一個線程使用,我們有一種很簡單的實現思想,就是 在共用數據里保存一個鎖 ,當沒有線程訪問時,鎖是空的,當有第一個線程訪問時,就 在鎖里保存這個線程的標識 並允許這個線程訪問共用數據。在當前線程釋放共用數據之前,如果再有其他線程想要訪問共用數據,就要 等待鎖釋放 。
我們把這種思想的三個關鍵點抽出來:
- 在共用數據里保存一個鎖
- 在鎖里保存這個線程的標識
- 其他線程訪問已加鎖共用數據要等待鎖釋放