多執行緒之記憶體可見性Volatile(一)
從這篇博文開始,我們開始分享一些多執行緒的內容,畢竟在工作中,使用多執行緒比較多。多總結一下,終歸沒有壞處。這個系列的文章不會特別長,爭取在3到5分鐘之間結束,主要以說明白內容,給出相應的解決方案,重點在於實踐。
如標題所示,這篇博文我們簡單的介紹一下記憶體可見性問題,之前,要簡單的介紹一下什麼是記憶體模型?
記憶體模型
什麼是JAVA 記憶體模型?
Java Memory Model (JAVA 記憶體模型)是描述執行緒之間如何通過記憶體(memory)來進行互動。 具體說來, JVM中存在一個主存區(Main Memory或Java Heap Memory),對於所有執行緒進行共享,而每個執行緒又有自己的工作記憶體(Working Memory),工作記憶體中儲存的是主存中某些變數的拷貝,執行緒對所有變數的操作並非發生在主存區,而是發生在工作記憶體中,而執行緒之間是不能直接相互訪問,變數在程式中的傳遞,是依賴主存來完成的。
Java記憶體模型的抽象示意圖如下:
從上圖來看,執行緒A與執行緒B之間如要通訊的話,必須要經歷下面2個步驟:
1、執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。
2、執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。
說明白了記憶體模型,我們看一看什麼是記憶體可見性?
記憶體可見性
記憶體可見性(Memory Visibility)是指當某個執行緒正在使用物件狀態而另一個執行緒在同時修改該狀態,需要確保當一個執行緒修改了物件狀態後,其他執行緒能夠立即看到發生的狀態變化。
由於執行緒之間的互動都發生在主記憶體中,但對於變數的修改又發生在自己的工作記憶體中,經常會造成讀寫共享變數的錯誤,我們也叫可見性錯誤。
可見性錯誤是指當讀操作與寫操作在不同的執行緒中執行時,我們無法確保執行讀操作的執行緒能適時地看到其他執行緒寫入的值,有時甚至是根本不可能的事情。
解決方案
我們可以通過同步來保證物件被安全地釋出。除此之外我們也可以使用一種更加輕量級的volatile變數,還可以使用ReentrantLock,CAS等等。
synchronized關鍵字
public class TestSynchronized {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
synchronized (td) {
if (td.getFlag()) {
System.out.println("主執行緒flag:" + td.getFlag());
break;
}
}
}
}
}
class ThreadDemo implements Runnable {
//共享變數
private boolean flag = false;
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
flag = true;
System.out.println("其他執行緒flag=" + getFlag());
}
}
同步鎖方案:會帶來效能問題,效率特別低,造成執行緒阻塞。
volatile關鍵字
java 提供了一種稍弱的同步機制,即volatile變數,用來確保將變數的更新操作通知到其他執行緒。當多個執行緒進行操作共享資料時,可以保證記憶體中的資料可見。 相較於synchronized是一種較為輕量級的同步策略。
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){
if(td.getFlag()){
System.out.println("主執行緒flag:" + td.getFlag());
break;
}
}
}
}
class ThreadDemo implements Runnable{
//共享變數
private volatile boolean flag = false;
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
flag = true;
System.out.println("其他執行緒flag=" + getFlag());
}
}
volatile的讀寫操作的過程:
(1)執行緒寫volatile變數的過程:
1、改變執行緒工作記憶體中volatile變數的副本的值
2、將改變後的副本的值從工作記憶體重新整理到主記憶體
(2)執行緒讀volatile變數的過程:
1、從主記憶體中讀取volatile變數的最新值到執行緒的工作記憶體中
2、從工作記憶體中讀取volatile變數的副本
volatile方案:
1、能夠保證volatile變數的可見性
2、不能保證變數狀態的”原子性操作(Atomic operations)”
雖然我們對Volatile的記憶體可見性有了一定理解,我們還需要對它有更加深刻的認識,不再多寫了:面試時被問到了volatile ,找個文章總結一下(早點看到就好了)
總結
如果大家既想解決記憶體可見性又想實現原子性操作,以上兩種方案均不可,可以使用lock或CAS,畢竟我們這篇部落格,介紹記憶體可見性,下篇的博文我們會深入的介紹原子操作和cas演算法。
後面的博文馬上殺到,敬請期待。