日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

volatile超详细讲解

發布時間:2025/3/19 编程问答 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 volatile超详细讲解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄

寫在前面

一、什么是volatile

二、JVM(java虛擬機)、JMM(java內存模型)

三、volatile內存可見性驗證

四、可見性說明

五、volitale不保證原子性驗證

六、volatile不保證原子性理論解釋

七:volatile不保證原子性問題解決

八、volatile指令重排

九、單例模式在多線程環境下可能存在安全問題


寫在前面

? ? 很多小伙伴相信都會聽說過volatile關鍵字,但是這個關鍵字有什么作用呢?也許大體也能明白,但是如果碰上較真的面試官,可能會直接蒙了。

? ? 今天就給大家充分分析一下volatile關鍵字到底有什么作用~

一、什么是volatile

? ? volatile是java的一個關鍵字,volatile是Java虛擬機提供的輕量級的同步機制。

? ? volatile有三個特性:1.保證可見性。2.不保證原子性。3.禁止指令重排序。

二、JVM(java虛擬機)、JMM(java內存模型)

? ? JMM(Java內存模型 Java Memory Model,簡稱JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

? ? JMM關于同步的規定:

1線程解鎖前,必須把共享變量的值刷新回主內存

2線程加鎖前,必須讀取主內存的最新值到自己的工作內存

3加鎖解鎖是同一把鎖

? ? 由于JMM運行程序的實體是線程,而每個線程創建時JM都會為其創建一個工作內存(有些地方稱為棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝的自己的工作內存空間,然后對變量進行操作,操作完成后再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲著主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:

三、volatile內存可見性驗證

/** * volatile驗證內存的可見性 */ public class VolatileTest {public static void main(String[] args) {Dt1 dt1 = new Dt1();//新線程,三秒中后將num值設置為100new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}dt1.setDt();System.out.println(Thread.currentThread().getName() + " date 0 -> " + dt1.num);}, "Thread1").start();while(dt1.num == 0){//num不變的話會一直卡在這//num不用volatile修飾的話,會一直卡在這,main的最后一段不會打印}System.out.println("main 結束,num值改變了!");} }/** * 1.不加volatile,num沒有可見性。 * 2.添加volatile可以解決可見性問題 */ class Dt1{//volatile int num = 0;int num = 0;public void setDt(){this.num = 100;} }

四、可見性說明

? ? 各個線程對主內存中共享變量的操作都是各個線程各自拷貝到自己的工作內存進行操作后再寫回到主內存中的。

? ? 這就可能存在一個線程AAA修改了共享變量X的值但還未寫回主內存時,另一個線程BBB又對主內存中同一個共享變量X進行操作,但此時AAA線程工作內存中共享變量X對線程BBB來說并不可見,這種工作內存與主內存同步延遲現象就造成了可見性問題。

五、volitale不保證原子性驗證

/** * 驗證volatile不保證數據的原子性 * 什么是原子性?在做某個業務時,中間的邏輯不可以被分割。 */ public class VolatileTest2 {public static void main(String[] args) {Dt2 dt2 = new Dt2();//20個線程,每個線程將num加1000次for (int i = 0;i < 20; i++){new Thread(() -> {for (int j = 0; j < 1000; j++) {dt2.addDt();}}).start();}while (Thread.activeCount() > 2){Thread.yield();}System.out.println(dt2.num);//發現,最后的打印結果并不是20000!所以volatile并不會保證++操作的原子性} }/** * 用volatile修飾,++操作并不會保證原子性 */ class Dt2{volatile int num = 0;//int num = 0;public void addDt(){this.num ++;} }

六、volatile不保證原子性理論解釋

num++會被分解成3個機器指令,三個指令并不是一個原子操作,所以volitale并不會保證操作的原子性:

七:volatile不保證原子性問題解決

1.使用synchronized(大材小用)

class Dt2{int num = 0;public synchronized void addDt(){this.num ++;} }

2.atomic原子類

import java.util.concurrent.atomic.AtomicInteger;/** * 使用AtomicInteger原子類 */ public class VolatileTestAtomicInteger {public static void main(String[] args) {Dt3 dt3 = new Dt3();//20個線程,每個線程將num加1000次for (int i = 0;i < 20; i++){new Thread(() -> {for (int j = 0; j < 1000; j++) {dt3.addDt();}}).start();}while (Thread.activeCount() > 2){Thread.yield();}System.out.println(dt3.num);//發現,最后的打印結果就是20000!} }class Dt3{AtomicInteger num = new AtomicInteger();//默認是0public void addDt(){num.getAndIncrement();} }

八、volatile指令重排

1.什么是有序性: 計算機在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分為以下3種: 源代碼->編譯器優化的重排->指令并行的重排->內存系統的重排->最終執行的指令。 單線程環境里面確保程序最終執行結果和代碼順序執行的結果一致。 處理器在進行重排序時必須要考慮指令之間的數據依賴性(先有你爹才能有你……)。 多線程環境中線程交替執行,由于編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。 (舉例:高考做卷子,優先做會的,并不是根據題目來順序做……) 2.指令重排案例: int x = 11; // 語句1 int y = 12; // 語句2 x = x + 5; // 語句3 y = x * x; // 語句4// 指令重排之后,可能會出現以下幾種情況(對結果并不會有影響) 1234 2134 1324//不可能出現4123。因為數據依賴性,語句4依賴于x聲明的值、x計算后的值、y的聲明。

/** * method2和method2多線程情況下,有可能會出現a=6,也有可能出現a=5,因為語句1和語句2指令重排 */ public class ReSortSeqDemo{int a = 0;boolean flag = false;public void method1(){a = 1; // 語句1flag = true; // 語句2}// 多線程環境中線程交替執行,由于編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。public void method2(){if(flag){a = a + 5; // 語句3System.out.println("value a = " + a);}} } 3.指令重排小總結 volatile實現禁止指令重排優化,從而避免多線程環境下程序出現亂序執行的現象。 先了解一個概念,內存屏障(Memory?Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個: 一是保證特定操作的執行順序, 二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。 由于編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory?Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory?Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前后的指令執行重排序優化。內存屏障另外一個作用是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。 4.線程安全性獲得保證 工作內存與主內存同步延遲現象導致的可見性問題: 可以使用synchronized或volatile關鍵字解決,它們都可以使一個線程修改后的變量立即對其他線程可見。 對于指令重排導致的可見性問題和有序性問題: 可以利用volatile關鍵字解決,因為volatile的另外一個作用就是禁止重排序優化。

九、單例模式在多線程環境下可能存在安全問題

1.代碼示例

public class SingletonDemo {//雙端檢鎖需要使用volatileprivate volatile static SingletonDemo instance = null;private SingletonDemo(){System.out.println("我是私有構造方法");}//這里加synchronized也可以實現單例,但是太重public static SingletonDemo getInstance(){if(instance == null){instance = new SingletonDemo();}return instance;}//DCL(Double check Lock 雙端檢鎖機制)public static SingletonDemo getInstance2(){if(instance == null){synchronized (SingletonDemo.class){if(instance == null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {//單線程下的單例模式,私有構造方法只執行一次 // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//并發模式下 // for (int i = 0; i < 10; i++) { // new Thread(() -> { // //并發下,單例模式的構造方法會被調用很多次,有線程不安全問題 // SingletonDemo.getInstance(); // }, String.valueOf(i)).start(); // }//雙端檢鎖for (int i = 0; i < 10; i++) {new Thread(() -> {//并發下,單例模式的構造方法會被調用很多次,有線程不安全問題SingletonDemo.getInstance2();}, String.valueOf(i)).start();}} } 2.為什么DCL(雙端檢鎖機制)一定要用volatile? DCL(雙端檢鎖)機制不一定線程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。 原因在于某一個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化。 instance = new SingletonDemo();// 可以分為以下3步完成(偽代碼) memory = allocate(); // 1.分配對象內存空間 instance(memory); // 2.初始化對象 instance = memory; // 3.設置instance指向剛分配的地址,此時instance!=null。 步驟2和步驟3不存在數據依賴關系,而且無論重排前還是重排后程序的執行結果在單線程中并沒有改變,因此這種重排優化是允許的。 假如重排后: instance = new SingletonDemo();// 可以分為以下3步完成(偽代碼) memory = allocate(); // 1.分配對象內存空間 instance = memory; // 3.設置instance指向剛分配的地址,此時還沒有完成初始化!此時如果有另一個線程也同時執行到這,導致這倆線程初始化的對象并不是同一個對象。 instance(memory); // 2.初始化對象

? ? 但是指令重排只會保證串行語義的執行的一致性(單線程),但并不會關心多線程間的語義一致性。

? ? 所以當一條線程訪問instance不為null時,由于指令重排序,instance實例未必已初始化完成,也就造成了線程安全問題,導致兩次取得的單例對象并不是同一個對象。

總結

以上是生活随笔為你收集整理的volatile超详细讲解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。