chapter10 多線程與併發編程java
操做系統中,進程process和線程thread,process是一個計算機程序中運行實例。一個計算機能夠建立多個process,這些process運行狀態各不相同,有本身的獨立的地址空間,包括程序內容和數據。它們間是互相隔離的。process擁有各類資源和狀態信息,包括打開的文件,子進程和信號處理器等。thread表示的是程序的執行流程,是cpu調度執行的基本單位unit。thread有本身的程序計數器,寄存器,堆棧和帆等。同一個進程中的線程共享相同的地址空間,同時共享進程所擁有的內在和其餘資源。引入thread來提升程序的運行性能。一個程序中主要存在cpu計算和io操做。io操做相對cpu操做來講比較耗時,並且不少是阻塞式的。當一個thread所執行的io操做被阻塞時,同一進程中的其餘thread可使用cpu來進行計算。不一樣os和編程語言中對thread的使用方式有很大的區別,因此對於跨平臺的多線程程序來講是一個很大的挑戰。但java平臺經過jvm解決了跨平臺的問題,使得相同api開發的程序在不一樣平臺上都可以正確運行。
編程
java.lang.Process /java.lang.ProcessBuilder類是進程,java.lang.Thread是表示線程。在JVM啓動後,一般只有一個普通的thread來運行程序的代碼,這個主要用來啓動java類的main方法來運行。程序在運行時能夠根據須要建立新的thread並啓動它來執行。除了普通thread外還有一類是守護thread(daemon thread)。daemon thread通常在後臺運行,提供程序運行時的服務,當jvm中運行的全部thread都是 daemon thread時,jvm終止運行。
api
thread表示一段程序的執行過程。繼承 thread類並重寫run方法。另外一種是實現runnable接口中的run方法。thread啓動是調用start運行,當thread代碼邏輯執行完後,thread自動結束。
緩存
10.1. 可見性 多線程
在一個多thread程序中,multi thread經過共同協做來完成指定的任務。thread間須要進行數據交換以協調各自的狀態。同一process中各個thread經過共享內存方式進行通訊。這些thread共享進程的地址空間,因此均可以自由的訪問所需的內存位置。互相協做的thread間對共享內存位置達成一致。一個thread在適當時候修改該內存的值,另一個thread在後續操做中經過讀取相同內存位置來獲得修改後的值。java中代碼沒法直接操做內存,而是經過不一樣類型的變量來間接訪問。thread a ,thread b共同協做完成任務,thread b須要等待threada完成後才能繼續運行,兩個thread可使用一個boolean類型的變量來協調狀態。when threada完成後修改該變量的值爲true,經過threadb。threadb運行時若是讀到該變量的值爲true,就開始本身的操做。使用共享內存方式在multi-thread中可能形成可見性相關的問題,即一個thread所做的修改對其餘thread並不可見,致使其餘thread仍然使用錯誤的值。好比threadb看不見threada對boolan狀態值的修改,形成threadb一直等待下去。
架構
單thread來講,可見性很容易理解和驗證。先改變一個變量的值,再讀取該變量的值,那麼所讀取的值必定是上次寫入操做寫的值。也就是說前面的寫入操做結果對後面的讀取操做是確定可見的。但在multi-thread中則不必定成立,若是不使用某些互斥或同步機制,則不能保證一個thread所寫入的值對另外一個thread是可見的,若是可見性條件不能成立,則程序運行過程當中就會出現問題。
併發
可見性問題總結 1 多thread 實際執行順序
app
2 cpu採用層次結構的多級緩存架構。提供L 1,L2,L3三級緩存,有時讀取數據時會直接從cache中讀取,致使數據的可見性問題 jvm
10.2 java memeory model java內存模型是一個抽象模型,只關注主存中的共享變量,屏蔽cpu及緩存等細節,簡化內存模型自身的定義。對象的實例域,靜態域和數據元素存儲在堆內存中,能夠被多個thread共享。編程語言
multithread中thread所執行的動做分爲兩類,一類是thread內部動做,好比操做方法聽局部變量等,另一種是thread間的動做。thread間動做由一個thread產生,同時被另外一個thread檢測至或受另外一thread的直接影響。包括讀取和寫入共享變量以及加鎖和解鎖等同步操做。thread間的動做不會產生可見性相關的問題,所以內存模型只考慮thread間動做。
常見同步關係以下所示,A和B保持同步含義是A必然發生在B前。
1 在一個監視器對象上的解鎖動做與相同對象上後續成功的加鎖動做保持同步
2對一個聲明volatile變量的寫入動做與同一變量的後續讀取操做保持同步
3 啓動一個thread的動做與該thread執行的第一個動做保持同步
4向thread共享變量寫入默認值動做與該thread執行的第一個動做保持同步。這種同步關係的含義是在thread運行以前,該thread所使用的所有對象從概念上說已經被建立出來,而且對象中變量被設置爲默認值。這種同步關係是保證thread所看到的變量值是肯定的。變量的值多是根據變量類型肯定的默認值,也多是其餘thread所設置的值,但不多是其餘的值
5threada運行時最後一個動做與另外一thread中任何能夠檢測到threada終止的動做保持同步
6若是threada中斷threadb,則threada的中斷動做與任何其餘thread檢測至threadb處於被中斷狀態的動做保持同步
還有一種同步關係是 在以前發生happens-before。它包括
1若是兩個動做a和b在一個thread中執行,同時在程序順序中a出如今b以前,則a在b以前發生
2一個對象的構造方法的結束在該對象的finalize方法運行以前發生
3若是動做a和動做b保持同步,則a在b以前發生
4 若是動做a在動做b以前發生,同時動做b在動做c以前發生,則a在c以前發生,也就是說「在以前發生」順序是傳遞性的。
數據競爭的存在是多thread運行時發生錯誤的根源。編寫多thread首要任務是找出並解決程序中存在的數據競爭。
10.3. volatile volatile用來對共享變量的訪問進行同步。對一個volatile變量的上一次寫入和下一次讀取之間存在「在以前發生」的順序。也就是說上一次寫入的操做結果對下一次讀取的操做是確定可見的。在寫入volatile變量值以後,cpu緩存中的內容會被寫回主存,在讀取volatile變量時,cpu緩存中的對應內容被置爲失效狀態,從新從主存中進行讀取。將變量聲明爲volatile至關於爲單個變量的讀取和寫入添加了同步操做。但volatile在使用時不須要利用鎖機制,所以性能要優於synchronized關鍵詞。關鍵詞volatile的主要做用是確保對一個變量的修改能正確被傳播到其餘thread中。最多見的使用場景是把循環結束的判斷條件聲明爲volatile。
如threada和threadb,threada在一個循環中不斷進行處理,threadb負責向threada發送中止處理信號。threada調用worker類的對象work方法,開始執行具體任務。在適當的時候,threadb會調用同一worker類的對象setdone方法來聲明終止任務的執行。把done聲明爲volatile是很重要的。只有這樣才能保證threadb對done變量所作的修改對於threada的後續操做是可見的。不然threada可能因爲沒法看到done變量值的變化而一直運行下去。
public class Worker{
private volatile boolean done;
public void setDone(boolean done){
this.done = done;
}
public void work(){
while(!done){
//perfome task here
}
}
}
使用volatile場景受限,寫入的變量新值與該變量當前值沒有關係,可使用,其它場景不可使用。
10.4 final
final聲明爲類時沒法被繼承,聲明爲方法時沒法在字類中被重寫。從內存模型角度來講,final關鍵詞最重要的是聲明一個域的值只能被初始化一次,並在初始化以後,該域的值沒法被修改。在multi-thread中final一般用來實現不可變對象immutable object。當對象中共享變量的值不可能發生變化時,在multi-thread訪問時就不會出現問題,也就不須要使用thread間的同步機制來進行處理。java中最多見的不可變對象是String類的對象。在多thread中應該儘可能可能使用不可變對象,以免使用同步機制。如下代碼把類中全部域聲明爲final,並在構造方法中進行init。
public class User{ private final String name; private final String email; public User(String name,String email){ this.name = name; this.email = email; } }
在構造方法成功完成以前,要確保正在建立的對象的引用不會被其餘thread訪問到,不然其餘thread可能看到部分建立完成的對象。下面是一個錯誤的示例。在構造方法中,把當前對象的引用賦值給另外一類的靜態變量會致使另外一類的thread看到還沒有建立完成的類的對象。該對象包含的變量可能沒有被初始化成正確的值。
public class WrongUser { private final String name; public WrongUser(String name){ UserHolder.user = this; this.name = name; } }
若是一個thread是在對象的構造方法成功完成以後才經過該對象的引用來進行訪問,則該thread確定能夠看到對象中final域被初始化以後的值。若是域沒有被聲明爲final,則在構造方法完成以後,其餘thread不必定能夠看到這個域被初始化以後的值,而有可能看到域的默認址。因爲final域具備這些特徵,編譯器對final域的處理很靈活。這些域可能被隨意地與其餘代碼進行從新排列。在代碼執行時,final域的值能夠被保存在寄存器中,而不用在主存中頻繁從新讀取。
10.5 atom operation 原子操做
在java中,對於非long型和double型的域的讀取和寫入操做是原子操做。對象引用的讀取和寫入操做也是原子操做。好比讀取一個int類型的域時,該域對應的內存地址中32位內容會被完整讀取,在讀取過程當中不會被其餘thread打斷。在進行寫入時也不會被中斷。在寫入非volatile的long型和double型的域時,分紅兩次操做來完成,一個long型或double型的域長度是64位,每次寫入32位,在一個thread寫入long型或double型的域的前32位以後,在寫入後32位以前,另外thread有可能訪問到這個域的值,從而讀取只完成部分寫入操做的錯誤值。所以在多thread中使用long型和double型的共享變量時,須要把變量聲明爲volatile,以保證讀取和寫入操做的完整性。