對於Java併發編程,通常來講有如下的關注點:java
線程安全性,正確性。web
線程的活躍性(死鎖,活鎖)編程
性能緩存
其中線程的安全性問題是首要解決的問題,線程不安全,運行出來的結果和預期不一致,那就連基本要求都沒達到了。安全
保證線程的安全性問題,本質上就是保證線程同步,實際上就是線程之間的通訊問題。咱們知道,在操做系統中線程通訊有如下幾種方式:多線程
信號量併發
信號app
管道jvm
共享內存socket
消息隊列
socket
java中線程通訊主要使用共享內存的方式。共享內存的通訊方式首先要關注的就是可見性和有序性。而原子性操做通常都是必要的,因此主要關注這三個問題。
原子性是指操做是不可分的。其表如今於對於共享變量的某些操做,應該是不可分的,必須連續完成。例如a++,對於共享變量a的操做,實際上會執行三個步驟:
讀取變量a的值
a的值+1
將值賦予變量a 。
這三個操做中任何一個操做過程當中,a的值被人篡改,那麼都會出現咱們不但願出現的結果。因此咱們必須保證這是原子性的。Java中的鎖的機制解決了原子性的問題。
可見性是值一個線程對共享變量的修改,對於另外一個線程來講是不是能夠看到的。
爲何會出現這種問題呢?
咱們知道,java線程通訊是經過共享內存的方式進行通訊的,而咱們又知道,爲了加快執行的速度,線程通常是不會直接操做內存的,而是操做緩存。
java線程內存模型:
實際上,線程操做的是本身的工做內存,而不會直接操做主內存。若是線程對變量的操做沒有刷寫會主內存的話,僅僅改變了本身的工做內存的變量的副本,那麼對於其餘線程來講是不可見的。而若是另外一個變量沒有讀取主內存中的新的值,而是使用舊的值的話,一樣的也能夠列爲不可見。
對於jvm來講,主內存是全部線程共享的java堆,而工做內存中的共享變量的副本是從主內存拷貝過去的,是線程私有的局部變量,位於java棧中。
那麼咱們怎麼知道何時工做內存的變量會刷寫到主內存當中呢?
這就涉及到java的happens-before關係了。
在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。
這我的的博客寫的不錯:http://ifeve.com/easy-happens-before/。
簡單來講,只要知足了happens-before關係,那麼他們就是可見的。
例如:
線程A中執行i=1,線程B中執行j=i。若是線程A的操做和線程B的操做知足happens-before關係,那麼j就必定等於1,不然j的值就是不肯定的。
happens-before關係以下:
程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;
鎖定規則:一個unLock操做先行發生於後面對同一個鎖額lock操做;
volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做;
傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做;
線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
從上面的happens-before規則,顯然,通常只須要使用volatile關鍵字,或者使用鎖的機制,就能實現內存的可見性了。
有序性是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。
爲何會出現不一致的狀況呢?
這是因爲重排序的緣故。
在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
舉個例子:
線程A:
context = loadContext();
inited = true;
線程B:
while(!inited ){
sleep
}
doSomethingwithconfig(context);
若是線程A發生了重排序:
inited = true;
context = loadContext();
那麼線程B就會拿到一個未初始化的content去配置,從而引發錯誤。
由於這個重排序對於線程A來講是不會影響線程A的正確性的,而若是loadContext()方法被阻塞了,爲了增長Cpu的利用率,這個重排序是可能的。
若是要防止重排序,須要使用volatile關鍵字,volatile關鍵字能夠保證變量的操做是不會被重排序的。