java并發(fā)編程之原子性、可見(jiàn)性、有序性
在java中,執(zhí)行下面這個(gè)語(yǔ)句
int i =12;
執(zhí)行線程必須先在自己的工作線程中對(duì)變量i所在的緩存行進(jìn)行賦值操作,然后再寫(xiě)入主存當(dāng)中。而不是直接將數(shù)值10寫(xiě)入主存(物理內(nèi)存)當(dāng)中。
1 原子性
定義:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
舉個(gè)最簡(jiǎn)單的例子,大家想一下假如為一個(gè)32位的變量賦值過(guò)程不具備原子性的話,會(huì)發(fā)生什么后果?
int i =12;
假若一個(gè)線程執(zhí)行到這個(gè)語(yǔ)句時(shí),暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過(guò)程:為低16位賦值,為高16位賦值。
那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫(xiě)入之后,突然被中斷,而此時(shí)又有一個(gè)線程去讀取i的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)。
1.1 java中的原子性操作
在Java中,對(duì)基本數(shù)據(jù)類(lèi)型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。
例如:
int x = 10; //語(yǔ)句1 int y = x; //語(yǔ)句2 x++; //語(yǔ)句3 x = x + 1; //語(yǔ)句4
語(yǔ)句1是直接將數(shù)值10賦值給x,也就是說(shuō)線程執(zhí)行這個(gè)語(yǔ)句的會(huì)直接將數(shù)值10寫(xiě)入到工作內(nèi)存中,所以是原子性操作。
語(yǔ)句2實(shí)際上包含2個(gè)操作,它先要去讀取x的值,再將y的值寫(xiě)入主存,雖然讀取x的值以及 將y的值寫(xiě)入主存 這2個(gè)操作都是原子性操作,但是合起來(lái)就不是原子性操作了。
語(yǔ)句3 語(yǔ)句4 同理,先將x的值讀取到高速緩存中,然后+1賦值后,再寫(xiě)入到主存中。
也就是說(shuō),只有簡(jiǎn)單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量,變量之間的相互賦值不是原子操作)才是原子操作。
2 可見(jiàn)性
定義:指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
2.1 可見(jiàn)性問(wèn)題
例如:
//線程1 int i =12; i=13; //線程2 int j=i;
假若執(zhí)行線程1的是CPU1
,執(zhí)行線程2的是CPU2
。當(dāng)線程1執(zhí)行 i =13
這句時(shí),會(huì)先把i的初始值加載到CPU1
的高速緩存中,然后賦值為13,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?3了,卻沒(méi)有立即寫(xiě)入到主存當(dāng)中。
此時(shí)線程2執(zhí)行 j = i
,它會(huì)先去主存讀取i的值并加載到CPU2
的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是12,那么就會(huì)使得j的值為12,而不是13。
這就是可見(jiàn)性問(wèn)題,也就是說(shuō) i 的值在線程一中修改了,沒(méi)有通知其他線程更新而導(dǎo)致的數(shù)據(jù)錯(cuò)亂。
2.2 解決可見(jiàn)性問(wèn)題
Java
提供了volatile
關(guān)鍵字來(lái)保證可見(jiàn)性。
也就是說(shuō)當(dāng)一個(gè)共享變量被volatile
修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
3 有序性
定義:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
3.1 單個(gè)線程內(nèi)程序的指令重排序
例如:
int i = 0; boolean flag = false; i = 1; //語(yǔ)句1 flag = true; //語(yǔ)句2
按照我們?nèi)粘5乃季S,程序的執(zhí)行過(guò)程是從上至下一行一行執(zhí)行的,就是說(shuō)按照代碼的順序來(lái)執(zhí)行,那么JVM在實(shí)際中一定會(huì)這樣嗎??? 答案是否定的,這里可能會(huì)發(fā)生指令重排序(Instruction Reorder
)。
指令重排序(Instruction Reorder
) 是指: 處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語(yǔ)句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
在Java
內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,但是重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
比如上面的代碼中,語(yǔ)句1和語(yǔ)句2誰(shuí)先執(zhí)行對(duì)最終的程序結(jié)果并沒(méi)有影響,那么就有可能在執(zhí)行過(guò)程中,語(yǔ)句2先執(zhí)行而語(yǔ)句1后執(zhí)行。
需要注意的是:處理器在進(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴(lài)性,如果一個(gè)指令I(lǐng)nstruction 2
必須用到Instruction
1的結(jié)果,那么處理器會(huì)保證Instruction 1
會(huì)在Instruction 2
之前執(zhí)行。
3.2 多線程內(nèi)程序的指令重排序
重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程就不一定了。
//線程1: context = loadContext(); //語(yǔ)句1 inited = true; //語(yǔ)句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
上面代碼中,由于語(yǔ)句1和語(yǔ)句2沒(méi)有數(shù)據(jù)依賴(lài)性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過(guò)程中先執(zhí)行語(yǔ)句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while
循環(huán),去執(zhí)行doSomethingwithconfig(context)
方法,而此時(shí)context
并沒(méi)有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。
3.3 保證有序性的解決方法
在Java里面,可以通過(guò)volatile
關(guān)鍵字來(lái)保證一定的“有序性”。
當(dāng)然可以通過(guò)synchronized
和Lock
來(lái)保證有序性,很顯然,synchronized
和Lock
保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
3.4 volatile 保證有序性的原理
volatile
關(guān)鍵字能禁止指令重排序,所以volatile
能在一定程度上保證有序性,也就是說(shuō):
當(dāng)程序執(zhí)行到volatile
變量的讀操作或者寫(xiě)操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見(jiàn);在其后面的操作肯定還沒(méi)有進(jìn)行;
在進(jìn)行指令優(yōu)化時(shí),不能將在對(duì)volatile變量訪問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把volatile變量后面的語(yǔ)句放到其前面執(zhí)行。
4 實(shí)例分析
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); } }
一般說(shuō)來(lái) 有10個(gè)線程分別進(jìn)行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000。但實(shí)際中并不是這樣,進(jìn)行過(guò)測(cè)試后會(huì)發(fā)現(xiàn),每次執(zhí)行結(jié)束后,得到的都是一個(gè)比10000要小的值。
4.1 原理分析
自增操作是不具備原子性的,它包括讀取變量的原始值到高速緩存中、進(jìn)行加1操作、寫(xiě)入主存中這三個(gè)過(guò)程。
也就是說(shuō)自增操作的三個(gè)子操作可能會(huì)分割開(kāi)執(zhí)行,就有可能導(dǎo)致下面這種情況出現(xiàn):
假如某個(gè)時(shí)刻變量inc的值為10,
線程1對(duì)變量進(jìn)行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對(duì)變量進(jìn)行自增操作,線程2也去讀取變量inc的原始值,
此時(shí) 變量inc的值還沒(méi)有任何改變,此時(shí)線程2拿到的值也為10,然后進(jìn)行加1操作,然后將值11寫(xiě)入到主存中,
然后線程1繼續(xù)進(jìn)行加1操作 這里線程1中 inc的值依然為10,進(jìn)行加1操作,然后將值11寫(xiě)入到主存中
那么兩個(gè)線程分別進(jìn)行了一次自增操作后,inc只增加了1。
4.2 synchronized 結(jié)合
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); } }
4.3 Lock 結(jié)合
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); } }
4.4 使用AtomicInteger替換int
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); } }
到此這篇關(guān)于java并發(fā)編程之原子性、可見(jiàn)性、有序性 的文章就介紹到這了,更多相關(guān)java并發(fā)編程之原子性、可見(jiàn)性、有序性 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java替換視頻背景音樂(lè)的實(shí)現(xiàn)示例
FFmpeg 是一個(gè)強(qiáng)大的開(kāi)源多媒體處理工具,被廣泛應(yīng)用于音視頻的錄制、轉(zhuǎn)碼、編輯等方面,本文主要介紹了Java替換視頻背景音樂(lè),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03一文詳解Spring事務(wù)的實(shí)現(xiàn)與本質(zhì)
這篇文章主要介紹了Spring中事務(wù)的兩種實(shí)現(xiàn)方式:聲明式事務(wù)、編程式事務(wù)以及他們的本質(zhì)。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-04-04List轉(zhuǎn)換成Map工具類(lèi)的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇List轉(zhuǎn)換成Map工具類(lèi)的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01java自定義實(shí)現(xiàn)base64編碼轉(zhuǎn)換
本文主要介紹了java 自定義實(shí)現(xiàn)base64編碼轉(zhuǎn)換的方法,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02java語(yǔ)言實(shí)現(xiàn)權(quán)重隨機(jī)算法完整實(shí)例
這篇文章主要介紹了java語(yǔ)言實(shí)現(xiàn)權(quán)重隨機(jī)算法完整實(shí)例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-11-11關(guān)于idea無(wú)法修改模板中jdk版本問(wèn)題
這篇文章主要介紹了關(guān)于idea無(wú)法修改模板中jdk版本問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10