天天看點

Java 記憶體模型與線程

1.概述

2.硬體的效率與一緻性

計算機cpu進行計算必定會關聯到記憶體的讀寫操作,實際情況是計算機儲存設備與處理器的運算速度有幾個數量級的差距,現代計算機不得不加入一層讀寫速度與處理器接近的高速緩存來作為記憶體與處理器之間的緩沖。

引入高速緩存區帶來了一些資料同步方面的複雜度。如何保證多核cpu之間緩存的一緻性問題,引出了一些緩存一緻性的規則即一緻性協定。(MSI、MESI、MOSI Synapse 、Firefly)

Java 記憶體模型與線程

(如圖 處理器 高速緩存 主記憶體間的互動關系)

3.java記憶體模型

java 記憶體模型(JMM)是為了屏蔽各種硬體和作業系統的記憶體通路差異,java虛拟機規範中試圖定義一種java記憶體模型。定義java記憶體模型并非一件容易的事情,既要保證嚴謹性避免并發記憶體通路操作産生歧義,又要足夠的寬松使jvm的實作有足夠的自由空間利用硬體的各種特性(寄存器,高速緩存和指令集中某些特有的指令)

3.1主記憶體與工作記憶體

java記憶體模型的主要目标是定義程式中各個變量的通路規則,從jvm中存儲變量到記憶體和從記憶體中取出變量這樣的底層細節。(這裡的變量和java中的變量還有些不一緻,這裡的變量不包括方法參數和局部變量,因為以上變量是線程獨有的)成員變量,類變量和構成資料的靜态元素

java記憶體模型規定所有變量存儲在主記憶體中,每條線程還有自己的工作記憶體,線程的工作記憶體中存儲着該線程用到主記憶體變量的副本拷貝

Java 記憶體模型與線程

(如圖 java主記憶體工作記憶體與線程之間的關系)

tips: 這裡所講的主記憶體與工作記憶體 與記憶體區域中的堆棧方法區并不是一個層次的記憶體劃分實際上兩者沒有關系。 程式運作主要讀寫的是工作記憶體

3.2記憶體間互動操作

主記憶體和工作記憶體之間的互動協定,即一個變量如何從主記憶體拷貝到工作記憶體,如何從工作記憶體同步到主記憶體之類的實作細節,java記憶體模型定義了8中操作。jvm的實作必須保證8種操作都是原子的。

lock 、unlock、 read、 load、 use、 assign(指派)、store、 write

1、lock(鎖定):作用于主記憶體的變量,它把一個變量辨別為一條線程獨占的狀态。

2、unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。

3、read(讀取):作用于主記憶體的變量,它把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用。

4、load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中。

5、use(使用):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳遞給執行引擎,每當虛拟機遇到一個需要使用到變量的值的位元組碼指令時将會執行這個操作。

6、assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值賦給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。

7、store(存儲):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳送到主記憶體中,以便随後的write操作使用。

8、write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中。

​​引用位址:進入​​

3.3對于volatile型變量的特殊規則

關鍵字volatile 可以說是java虛拟機提供的最輕量級的同步機制,但是它并不容易完全被正确完整地了解以至于許多程式員都習慣不去使用它,遇到需要處理多線程的情況一律使用Synchronized來進行同步。

java記憶體模型對volatile專門定義了一些特殊的通路規則,當一個變量被定義為volatile 後将具備兩種特性

1. 保證此變量對所有線程的可見性,

一個線程修改了volatile變量後,新值對于其它線程來說是可以立即得知的。普通變量不能做到隻一點,普通變量隻能通過主記憶體來完成線程之間變量值的共享。 (Thread A 更改了變量,然後同步到主記憶體,Thread B 通過讀取主記憶體實作共享)

2.第二個語義是禁止指令重排序優化

​​demo 例子​​

所謂禁止指令重排所指的重排序優化是機器級的優化操作,提前執行是指這句話對應的彙編代碼被提前執行。

為什麼添加了volatile辨識後會達到這種效果?

簡單的了解就是有volatile修飾的變量,指派後多執行了一個 lock xxx 操作,這個操作相當于一個記憶體屏障(memory barrier 或 memory fence),排序時不能把後面的指令重排序到記憶體屏障之前的位置。

tip: lock 屬于位元組碼指令,對目前操作加鎖,一個cpu通路記憶體時,不需要記憶體屏障,但如果多個cpu通路同一塊記憶體,且其中有一個在觀測另一個,就需要記憶體屏障來保證一緻性了

對于volatile 與Synchronized 如何選擇?

來自于volatile的語義能否滿足使用場景的需求

3.4對long和double型變量的特殊規則

​​[long 和 double 的非原子性協定]​​ ,在32位和64位作業系統下,java記憶體模型,允許虛拟機将沒有被volatile修飾的64位資料的讀寫操作劃分兩次32位操作來進行,即允許虛拟機實作選擇可以不保證64位資料類型的load store read 和 write 操作的原子性

3.5原子性、可見性與有序性

3.5.1 原子性

java記憶體模型是圍繞着在并發過程中如何處理原子性可見性和有序性這3個特征來建立的

對于基本類型的通路讀寫 有記憶體模型來直接保證,通過 記憶體間互動操作 8種操作符号實作,如果需要更大範圍的原子性操作,java記憶體模型還提供了lock 和 unlock,雖然沒有放開給使用者直接調用,但卻提供了更高層次的位元組碼指令 monitorenter monitorexit 來隐式的使用這兩個操作,反映到java代碼中就是同步快 Synchronized。

3.5.2 可見性

當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。 volaile 變量中java記憶體模型通過在變量修改後将新值同步回主記憶體,并在變量讀取前從主記憶體重新整理變量值這種依賴主記憶體作為傳遞媒介的方式來實作可見性,普通變量也是通過主記憶體作為傳遞媒介,但是差別是volatile修飾的變量,由volatile的特殊規則保證了新值能立即同步到主記憶體,而普通變量不行。

synchronized 和 final 也能實作可見性,一個使用lock unlock,同步塊可見性是由對一個變量執行unlock操作之前必須先把次變量同步回主記憶體中(store,write操作)這條規則獲得,final 修飾字段在構造器一旦初始化完成,其他線程就能看到這個final字段的值,(注意有 ​​this 引用逃逸問題​​)

3.5.3 有序性

java 記憶體模型的有序性可以總結一句話:如果在本線程内觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序,前半句指的“線程内表現串行的語義”,後半句是指“指令重排”現象和工作記憶體與主記憶體同步延遲現象。

java語言提供了volatile和synchronized兩個關鍵字保證線程之間操作的有序性,volatile本身就包括禁止指令重排序的語義,而synchornized 是由一個變量在同一個時刻隻允許一條線程對其進行lock操作,這條規則獲得的,這條規則決定了持有同一鎖的兩個同步快隻能串行進入。

3.6先行發生原則

​​引用​​

4.java與線程

并發并一定要依賴多線程(入php中很常見的多程序并發),但是在java裡面談論并發,大多數都與線程脫不開關系。

4.1線程的實作

我們知道線程是比程序更輕量級的排程執行機關,線程是cpu排程的基本機關,現成的引入可以把一個程序的資源配置設定和執行排程分開,各個線程既可以共享程序資源(記憶體位址,檔案i/o)又可以獨立排程。

主流作業系統都提供了線程的實作,我們注意到thread類的大部分的java api都是native的。

實作線程的方法主要有三種方式:

使用核心線程實作

Java 記憶體模型與線程

如圖: 輕量級程序 lwp,與核心線程之間 1:1的關系

使用使用者線程(除了核心線程其他的都是使用者線程)

Java 記憶體模型與線程

使用者線程加輕量級程序混合實作。

Java 記憶體模型與線程

4.2java線程排程

線程排程是指系統為線程配置設定處理器使用權的過程,主要排程方式有兩種,分别是協同式排程和搶占式線程排程

協同式排程,線程的執行時間由線程本身來控制,線程把自己的工作執行完了之後要主動通知系統切換到另一個線程上

搶占式排程的多線程系統,每個線程将有系統配置設定執行時間,線程的切換不由線程本身決定。

線程優先級的引入,我們可以建議系統給某些線程多配置設定一些執行時間另一些線程可以少配置設定一些時間,java語言一共10個級别優先級。

tip: 線程優先級并不靠譜,原因是java的線程是通過映射到原生線程上來實作的,比如window 系統隻有7種優先級。所有會有 一些優先級相同的情況發生。

4.3狀态轉換

小結