轉載請備註地址:https://juejin.im/post/5ab70c996fb9a028c812d06fhtml
系列文章傳送門:java
Java多線程學習(二)synchronized關鍵字(1)面試
Java多線程學習(二)synchronized關鍵字(2)編程
Java多線程學習(四)等待/通知(wait/notify)機制多線程
系列文章將被優先更新於微信公衆號「Java面試通關手冊」,歡迎廣大Java程序員和愛好技術的人員關注。併發
本節思惟導圖:編程語言
思惟導圖源文件+思惟導圖軟件關注微信公衆號:**「Java面試通關手冊」回覆關鍵字:「Java多線程」**免費領取。ide
在程序設計中,尤爲是在C語言、C++、C#和Java語言中,使用volatile關鍵字聲明的變量或對象一般具備與優化、多線程相關的特殊屬性。一般,volatile關鍵字用來阻止(僞)編譯器認爲的沒法「被代碼自己」改變的代碼(變量/對象)進行優化。如在C語言中,volatile關鍵字能夠用來提醒編譯器它後面所定義的變量隨時有可能改變,所以編譯後的程序每次須要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數。若是沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,若是這個變量由別的程序更新了的話,將出現不一致的現象。據
在C環境中,volatile關鍵字的真實定義和適用範圍常常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字,但在這些編程語言中volatile的用法和語義卻截然不同。
在 JDK1.2 以前,Java的內存模型實現老是從主存(即共享內存)讀取變量,是不須要進行特別的注意的。而在當前的 Java 內存模型下,線程能夠把變量保存本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致。
要解決這個問題,就須要把變量聲明爲 volatile,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。volatile 修飾的成員變量在每次被線程訪問時,都強迫從主存(共享內存)中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到主存(共享內存)。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值,這樣也就保證了同步數據的可見性。
RunThread.java
private boolean isRunning = true;
int m;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("進入run了");
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
}
System.out.println(m);
System.out.println("線程被中止了!");
}
}
複製代碼
Run.java
public class Run {
public static void main(String[] args) throws InterruptedException {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已經賦值爲false");
}
}
複製代碼
運行結果:
RunThread類中的isRunning變量沒有加上volatile關鍵字時,運行以上代碼會出現死循環,這是由於isRunning變量雖然被修改可是沒有被寫到主存中,這也就致使該線程在本地內存中的值一直爲true,這樣就致使了死循環的產生。
解決辦法也很簡單:isRunning變量前加上volatile關鍵字便可。
這樣運行就不會出現死循環了。 加上volatile關鍵字後的運行結果:你是否是覺得到這就完了?
不存在的!!!(這裏還有一點須要強調,下面的內容必定要看,否則你在用volatile關鍵字時會很迷糊,由於書籍幾乎都沒有提這個問題)
假如你把while循環代碼里加上任意一個輸出語句或者sleep方法你會發現死循環也會中止,無論isRunning變量是否被加上了上volatile關鍵字。
加上輸出語句:
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
System.out.println(m);
}
複製代碼
加上sleep方法:
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
複製代碼
這是爲何呢?
由於:JVM會盡力保證內存的可見性,即使這個變量沒有加同步關鍵字。換句話說,只要CPU有時間,JVM會盡力去保證變量值的更新。這種與volatile關鍵字的不一樣在於,volatile關鍵字會強制的保證線程的可見性。而不加這個關鍵字,JVM也會盡力去保證可見性,可是若是CPU一直有其餘的事情在處理,它也沒辦法。最開始的代碼,一直處於死循環中,CPU處於一直佔用的狀態,這個時候CPU沒有時間,JVM也不能強制要求CPU分點時間去取最新的變量值。而加了輸出或者sleep語句以後,CPU就有可能有時間去保證內存的可見性,因而while循環能夠被終止。
《Java併發編程藝術》這本書上說保證可是在自增操做(非原子操做)上不保證,《Java多線程編程核心藝術》這本書說不保證。
我我的更傾向於這種說法:volatile沒法保證對變量原子性的。我我的感受《Java併發編程藝術》這本書上說volatile關鍵字保證原子性嗎可是在自增操做(非原子操做)上不保證這種說法是有問題的。只是我的見解,但願不要被噴。能夠看下面測試代碼:
MyThread.java
public class MyThread extends Thread {
volatile public static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count=i;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
}
複製代碼
Run.java
public class Run {
public static void main(String[] args) {
MyThread[] mythreadArray = new MyThread[100];
for (int i = 0; i < 100; i++) {
mythreadArray[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
mythreadArray[i].start();
}
}
}
複製代碼
運行結果:
上面的「count=i;」是一個原子操做,可是運行結果大部分都是正確結果99,可是也有部分不是99的結果。
解決辦法:使用synchronized關鍵字加鎖。(這只是一種方法,Lock和AtomicInteger原子類均可以,由於以前學過synchronized關鍵字,因此咱們使用synchronized關鍵字的方法)
修改MyThread.java以下:
public class MyThread extends Thread {
public static int count;
synchronized private static void addCount() {
for (int i = 0; i < 100; i++) {
count=i;
}
System.out.println("count=" + count);
}
@Override
public void run() {
addCount();
}
}
複製代碼
這樣運行輸出的count就都爲99了,因此要保證數據的原子性仍是要使用synchronized關鍵字。
參考:
《Java多線程編程核心技術》
《Java併發編程的藝術》
極客學院Java併發編程wiki: http://wiki.jikexueyuan.com/project/java-concurrency/volatile1.html