JMM
JMM(Java內(nèi)存模型逸月,Java Memory Model)本身是一種抽象的概念矩屁,并不真實(shí)存在抡柿,它描述的是一組規(guī)則或規(guī)范舔琅,通過(guò)規(guī)范定制了程序中的各個(gè)變量的訪問(wèn)方式。
JMM關(guān)于同步的規(guī)定:
- 線程解鎖前沙绝,必須把共享變量的值刷新回主內(nèi)存搏明。
- 線程加鎖前鼠锈,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存。
- 加鎖解鎖必須是同一個(gè)鎖星著。
特點(diǎn):
- 原子性:即一個(gè)操作或多個(gè)操作在執(zhí)行的過(guò)程中购笆,要成功都成功,要失敗都失敗虚循。
- 可見性:多個(gè)線程訪問(wèn)同一個(gè)變量時(shí)同欠,當(dāng)一個(gè)線程修改該變量時(shí),其他線程可見横缔。
- 有序性:保證程序運(yùn)行的順序是代碼的順序铺遂。 在java 內(nèi)存模型中,為了效率茎刚,是允許編譯器和處理器對(duì)指令進(jìn)行重排序的襟锐,對(duì)單線程運(yùn)行不會(huì)影響,但是會(huì)影響多線程運(yùn)行結(jié)果膛锭。
happens-before
了解有序性后需要了解一下happens-before原則:
定義:
- 如果一個(gè)操作happens-before另一個(gè)操作粮坞,那么第一個(gè)操作的執(zhí)行結(jié)果對(duì)第二個(gè)操作執(zhí)行結(jié)果可見,而且第一個(gè)操作順序在第二個(gè)之前
- 如果兩個(gè)操作存在happens-before關(guān)系初狰,并不意味著一定按照happens-before順序執(zhí)行莫杈。如果重排序后的運(yùn)行的值和happens-before運(yùn)行結(jié)果一樣,那么這種重排序并不違法
規(guī)則(來(lái)源 深入理解 Java 虛擬機(jī))
- 程序次序規(guī)則:一個(gè)線程內(nèi)奢入,按照代碼順序筝闹,書寫在前面的操作,happens-before 于書寫在后面的操作腥光。
- 鎖定規(guī)則:一個(gè) unLock 操作关顷,happens-before 于后面對(duì)同一個(gè)鎖的 lock 操作。
- volatile 變量規(guī)則:對(duì)一個(gè)變量的寫操作武福,happens-before 于后面對(duì)這個(gè)變量的讀操作解寝。
- 傳遞規(guī)則:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C艘儒,則可以得出,操作 A happens-before 操作C
- 線程啟動(dòng)規(guī)則:Thread 對(duì)象的 start 方法夫偶,happens-before 此線程的每個(gè)一個(gè)動(dòng)作界睁。
- 線程中斷規(guī)則:對(duì)線程 interrupt 方法的調(diào)用,happens-before 被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生兵拢。
- 線程終結(jié)規(guī)則:線程中所有的操作翻斟,都 happens-before 線程的終止檢測(cè),我們可以通過(guò)Thread.join() 方法結(jié)束说铃、Thread.isAlive() 的返回值手段访惜,檢測(cè)到線程已經(jīng)終止執(zhí)行嘹履。
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成,happens-before 它的 finalize() 方法的開始
JMM內(nèi)存模型:
數(shù)據(jù)一致性的問(wèn)題(可見性問(wèn)題)
從上圖可以看到债热,計(jì)算機(jī)在執(zhí)行的過(guò)程中砾嫉,由于每條指令都是在CPU中運(yùn)行,這樣就會(huì)涉及到去主存中讀取數(shù)據(jù)窒篱,但是主存的運(yùn)行速度沒有CPU運(yùn)行速度快焕刮,所以有了CPU高速緩存,CPU高速緩存屬于某個(gè)CPU獨(dú)有墙杯,只與運(yùn)行在該CPU的線程有關(guān)配并。這種情形解決了效率的問(wèn)題,但是也帶來(lái)了新的問(wèn)題高镐,即數(shù)據(jù)一致性溉旋,當(dāng)CPU第一次從主存中獲取后,會(huì)將信息放入到CPU高速緩存中嫉髓,這是有其他線程修改了主存的信息观腊,這是就會(huì)引起CPU緩存的信息和主存的信息不一致。
volatile
volatile 可以理解為輕量級(jí)的synchronized岩喷。在多線程的開發(fā)過(guò)程中恕沫,保證了內(nèi)存可見性及禁止重排序。
特點(diǎn)
- 保證可見性
- 不保證原子性
- 禁止指令重排序
保證可見性
class Demo {
// private volatile int number = 0; //1
private int number = 0; //2
public void add() {
this.number = 100;
}
public int getNumber() {
return number;
}
public Demo setNumber(int number) {
this.number = number;
return this;
}
}
/**
* 兩種運(yùn)行結(jié)果
* 當(dāng)執(zhí)行1時(shí)候纱意,線程1修改完后婶溯,立刻輸出 內(nèi)存可見
* 當(dāng)執(zhí)行2時(shí)候,線程1修改完后偷霉,程序死循環(huán)
*/
public class test {
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(() -> {
System.err.println(Thread.currentThread().getName() + "開始 執(zhí)行");
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
demo.add();
System.err.println(Thread.currentThread().getName() + "已經(jīng)執(zhí)行完,當(dāng)前number = " + demo.getNumber());
}, "線程1").start();
while (demo.getNumber() == 0) {
}
System.err.println("內(nèi)存可見");
}
}
不保證原子性
volatile 不能保證復(fù)合操作的原子性迄委。如下:
//創(chuàng)建初始值a = 0
private static volatile Integer a = 0;
//a ++;
public static void add() {
a++;
}
public static void main(String[] args) throws InterruptedException {
// 模擬一共一百個(gè)線程同時(shí)對(duì)a進(jìn)行a++操作,如果是原子的操作类少,則最終結(jié)果為100叙身;
CountDownLatch countDownLatch = new CountDownLatch(100);
//創(chuàng)建一個(gè)線程池(快速創(chuàng)建,但是在開發(fā)過(guò)程中不要這么寫硫狞,可能會(huì)內(nèi)存泄露信轿,后面會(huì)單獨(dú)講解)
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
add();
countDownLatch.countDown();
});
}
countDownLatch.await();
//嘗試執(zhí)行了5次
System.err.println(a); //結(jié)果:100、99残吩、92财忽、93、100
// 關(guān)閉線程池
executor.shutdown();
}
為什么volatile無(wú)法保證復(fù)合操作的原子性泣侮?
public class Demo2 {
private volatile int a = 0;
public void add() {
a++;
}
}
public class com.hhb.concurrency.atguigu.thread.Demo2 {
public com.hhb.concurrency.atguigu.thread.Demo2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field b:I
9: aload_0
10: iconst_0
11: putfield #3 // Field a:I
14: return
public void add(); //對(duì)應(yīng)上面add()方法
Code:
0: aload_0 //從局部變量0中裝載引用類型值
1: dup //復(fù)制棧頂部一個(gè)字長(zhǎng)內(nèi)容
2: getfield #3 //1即彪、先獲取a的值
5: iconst_1
6: iadd // 2、真正的對(duì)a++
7: putfield #3 // 3活尊、 最后在賦值給a
10: return
}
通過(guò)上面的代碼可以看到隶校,a++的操作在java中看起來(lái)是一個(gè)操作漏益,但是在執(zhí)行的時(shí)候,被分為了下面三個(gè)操作深胳;那么問(wèn)題就出現(xiàn)在下面那三個(gè)步驟中绰疤,當(dāng)A線程獲取到a值,并對(duì)a進(jìn)行++操作完后稠屠,正要執(zhí)行(7)的時(shí)候峦睡,B線程開始獲取a的值,此時(shí)A線程中a的值還沒有刷新回主內(nèi)存权埠,所以B在獲取到的a的值還是0榨了,然后繼續(xù)執(zhí)行a+1的操作,并刷新a在主內(nèi)存中的值攘蔽,a=1龙屉,然后A線程在執(zhí)行,刷新主內(nèi)存满俗,a=1转捕,此時(shí)兩個(gè)線程分別都進(jìn)行的a++,目標(biāo)應(yīng)該是2唆垃,但是實(shí)際結(jié)果是1.
2: getfield #3 //1五芝、先獲取a的值
5: iconst_1
6: iadd // 2、真正的對(duì)a++
7: putfield #3 // 3辕万、 最后在賦值給a
禁止重排序
- 編譯器重排序:編譯器在不影響單線程運(yùn)行結(jié)果的前提之前枢步,可以重新安排語(yǔ)句的執(zhí)行順序
- 處理器重排序:不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)的機(jī)器碼的執(zhí)行順序渐尿。
volatile如何做到的禁止重排序醉途?
內(nèi)存屏障 ( Memory Barrier)
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU砖茸,不管什么指令都不能和這條Memory Barrier指令重排序隘擎,也就是說(shuō)通過(guò)插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。內(nèi)存屏障另外一個(gè)作用就是強(qiáng)制刷新出各種CPU的緩存數(shù)據(jù)凉夯,因此在CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本货葬。
策略:
- 在每個(gè)volatile寫之前,插入一個(gè)StoreStore屏障
- 在每個(gè)volatile寫之后劲够,插入一個(gè)StoreLoad屏障
- 在每個(gè)volatile讀之前宝惰,插入一個(gè)LoadLoad屏障
- 在每個(gè)volatile讀之后,插入一個(gè)LoadStore屏障
原因:
- StoreStore屏障:保證在volatile寫之前再沧,其前面的所有的普通寫操作,都已經(jīng)刷新到主存中
- StoreLoad屏障:避免volatile寫和后面發(fā)發(fā)生的volatile 讀/寫操作重排序
- LoadLoad屏障:禁止處理器把上面的volatile讀與下面的普通讀重排序
- LoadStore屏障:禁止處理器把上面的volatile讀與下面普通寫重排序
禁止重排序最著名的例子:雙重檢查的單例模式
/**
* 創(chuàng)建對(duì)象的過(guò)程:
* 1尊残、 memory = allocate() 分配對(duì)象內(nèi)存空間
* 2炒瘸、ctorInstance() 初始化對(duì)象
* 3淤堵、instance = memory 設(shè)置instance指向剛才的分配的內(nèi)存
* <p>
* 不安全的原因:
* cpu和jvm優(yōu)化,發(fā)生了指令重排序
* <p>
* 上面的過(guò)程變成了
* 1顷扩、 memory = allocate() 分配對(duì)象內(nèi)存空間
* 2拐邪、instance = memory 設(shè)置instance指向剛才的分配的內(nèi)存
* 3、ctorInstance() 初始化對(duì)象
* <p>
* <p>
* 假設(shè)現(xiàn)在有兩個(gè)線程 A隘截、B
* B線程執(zhí)行到了4扎阶,但是執(zhí)行到上面創(chuàng)建對(duì)象的第二步,還沒有初始化時(shí)
* A線程指向到了1步驟婶芭,就會(huì)直接返回
*/
//private volatile static SingletonExample5 instance;
private static SingletonExample5 instance;
private SingletonExample5() {
}
public static SingletonExample5 getInstance() {
if (instance == null) { // 1
synchronized (SingletonExample5.class) { // 2
if (instance == null) { // 3
instance = new SingletonExample5(); // 4
}
}
}
return instance; //5
}