原文連接:http://www.javashuo.com/article/p-nlvflzmb-ge.htmlhtml
Java併發編程系列:java
Java 併發編程:核心理論 程序員
併發編程時Java程序員最重要的技能之一,也是最難掌握的一種技能。他要求編程者對計算機最底層的運做原理有深入的理解,同時要求編程者邏輯清晰、思惟縝密,這樣才能寫出高效、安全、可靠的多線程併發程序。本系列會從線程間協調的方式(wait、notify、notifyAll)、Synchronized及volatile的本質入手,詳細解釋JDK爲咱們提供的每種併發工具和底層實現機制。在此基礎上,咱們會進一步分析java.util.concurrent包的工具類,包括其使用方式、實現源碼及其背後的原理。本文是該系列的第一遍文章,是這系列中最核心的理論部分,以後的文檔都會以此爲基礎來分析和解釋多線程
數據共享性是線程安全的主要緣由之一。若是全部的數據只是在線程內有效,那就不存在線程安全問題,這也是咱們在編程的時候常常不須要考慮線程安全的主要緣由之一。可是,在多線程編程中,數據共享是不可避免的。最典型的場景是數據庫中的數據,爲了保證數據的一致性,咱們一般須要共享同一個數據庫中的數據,即便是在主從的狀況下,訪問的也是同一份數據,主從只是爲了訪問的效率和數據安全,而對同一份數據作的副本。咱們如今,經過一個簡單的示例來演示多線程下共享數據致使的問題:併發
代碼段一:ide
package com.paddx.test.concurrent; public class ShareData { public static int count = 0; public static void main(String[] args) { final ShareData data = new ShareData(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { //進入的時候暫停1毫秒,增長併發問題出現的概率
Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 100; j++) { data.addCount(); } System.out.print(count + " "); } }).start(); } try { //主程序暫停3秒,以保證上面的程序執行完成
Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count=" + count); } public void addCount() { count++; } }
上述代碼的目的是對count進行加一的操做,執行1000次,由10個線程來實現,每一個線程執行100次,咱們想要的應該輸出1000。可是,上面程序的結果不是這樣的。下面是某次的執行結果(每次運行的結果不必定相同,有時候也可能獲取到正確的結果):
能夠看出,對共享變量的操做,在多線程環境下很容易出現各類意想不到的結果。
資源互斥是隻同時只容許一個訪問者對其訪問,具備惟一性和排他性。咱們一般容許多個線程對數據進行讀操做,但同時只容許一個線程對數據進行寫操做。因此咱們一般將鎖分爲共享鎖和排它鎖,也叫作讀鎖和寫鎖。若是資源不具備互斥性,即便是共享資源,咱們也不須要擔憂線程安全。例如,對於不可變的數據共享,全部線程都只能對其進行讀操做,因此不用考慮線程安全問題。可是對共享數據的寫操做,通常就須要保證互斥性,上述例子中就是由於沒有保證互斥性才致使數據的修改產生問題。Java中提供多種機制來保證互斥性,最簡單的方式是使用synchronized,如今咱們在上面程序中加上synchronized再執行:
代碼段二:
package com.paddx.test.concurrent; public class ShareData { public static int count = 0; public static void main(String[] args) { final ShareData data = new ShareData(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { //進入的時候暫停1毫秒,增長併發問題出現的概率
Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 100; j++) { data.addCount(); } System.out.print(count + " "); } }).start(); } try { //主程序暫停3秒,以保證上面的程序執行完成
Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count=" + count); } /** * 增長 synchronized 關鍵字 */
public synchronized void addCount() { count++; } }
如今再執行上述代碼,會發現不管執行多少次,返回的最終結果都是1000.
原子性就是指對數據的操做是一個獨立的,不可分割的總體。換句話說,就是一次操做,是一個連續不可中斷的過程,數據不會執行到一半的時候被其餘線程所修改。保證原子性的最簡單方式就是操做系統指令,就是說若是一次操做對應一條操做系統指令,這樣確定能夠保證原子性。可是不少操做不能經過一條指令就完成。例如:對long類型的運算,不少系統就須要分紅多條指令分別對高位和地位進行操做才能完成。還好比,咱們常用的整數i++的操做,其實須要分紅三個步驟:(1)讀取整數i的值;(2)對i進行加1的操做;(3)將結果寫回內存。這個過程在多線程下就可能出現以下現象:
這也是代碼段一執行的結果爲何不正確的緣由。對於這種組合操做,要保證原子性,最多見的方式是加鎖,如Java中的Synchronized或Lock均可以實現,代碼段二就是經過synchronized實現的。除了鎖之外,還有一種方式就是CAS(Compare And Swap),即修改數據以前先比較與以前讀取到的值是否一致,若是一致,則進行修改,若是不一致則從新執行,這也是樂觀鎖的實現原理。不過CAS在某些場景下不必定有效,好比另外一線程先修改了某個值,而後再改回原來的值,這種狀況下,CAS是沒法判斷的
要理解可見性,須要先對JVM的內存模型有必定的瞭解,JVM的內存模型與操做系統相似,如圖所示:
從這個圖中咱們能夠看出,每一個線程都有一個本身的工做內存(至關於CPU高級緩衝區,這麼作的目的仍是在於進一步縮小存儲系統與cpu之間速度的差別,提升性能),對於共享變量,線程每次讀取到的是工做內存中共享變量的副本,寫入的時候也直接修改工做內存中副本的值,而後在某個時間再將工做內存與主內存中的值進行同步。這樣致使的問題是,若是線程1對某個變量進行了修改,線程2卻有可能看不到線程1對共享變量所作的修改。經過下面這段程序咱們能夠掩飾一下不可見的問題:
package com.paddx.test.concurrent; public class VisibilityTest { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if (!ready) { System.out.println(ready); } System.out.println(number); } } private static class WriterThread extends Thread { public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } number = 100; ready = true; } } public static void main(String[] args) { new WriterThread().start(); new ReaderThread().start(); } }
從直觀上理解,這段程序應該只會輸出100,ready的值是不會打印出來的。實際上,若是屢次執行上面代碼的話,可能會出現多種不一樣的結果,下面是運行出來的某兩次的結果:
固然,這個結果也只能說是有多是可見性形成的,當寫線程(WriterThread)設置ready=true後,讀線程(ReaderThread)看不到修改後的結果,因此會打印false,對於第二個結果,也就是執行if(!ready)時尚未讀取到寫線程的結果,但執行System.out.println(ready)時讀取到了寫線程執行的結果。不過,這個結果也有多是線程的交替執行所形成的。Java中可經過Synchronized或Volatile來保證可見性,具體細節會在後續文章中分析。
爲了提升性能,編譯器和處理器可能會對指令進行重排序。重排序能夠分爲三種:
(1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
(2)指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
(3)內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
咱們能夠直接參考一下JSR 133中對重排序問題的描述:
(1) (2)
先看上圖中的(1)源碼部分,從源碼來看,要麼指令1先執行要麼指令3先執行。若是指令1先執行,r2不該該能看到指令4中寫入的值。若是指令3先執行,r1不該該能看到指令2的值。可是運行結果卻可能出現r2==2,r1==1的狀況,這就是「重排序」致使的結果。上圖(2)便是一種可能出現的合法的編譯結果,編譯後,指令1和指令2的順序可能就互換了。所以,纔會出現r2==2,r1==1的結果。Java中也可經過Synchronized或volatile來保證順序性。
本文對Java併發編程中的理論基礎進行了講解,有些東西在後續的分析彙總還會作更詳細的討論,如可見性、順序性等。後續的文章都會以本章內容做爲理論基礎來討論。