最近在繼續加緊準備春招,加強一下Java多線程這一塊,這篇文章是對於Java內存模型的一些總結,只是簡單地談談
Java內存模型
Java實現會帶來不同的“翻譯”,不同CPU平臺的機器指令又千差萬別,無法保證併發安全的效果一致。
JVM內存結構 VS Java內存模型 VS Java對象模型
三個截然不同的概念,容易混淆
JVM內存結構
和Java虛擬機的運行時區域有關
- 組成:堆,虛擬機棧,方法區,本地方法棧,程序計數器
Java對象模型
和Java對象在虛擬機中的表現形式有關
- 是Java對象自身的存儲模型
- JVM會給這個類創建一個instanceKlass,保存在方法區,用來在JVM層表示該Java類
- 當我們在Java代碼中,使用new創建一個對象的時候,JVM會創建一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據
Java內存模型(JMM)
爲什麼需要JMM(Java Memory Model)
- C語言不存在內存模型的概念
- 依賴處理器,不同處理器結果不一樣,可能一個程序在不同處理器運行結果不同
- 無法保障併發安全
- 需要一個標準,讓多線程運行的結果可以預期
JMM是一種規範
即這是一組規範,需要各個JVM的實現來遵循JMM規範,以便於開發者可以利用這些規範,更加方便地開發多線程程序。如沒有這樣的規範,那麼可能經過不同JVM的不同規則的重排序之後,導致不同的虛擬機上運行的結果不一樣,就會產生問題。
JMM是工具類和關鍵字的原理
- volatile、synchronized、Lock等的原理都是JMM
- 若沒有JMM,就需要我們自己制定什麼時候用內存柵欄,即什麼時候同步,很麻煩
最重要的3點內容:重排序、可見性、原子性
重排序
package jmm;
import java.util.concurrent.CountDownLatch;
/**
* @Auther: Bob
* @Date: 2020/2/15 15:41
* @Description: 重排序的演示
* 直到達到某個條件才停止,用來測試小概率時間
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 1 && y == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
執行結果:第453次纔出現了1,1
什麼是重排序
在線程1內部的兩行代碼的實際執行順序和代碼在Java文件中的順序不一致,代碼指令並不是嚴格按照代碼語句順序執行的,它們的順序被改變了,這就是重排序,這裏被顛倒的是y = a 和 b = 1 這兩行語句
重排序的好處:提高處理速度
重排序的3種情況
- 編譯器優化:包括JVM,JIT編譯器等
- CPU指令重排:就算編譯器不發生重拍,CPU也可能對指令進行重拍
- 內存的“重排序”:線程A的修改線程B卻看不到,引出可見性問題(不是真正的重排序)
可見性
什麼是可見性
package jmm;
/**
* @Auther: Bob
* @Date: 2020/2/15 16:37
* @Description: 演示可見性帶來的問題
*/
public class FieldVisibility {
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
有可見性問題的原因:
- 高速緩存的容量比主內存小,但是速度僅次於寄存器,所以在CPU和主內存之間就多了Cache層
- 線程間的對於共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的
- 如果所有核心都只用一個緩存,那麼就不存在內存可見性問題了
- 每個核心都會將自己需要的數據讀到獨佔緩存中,數據修改後也是寫入到緩存中,然後等待輸入到主存中。所以會導致讀取的值是一個已經過期的值
“利用volatile關鍵字可以解決問題”
volatile關鍵字
volatile是什麼
- voltile是一種同步機制,比synchronized或者Lock相關類更輕量,因爲適用vilatile並不會發生上下文切換等開銷很大的行爲。
- 如果一個變量修改成volatile,那麼JVM就知道了這個變量可能被併發修改
- 開銷小,相應能力也小,volatile做不到synchronized那樣的原子保護,volatile只在有限場景下才能發揮作用
volatile適用場景與不適用場景
- 不適用:a++
- 適用場景1:boolean flag,若一哥共享變量在程序中只是被各個線程賦值,而無其他操作,那麼可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身具有原子性,而volatile又保證了可見性,所以足以保證線程安全。
- 適用場合2:作爲刷新之前變量的觸發器
volatile的兩點作用
- 可見性:讀一個volatile變量之前,需要先使相應的本地緩存失效,這樣就必須讀取到主內存讀取最新值,寫一個volatile屬性會立即刷入到主內存。
- 禁止指令重排序優化:解決單利雙重鎖亂序問題
volatile和synchronized的關係
如果一個共享變量自始至終只被各個線程賦值,而沒有其他的操作,那麼久可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身是有原子性的,而volatile又保證了可見性,所以就足以保證線程安全,此時volatile可以看做輕量版的synchronized。
用volatile可以修正重排序問題
volatile小結
除了volatile可以讓變量保證可見性外,synchronized、Lock、併發集合、Thread.join()和Thread.start()等都可以保證的可見性
對synchronized可見性的正確理解
- synchronized不僅保證了原子性,還保證了可見性
- synchronized不僅讓被保護的代碼安全,還近朱者赤(解鎖之前的所有操作另一個線程都能看到)
原子性
什麼是原子性
一系列的操作,要麼全部執行成功,要麼全部不執行,是不可分割的
Java中的原子操作有哪些
- 除了long和double之外的基本類型(int,byte,boolean,short,char,float)的賦值操作,根據Oracle的官方文檔,在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的,對於64位的值的寫入 ,可以分爲兩個32位的操作進行寫入,讀取錯誤,使用volatile解決。(在實際開發中,商用虛擬機中不會出現)
- 所有引用reference的賦值操作,不論是32位還是64位的操作系統
- java.concurrent.Atomic.*包中所有類的原子操作
原子操作 + 原子操作 != 原子操作
- 簡單地把原子操作組合在一起,並不能保證整體依然具有原子性
- 全同步的HashMap也不完全安全
總結得不太好,多請見諒,多數都只是涉及概念層面的東西。。。