摘要:安全性小結(jié)我們上邊介紹了原子性操作內(nèi)存可見性以及指令重排序三個(gè)在多線程執(zhí)行過程中會(huì)影響到安全性的問題。
指令重排序
如果說內(nèi)存可見性問題已經(jīng)讓你抓狂了,那么下邊的這個(gè)指令重排序的事兒估計(jì)就要罵娘了~這事兒還得從一段代碼說起:
public class Reordering { private static boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { Thread.yield(); } System.out.println(num); } }, "t1"); t1.start(); num = 5; flag = true; } }
需要注意到flag并不是一個(gè)volatile變量,也就是說它存在內(nèi)存可見性問題,但是即便如此,num = 5也是寫在flag = true的前邊的,等到t1線程檢測到了flag值的變化,num值的變化應(yīng)該是早于flag值刷新到主內(nèi)存的,所以線程t1最后的輸出結(jié)果肯定是5?。?!
no!no!no! 輸出的結(jié)果也可能是0,也就是說flag = true可能先于num = 5執(zhí)行,有沒有亮瞎你的狗眼~ 這些代碼最后都會(huì)變成機(jī)器能識(shí)別的二進(jìn)制指令,我們把這種指令不按書寫順序執(zhí)行的情況稱為指令重排序。大多數(shù)現(xiàn)代處理器都會(huì)采用將指令亂序執(zhí)行的方法,在條件允許的情況下,直接運(yùn)行當(dāng)前有能力立即執(zhí)行的后續(xù)指令,避開獲取下一條指令所需數(shù)據(jù)時(shí)造成的等待。通過亂序執(zhí)行的技術(shù),處理器可以大大提高執(zhí)行效率。
Within-Thread As-If-Serial Semantics既然存在指令重排序這種現(xiàn)象,為什么我們之前寫代碼從來沒感覺到呢?到了多線程這才發(fā)現(xiàn)問題?
指令重排序不是隨便排,一個(gè)一萬行的程序直接把最后一行當(dāng)成第一行就給執(zhí)行那不就逆天了了么,指令重排序是需要遵循代碼依賴情況的。比如下邊幾行代碼:
int i = 0, b = 0; i = i + 5; //指令1 i = i*2; //指令2 b = b + 3; //指令3
對于上邊標(biāo)注的3個(gè)指令來說,指令2是對指令1有依賴的,所以指令2不能被排到指令1之前執(zhí)行。但是指令3跟指令1和指令2都沒有關(guān)系,所以指令3可以被排在指令1之前,或者指令1和指令2中間或者指令2后邊執(zhí)行都可以~ 這樣在單線程中執(zhí)行這段代碼的時(shí)候,最終結(jié)果和沒有重排序的執(zhí)行結(jié)果是一樣的,所以這種重排序有著Within-Thread As-If-Serial Semantics的含義,翻譯過來就是線程內(nèi)表現(xiàn)為串行的語義。
但是這種指令重排序在單線程中沒有任何問題的,但是在多線程中,就引發(fā)了我們上邊在執(zhí)行flag = true后,num的值仍然不能確定是0還是5~
抑制重排序在多線程并發(fā)編程的過程中,執(zhí)行重排序有時(shí)候會(huì)造成錯(cuò)誤的后果,比如一個(gè)線程在main線程中調(diào)用setFlag(true)的前邊修改了某些程序配置項(xiàng),而在t1線程里需要用到這些配置項(xiàng),所以會(huì)造成配置缺失的錯(cuò)誤。但是java給我們提供了一些抑制指令重排序的方式。
同步代碼抑制指令重排序
將需要抑制指令重排序的代碼放入同步代碼塊中:
public class Reordering { private static boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!getFlag()) { Thread.yield(); } System.out.println(num); } }, "t1"); t1.start(); num = 5; setFlag(true); } public synchronized static void setFlag(boolean flag) { Reordering.flag = flag; } public synchronized static boolean getFlag() { return flag; } }
在獲取鎖的時(shí)候,它前邊的操作必須已經(jīng)執(zhí)行完成,不能和同步代碼塊重排序;在釋放鎖的時(shí)候,同步代碼塊中的代碼必須全部執(zhí)行完成,不能和同步代碼塊后邊的代碼重排序。
加了鎖之后,num=5就不能和flag=true的代碼進(jìn)行重排序了,所以在線程2中看到的num值肯定是5,而不會(huì)是0嘍~
雖然抑制重排序可以保證多線程程序按照我們期望的執(zhí)行順序進(jìn)行執(zhí)行,但是它抑制了處理器對指令執(zhí)行的優(yōu)化,原來能并行執(zhí)行的指令現(xiàn)在只能串行執(zhí)行,會(huì)導(dǎo)致一定程度的性能下降,所以加鎖只能保證在執(zhí)行同步代碼塊時(shí),它之前的代碼已經(jīng)執(zhí)行完成,在同步代碼塊執(zhí)行完成之前,代碼塊后邊的代碼是不能執(zhí)行的,也就是只保證加鎖前、加鎖中、加鎖后這三部分的執(zhí)行時(shí)序,但是同步代碼塊之前的代碼可以重排序,同步代碼塊中的代碼可以重排序,同步代碼塊之后的代碼也可以進(jìn)行重排序,在保證執(zhí)行順序的基礎(chǔ)上,盡最大可能讓性能得到提升,比方說下邊這段代碼:
int i = 1; int j = 2; synchronized (Reordering.class) { int m = 3; int n = 4; } int x = 5; int y = 6;
它的一個(gè)執(zhí)行時(shí)序可能是:
volatile變量抑制指令重排序
還是那句老話,加鎖會(huì)導(dǎo)致競爭同一個(gè)鎖的線程阻塞,造成線程切換,代價(jià)比較大,volatile變量也提供了一些抑制指令重排序的語義,上邊的程序可以改成這樣:
public class Reordering { private static volatile boolean flag; private static int num; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { Thread.yield(); } System.out.println(num); } }); t1.start(); num = 5; flag = true; } } `` 也就是把``flag``聲明為``volatile變量``,這樣也能起到抑制重排序的效果,``volatile變量``具體抑制重排序的規(guī)則如下: 1. volatile寫之前的操作不會(huì)被重排序到volatile寫之后。 2. volatile讀之后的操作不會(huì)被重排序到volatile讀之前。 3. 前邊是volatile寫,后邊是volatile讀,這兩個(gè)操作不能重排序。 ![圖片描述][3] 除了這三條規(guī)定以外,其他的操作可以由處理器按照自己的特性進(jìn)行重排序,換句話說,就是怎么執(zhí)行著快,就怎么來。比如說:
flag = true;
num = 5;
``
在volatile變量之后進(jìn)行普通變量的寫操作,那就可以重排序嘍,直到遇到一條volatile讀或者有執(zhí)行依賴的代碼才會(huì)阻止重排序的過程。
final變量抑制指令重排序
在java語言中,用final修飾的字段被賦予了一些特殊的語義,它可以阻止某些重排序,具體的規(guī)則就這兩條:
在構(gòu)造方法內(nèi)對一個(gè)final字段的寫入,與隨后把這個(gè)被構(gòu)造對象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
初次讀一個(gè)包含final字段對象的引用,與隨后初次讀這個(gè)final字段,這兩個(gè)操作不能重排序。
可能大家看的有些懵逼,趕緊寫代碼理解一下:
public class FinalReordering { int i; final int j; static FinalReordering obj; public FinalReordering() { i = 1; j = 2; } public static void write() { obj = new FinalReordering(); } public static void read() { FinalReordering finalReordering = FinalReordering.obj; int a = finalReordering.i; int b = finalReordering.j; } }
我們假設(shè)有一個(gè)線程執(zhí)行write方法,另一個(gè)線程執(zhí)行read方法。
先看一下對final字段進(jìn)行寫操作時(shí),不同線程執(zhí)行write方法和read方法的一種可能情況是:
從上圖中可以看出,普通的字段可能在構(gòu)造方法完成之后才被真正的寫入值,所以另一個(gè)線程在訪問這個(gè)普通變量的時(shí)候可能讀到了0,這顯然是不符合我們的預(yù)期的。但是final字段的賦值不允許被重排序到構(gòu)造方法完成之后,所以在把該字段所在對象的引用賦值出去之前,final字段肯定是被賦值過了,也就是說這兩個(gè)操作不能被重排序。
再來看一下初次讀取final字段的情況,下邊是不同線程執(zhí)行write方法和read方法的一種可能情況:
從上圖可以看出,普通字段的讀取操作可能被重排序到讀取該字段所在對象引用前邊,自然會(huì)得到NullPointerException異常嘍,但是對于final字段,在讀final字段之前,必須保證它前邊的讀操作都執(zhí)行完成,也就是說必須先進(jìn)行該字段所在對象的引用的讀取,再讀取該字段,也就是說這兩個(gè)操作不能進(jìn)行重排序。
值得注意的是,讀取對象引用與讀取該對象的字段是存在間接依賴的關(guān)系的,對象引用都沒有被賦值,還讀個(gè)錘子對象的字段嘍,一般的處理器默認(rèn)是不會(huì)重排序這兩個(gè)操作的,可是有一些為了性能不顧一切的處理器,比如alpha處理器,這種處理器是可能把這兩個(gè)操作進(jìn)行重排序的,所以這個(gè)規(guī)則就是給這種處理器貼身設(shè)計(jì)的~ 也就是說對于final字段,不管在什么處理器上,都得先進(jìn)行對象引用的讀取,再進(jìn)行final字段的讀取。但是并不保證在所有處理器上,對于對象引用讀取和普通字段讀取的順序是有序的。
安全性小結(jié)我們上邊介紹了原子性操作、內(nèi)存可見性以及指令重排序三個(gè)在多線程執(zhí)行過程中會(huì)影響到安全性的問題。
synchronized可以把三個(gè)問題都解決掉,但是伴隨著這種萬能特性,是多線程在競爭同一個(gè)鎖的時(shí)候會(huì)造成線程切換,導(dǎo)致線程阻塞,這個(gè)對性能的影響是非常大的。
volatile不能保證一系列操作的原子性,但是可以保證對于一個(gè)變量的讀取和寫入是原子性的,一個(gè)線程對某個(gè)volatile變量的寫入是可以立即對其他線程可見的,另外,它還可以禁止處理器對一些指令執(zhí)行的重排序。
final變量依靠它的禁止重排序規(guī)則,保證在使用過程中的安全性。一旦被賦值成功,它的值在之后程序執(zhí)行過程中都不會(huì)改變,也不存在所謂的內(nèi)存可見性問題。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/74148.html
摘要:假設(shè)不發(fā)生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時(shí)刷新到主存中。線程的最后操作與線程發(fā)現(xiàn)線程已經(jīng)結(jié)束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關(guān)于 Java 并發(fā)也算是寫了好幾篇文章了,本文將介紹一些比較基礎(chǔ)的內(nèi)容,注意,閱讀本文需要一定的并發(fā)基礎(chǔ)。 本文的...
摘要:并發(fā)編程的挑戰(zhàn)并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線程就能讓程序最大限度的并發(fā)執(zhí)行。的實(shí)現(xiàn)原理與應(yīng)用在多線程并發(fā)編程中一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖。 并發(fā)編程的挑戰(zhàn) 并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是,并不是啟動(dòng)更多的線程就能讓程序最大限度的并發(fā)執(zhí)行。如果希望通過多線程執(zhí)行任務(wù)讓程序運(yùn)行的更快,會(huì)面臨非常多的挑戰(zhàn):(1)上下文切換(2)死...
摘要:本文會(huì)先闡述在并發(fā)編程中解決的問題多線程可見性,然后再詳細(xì)講解原則本身。所以與內(nèi)存之間的高速緩存就是導(dǎo)致線程可見性問題的一個(gè)原因。原則上面討論了中多線程共享變量的可見性問題及產(chǎn)生這種問題的原因。 Happens-Before是一個(gè)非常抽象的概念,然而它又是學(xué)習(xí)Java并發(fā)編程不可跨域的部分。本文會(huì)先闡述Happens-Before在并發(fā)編程中解決的問題——多線程可見性,然后再詳細(xì)講解H...
摘要:這個(gè)規(guī)則比較好理解,無論是在單線程環(huán)境還是多線程環(huán)境,一個(gè)鎖處于被鎖定狀態(tài),那么必須先執(zhí)行操作后面才能進(jìn)行操作。線程啟動(dòng)規(guī)則獨(dú)享的方法先行于此線程的每一個(gè)動(dòng)作。 1. 指令重排序 關(guān)于指令重排序的概念,比較復(fù)雜,不好理解。我們從一個(gè)例子分析: public class SimpleHappenBefore { /** 這是一個(gè)驗(yàn)證結(jié)果的變量 */ private st...
摘要:并發(fā)編程關(guān)鍵字解析解析概覽內(nèi)存模型的相關(guān)概念并發(fā)編程中的三個(gè)概念內(nèi)存模型深入剖析關(guān)鍵字使用關(guān)鍵字的場景內(nèi)存模型的相關(guān)概念緩存一致性問題。事實(shí)上,這個(gè)規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。 Java并發(fā)編程:volatile關(guān)鍵字解析 1、解析概覽 內(nèi)存模型的相關(guān)概念 并發(fā)編程中的三個(gè)概念 Java內(nèi)存模型 深入剖析volatile關(guān)鍵字 ...
閱讀 2340·2021-11-22 14:56
閱讀 1478·2021-09-24 09:47
閱讀 913·2019-08-26 18:37
閱讀 2832·2019-08-26 12:10
閱讀 1528·2019-08-26 11:55
閱讀 3150·2019-08-23 18:07
閱讀 2306·2019-08-23 14:08
閱讀 612·2019-08-23 12:12