Java併發編程基礎-線程安全問題及JMM(volatile)

什麼狀況下應該使用多線程 :

  線程出現的目的是什麼?解決進程中多任務的實時性問題?其實簡單來講,也就是解決「阻塞」的問題,阻塞的意思就是程序運行到某個函數或過程後等待某些事件發生而暫時中止 CPU 佔用的狀況,也就是說會使得 CPU 閒置。還有一些場景就是好比對於一個函數中的運算邏輯的性能問題,咱們能夠經過多線程的技術,使得一個函數中的多個邏輯運算經過多線程技術達到一個並行執行,從而提高性能因此,多線程最終解決的就是「等待」的問題,因此簡單總結的使用場景java

Ø 經過並行計算提升程序執行性能。linux

Ø 須要等待網絡、I/O 響應致使耗費大量的執行時間,能夠採用異步線程的方式來減小阻塞。c++

  如何應用多線程 在 Java 中,有多種方式來實現多線程。繼承 Thread 類、實現 Runnable 接口、使用 ExecutorService、Callable、Future 實現帶返回結果的多線程。編程

  繼承 Thread 類建立線程Thread:api

  類本質上是實現了 Runnable 接口的一個實例,表明一個線程的實例。啓動線程的惟一方法就是經過 Thread 類的 start()實例方法。start()方法是一個native 方法,它會啓動一個新線程,並執行 run()方法。這種方式實現多線程很簡單,經過本身的類直接 extend Thread,並複寫 run()方法,就能夠啓動新線程並執行本身定義的 run()方法。數組

public class MyThread extends Thread { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
}
MyThread myThread1 = new MyThread(); 
MyThread myThread2 = new MyThread(); 
myThread1.start(); 
myThread2.start(); 

  實現 Runnable 接口建立線程 :緩存

  若是本身的類已經 extends 另外一個類,就沒法直接 extends Thread,此時,能夠實現一個 Runnable 接口。安全

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 

  實現 Callable 接口經過 FutureTask 包裝器來建立:網絡

  Thread 線程有的時候,咱們可能須要讓一步執行的線程在執行完成之後,提供一個返回值給到當前的主線程,主線程須要依賴這個值進行後續的邏輯處理,那麼這個時候,就須要用到帶返回值的線程了。Java 中提供了這樣的實現方式。多線程

public class CallableDemo implements Callable<String> {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService executorService=Executors.newFixedThreadPool(1);
    CallableDemo callableDemo=new CallableDemo();
    Future<String> future=executorService.submit(callableDemo);
    System.out.println(future.get());//阻塞
    executorService.shutdown();
  }
  @Override
  public String call() throws Exception {
    int a=1;
    int b=2;
    System.out.println(a+b);
    return "執行結果:"+(a+b);
  }
}

  如何把多線程用得更加優雅 :合理的利用異步操做,能夠大大提高程序的處理性能,下面這個案例,若是看過 zookeeper 源碼的同窗應該都見過經過阻塞隊列以及多線程的方式,實現對請求的異步化處理,提高處理性能。接下去就模仿zk源碼的實現方式,經過異步化處理實現去打印跟保存請求信息的功能:

  request:(請求信息類)

public class Request {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Request{" +
        "name='" + name + '\'' +
        '}';
    }
} 

  RequestProcessor :(請求處理接口)

public interface RequestProcessor {
    void processRequest(Request request);
}

  PrintProcessor :(打印請求實現類),這裏採用了鏈式調用的處理過程,這種方式在不少的中間件中出現過,包括activeMQ在封裝請求處理的時候所用到的tcpTransport,以及zookeeper的源碼中都有體現,以及在dubbo的源碼中的 Cluster 最後也是採用鏈式封裝成了一個 MockCluster進行鏈式處理。

public class PrintProcessor extends Thread implements RequestProcessor{
  LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<Request>();
  private final RequestProcessor nextProcessor;
  public PrintProcessor(RequestProcessor nextProcessor) {
    this.nextProcessor = nextProcessor;
  }
  @Override
  public void run() {
    while (true) {
      try {
        Request request=requests.take();
        System.out.println("print data:"+request.getName());
        nextProcessor.processRequest(request);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  //處理請求
  public void processRequest(Request request) {
    requests.add(request);
  }
}

  SaveProcessor:(保存請求信息處理類)

public class SaveProcessor extends Thread implements RequestProcessor{
  LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<Request>();
  @Override
  public void run() {
    while (true) {
      try {
        Request request=requests.take();
        System.out.println("begin save request info:"+request);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  //處理請求
  public void processRequest(Request request) {
    requests.add(request);
  }
}

  Demo:

public class Demo {
  PrintProcessor printProcessor;
  protected Demo(){
    SaveProcessor saveProcessor=new SaveProcessor();
    saveProcessor.start();
    printProcessor=new PrintProcessor(saveProcessor);
    printProcessor.start();
  }
  private void doTest(Request request){
    printProcessor.processRequest(request);
  }
  public static void main(String[] args) {
    Request request=new Request();
    request.setName("Mic");
    new Demo().doTest(request);
  }
}

  這樣就實現了一個簡單的異步化的,鏈式調用過程,這樣看起來多線程的代碼顯得比較優雅。

Java 併發編程的基礎 :

  線程做爲操做系統調度的最小單元,而且可以讓多線程同時執行,極大的提升了程序的性能,在多核環境下的優點更加明顯。可是在使用多線程的過程當中,若是對它的特性和原理不夠理解的話,很容易形成各類問題。

  線程的狀態:

  Java 線程既然可以建立,那麼也勢必會被銷燬,因此線程是存在生命週期的,那麼咱們接下來從線程的生命週期開始去了解線程。線程一共有 6 種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

  NEW:初始狀態,線程被構建,可是尚未調用 start 方法。

  RUNNABLED:運行狀態,JAVA 線程把操做系統中的就緒和運行兩種狀態統一稱爲「運行中」。

  BLOCKED:阻塞狀態,表示線程進入等待狀態,也就是線程由於某種緣由放棄了 CPU 使用權,阻塞也分爲幾種狀況。

  Ø 等待阻塞:運行的線程執行 wait 方法,jvm 會把當前線程放入到等待隊列。

  Ø 同步阻塞:運行的線程在獲取對象的同步鎖(synchronized)時,若該同步鎖被其餘線程鎖佔用了,那麼 jvm 會把當前的線程放入到鎖池中。

  Ø 其餘阻塞:運行的線程執行 Thread.sleep 或者 t.join 方法,或者發出了 I/O請求時,JVM 會把當前線程設置爲阻塞狀態,當 sleep 結束、join 線程終止、io 處理完畢則線程恢復。

  WAITING:等待狀態。

  TIME_WAITING:超時等待狀態,超時之後自動返回。

  TERMINATED:終止狀態,表示當前線程執行完畢。

  經過相應命令顯示線程狀態 :

• 打開終端或者命令提示符,鍵入「jps」,(JDK1.5 提供的一個顯示當前全部 java進程 pid 的命令),能夠得到相應進程的 pid

• 根據上一步驟得到的 pid,繼續輸入 jstack pid(jstack 是 java 虛擬機自帶的一種堆棧跟蹤工具。jstack 用於打印出給定的 java 進程 ID 或 core file 或遠程調試服務的 Java 堆棧信息)

   線程的中止 :

  線程的終止,並非簡單的調用 stop 命令去。雖然 api 仍然能夠調用,可是和其餘的線程控制方法如 Thread.currentThread.suspend(相似linux的kill,暴力)、thread.resume() 同樣都是過時了的不建議使用,就拿 stop 來講,stop 方法在結束一個線程時並不會保證線程的資源正常釋放,所以會致使程序可能出現一些不肯定的狀態。要優雅的去中斷一個線程,在線程中提供了一個 interrupt 方法.或者經過成員變量:volatile boolean isStop,這樣的判斷。

  interrupt 方法 :

  當其餘線程經過調用當前線程的 interrupt 方法,表示向當前線程打個招呼,告訴他能夠中斷線程的執行了,至於何時中斷,取決於當前線程本身。線程經過檢查資深是否被中斷來進行相應,能夠經過 isInterrupted()來判斷是否被中斷。經過下面這個例子,來實現了線程終止的邏輯。

public class InterruptDemo {
  private static int i;
  public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
      while(!Thread.currentThread().isInterrupted()){
        i++;
      }
      System.out.println("Num:"+i);
    },"interruptDemo");
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();
  }
}

  這種經過標識位或者中斷操做的方式可以使線程在終止時有機會去清理資源,而不是武斷地將線程中止,所以這種終止線程的作法顯得更加安全和優雅。

  上面的案例中,經過 interrupt,設置了一個標識告訴線程能夠終止了,線程中還提供了靜態方法 Thread.interrupted()對設置中斷標識的線程復位。好比在上面的案例中,外面的線程調用 thread.interrupt 來設置中斷標識,而在線程裏面,又經過 Thread.interrupted 把線程的標識又進行了復位。

public class InterruptDemo {
  public static void main(String[] args) throws InterruptedException{
    Thread thread=new Thread(()->{
    while(true){
      boolean ii=Thread.currentThread().isInterrupted();
      if(ii){
        System.out.println("before:"+ii);
        Thread.interrupted();//對線程進行復位,中斷標識爲 false
        System.out.println("after:"+Thread.currentThread().isInterrupted());
      }
    }
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();//設置中斷標識,中斷標識爲 true
  }
}

  其餘的線程復位

  除了經過 Thread.interrupted 方法對線程中斷標識進行復位之外,還有一種被動復位的場景,就是對拋出 InterruptedException 異常的方法,在InterruptedException 拋出以前,JVM 會先把線程的中斷標識位清除,而後纔會拋出 InterruptedException,這個時候若是調用 isInterrupted 方法,將會返回 false。

線程的安全性問題 :

  你們都知道,線程會存在安全性問題,其實線程安全問題能夠總結爲: 可見性、原子性、有序性這幾個問題。

  可見性:如下demo就是說明了一個可見性的問題,此狀況下主線程修改了線程終止標識變量,但是thread線程無從得知,一直認爲該值是false,形成線程進入死循環。須要對變量 stop添加 volatile解決。

 

public class visibleDemo {
	
	private static boolean stop = false;
	
	public static void main(String[] args) throws InterruptedException {
		
		Thread thread =new Thread(()-> {
			int i=0;
			while(!stop) {
				i++;
			}
		});
		thread.start();
		TimeUnit.SECONDS.sleep(1);
		stop =true;
	}

}

  原子性:如下案例循環1000次所獲得的結果理論上會等於1000,但是實際會小於。這就是操做的原子性問題,好比兩個線程同時拿到這個 i 的值,此刻值爲100,線程1 增長1之後把值設置回去爲101,而此刻線程2繼線程1之後又把本身增長之後的值又放回去了,此刻仍是101.

public class AtomicDemo {

	private static int count =0;
	
	public static void  inc() {
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		count ++;
	}
	public static void main(String[] args) throws InterruptedException {
		for(int i=0;i<1000;i++) {
			new Thread(AtomicDemo::inc).start();
		}
		Thread.sleep(4000);
		System.out.println("運行結果:"+count);
	}
}

  順序性:順序性問題沒法演示,是因爲編譯期間CPU在不影響程序結果的時候,對於代碼指令進行優化重排序,致使代碼運行順序與編寫時的不一致。

CPU 高速緩存 :

  線程是 CPU 調度的最小單元,線程涉及的目的最終仍然是更充分的利用計算機處理的效能,可是絕大部分的運算任務不能只依靠處理器「計算」就能完成,處理器還須要與內存交互,好比讀取運算數據、存儲運算結果,這個 I/O 操做是很難消除的。而因爲計算機的存儲設備與處理器的運算速度差距很是大,因此現代計算機系統都會增長一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存和處理器之間的緩衝:將運算須要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中。

  高速緩存從下到上越接近 CPU 速度越快,同時容量也越小。如今大部分的處理器都有二級或者三級緩存,從下到上依次爲 L3 cache, L2 cache, L1 cache. 緩存又能夠分爲指令緩存和數據緩存,指令緩存用來緩存程序的代碼,數據緩存用來緩存程序的數據:

L1 Cache,一級緩存,本地 core(CPU) 的緩存,分紅 32K 的數據緩存 L1d 和 32k 指令緩存 L1i,訪問 L1 須要 3cycles,耗時大約 1ns;

L2 Cache,二級緩存,本地 core 的緩存,被設計爲 L1 緩存與共享的 L3 緩存之間的緩衝,大小爲 256K,訪問 L2 須要 12cycles,耗時大約 3ns;

L3 Cache,三級緩存,在同插槽的全部 core 共享 L3 緩存,分爲多個 2M 的段,訪問 L3 須要 38cycles,耗時大約 12ns;

緩存一致性問題 :

  CPU-0 讀取主存的數據,緩存到 CPU-0 的高速緩存中,CPU-1 也作了一樣的事情,而 CPU-1 把 count 的值修改爲了 2,而且同步到 CPU-1 的高速緩存,可是這個修改之後的值並無寫入到主存中,CPU-0 訪問該字節,因爲緩存沒有更新,因此仍然是以前的值,就會致使數據不一致的問題,從而引起原子性問題。引起這個問題的緣由是由於多核心 CPU 狀況下存在指令並行執行,而各個CPU 核心之間的數據不共享從而致使緩存一致性問題,這也是從硬件層面所致使的數據的可見性問題,爲了解決這個問題,CPU 生產廠商提供了相應的解決方案

總線鎖 :

  當一個 CPU 對其緩存中的數據進行操做的時候,往總線中發送一個 Lock 信號。其餘處理器的請求將會被阻塞,那麼該處理器能夠獨佔共享內存。總線鎖至關於把 CPU 和內存之間的通訊鎖住了,因此這種方式會致使 CPU 的性能降低,因此 P6 系列之後的處理器,出現了另一種方式,就是緩存鎖。

緩存鎖 :

  若是緩存在處理器緩存行中的內存區域在 LOCK 操做期間被鎖定,當它執行鎖操做回寫內存時,處理不在總線上聲明 LOCK 信號,而是修改內部的緩存地址,而後經過緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域的數據,當其餘處理器回寫已經被鎖定的緩存行的數據時會致使該緩存行無效。因此若是聲明瞭 CPU 的鎖機制,會生成一個 LOCK 指令,會產生兩個做用

1. Lock 前綴指令會引發引發處理器緩存回寫到內存,在 P6 之後的處理器中,LOCK 信號通常不鎖總線,而是鎖緩存

2. 一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效

緩存一致性協議 :

  處理器上有一套完整的協議,來保證 Cache 的一致性,比較經典的應該就是MESI 協議了,它的方法是在 CPU 緩存中保存一個標記位,這個標記爲有四種狀態

Ø M(Modified) 修改緩存,當前 CPU 緩存已經被修改,表示已經和內存中的數據不一致了

Ø I(Invalid) 失效緩存,說明 CPU 的緩存已經不能使用了

Ø E(Exclusive) 獨佔緩存,當前 cpu 的緩存和內存中數據保持一直,並且其餘處理器沒有緩存該數據

Ø S(Shared) 共享緩存,數據和內存中數據一致,而且該數據存在多個 cpu緩存中每一個 Core 的 Cache 控制器不只知道本身的讀寫操做,也監聽其它 Cache 的讀寫操做,嗅探(snooping)"協議

CPU 的讀取會遵循幾個原則:

1. 若是緩存的狀態是 I,那麼就從內存中讀取,不然直接從緩存讀取

2. 若是緩存處於 M 或者 E 的 CPU 嗅探到其餘 CPU 有讀的操做,就把本身的緩存寫入到內存,並把本身的狀態設置爲 S

3. 只有緩存狀態是 M 或 E 的時候,CPU 才能夠修改緩存中的數據,修改後,緩存狀態變爲 M

CPU 的優化執行 :

  除了增長高速緩存之外,爲了更充分利用處理器內內部的運算單元,處理器可能會對輸入的代碼進行亂序執行優化,處理器會在計算以後將亂序執行的結果充足,保證該結果與順序執行的結果一直,但並不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致,這個是處理器的優化執行;還有一個就是編程語言的編譯器也會有相似的優化,好比作指令重排來提高性能。

   其實原子性、可見性、有序性問題,是咱們抽象出來的概念,他們的核心本質就是上面提到的緩存一致性問題、處理器優化問題致使的指令重排序問題。好比緩存一致性就致使可見性問題、處理器的亂序執行會致使原子性問題、指令重排會致使有序性問題。爲了解決這些問題,因此在 JVM 中引入了 JMM 的概念。

Java Memory Model 內存模型 :

  內存模型定義了共享內存系統中多線程程序讀寫操做行爲的規範,來屏蔽各類硬件和操做系統的內存訪問差別,來實現 Java 程序在各個平臺下都能達到一致的內存訪問效果。Java 內存模型的主要目標是定義程序中各個變量的訪問規則,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量(這裏的變量,指的是共享變量,也就是實例對象、靜態字段、數組對象等存儲在堆內存中的變量。而對於局部變量這類的,屬於線程私有,不會被共享)。經過這些規則來規範對內存的讀寫操做,從而保證指令執行的正確性。它與處理器有關、與緩存有關、與併發有關、與編譯器也有關。他解決了 CPU多級緩存、處理器優化、指令重排等致使的內存訪問問題,保證了併發場景下的可見性、原子性和有序性,。內存模型解決併發問題主要採用兩種方式:限制處理器優化和使用內存屏障。

  Java 內存模型定義了線程和內存的交互方式,在 JMM 抽象模型中,分爲主內存、工做內存。主內存是全部線程共享的,工做內存是每一個線程獨有的。線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存中的變量。而且不一樣的線程之間沒法訪問對方工做內存中的變量,線程間的變量值的傳遞都須要經過主內存來完成,他們三者的交互關係以下:

  因此,總的來講,JMM 是一種規範,目的是解決因爲多線程經過共享內存進行通訊時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。目的是保證併發編程場景中的原子性、可見性和有序性。

JMM怎麼解決原子性、可見性、有序性的問題?

  在Java中提供了一系列和併發處理相關的關鍵字,好比volatile、Synchronized、final、juc(java.util.concurrent)等,這些就是Java內存模型封裝了底層的實現後提供給開發人員使用的關鍵字,在開發多線程代碼的時候,咱們能夠直接使用synchronized等關鍵詞來控制併發,使得咱們不須要關心底層的編譯器優化、緩存一致性的問題了,因此在Java內存模型中,除了定義了一套規範,還提供了開放的指令在底層進行封裝後,提供給開發人員使用。

  原子性保障:在java中提供了兩個高級的字節碼指令monitorenter和monitorexit,在Java中對應的Synchronized來保證代碼塊內的操做是原子的

  可見性:Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,可使用volatile來保證多線程操做時變量的可見性。除了volatile,Java中的synchronized和final兩個關鍵字也能夠實現可見性

  有序性:在Java中,可使用synchronized和volatile來保證多線程之間操做的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只容許一條線程操做。

volatile如何保證可見性

下載hsdis工具 ,https://sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download 解壓後存放到jre目錄的server路徑下:

  因爲我這邊使用的是 STS 工具,須要以下配置:run-> run configurations-> argments 加入-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

  而後用一個簡單的例子來看一下這段程序所造成的彙編語言中的差異:

public class ThreadDemo {

	private static volatile ThreadDemo instance=null;
	
	public static synchronized ThreadDemo getInstance() {
		if(instance ==null ) {
			instance =new ThreadDemo();
		}
		return instance;
	}
	
	public static void main(String[] args) {
		ThreadDemo.getInstance();
	}
}

  能夠看到本段示例中成員變量添加了 volatile 關鍵字,來看看他的彙編語言中最重要的一段:

  當不添加 volatile 關鍵字的時候,會發現搜索 lock 指令已經搜索不出來了。

  volatile變量修飾的共享變量,在進行寫操做的時候會多出一個 lock前綴的彙編指令,這個指令在前面CPU高速緩存的時候提到過,會觸發總線鎖或者緩存鎖,經過緩存一致性協議來解決可見性問題,對於聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,把這個變量所在的緩存行的數據寫回到系統內存,再根據咱們前面提到過的MESI的緩存一致性協議,來保證多CPU下的各個高速緩存中的數據的一致性。

volatile防止指令重排序:

  指令重排的目的是爲了最大化的提升CPU利用率以及性能,CPU的亂序執行優化在單核時代並不影響正確性,可是在多核時代的多線程可以在不一樣的核心上實現真正的並行,一旦線程之間共享數據,就可能會出現一些不可預料的問題,指令重排序必需要遵循的原則是,不影響代碼執行的最終結果,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序,(這裏所說的數據依賴性僅僅是針對單個處理器中執行的指令和單個線程中執行的操做.)這個語義,實際上就是as-if-serial語義,無論怎麼重排序,單線程程序的執行結果不會改變,編譯器、處理器都必須遵照as-if-serial語義。

  多核心多線程下的指令重排影響:

private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        a = 1;
        x = b;
    });
    Thread t2 = new Thread(() -> {
        b = 1;
        y = a;
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("x=" + x + "->y=" + y);
}

  若是不考慮編譯器重排序和緩存可見性問題,上面這段代碼可能會出現的結果是 x=0,y=1; x=1,y=0; x=1,y=1這三種結果,由於多是前後執行t1/t2,也多是反過來,還多是t1/t2交替執行,可是這段代碼的執行結果也有多是x=0,y=0。這就是在亂序執行的狀況下會致使的一種結果,由於線程t1內部的兩行代碼之間不存在數據依賴,所以能夠把x=b亂序到a=1以前;同時線程t2中的y=a也能夠早於t1中的a=1執行,那麼他們的執行順序多是

l t1: x=b

l t2:b=1

l t2:y=a

l t1:a=1

  因此從上面的例子來看,重排序會致使可見性問題。可是重排序帶來的問題的嚴重性遠遠大於可見性,由於並非全部指令都是簡單的讀或寫,好比DCL的部分初始化問題。因此單純的解決可見性問題還不夠,還須要解決處理器重排序問題。

內存屏障:

  內存屏障須要解決咱們前面提到的兩個問題,一個是編譯器的優化亂序和CPU的執行亂序,咱們能夠分別使用優化屏障和內存屏障這兩個機制來解決。

從CPU層面來了解一下什麼是內存屏障:

  CPU的亂序執行,本質仍是,因爲在多CPU的機器上,每一個CPU都存在cache,當一個特定數據第一次被特定一個CPU獲取時,因爲在該CPU緩存中不存在,就會從內存中去獲取,被加載到CPU高速緩存中後就能從緩存中快速訪問。當某個CPU進行寫操做時,它必須確保其餘的CPU已經將這個數據從他們的緩存中移除,這樣才能讓其餘CPU安全的修改數據。顯然,存在多個cache時,咱們必須經過一個cache一致性協議來避免數據不一致的問題,而這個通信的過程就可能致使亂序訪問的問題,也就是運行時的內存亂序訪問。如今的CPU架構都提供了內存屏障功能,在x86的cpu中,實現了相應的內存屏障寫屏障(store barrier)、讀屏障(load barrier)和全屏障(Full Barrier),主要的做用是

Ø 防止指令之間的重排序

Ø 保證數據的可見性

store barrier:

  store barrier稱爲寫屏障,至關於storestore barrier, 強制全部在storestore內存屏障以前的全部執行,都要在該內存屏障以前執行,併發送緩存失效的信號。全部在storestore barrier指令以後的store指令,都必須在storestore barrier屏障以前的指令執行完後再被執行。也就是進制了寫屏障先後的指令進行重排序,是的全部store barrier以前發生的內存更新都是可見的(這裏的可見指的是修改值可見以及操做結果可見)。

load barrier:

  load barrier稱爲讀屏障,至關於loadload barrier,強制全部在load barrier讀屏障以後的load指令,都在loadbarrier屏障以後執行。也就是進制對load barrier讀屏障先後的load指令進行重排序, 配合store barrier,使得全部store barrier以前發生的內存更新,對load barrier以後的load操做是可見的

Full barrier:

  full barrier成爲全屏障,至關於storeload,是一個全能型的屏障,由於它同時具有前面兩種屏障的效果。強制了全部在storeload barrier以前的store/load指令,都在該屏障以前被執行,全部在該屏障以後的的store/load指令,都在該屏障以後被執行。禁止對storeload屏障先後的指令進行重排序。

  總結:內存屏障只是解決順序一致性問題,不解決緩存一致性問題,緩存一致性是由cpu的緩存鎖以及MESI協議來完成的。而緩存一致性協議只關心緩存一致性,不關心順序一致性。因此這是兩個問題。

編譯器層面如何解決指令重排序問題:

  在編譯器層面,經過volatile關鍵字,取消編譯器層面的緩存和重排序。保證編譯程序時在優化屏障以前的指令不會在優化屏障以後執行。這就保證了編譯時期的優化不會影響到實際代碼邏輯順序。若是硬件架構自己已經保證了內存可見性,那麼volatile就是一個空標記,不會插入相關語義的內存屏障。若是硬件架構自己不進行處理器重排序,有更強的重排序語義,那麼volatile就是一個空標記,不會插入相關語義的內存屏障。在JMM中把內存屏障指令分爲4類,經過在不一樣的語義下使用不一樣的內存屏障來進制特定類型的處理器重排序,從而來保證內存的可見性:

LoadLoad Barriers, load1 ; LoadLoad; load2 , 確保load1數據的裝載優先於load2及全部後續裝載指令的裝載

StoreStore Barriers,store1; storestore;store2 , 確保store1數據對其餘處理器可見優先於store2及全部後續存儲指令的存儲

LoadStore Barries, load1;loadstore;store2, 確保load1數據裝載優先於store2以及後續的存儲指令刷新到內存

StoreLoad Barries, store1; storeload;load2, 確保store1數據對其餘處理器變得可見, 優先於load2及全部後續裝載指令的裝載;這條內存屏障指令是一個全能型的屏障,在前面講cpu層面的內存屏障的時候有提到。它同時具備其餘3條屏障的效果。

  咱們能夠經過 JVM源碼來看一下這個所謂的 Barriers 。首先搞一個很是簡單的demo:

public class Demo {
	static volatile int i;
	public static void main(String[] args) {
		i=10;
	}
}

  而後經過javap -c Demo.class,去查看字節碼(若是沒.class,先編譯):

  會發現這裏有一個 ACC_VOLATILE,當把代碼中的 volatile 關鍵字拿掉之後就沒有這東西了。因此咱們經過這個差別去源碼中尋找。導入JVM源碼全局搜索這個東西,最後會在 accessFlags.hpp 這個文件中找到以下代碼:這裏定義了java中全部的關鍵字信息,咱們從其中看到了剛剛在字節碼中出現的ACC_VOLATILE,這裏定義了一個方法 is_volatile();

  全局搜索 is_volatile 方法,會在 bytecodeInterpreter.cpp 文件中找到該方法:

// 存儲值
 // Now store the result
 //
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) { // 判斷是否存在 volatile關鍵字
  if (tos_type == itos) {
    obj->release_int_field_put(field_offset, STACK_INT(-1));
  } else if (tos_type == atos) {
    VERIFY_OOP(STACK_OBJECT(-1));
    obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
    OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
  } else if (tos_type == btos) {
    obj->release_byte_field_put(field_offset, STACK_INT(-1));
  } else if (tos_type == ltos) {
    obj->release_long_field_put(field_offset, STACK_LONG(-1));
  } else if (tos_type == ctos) {
    obj->release_char_field_put(field_offset, STACK_INT(-1));
  } else if (tos_type == stos) {
    obj->release_short_field_put(field_offset, STACK_INT(-1));
  } else if (tos_type == ftos) {
    obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
  } else {
    obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
  }
  OrderAccess::storeload();//加入屏障
} else { ......

  這裏判斷了是否 volatile 進行了修飾,再判斷該變量的類型,咱們的 demo裏面是個int,因此他會調用 release_int_field_put() 這個方法,最後爲該操做添加了一個storeload 屏障。這個屏障的做用上面有說到了。搜索到在 oop.inline.hpp 文件中。

inline jint oopDesc::int_field_acquire(int offset) const{ 
  return OrderAccess::load_acquire(int_field_addr(offset));     
}
inline void oopDesc::release_int_field_put(int offset, jint contents){ 
  OrderAccess::release_store(int_field_addr(offset), contents);  
}

  繼而調用  OrderAccess::release_store 來執行,在orderAccess.hpp 文件中:

static void     release_store(volatile jubyte*  p, jubyte  v);

  在這裏的 volatile 已經不在是 java 語言中的含義了,這裏的volatile在 c++中的意思理解爲語言級別的內存屏障。volatile 指出 i 是隨時可能發生變化的,每次使用它的時候必須從 i的地址中讀取。不會去作代碼優化,好比說指令重排序。強制對緩存的修改,當即寫入到主內存,若是是寫操做會致使其餘緩存失效。其中有幾個規則來防止指令重排序:

  1.對每一個 volatile 寫操做的前面會插入 storestore barrier

  2.對每一個 volatile 寫操做的後面會插入 storeload barrier

  3.對每一個 volatile 讀操做的前面會插入 loadloadbarrier

  4.對每一個 volatile 讀操做的後面會插入 loadstore barrier

  能夠發現該方法在本類中並無實現,這裏的實現是根據運行環境的操做系統來決定的,這也正是JVM爲何能一次編寫處處運行的緣由之一哦。能夠在源碼包中的  hotspot/src/os_cpu中找到:

  在源碼的最開始的 bytecodeInterpreter.cpp 文件所操做的方法最後是執行了一個  OrderAccess::storeload() 來添加屏障。對應是實現以 linux_x86 爲例 是在 orderAccess_linux_x86.inline.hpp文件中:咱們發現了 4種屏障指令 都在這裏了。而storeload 繼續調用了 fence() 方法

// Implementation of class OrderAccess.

inline void OrderAccess::loadload()   { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore()  { acquire(); }
inline void OrderAccess::storeload()  { fence(); }

  fence() 方法:該方法加入了個彙編指令 「lock」 ,這個指令似曾相識啊?也就是上面 volatile怎麼解決可見性的問題終提到的查看彙編指令中lock 的來源,從而解決緩存一致性協議來解決可見性問題。

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64// 這裏的volatile是禁止編譯器對代碼的優化重排序
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

   volatile 能夠保證可見性以及避免指令重排序帶來的問題,可是它沒法保證在多線程環境下程序的並行執行,內存屏障沒法保證屏障先後的程序執行過程當中沒有其餘線程對某個變量的修改操做。好比一個 i++ 操做,對一個原子遞增的操做,是一個符合操做,會分爲三個步驟:1.讀取volatile變量的值到local;2.增長變量的值;3.把local的值寫回讓其餘線程可見,可是在store以前,其餘線程拿到的是舊的值去加1,這就是它不能保證原子性的緣由。這裏須要利用另一個關鍵字來解決該問題:synchronized。

相關文章
相關標籤/搜索