Java內(nèi)存模型
JMM(java內(nèi)存模型)
java虛擬機(jī)有自己的內(nèi)存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。
JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:共享變量存儲在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存保存了被該線程使用到的主內(nèi)存的副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。這三者之間的交互關(guān)系如下

計(jì)算機(jī)在執(zhí)行程序時(shí),每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。由于程序運(yùn)行過程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的,這時(shí)就存在一個(gè)問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度比起來要慢的多,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進(jìn)行,會大大降低指令執(zhí)行的速度。因此在CPU里面就有了高速緩存。
也就是,當(dāng)程序在運(yùn)行過程中,會將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。舉個(gè)簡單的例子,比如下面的這段代碼:
i = i + 1;
當(dāng)線程執(zhí)行這個(gè)語句時(shí),會先從主存當(dāng)中讀取i的值,然后復(fù)制一份到高速緩存當(dāng)中,然后CPU執(zhí)行指令對i進(jìn)行加1操作,然后將數(shù)據(jù)寫入高速緩存,最后將高速緩存中i最新的值刷新到主存當(dāng)中。
這個(gè)代碼在單線程中運(yùn)行是沒有任何問題的,但是在多線程中運(yùn)行就會有問題了。在多核CPU中,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對單核CPU來說,其實(shí)也會出現(xiàn)這種問題,只不過是以線程調(diào)度的形式來分別執(zhí)行的)。本文我們以多核CPU為例。
比如同時(shí)有2個(gè)線程執(zhí)行這段代碼,假如初始時(shí)i的值為0,那么我們希望兩個(gè)線程執(zhí)行完之后i的值變?yōu)?。但是事實(shí)會是這樣嗎?
可能存在下面一種情況:初始時(shí),兩個(gè)線程分別讀取i的值存入各自所在的CPU的高速緩存當(dāng)中,然后線程1進(jìn)行加1操作,然后把i的最新值1寫入到內(nèi)存。此時(shí)線程2的高速緩存當(dāng)中i的值還是0,進(jìn)行加1操作之后,i的值為1,然后線程2把i的值寫入內(nèi)存。
最終結(jié)果i的值是1,而不是2。這就是著名的緩存一致性問題。通常稱這種被多個(gè)線程訪問的變量為共享變量。
并發(fā)編程中的三個(gè)概念
在并發(fā)編程中,我們通常會遇到以下三個(gè)問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個(gè)概念:
1.原子性
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個(gè)操作不具備原子性,會造成什么樣的后果。假如從賬戶A減去1000元之后,操作突然中止。然后又從B取出了500元,取出500元之后,再執(zhí)行 往賬戶B加上1000元 的操作。這樣就會導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個(gè)轉(zhuǎn)過來的1000元。
所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
2.可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
舉個(gè)簡單的例子,看下面這段代碼:
1 //線程1執(zhí)行的代碼
2 int i = 0;
3 i = 10;
4?
5 //線程2執(zhí)行的代碼
6 j = i;
假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時(shí),會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中。
此時(shí)線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會使得j的值為0,而不是10。
這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。
3.有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。舉個(gè)簡單的例子,看下面這段代碼:
int i = 0;? ? ? ? ? ? ?
boolean flag = false;
i = 1;? ? ? ? ? ? ? ? //語句1?
flag = true;? ? ? ? ? //語句2
從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。
一般來說,處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力
比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是重排序也需要遵守一定規(guī)則:
1.重排序操作不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。
比如:a=1;b=a; 這個(gè)指令序列,由于第二個(gè)操作依賴于第一個(gè)操作,所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會被重排序。
2.重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變
比如:a=1;b=2;c=a+b這三個(gè)操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關(guān)系,所以可能會發(fā)生重排序,但是c=a+b這個(gè)操作是不會被重排序的,因?yàn)樾枰WC最終的結(jié)果一定是c=a+b=3。
volatile關(guān)鍵字
? volatile是Java提供的一種輕量級的同步機(jī)制。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級。
一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。
2)禁止進(jìn)行指令重排序。
1、共享變量的可見性
public class TestVolatile {
? ? public static void main(String[] args) {
? ? ? ? ThreadDemo td = new ThreadDemo();
? ? ? ? new Thread(td).start();
? ? ? ? while(true){
? ? ? ? ? ? if(td.isFlag()){
? ? ? ? ? ? ? ? System.out.println("------------------");
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
class ThreadDemo implements Runnable {
? ? private? boolean flag = false;
? ? @Override
? ? public void run() {
? ? ? ? try {
? ? ? ? ? ? Thread.sleep(200);
? ? ? ? } catch (InterruptedException e) {
? ? ? ? }
? ? ? ? flag = true;
? ? ? ? System.out.println("flag=" + isFlag());
? ? }
? ? public boolean isFlag() {
? ? ? ? return flag;
? ? }
}
上面這個(gè)例子,開啟一個(gè)多線程去改變flag為true,main 主線程中可以輸出"------------------"嗎?
答案是NO!?
這個(gè)結(jié)論會讓人有些疑惑,可以理解。開啟的線程雖然修改了flag 的值為true,但是還沒來得及寫入主存當(dāng)中,此時(shí)main里面的 td.isFlag()還是false,但是由于?while(true)? 是底層的指令來實(shí)現(xiàn),速度非常之快,一直循環(huán)都沒有時(shí)間去主存中更新td的值,所以這里會造成死循環(huán)!運(yùn)行結(jié)果如下:

此時(shí)線程是沒有停止的,一直在循環(huán)。
如何解決呢?只需將 flag?聲明為volatile,即可保證在開啟的線程A將其修改為true時(shí),main主線程可以立刻得知:
第一:使用volatile關(guān)鍵字會強(qiáng)制將修改的值立即寫入主存;
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力
第二:使用volatile關(guān)鍵字的話,當(dāng)開啟的線程進(jìn)行修改時(shí),會導(dǎo)致main線程的工作內(nèi)存中緩存變量flag的緩存行無效(反映到硬件層的話,就是CPU的L1緩存中對應(yīng)的緩存行無效);
第三:由于線程main的工作內(nèi)存中緩存變量flag的緩存行無效,所以線程main再次讀取變量flag的值時(shí)會去主存讀取。
volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個(gè)共享變量聲明為volatile后,會有以下效應(yīng):
1.當(dāng)寫一個(gè)volatile變量時(shí),JMM會把該線程對應(yīng)的本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去;
2.這個(gè)寫會操作會導(dǎo)致其他線程中的緩存無效。
2、禁止進(jìn)行指令重排序
這里我們引用上篇文章單例里面的例子
1 class Singleton{
2? ? private volatile static Singleton instance = null;
3
4? ? private Singleton() {
5? ? }
6? ? ?
7? ? public static Singleton getInstance() {
8? ? ? ? if(instance==null) {
9? ? ? ? ? ? synchronized (Singleton.class) {
10? ? ? ? ? ? ? ? if(instance==null)
11? ? ? ? ? ? ? ? ? ? instance = new Singleton();
12? ? ? ? ? ? }
13? ? ? ? }
14? ? ? ? return instance;
15? ? }
16 }
instance = new?Singleton(); 這段代碼可以分為三個(gè)步驟:
1、memory = allocate() 分配對象的內(nèi)存空間
2、ctorInstance() 初始化對象
3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
但是此時(shí)有可能發(fā)生指令重排,CPU 的執(zhí)行順序可能為:
1、memory = allocate() 分配對象的內(nèi)存空間
3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存
2、ctorInstance() 初始化對象
在單線程的情況下,1->3->2這種順序執(zhí)行是沒有問題的,但是如果是多線程的情況則有可能出現(xiàn)問題,線程A執(zhí)行到11行代碼,執(zhí)行了指令1和3,此時(shí)instance已經(jīng)有值了,值為第一步分配的內(nèi)存空間地址,但是還沒有進(jìn)行對象的初始化;
此時(shí)線程B執(zhí)行到了第8行代碼處,此時(shí)instance已經(jīng)有值了則return instance,線程B 使用instance的時(shí)候,就會出現(xiàn)異常。
這里可以使用 volatile 來禁止指令重排序。
從上面知道volatile關(guān)鍵字保證了操作的可見性和有序性,但是volatile能保證對變量的操作是原子性嗎?
下面看一個(gè)例子:
package com.mmall.concurrency.example.count;
import java.util.concurrent.CountDownLatch;
/**
* @author: ChenHao
* @Description:
* @Date: Created in 15:05 2018/11/16
* @Modified by:
*/
public class CountTest {
? ? // 請求總數(shù)
? ? public static int clientTotal = 5000;
? ? public static volatile int count = 0;
? ? public static void main(String[] args) throws Exception {
? ? ? ? //使用CountDownLatch來等待計(jì)算線程執(zhí)行完
? ? ? ? final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
? ? ? ? //開啟clientTotal個(gè)線程進(jìn)行累加操作
? ? ? ? for(int i=0;i<clientTotal;i++){
? ? ? ? ? ? new Thread(){
? ? ? ? ? ? ? ? public void run(){
? ? ? ? ? ? ? ? ? ? count++;//自加操作
? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }.start();
? ? ? ? }
? ? ? ? //等待計(jì)算線程執(zhí)行完
? ? ? ? countDownLatch.await();
? ? ? ? System.out.println(count);
? ? }
}
執(zhí)行結(jié)果:

針對這個(gè)示例,一些同學(xué)可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那么結(jié)果不應(yīng)該是5000么?
問題就出在count++這個(gè)操作上,因?yàn)閏ount++不是個(gè)原子性的操作,而是個(gè)復(fù)合操作。我們可以簡單講這個(gè)操作理解為由這三步組成:
1.讀取count
2.count 加 1
3.將count 寫到主存
所以,在多線程環(huán)境下,有可能線程A將count讀取到本地內(nèi)存中,此時(shí)其他線程可能已經(jīng)將count增大了很多,線程A依然對過期的本地緩存count進(jìn)行自加,重新寫到主存中,最終導(dǎo)致了count的結(jié)果不合預(yù)期,而是小于5000。
那么如何來解決這個(gè)問題呢?下面我們來看看
Atomic包
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實(shí)現(xiàn)原子性操作的(Compare And Swap)
package com.mmall.concurrency.example.count;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author: ChenHao
* @Description:
* @Date: Created in 15:05 2018/11/16
* @Modified by:
*/
public class CountTest {
? ? // 請求總數(shù)
? ? public static int clientTotal = 5000;
? ? public static AtomicInteger count = new AtomicInteger(0);
? ? public static void main(String[] args) throws Exception {
? ? ? ? //使用CountDownLatch來等待計(jì)算線程執(zhí)行完
? ? ? ? final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
?????? ?在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力
? ? ? ? //開啟clientTotal個(gè)線程進(jìn)行累加操作
? ? ? ? for(int i=0;i<clientTotal;i++){
? ? ? ? ? ? new Thread(){
? ? ? ? ? ? ? ? public void run(){
? ? ? ? ? ? ? ? ? ? count.incrementAndGet();//先加1,再get到值
? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }.start();
? ? ? ? }
? ? ? ? //等待計(jì)算線程執(zhí)行完
? ? ? ? countDownLatch.await();
? ? ? ? System.out.println(count);
? ? }
}
執(zhí)行結(jié)果:

下面我們來看看原子類操作的基本原理
1 public final int incrementAndGet() {
2? ? ? return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
3 }
4
5 public final int getAndAddInt(Object var1, long var2, int var4) {
6? ? int var5;
7? ? do {
8? ? ? ? var5 = this.getIntVolatile(var1, var2);
9? ? } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
10
11? ? return var5;
12 }
13
14 /***
15 * 獲取obj對象中offset偏移地址對應(yīng)的整型field的值。
16 * @param obj 包含需要去讀取的field的對象
17 * @param obj中整型field的偏移量
18 */
19 public native int getIntVolatile(Object obj, long offset);
20
21 /**
22 * 比較obj的offset處內(nèi)存位置中的值和期望的值,如果相同則更新。此更新是不可中斷的。
23 *
24 * @param obj 需要更新的對象
25 * @param offset obj中整型field的偏移量
26 * @param expect 希望field中存在的值
27 * @param update 如果期望值expect與field的當(dāng)前值相同,設(shè)置filed的值為這個(gè)新值
28 * @return 如果field的值被更改返回true
29 */
30 public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
首先介紹一下什么是Compare And Swap(CAS)?簡單的說就是比較并交換。
CAS 操作包含三個(gè)操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。CAS 有效地說明了“我認(rèn)為位置 V 應(yīng)該包含值 A;如果包含該值,則將 B 放到這個(gè)位置;否則,不要更改該位置,只告訴我這個(gè)位置現(xiàn)在的值即可?!?Java并發(fā)包(java.util.concurrent)中大量使用了CAS操作,涉及到并發(fā)的地方都調(diào)用了sun.misc.Unsafe類方法進(jìn)行CAS操作。
?我們來分析下incrementAndGet的邏輯:
1.先獲取當(dāng)前的value值
2.調(diào)用compareAndSet方法來來進(jìn)行原子更新操作,這個(gè)方法的語義是:
先檢查當(dāng)前value是否等于obj中整型field的偏移量處的值,如果相等,則意味著obj中整型field的偏移量處的值?沒被其他線程修改過,更新并返回true。如果不相等,compareAndSet則會返回false,然后循環(huán)繼續(xù)嘗試更新。
第一次count 為0時(shí)線程A調(diào)用incrementAndGet時(shí),傳參為?var1=AtomicInteger(0),var2為var1 里面 0?的偏移量,比如為8090,var4為需要加的數(shù)值1,var5為線程工作內(nèi)存值,在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力do里面會先執(zhí)行一次,通過getIntVolatile?獲取obj對象中offset偏移地址對應(yīng)的整型field的值此時(shí)var5=0;while 里面compareAndSwapInt?比較obj的8090處內(nèi)存位置中的值和期望的值var5,如果相同則更新obj的值為(var5+var4=1),此時(shí)更新成功,返回true,則?while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));結(jié)束循環(huán),return var5。
當(dāng)count 為0時(shí),線程B 和線程A 同時(shí)讀取到 count,進(jìn)入到第 8 行代碼處,線程B 也是取到的var5=0,當(dāng)線程B 執(zhí)行到compareAndSwapInt時(shí),線程A已經(jīng)執(zhí)行完compareAndSwapInt,已經(jīng)將內(nèi)存地址為8090處的值修改為1,此時(shí)線程B 執(zhí)行compareAndSwapInt返回false,則繼續(xù)循環(huán)執(zhí)行do里面的語句,再次取內(nèi)存地址偏移量為8090處的值為1,再去執(zhí)行compareAndSwapInt,更新obj的值為(var5+var4=2),返回為true,結(jié)束循環(huán),return var5。
CAS的ABA問題
當(dāng)然CAS也并不完美,它存在"ABA"問題,假若一個(gè)變量初次讀取是A,在compare階段依然是A,但其實(shí)可能在此過程中,它先被改為B,再被改回A,而CAS是無法意識到這個(gè)問題的。CAS只關(guān)注了比較前后的值是否改變,而無法清楚在此過程中變量的變更明細(xì),這就是所謂的ABA漏洞。?
如果大家想學(xué)習(xí)以上路線內(nèi)容,在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群。交流學(xué)習(xí)群號874811168 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多