什麼叫「可見性」?java
一個線程對共享變量值的修改,可以被其餘線程及時看到。緩存
共享變量:若是一個變量在多個線程的工做內存中存在副本,那麼這個變量就是這幾個線程的共享變量。安全
全部變臉都存在主內存中,每一個線程都有本身獨立的工做內存,裏面保存該線程使用大的變量副本,關係以下圖所示:多線程
多線程遵照的兩條規定eclipse
1.線程對共享變量全部的操做都只能在本身的工做內存中完成,沒法直接從主內存中讀寫ide
2.不一樣線程之間沒法訪問其餘線程中的變量,線程中變量值的傳遞須要經過主內存來完成。性能
共享變量可見性的實現原理優化
線程1對共享變量的修改若是要被線程2及時看到,須要通過2個步驟:this
1.把工做內存1中更新過的共享變量值刷新到主內存中spa
2.把主內存中最新的共享變量的值更新打工做內存2中
以上2個步驟,任意一個出現問題,都會致使共享變量沒法被其餘線程及時看到,沒法實現可見性,致使其餘線程讀取的數據不許確從而產生線程不安全。
共享變量可見性的實現方式
Java語言層面支持的可見性實現方式有2種,分別是synchronized、volatile。
synchronized:可以實現原子性(同步)和可見性
volatile:可以保證可見性,可是沒法保證原子性
synchronized是如何實現可見性?
java內存模型(JMM)中關於synchronized的兩條規定:
1).線程解鎖前,必須把共享變量的最新值刷新到主內存中
2).線程加鎖時,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新值(注意:加鎖與解鎖須要是同一把鎖)
線程執行互斥代碼的過程
1.得到互斥鎖
2.清空工做內存
3.從主內存拷貝變量的最新副本到工做內存中
4.執行代碼
5.將更改後的共享變量值刷新到主內存中
6.釋放互斥鎖
指令重排序
代碼書寫的順序與實際執行的順序不一樣,指令重排序是編譯器或者處理器爲了提升程序性能而作的優化。
目前的指令從排序有3種方式:
1.編譯器優化的重排序(編譯器優化)
2.處理器優化的重排序(處理器優化)
3.內存優化的重排序(處理器優化)
as-if-serial
不管如何重排序,程序執行的結果都應該與代碼順序執行的結果一致(java編譯器和處理器運行時都會保證在單線程中遵循as-if-serial規則,多線程存在程序交錯執行時,則不遵照)
舉例:
int num1 = 1;
int num2 = 2;
int num3 = num1 + num2;
上面3行代碼,在單線程時,第一、2行能夠進行重排序,可是第3行不能夠,不然結果將不同,因此從排序不會給單線程帶來內存可見性的問題。
而在多線程中,程序交錯執行時,重排序則會形成內存可見性的問題。
Synchronized實現可見性的代碼,如下的這個類SynchronizedDemo
public class SynchronizedDemo { // 共享變量 private boolean ready = false; private int num = 1; private int result = 0; // 寫操做 public void write() { ready = true; // 1.1 num = 2; // 1.2 } // 讀操做 public void read() { if (ready) { // 2.1 result = num * 3; // 2.2 } System.out.println("result = " + result); } private class ReadWriteThread extends Thread { private boolean flag; public ReadWriteThread(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { write(); } else { read(); } } } public static void main(String[] args) { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); synchronizedDemo.new ReadWriteThread(false).start(); synchronizedDemo.new ReadWriteThread(true).start(); } }
上面的這一段代碼重排序後的執行順序多是
1. 1.2-->2.1-->2.2-->1.1; result=0
2. 1.1-->2.1-->2.2-->1.2; result=3
......
致使共享變量在線程之間不可見的緣由
1.線程的交叉執行
2.重排序結合線程交叉執行
3.共享變量更新後的值,沒有在工做內存與主內存間及時刷新
安全的代碼,加入synchronized關鍵字
// 寫操做 public synchronized void write() { ready = true; // 1.1 num = 3; // 1.2 } // 讀操做 public synchronized void read() { if (ready) { // 2.1 result = num * 2; // 2.2 } System.out.println("result = " + result); }
volatile是如何實現可見性?
深刻來講,是經過加入內存屏障和禁止重排序優化來實現的。
對volatile變量執行寫操做時,會在寫操做後加入一條store屏障指令,會將cup數據強制刷新到主內存中去
對volatile變量執行讀操做時,會在讀操做前加入一條load屏障指令,強制緩存器中的緩存失效,每次使用都要去主內存中從新獲取數據
通俗地講,volatile變量在每次被訪問的時候,都強迫從主內存中讀取該變量的值,而當該變量在發生變化時,又會強迫變量講最新的值刷新到主內存中,這樣,任意時刻,不一樣的線程總能看到該變量的最新值。
線程寫volatile變量的過程:
1.改變線程工做內存中volatile變量副本的值
2.將改變的副本的值從工做內存中刷新到主內存中
線程讀volatile變量的過程:
1.從主內存中讀取volatile變量的最新值到工做內存中
2.從工做內存中讀取volatile變量的副本
volatile不能保證原子性,請看下面的代碼:
public class VolatileDemo { private volatile int num = 0; public int getNum() { return this.num; } public void increase() { // num++,不是原子操做,這裏會先讀取,再加1 this.num++; } public static void main(String[] args) { final VolatileDemo volatileDemo = new VolatileDemo(); // 建立500個子線程,執行increase方法,每次都讓num加1 for (int i = 0; i < 500; i++) { new Thread(new Runnable() { @Override public void run() { volatileDemo.increase(); } }).start(); } // 等到全部子線程執行完畢,eclipse這裏是1,IntelliJ IDEA執行用戶代碼的時候,實際是經過反射方式去調用,而與此同時會建立一個Monitor Ctrl-Break 用於監控目的,全部是2 while (Thread.activeCount() > 2) { Thread.yield(); } // 因爲num使用了volatile關鍵字,因此預期值應該是500 System.out.println("當前的num值=" + volatileDemo.getNum()); } }
執行後,會發現,有時候不是500,而是499或者498或者497等等,緣由是num++不是原子操做,volatile只能保證變量修改後的可見性,可是沒法保證原子性,請看下面的步驟:
假設如今num=5
1.線程A讀取num的值,線程A的工做內存中,num=5
2.線程B也讀取了num的值,線程B的工做內存中,num=5
3.線程B進行加1操做,線程B的工做內存中,num=6
4.線程B寫入最新的num值,主線程中num的值變爲6
5.線程A執行加1操做,線程A的工做內存中,num=6
6.線程A寫入最新的num值,主線程中num的值變爲6
這樣,兩個線程各自執行了一次加1操做,可是主線程中的數據num=6,這就是因爲volatile沒辦法保證代碼的原子性,使得讀和寫不是一塊兒的
解決方案:
1.使用synchronized關鍵字
2.使用ReentrantLock
3.使用AtomicInteger
volatile的適用場景
1.對變量的寫入操做不依賴其當前值
不知足:num++、count = count * 5
知足:boolean值變量,記錄溫度變化的變量等等
2.該變量沒有包含在具備其餘變量的不變式中
不知足:low < up
通常的應用場景不少會不知足其中一個,因此volatile是使用沒喲synchronized這麼普遍。
synchronized與volatile比較
1.volatile不須要加鎖,比synchronized更輕量級,不會阻塞線程
2.從內存的角度,volatile讀操做至關於加鎖,寫操做至關於解鎖
3.synchronized既能保證原子性又能保證可見性,而volatile只能保證可見性沒法保證原子性