文章目錄
- 一、JMM講解
- 1、什么是JMM?
- 2、JMM的底層訪問程序圖:
- 3、JMM的三大特點:
- 二、volatile 講解
- 1、什么是volatile?
- 2、volatile 的特點
- 2.1、代碼案例演示:
- 2.1.1 volatile可見性代碼演示:
- 2.1.2、volatile不保證原子性代碼演示:
- 2.1.3、volatile 有序性(禁止指令重排)演示和總結:
- 3、volatile 的示例
- 4、volatile關鍵字和synchronized 關鍵字比較
一、JMM講解
1、什么是JMM?
1.1、JMM的簡介:
JMM(Java記憶體模型Java Memory Model,簡稱JMM)本身是一種抽象的概念 并不真實存在,它描述的是一組規則或規范通過規范定制了程式中各個變數(包括實體欄位,靜態欄位和構成陣列物件的元素)的訪問方式,
1.2、JMM關于同步規定:
a、執行緒解鎖前,必須把共享變數的值重繪回主記憶體,
b、執行緒加鎖前,必須讀取主記憶體的最新值到自己的作業記憶體,
c、加鎖解鎖是同一把鎖,
1.3、底層原理:
JVM運行程式的物體是執行緒,每個執行緒創建時JVM都會為其創建一個作業記憶體(有些地方成為堆疊空間),作業記憶體是每個執行緒的私有資料區域,而Java記憶體模型中規定所有變數都存盤在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可訪問,但執行緒對變數的操作(讀取賦值等)必須在作業記憶體中進行;首先要將變數從主記憶體拷貝到自己的作業空間,然后對變數進行操作,操作完成再將變數寫回主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的作業記憶體儲存著主記憶體中的變數副本拷貝,
2、JMM的底層訪問程序圖:

3、JMM的三大特點:
a、保證可見性,
b、保證原子性,
c、保證有序性,
二、volatile 講解
1、什么是volatile?
volatile 是 Java 虛擬機提供的輕量級的同步機制,常被稱為"輕量級的synchronized",
2、volatile 的特點
a、保證可見性,
b、保證有序性(禁止指令重排),
c、保證原子性,
2.1、代碼案例演示:
2.1.1 volatile可見性代碼演示:
import java.util.concurrent.TimeUnit;
class MyData{
boolean flag = false; // 沒有加volatile ,就沒有可見性
//volatile boolean flag = false; // 加了volatile ,就有可見性
public void modifyTrue(){
flag = true;
}
}
/**
* 驗證volatile的可見性
*/
public class VolatileDemoTest {
public static void main(String[] args) {
MyData myData = new MyData();
System.out.println(Thread.currentThread().getName() + "\t 開始");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 執行緒進入");
//暫停一會兒執行緒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.modifyTrue();
System.out.println(Thread.currentThread().getName() + "\t 執行緒修改結束, number = "+myData.flag);
}, "AAA").start();
// 第2個執行緒 main執行緒
while (myData.flag == false){
// main執行緒一致回圈等待,直到flag不為false
}
System.out.println(Thread.currentThread().getName() + "\t 主執行緒結束number = "+myData.flag);
}
}
a、當代碼中 boolean flag = false; 前面沒有加 volatile關鍵字的時候,程式沒有退出;說明執行緒AAA將flag修改成了true,然后沒有通知到主執行緒的flag,導致程式一直不退出,

b、當代碼中 volatile boolean flag = false; 代碼前面加了關鍵字volatile,當執行緒AAA將flag修改成了true時,將修改的結果通知主執行緒,主執行緒收到訊息后,就退出回圈,程式結束退出,


2.1.2、volatile不保證原子性代碼演示:
class MyData{
volatile int number = 0;
// 此時number 前面加了volatile 關鍵字修飾,volatile不保證原子性
public void addPlus(){
number ++;
}
}
/**
* 驗證volatile不保證原子性
* 1.原子性指的是什么意思:
* 不可分割, 完整性, 也即某個執行緒正在做某個具體業務時,需要整體完整性
* 要么同時成功,要么同時失敗
*
* 4.如何解決
* 4.1 加上synchronized,但是synchronized太重,殺雞不用牛刀
* 4.2 使用AtomicInteger(推薦)
*/
public class VolatileNoAtomic {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
}
}, String.valueOf(i)).start();
}
//需要等待上面20個執行緒計算完成后,再用main取得最終結果
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "最終 volatile number:" +myData.number);
}
}
a、運行結果:如果volatile保證原子性,那么運行結果應該是20*1000 等于20000 才是正確的,


b、那么如何保證原子性呢?
第一:可以使用加上synchronized關鍵字,但是不推薦,因為處理一件小的事情,用加上synchronized關鍵字重量級的鎖,有點得不償失,
第二:推薦使用AtomicInteger,AtomicInteger底層是通過CAS和Unsafe類來保證原子性的,沒有實際加鎖,而是使用了自旋鎖的思想,
AtomicInteger 保證原子性代碼演示:
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int number = 0;
// 此時number 前面加了volatile 關鍵字修飾,volatile不保證原子性
public void addPlus(){
number ++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 驗證volatile不保證原子性
* 1.原子性指的是什么意思:
* 不可分割, 完整性, 也即某個執行緒正在做某個具體業務時,需要整體完整性
* 要么同時成功,要么同時失敗
*
* 2.如何解決
* 2.1 加上synchronized,但是synchronized太重,殺雞不用牛刀
* 2.2 使用AtomicInteger(推薦)
*/
public class VolatileNoAtomic {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
myData.addMyAtomic();
}
}, String.valueOf(i)).start();
}
//需要等待上面20個執行緒計算完成后,再用main取得最終結果
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "最終 volatile number:" +myData.number);
System.out.println(Thread.currentThread().getName() + "最終 AtomicInteger number:" +myData.atomicInteger);
}
}
運行結果:使用volatie 修飾的不保證原子性,結果小于20000;使用AtomicInteger修飾的保證原子性,結果等于20000.

2.1.3、volatile 有序性(禁止指令重排)演示和總結:
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 構造方法");
}
/**
* 雙重檢測機制
*
* @return
*/
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
總結:
a、private static volatile SingletonDemo instance = null; instance 前面加了volatile可以禁止指令重排,比如代碼中 instance = new SingletonDemo();這行代碼,執行的順序應該是:
1.為instance 分配記憶體空間;
2.初始化instance ;
3.將instance 指向分配的記憶體地址,
如果程式發生指令重排,順序可能是1->3->2; 這樣當物件被呼叫的時候,雖然物件不為空,但是沒有初始化,
b、因此volatile可以禁止指令重排,保證程式在多執行緒環境下也可以安全運行,
c、詳解:
DCL(雙端檢鎖) 機制不一定執行緒安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因在于某一個執行緒在執行到第一次檢測,讀取到的instance不為null時,instance的參考物件可能沒有完成初始化.
instance=new SingletonDem(); 可以分為以下步驟(偽代碼)
memory=allocate();//1.分配物件記憶體空間
instance(memory);//2.初始化物件
instance=memory;//3.設定instance的指向剛分配的記憶體地址,此時instance!=null
步驟2和步驟3不存在資料依賴關系.而且無論重排前還是重排后程式執行的結果在單執行緒中并沒有改變,因此這種重排優化是允許的.
memory=allocate();//1.分配物件記憶體空間
instance=memory;//3.設定instance的指向剛分配的記憶體地址,此時instance!=null 但物件還沒有初始化完.
instance(memory);//2.初始化物件
但是指令重排只會保證串行語意的執行一致性(單執行緒) 并不會關心多執行緒間的語意一致性
所以當一條執行緒訪問instance不為null時,由于instance實體未必完成初始化,也就造成了執行緒安全問題.
3、volatile 的示例
1、面試官:專案中有用到valotile嗎?
答:一般用于 單例模式DCL代碼 (即雙重校驗鎖機制),如上面 2.1.3 的禁止指令重排的代碼,
4、volatile關鍵字和synchronized 關鍵字比較
a、volatile關鍵字的性能要優于synchronized關鍵字,因為volatile關鍵字是執行緒同步的輕量級實作,而synchronized關鍵字是重量級的,
b、volatile關鍵字只能作用于變數;而synchronized關鍵字可以修飾方法和代碼塊,
c、多執行緒訪問volatile關鍵字不會發生阻塞,而訪問synchronized關鍵字可能會發生阻塞,
d、volatile關鍵字保證了資料的可見性,而不能保證資料的原子性;而synchronized關鍵字兩者都可以,
e、volatile關鍵字主要用于解決變數在多個執行緒之間的可見性,而synchronized關鍵字主要用于解決多執行緒之間訪問資源的同步性,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/267035.html
標籤:其他
上一篇:西門子200SMART(三)
