[jvm虛擬機(jī)]java內(nèi)存模型

本文并非我的原創(chuàng)文章,而是我學(xué)習(xí)jvm時(shí)的筆記。文中的材料與數(shù)據(jù)大部分來自于其它資料,詳細(xì)請(qǐng)查看本文的引用章節(jié)。

JAVA內(nèi)存模型

猶記得大學(xué)時(shí)操作系統(tǒng)課上,我們迷茫的眼神注視著帶著厚眼鏡教授向我們一遍遍的強(qiáng)調(diào),一個(gè)程序最少有一個(gè)進(jìn)程組成,進(jìn)程是操作系統(tǒng)提供獨(dú)立資源供應(yīng)用程序運(yùn)行的基本單位。另外老師向我們講到,為了更好的提高計(jì)算機(jī)的并行計(jì)算能力,計(jì)算機(jī)科學(xué)家們又設(shè)計(jì)了線程。線程是比進(jìn)程更小的單位,一個(gè)進(jìn)程可以由多個(gè)線程組成。同時(shí)線程也是在得到cpu時(shí)間片時(shí)可運(yùn)行的最小的單位。尤記得在這些理論基礎(chǔ)下,我慢慢的學(xué)會(huì)了使用C在LINUX環(huán)境下使用多進(jìn)程和多線程進(jìn)行編程。這些并發(fā)API都是LINUX提供的標(biāo)準(zhǔn)API,程序在編譯鏈接之后可以直接調(diào)用通過系統(tǒng)內(nèi)核創(chuàng)建進(jìn)程或線程,在類UNIX操作系統(tǒng)下這樣去做可以讓程序有更高的性能。但是這種編程方式所編寫出來的代碼是與操作系統(tǒng)綁定的,我在linux下明明可以完美運(yùn)行的代碼,在windows下連編譯甚至都做不到。除非是用 windows API重新把與操作系統(tǒng)進(jìn)行交互的那些代碼給替換掉,否則就不要想著讓程序去跨平臺(tái)運(yùn)行了。
在進(jìn)行并發(fā)編程時(shí)相對(duì)于C,我更喜歡java的編程體驗(yàn)。java消除操作系統(tǒng)之間的差異,原生的對(duì)多線程應(yīng)用提供了很好的支持,特別是在jdk1.5之后,jdk還提供了currency包,更好的讓程序員們無需去關(guān)注并發(fā)的難點(diǎn)與細(xì)節(jié),更專心的關(guān)注應(yīng)用的業(yè)務(wù)需求實(shí)現(xiàn)。
在多線程編程中最長遇到的問題就是線程安全問題,其中又以內(nèi)存中的數(shù)據(jù)安全問題最為常見(這個(gè)數(shù)據(jù)安全指的是發(fā)生臟讀、幻讀等并發(fā)編程中會(huì)遇到的錯(cuò)誤)。為了消除不同硬件和操作系統(tǒng)對(duì)內(nèi)存操作的差異,在硬件設(shè)備和操作系統(tǒng)的內(nèi)存模型之上,java虛擬機(jī)規(guī)范定義了一種java內(nèi)存模型(JAVA Memory Model,簡稱JMM),將內(nèi)存分為了工作內(nèi)存和主內(nèi)存。JMM主要定義了JVM中在內(nèi)存中操作變量的規(guī)則和細(xì)節(jié),用來解決在并發(fā)競(jìng)爭(zhēng)對(duì)變量操作時(shí)可能會(huì)發(fā)生的各種問題。JMM完全兼容CPU的多級(jí)cache機(jī)制,并且支持編譯器的代碼重排序。
注意:JMM內(nèi)存模型的概念不同于jvm中6大內(nèi)存區(qū)域的概念,兩者不可強(qiáng)行混為一談。

主內(nèi)存和工作內(nèi)存

JMM規(guī)定了所有的全局變量都存于主內(nèi)存(Main Memory)中,一般情況下這些全局變量都會(huì)被保存到堆里。每個(gè)線程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中會(huì)保存當(dāng)前線程所需用到的主內(nèi)存中全局變量的拷貝和自己的局部變量。一般情況下工作內(nèi)存指的是棧內(nèi)存,從物理上來講,工作內(nèi)存一般情況下都會(huì)工作于cpu的cache里。一個(gè)線程對(duì)全局變量的任何操作必須在自己的工作內(nèi)存中進(jìn)行,不允許直接操作工作內(nèi)存。不同線程不能訪問對(duì)方的工作內(nèi)容,只能通過主內(nèi)存進(jìn)行數(shù)據(jù)交換。



{% asset_img /images/java/jvm/JMM交互關(guān)系圖.png JMM交互關(guān)系圖 %}

工作內(nèi)存在對(duì)主內(nèi)存中變量做拷貝時(shí),如果變量是基本類型的,則會(huì)拷貝其值;如果是引用類型的,那么僅僅會(huì)拷貝其引用

JMM內(nèi)存操作

JMM定義了8種主內(nèi)存與工作內(nèi)存之間的具體交互操作,虛擬機(jī)在實(shí)現(xiàn)JMM時(shí)必須保證每種內(nèi)存操作都是原子的。表1詳細(xì)列出了JMM的8種內(nèi)存操作。

| 指令 | 操作名 | 作用區(qū)域 | 描述
| :------ | :------ | :------ |
| lock | 鎖定 | 主內(nèi)存 | 把一個(gè)變量標(biāo)識(shí)為一個(gè)線程獨(dú)占的狀態(tài)
| unlock | 解鎖 | 主內(nèi)存 | 把一個(gè)處于鎖定狀態(tài)的變量釋放出來
| read | 讀取 | 主內(nèi)存 | 把一個(gè)變量的值從主內(nèi)存讀取到工作內(nèi)存中,以便于隨后的load指令使用
| load | 載入 | 工作內(nèi)存 | 把read指令從主內(nèi)存中讀取的變量值放入到工作內(nèi)存的變量副本中
| use | 使用 | 工作內(nèi)存 | 將工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎
| assign | 賦值 | 工作內(nèi)存 | 把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
| store | 存儲(chǔ) | 工作內(nèi)存 | 把一個(gè)工作內(nèi)存中變量的值傳送到主內(nèi)存中,以便于隨后的write指令使用
| write | 寫入 | 主內(nèi)存 | 把store指令從工作內(nèi)存?zhèn)鞒龅淖兞康闹祵懭氲街鲀?nèi)存的變量中
表1 JMM內(nèi)存操作指令表

20170702 01:48 編輯到JMM內(nèi)存操作指令表

JMM規(guī)定了在使用以上8種內(nèi)存操作時(shí)必須遵守以下規(guī)則:

  • read,load或store,write必須成對(duì)出現(xiàn)。如:執(zhí)行read操作從主內(nèi)存讀取一個(gè)變量后,必須在工作內(nèi)存使用load載入這個(gè)變量。兩者之間的順序不可錯(cuò),但兩者之間可以穿插其它指令。同理,在工作內(nèi)存中對(duì)一個(gè)變量使用了store指令,必須在主內(nèi)存中使用write指令進(jìn)行寫入。
  • 不允許一個(gè)線程丟棄它最近做的assign操作。變量在工作內(nèi)存中改變了之后必須將這個(gè)變化同步到主內(nèi)存中。
  • 不允許一個(gè)線程沒有發(fā)生過assign操作就將數(shù)據(jù)從工作內(nèi)存同步到主內(nèi)存。
  • 只能在主內(nèi)存中新建全局變量,不能在工作內(nèi)存中直接使用一個(gè)未被初始化的變量。可以使用assign和load指令在工作內(nèi)存對(duì)一個(gè)變量進(jìn)行初始化操作。
  • 一個(gè)變量在同一時(shí)刻只能被一個(gè)線程執(zhí)行l(wèi)ock操作。
  • 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock,那將清空工作內(nèi)存中此變量的值。在執(zhí)行引擎使用這個(gè)變量之前,需要重新load或assign重新初始化變量的值。
  • unlock只能解鎖被本線程鎖定的變量。
  • 對(duì)一個(gè)變量執(zhí)行unlock之前,必須將此變量同步回主內(nèi)存。

volatile 關(guān)鍵字

volatile是java語言所提供的關(guān)鍵字,使java最輕量級(jí)的內(nèi)存同步的措施。與 synchronized 塊相比,volatile 變量所需的編碼較少,并且運(yùn)行時(shí)開銷也較少,但是它所能實(shí)現(xiàn)的功能也僅是 synchronized 的一部分。

java中鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個(gè)線程持有某個(gè)特定的鎖,因此可使用該特性實(shí)現(xiàn)對(duì)共享數(shù)據(jù)的協(xié)調(diào)訪問協(xié)議,這樣,一次就只有一個(gè)線程能夠使用該共享數(shù)據(jù)。可見性要更加復(fù)雜一些,它必須確保釋放鎖之前對(duì)共享數(shù)據(jù)做出的更改對(duì)于隨后獲得該鎖的另一個(gè)線程是可見的 。
volatile 變量具有 synchronized 的可見性特性,但是不具備鎖的原子特性。所以即便volatile沒有不一致的問題,但volatile變量在并發(fā)的運(yùn)算下并不是原子操作,所以依然可能會(huì)有安全問題。

  package com.github.weiwei02.jvm.jmm.volatile_test;

  /**
  * volatile 線程安全性測(cè)試
  * @author Wang Weiwei <email>weiwei02@vip.qq.com / weiwei.wang@100credit.com</email>
  * @version 1.0
  * @sine 2017/7/2
  */
  public class VolatileTest {
  public static volatile int race = 0;
  public static final int THREAD_COUNT=20;

  public static void main(String[] args) {
      Thread threads[] = new Thread[THREAD_COUNT];
      System.out.println(race);
      for (int i =0; i < THREAD_COUNT; i++){
          threads[i] = new Thread(() -> {
              for (int j = 0; j < 1000 ; j++) {
                  increase();
              }
          });
          threads[i].start();
      }


      //等待所有累加線程都結(jié)束
      while (Thread.activeCount() > 1){
          Thread.yield();
      }

      //等待所有線程執(zhí)行完畢之后,打印race的最終值
      System.out.println(race);
  }

  private static void increase() {
      race++;
  }
  }

代碼1 volatile 線程安全測(cè)試

程序的執(zhí)行結(jié)果如圖2所示。


volatile 線程安全測(cè)試
volatile 線程安全測(cè)試

圖2 volatile 線程安全測(cè)試

這個(gè)程序執(zhí)行的結(jié)果應(yīng)該是20000,可實(shí)際執(zhí)行所得到的結(jié)果只有18439.并且多次執(zhí)行本程序都會(huì)得到不一樣的結(jié)果。所以會(huì)出現(xiàn)這種情況的原因是 race++ 這個(gè)操作并不是原子操作,volatile關(guān)鍵字只能保證在使用race變量時(shí),工作線程能夠從主內(nèi)存中拿取最新的race的值,并不能保證進(jìn)行過++操作之后寫回到主內(nèi)存期間其它線程沒有對(duì)race變量進(jìn)行修改。
volatile關(guān)鍵字只能保證可見性,只有在以下兩種條件下,我們可以認(rèn)為volatile是線程安全的;

  1. 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠保證只有一個(gè)線程修改變量的值。
  2. 變量不需要與其它的變臉共同參與不變約束。

當(dāng)不符合這兩種條件時(shí),想要保證volatile變量的原子性,只能通過 機(jī)制來實(shí)現(xiàn)。

volatile變量的第二個(gè)重要作用是禁止編譯器進(jìn)行指令重排序。普通的變量僅僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。從硬件層面上來講,是指CPU允許多條指令不按程序規(guī)定的順序分開發(fā)送到個(gè)相應(yīng)的電路單元來處理。CPU必須要正確的處理指令的依賴情況,不能將指令任意的排序。比如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3將地址B中的值減去3。在這一系列指令中,指令1,2之間是有依賴關(guān)系的,不能進(jìn)行重排序。但指令3的執(zhí)行順序可以改到指令1之前也可以放到指令1之后,只要能夠保證在執(zhí)行完畢這三條指令程序中A,B的值是正確的即可。volatile在某些情況下性能高于鎖,由于虛擬機(jī)對(duì)鎖實(shí)行有多種消除和優(yōu)化,我們很難衡量volatile會(huì)比鎖快多少。

原子性、可見性和有序性

20170803 23:11:51 原子性、可見性和有序性
JAVA內(nèi)存模型的核心問題時(shí)為了解決多線程應(yīng)用中的原子性、可見性與有序性的問題,保證了這三點(diǎn)才能使多線程應(yīng)用有數(shù)據(jù)安全性可言。

  • 原子性 (Atomicity)
    由java內(nèi)存模型來直接保證原子性的操作包括read、load、assign、use、store和write。對(duì)于一個(gè)變量的基本讀寫操作都是具有原子性的。如果需要對(duì)多種這幾種基本讀寫操作的操作集合保證原子性,可以使用lock和unlock。
  • 可見性(Visibility)
    可見性是指當(dāng)變量的值被一個(gè)線程修改了之后,其它線程能夠立即知道這個(gè)修改。JAVA內(nèi)存模型時(shí)通過變量內(nèi)容修改后立刻同步到主內(nèi)存,讀取變量的值之前先從內(nèi)存刷新這種方式來保證可見性。JAVA中除了volatile之外還有synchronized、final兩個(gè)關(guān)鍵字能夠?qū)崿F(xiàn)內(nèi)存可見性。synchronized的可見性是由"對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步會(huì)主內(nèi)存中(執(zhí)行store,write操作)"這條規(guī)則獲得的。final的可見性是由于被final修飾的變量在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒有吧this的引用傳遞出去(this引用是一件非常危險(xiǎn)的事,其它線程可能通過這個(gè)引用訪問到初始化一半的對(duì)象),,那在其它線程中就能看見final變量的值。
  • 有序性
    如果在本線程內(nèi)觀察,所有的操作都是有序的,如果在一個(gè)線程中觀察另外一個(gè)線程,所有的操作都是無序的。前半句指的是“線程內(nèi)表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics),后半句指的是“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

先行發(fā)生原則

20170804 02:02:23 先行發(fā)生原則

先行發(fā)生原則(Happens-before)是JAVA內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果操作A先行發(fā)生于操作B,操作A造成的影響就能被操作B觀察到。JAVA只能怪的先行發(fā)生原則無需任何同步協(xié)助,可以直接編碼使用。以下列出JAVA中的先行發(fā)生原則,如果兩個(gè)操作之間的關(guān)系不在下面的規(guī)則中,且無法通過下面的規(guī)則推導(dǎo)出來,虛擬機(jī)就能隨意的對(duì)他們進(jìn)行指令重排序。

  • 程序次序規(guī)則(Program Order Rule)
    :在一個(gè)線程內(nèi),按照程序的代碼順序,書寫在前的操作先于書寫在后面的操作發(fā)生。準(zhǔn)確的說,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)樾枰紤]分支、循環(huán)等結(jié)構(gòu)。

  • 管程鎖定規(guī)則(Monitor Lock Rule)
    : 一個(gè)unlocak操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。

  • volatile變量規(guī)則(Volatile Variable Rule)
    :對(duì)一個(gè)volatile的寫操作,先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。

  • 線程啟動(dòng)規(guī)則(Thread Start Rule)
    :線程的start方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。

  • 線程終止規(guī)則(Thread Termination Rule)
    : 線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過Thread.jion()方法結(jié)束、Thread.isAlive()的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行。

  • 線程中斷原則(Thread Interruption Rule)
    :對(duì)線程的interrupt()方法的調(diào)用先行發(fā)生于被中斷的線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測(cè)到是否有中斷發(fā)生。

  • 對(duì)象終結(jié)規(guī)則(Finalizer Rule)
    :一個(gè)對(duì)象的初始化完成(夠構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的fnalize()方法開始。

  • 傳遞性(Transitivity)
    :如果A操作先行發(fā)生于B操作,B操作先行發(fā)生于C操作,就可以得出A先行發(fā)生于C的結(jié)論。

時(shí)間先后順序與先行發(fā)生原則之間基本沒有關(guān)系,所以當(dāng)我們需要衡量并發(fā)安全問題的時(shí)候不要受到時(shí)間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。

20170804 23:59:46 先行發(fā)生原則

引用

本文是對(duì)class文件的學(xué)習(xí)筆記,筆記的內(nèi)容并非是原創(chuàng),而是大量參考其它資料。在寫作本文的過程中引用了以下資料,為為在此深深謝過以下資料的作者。

  1. 《The Java Virtual Machine Specification》
  2. 《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐/周志明著.——2版.——北京:機(jī)械工業(yè)出版社,2013.6》

關(guān)于

原文鏈接 https://weiwei02.github.io/2017/07/02/jvm/001-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/
我的github: https://github.com/weiwei02/
我相信技術(shù)能夠改變世界。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 注:此文是我在讀完周志明老師的深入理解Java虛擬機(jī)之后總結(jié)的一篇文章,請(qǐng)閱讀此書獲取更加詳細(xì)的信息. 在介紹Ja...
    AlstonWilliams閱讀 412評(píng)論 0 1
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,366評(píng)論 11 349
  • 材料所限,只有白紙,沒有卡紙,只能做個(gè)用彩鉛涂了顏色的軟軟的站不起來的賀卡(哭T﹏T)。 很美啊~喜歡今天的作業(yè)喜...
    0蘇幕遮0閱讀 311評(píng)論 2 2
  • 本文參加#未完待續(xù),就要表白#活動(dòng),本人承諾,文章內(nèi)容為原創(chuàng),且未在其他平臺(tái)發(fā)表過。 走過四季,撫摸著青農(nóng)的風(fēng),感...
    時(shí)光中匆匆過客閱讀 265評(píng)論 0 5
  • “不想吃好吃的吃貨沒有未來” 吃貨幫 ◆◆◆ 從前車馬很遠(yuǎn),書信很慢, 一生只夠愛一個(gè)人 但是小編夏天只愛它們呀~...
    ititan閱讀 198評(píng)論 0 1