摘要:我們使用命令查看字節(jié)碼會發(fā)現(xiàn)在虛擬機(jī)中這個自增運(yùn)算使用了條指令。其實(shí)這么說也不是最嚴(yán)謹(jǐn)?shù)?,因?yàn)榧词菇?jīng)過編譯后的字節(jié)碼只使用了一條指令進(jìn)行運(yùn)算也不代表這條指令就是原子操作。
volatile的語義:
1、保證被volatile修飾的變量對所有其他的線程的可見性。
2、使用volatile修飾的變量禁止指令重排優(yōu)化。
看代碼:
public class InheritThreadClass extends Thread{ private static volatile int a = 0; @Override public void run() { for(int i = 0; i < 1000; i++){ a++; } } public static void main(String[] args) { InheritThreadClass[] threads = new InheritThreadClass[100]; for(int i=0; i < 100; i++){ threads[i] = new InheritThreadClass(); threads[i].start(); } //等待所有子線程結(jié)束 while(Thread.activeCount() > 1){ Thread.yield(); } //這段代碼會在所有子線程運(yùn)行完畢之后執(zhí)行 System.out.println(a); //(1) } }
上面的代碼中創(chuàng)建了100個線程,然后在每個線程中對變量a進(jìn)行了1000次的自增運(yùn)算,那么也就意味著,如果這段代碼可以正確的并發(fā)運(yùn)行,最后在代碼(1)處應(yīng)該輸出100000。但是多次運(yùn)行你會發(fā)現(xiàn)每次輸出的結(jié)果并不是我們預(yù)期的那樣,而都是小于等于100000。也就是說每次運(yùn)行的結(jié)果是不固定的不一樣的,這是為什么呢? 因?yàn)橥ㄟ^上面volatile關(guān)鍵字的語義我們知道被該關(guān)鍵字修飾的變量對所有的線程是可見的啊,那怎么會出現(xiàn)這種情況呢?難道語義有錯? 那是不可能的,語義肯定是沒有錯的。
我們知道每一個線程都有自己的私有內(nèi)存,而線程之間的通信是通過主存來實(shí)現(xiàn)的,volatile在這里保證多線程的可見性的意思是說:如果一個線程修改了被volatile關(guān)鍵字修飾的變量,會立馬刷新到主內(nèi)存中,其他需要使用這個變量的線程不在從自己的私有內(nèi)存中獲取了,而是直接從主內(nèi)存中獲取。雖然volatile關(guān)鍵字保證了變量對所有線程的可見性,但是java代碼中的運(yùn)算操作并非原子操作。
我們使用javap命令查看字節(jié)碼(javap -verbose InheritThreadClass.class)會發(fā)現(xiàn)在虛擬機(jī)中這個自增運(yùn)算使用了4條指令(getstatic, iconst_1, iadd, putstatic)。 當(dāng)getstatic指令把a(bǔ)的值壓入棧頂時,volatile關(guān)鍵字保證了a的值此時是正確的,但是在執(zhí)行iconst_1、iadd這些指令時其他線程有可能已經(jīng)把a的值加大了,而已經(jīng)在操作棧頂?shù)闹稻妥兂闪诉^期的數(shù)據(jù)了,所以putstatic指令執(zhí)行后可能又把較小的a值同步回主內(nèi)存了。 所以它不是一個原子運(yùn)算,因此在多線程的情況下它并不是一個安全的操作。其實(shí)這么說也不是最嚴(yán)謹(jǐn)?shù)模驗(yàn)榧词菇?jīng)過編譯后的字節(jié)碼只使用了一條指令進(jìn)行運(yùn)算也不代表這條指令就是原子操作。因?yàn)橐粭l字節(jié)碼指令在解釋執(zhí)行時,解釋器需要運(yùn)行許多行代碼才能實(shí)現(xiàn)該條指令的語義,而即使是編譯執(zhí)行,一條字節(jié)碼指令也可能需要轉(zhuǎn)化成多條本地機(jī)器碼指令。
所以有關(guān)volatile的變量對其他線程的”可見性“的語義描述并不能得出這樣的結(jié)論:基于volatile變量的運(yùn)算在高并發(fā)下是安全的。
那這種在高并發(fā)下的自增運(yùn)算如何做到線程安全呢?可以使用synchronized,但是加鎖的話性能開銷太大,高并發(fā)下不是一個明智之選??梢允褂貌l(fā)包java.util.concurrent.atomic下的AtomicInteger原子類。
看代碼:
private static volatile AtomicInteger a = new AtomicInteger(0); @Override public void run() { for(int i = 0; i < 1000; i++){ a.getAndIncrement(); } }
上面的代碼就可以在高并發(fā)下正確的運(yùn)行,每次輸出都是100000。
看AtomicInteger源碼:
**//部分關(guān)鍵字段** private static final Unsafe unsafe = Unsafe.getUnsafe(); /* valueOffset這個是指類中相應(yīng)字段在該類的偏移量, 在下面的靜態(tài)塊中調(diào)用objectFieldOffset()方法初始化。 */ private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //objectFieldOffset方法是一個本地方法 public native long objectFieldOffset(Field field); // AtomicInteger的構(gòu)造器之一 public AtomicInteger(int initialValue) { value = initialValue; } //getAndIncrement()這個方法的源碼實(shí)現(xiàn) public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } //get()方法的實(shí)現(xiàn) public final int get() { return value; } /*compareAndSet(int expect, int update)方法內(nèi)部又直接調(diào)用了 *unsafe的compareAndSwapInt方法,這里直接上compareAndSwapInt源碼的實(shí)現(xiàn) *在obj的offset位置比較內(nèi)存中的值和期望的值,如果相同則更新。 *這是一個本地方法,應(yīng)該是原子的,因此提供了一種不可中斷的方式更新 */ public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/66721.html
摘要:所以接下來,我們需要簡單的介紹下多線程中的并發(fā)通信模型。比如中,以及各種鎖機(jī)制,均為了解決線程間公共狀態(tài)的串行訪問問題。 并發(fā)的學(xué)習(xí)門檻較高,相較單純的羅列并發(fā)編程 API 的枯燥被動學(xué)習(xí)方式,本系列文章試圖用一個簡單的栗子,一步步結(jié)合并發(fā)編程的相關(guān)知識分析舊有實(shí)現(xiàn)的不足,再實(shí)現(xiàn)邏輯進(jìn)行分析改進(jìn),試圖展示例子背后的并發(fā)工具與實(shí)現(xiàn)原理。 本文是本系列的第一篇文章,提出了一個簡單的業(yè)務(wù)場景...
摘要:對象頭的另外一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,換句話說,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身,這點(diǎn)將在節(jié)討論。 目錄介紹 1.關(guān)于int和Integer的問題區(qū)別分析 2.Integer的值緩存的原理 2.1 Java 5 中引入緩存特性 2.2 Intege...
摘要:今天給大家總結(jié)一下,面試中出鏡率很高的幾個多線程面試題,希望對大家學(xué)習(xí)和面試都能有所幫助。指令重排在單線程環(huán)境下不會出先問題,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實(shí)例。使用可以禁止的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。 下面最近發(fā)的一些并發(fā)編程的文章匯總,通過閱讀這些文章大家再看大廠面試中的并發(fā)編程問題就沒有那么頭疼了。今天給大家總結(jié)一下,面試中出鏡率很高的幾個多線...
摘要:目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時,存在的原子性可見性緩存一致性以及有序性問題。最多只有一個線程能持有鎖。線程加入規(guī)則對象的結(jié)束先行發(fā)生于方法返回。 前言 學(xué)習(xí)情況記錄 時間:week 1 SMART子目標(biāo) :Java 多線程 學(xué)習(xí)Java多線程,要了解多線程可能出現(xiàn)的并發(fā)現(xiàn)象,了解Java內(nèi)存模型的知識是必不可少的。 對學(xué)習(xí)到的重要知識點(diǎn)進(jìn)行的記錄。 注:這里提到的是Ja...
摘要:該類將整數(shù)值與引用關(guān)聯(lián)起來,可用于原子的更數(shù)據(jù)和數(shù)據(jù)的版本號。 CAS的全稱為Compare And Swap,直譯就是比較交換。是一條CPU的原子指令,其作用是讓CPU先進(jìn)行比較兩個值是否相等,然后原子地更新某個位置的值,其實(shí)現(xiàn)方式是基于硬件平臺的匯編指令,在intel的CPU中,使用的是cmpxchg指令,就是說CAS是靠硬件實(shí)現(xiàn)的,從而在硬件層面提升效率。 CSA 原理 利用CP...
閱讀 2075·2023-04-25 22:58
閱讀 1427·2021-09-22 15:20
閱讀 2706·2019-08-30 15:56
閱讀 2000·2019-08-30 15:54
閱讀 2117·2019-08-29 12:31
閱讀 2741·2019-08-26 13:37
閱讀 605·2019-08-26 13:25
閱讀 2107·2019-08-26 11:58