java高併發系列 - 第4天:JMM相關的一些概念

JMM(java內存模型),因爲併發程序要比串行程序複雜不少,其中一個重要緣由是併發程序中數據訪問一致性安全性將會受到嚴重挑戰。如何保證一個線程能夠看到正確的數據呢?這個問題看起來很白癡。對於串行程序來講,根本就是小菜一碟,若是你讀取一個變量,這個變量的值是1,那麼你讀取到的必定是1,就是這麼簡單的問題在並行程序中竟然變得複雜起來。事實上,若是不加控制地任由線程胡亂並行,即便本來是1的數值,你也可能讀到2。所以咱們須要在深刻了解並行機制的前提下,再定義一種規則,保證多個線程間能夠有小弟,正確地協同工做。而JMM也就是爲此而生的。java

JMM關鍵技術點都是圍繞着多線程的原子性、可見性、有序性來創建的。咱們須要先了解這些概念。安全

原子性

原子性是指操做是不可分的,要麼所有一塊兒執行,要麼不執行。在java中,其表如今對於共享變量的某些操做,是不可分的,必須連續的完成。好比a++,對於共享變量a的操做,實際上會執行3個步驟:性能優化

1.讀取變量a的值,假如a=1
2.a的值+1,爲2
3.將2值賦值給變量a,此時a的值應該爲2多線程

這三個操做中任意一個操做,a的值若是被其餘線程篡改了,那麼都會出現咱們不但願出現的結果。因此必須保證這3個操做是原子性的,在操做a++的過程當中,其餘線程不會改變a的值,若是在上面的過程當中出現其餘線程修改了a的值,在知足原子性的原則下,上面的操做應該失敗。併發

java中實現原子操做的方法大體有2種:鎖機制無鎖CAS機制,後面的章節中會有介紹。高併發

可見性

可見性是指一個線程對共享變量的修改,對於另外一個線程來講是不是能夠看到的。有些同窗會說修改同一個變量,那確定是能夠看到的,難道線程眼盲了?性能

爲何會出現這種問題呢?優化

看一下java線程內存模型:線程

  • 咱們定義的全部變量都儲存在主內存
  • 每一個線程都有本身獨立的工做內存,裏面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)
  • 線程對共享變量全部的操做都必須在本身的工做內存中進行,不能直接從主內存中讀寫(不能越級)
  • 不一樣線程之間也沒法直接訪問其餘線程的工做內存中的變量,線程間變量值的傳遞須要經過主內存來進行。(同級不能相互訪問)

線程須要修改一個共享變量X,須要先把X從主內存複製一份到線程的工做內存,在本身的工做內存中修改完畢以後,再從工做內存中回寫到主內存。
若是線程對變量的操做沒有刷寫回主內存的話,僅僅改變了本身的工做內存的變量的副本,那麼對於其餘線程來講是不可見的。
而若是另外一個變量沒有讀取主內存中的新的值,而是使用舊的值的話,一樣的也能夠列爲不可見。code

共享變量可見性的實現原理:

線程A對共享變量的修改要被線程B及時看到的話,須要進過如下步驟:

1.線程A在本身的工做內存中修改變量以後,須要將變量的值刷新到主內存中
2.線程B要把主內存中變量的值更新到工做內存中

關於線程可見性的控制,可使用volatilesynchronized來實現,後面章節會有詳細介紹。

有序性

有序性指的是程序按照代碼的前後順序執行。

爲了性能優化,編譯器和處理器會進行指令衝排序,有時候會改變程序語句的前後順序,好比程序。

int a = 1;  //1
int b = 20; //2
int c = a + b; //3

編譯器優化後可能變成

int b = 20;  //1
int a = 1; //2
int c = a + b; //3

上面這個例子中,編譯器調整了語句的順序,可是不影響程序的最終結果。

在單例模式的實現上有一種雙重檢驗鎖定的方式,代碼以下:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

咱們先看instance = new Singleton();

未被編譯器優化的操做:

  1. 指令1:分配一款內存M
  2. 指令2:在內存M上初始化Singleton對象
  3. 指令3:將M的地址賦值給instance變量

編譯器優化後的操做指令:

  1. 指令1:分配一塊內存S
  2. 指令2:將M的地址賦值給instance變量
  3. 指令3:在內存M上初始化Singleton對象

如今有2個線程,恰好執行的代碼被編譯器優化過,過程以下:

最終線程B獲取的instance是沒有初始化的,此時去使用instance可能會產生一些意想不到的錯誤。

如今比較好的作法就是採用靜態內部內的方式實現:

public class SingletonDemo {
    private SingletonDemo() {
    }
    private static class SingletonDemoHandler{
        private static SingletonDemo instance = new SingletonDemo();
    }
    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }
}

java高併發系列交流羣

相關文章
相關標籤/搜索