面試官:你對多線程熟悉嗎,談談線程安全中的原子性,有序性和可見性?

對於Java併發編程,通常來講有如下的關注點:java

  1. 線程安全性,正確性。web

  2. 線程的活躍性(死鎖,活鎖)編程

  3. 性能緩存

其中線程的安全性問題是首要解決的問題,線程不安全,運行出來的結果和預期不一致,那就連基本要求都沒達到了。安全

保證線程的安全性問題,本質上就是保證線程同步,實際上就是線程之間的通訊問題。咱們知道,在操做系統中線程通訊有如下幾種方式:多線程

  1. 信號量併發

  2. 信號app

  3. 管道jvm

  4. 共享內存socket

  5. 消息隊列

  6. socket

java中線程通訊主要使用共享內存的方式。共享內存的通訊方式首先要關注的就是可見性和有序性。而原子性操做通常都是必要的,因此主要關注這三個問題。

1.原子性

原子性是指操做是不可分的。其表如今於對於共享變量的某些操做,應該是不可分的,必須連續完成。例如a++,對於共享變量a的操做,實際上會執行三個步驟:

  1. 讀取變量a的值

  2. a的值+1

  3. 將值賦予變量a 。

這三個操做中任何一個操做過程當中,a的值被人篡改,那麼都會出現咱們不但願出現的結果。因此咱們必須保證這是原子性的。Java中的鎖的機制解決了原子性的問題。

2.可見性

可見性是值一個線程對共享變量的修改,對於另外一個線程來講是不是能夠看到的。

爲何會出現這種問題呢?

咱們知道,java線程通訊是經過共享內存的方式進行通訊的,而咱們又知道,爲了加快執行的速度,線程通常是不會直接操做內存的,而是操做緩存。

java線程內存模型:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

實際上,線程操做的是本身的工做內存,而不會直接操做主內存。若是線程對變量的操做沒有刷寫會主內存的話,僅僅改變了本身的工做內存的變量的副本,那麼對於其餘線程來講是不可見的。而若是另外一個變量沒有讀取主內存中的新的值,而是使用舊的值的話,一樣的也能夠列爲不可見。

對於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關係以下:

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;

  2. 鎖定規則:一個unLock操做先行發生於後面對同一個鎖額lock操做;

  3. volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做;

  4. 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;

  5. 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做;

  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;

  7. 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;

  8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;

從上面的happens-before規則,顯然,通常只須要使用volatile關鍵字,或者使用鎖的機制,就能實現內存的可見性了。

3.有序性

有序性是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。

爲何會出現不一致的狀況呢?

這是因爲重排序的緣故。

在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關鍵字能夠保證變量的操做是不會被重排序的。

相關文章
相關標籤/搜索