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

推薦學習:蘑菇街Java大牛純手打肛出的一份多線程文檔,請別丟進收藏夾吃灰

注,本篇只是解析基本概念,用作面試應答,非深入

對於Java併發編程,一般來說有以下的關注點:

  1. 線程安全性,正確性。
  2. 線程的活躍性(死鎖,活鎖)
  3. 性能

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

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

  1. 信號量
  2. 信號
  3. 管道
  4. 共享內存
  5. 消息隊列
  6. socket

java中線程通信主要使用共享內存的方式。共享內存的通信方式首先要關注的就是可見性和有序性。而原子性操作一般都是必要的,所以主要關注這三個問題。

1.原子性

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

  1. 讀取變量a的值
  2. a的值+1
  3. 將值賦予變量a 。

這三個操作中任何一個操作過程中,a的值被人篡改,那麼都會出現我們不希望出現的結果。所以我們必須保證這是原子性的。Java中的鎖的機制解決了原子性的問題。

2.可見性

可見性是值一個線程對共享變量的修改,對於另一個線程來說是否是可以看到的。

爲什麼會出現這種問題呢?

我們知道,java線程通信是通過共享內存的方式進行通信的,而我們又知道,爲了加快執行的速度,線程一般是不會直接操作內存的,而是操作緩存。

java線程內存模型:

實際上,線程操作的是自己的工作內存,而不會直接操作主內存。如果線程對變量的操作沒有刷寫會主內存的話,僅僅改變了自己的工作內存的變量的副本,那麼對於其他線程來說是不可見的。而如果另一個變量沒有讀取主內存中的新的值,而是使用舊的值的話,同樣的也可以列爲不可見。

對於jvm來說,主內存是所有線程共享的java堆,而工作內存中的共享變量的副本是從主內存拷貝過去的,是線程私有的局部變量,位於java棧中。

那麼我們怎麼知道什麼時候工作內存的變量會刷寫到主內存當中呢?

這就涉及到java的happens-before關係了。

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在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關鍵字可以保證變量的操作是不會被重排序的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章