摘要:此時(shí),就出現(xiàn)了線程不安全問(wèn)題了。因?yàn)榈某跏贾禃?huì)是因此,重排序是有可能導(dǎo)致線程安全問(wèn)題的。真的能完全保證一個(gè)變量的線程安全嗎我們通過(guò)上面的講解,發(fā)現(xiàn)關(guān)鍵字還是挺有用的,不但能夠保證變量的可見(jiàn)性,還能保證代碼的有序性。
對(duì)于volatile這個(gè)關(guān)鍵字,相信很多朋友都聽(tīng)說(shuō)過(guò),甚至使用過(guò),這個(gè)關(guān)鍵字雖然字面上理解起來(lái)比較簡(jiǎn)單,但是要用好起來(lái)卻不是一件容易的事。
這篇文章將從多個(gè)方面來(lái)講解volatile,讓你對(duì)它更加理解。
計(jì)算機(jī)中為什么會(huì)出現(xiàn)線程不安全的問(wèn)題volatile既然是與線程安全有關(guān)的問(wèn)題,那我們先來(lái)了解一下計(jì)算機(jī)在處理數(shù)據(jù)的過(guò)程中為什么會(huì)出現(xiàn)線程不安全的問(wèn)題。
大家都知道,計(jì)算機(jī)在執(zhí)行程序時(shí),每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過(guò)程中會(huì)涉及到數(shù)據(jù)的讀取和寫(xiě)入。由于程序運(yùn)行過(guò)程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的,這時(shí)就存在一個(gè)問(wèn)題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫(xiě)入數(shù)據(jù)的過(guò)程跟CPU執(zhí)行指令的速度比起來(lái)要慢的多,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過(guò)和內(nèi)存的交互來(lái)進(jìn)行,會(huì)大大降低指令執(zhí)行的速度。
為了處理這個(gè)問(wèn)題,在CPU里面就有了高速緩存(Cache)的概念。當(dāng)程序在運(yùn)行過(guò)程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫(xiě)入數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。
我舉個(gè)簡(jiǎn)單的例子,比如cpu在執(zhí)行下面這段代碼的時(shí)候,
t = t + 1;
會(huì)先從高速緩存中查看是否有t的值,如果有,則直接拿來(lái)使用,如果沒(méi)有,則會(huì)從主存中讀取,讀取之后會(huì)復(fù)制一份存放在高速緩存中方便下次使用。之后cup進(jìn)行對(duì)t加1操作,然后把數(shù)據(jù)寫(xiě)入高速緩存,最后會(huì)把高速緩存中的數(shù)據(jù)刷新到主存中。
這一過(guò)程在單線程運(yùn)行是沒(méi)有問(wèn)題的,但是在多線程中運(yùn)行就會(huì)有問(wèn)題了。在多核CPU中,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核CPU來(lái)說(shuō),其實(shí)也會(huì)出現(xiàn)這種問(wèn)題,只不過(guò)是以線程調(diào)度的形式來(lái)分別執(zhí)行的,本次講解以多核cup為主)。這時(shí)就會(huì)出現(xiàn)同一個(gè)變量在兩個(gè)高速緩存中的值不一致問(wèn)題了。
例如:
兩個(gè)線程分別讀取了t的值,假設(shè)此時(shí)t的值為0,并且把t的值存到了各自的高速緩存中,然后線程1對(duì)t進(jìn)行了加1操作,此時(shí)t的值為1,并且把t的值寫(xiě)回到主存中。但是線程2中高速緩存的值還是0,進(jìn)行加1操作之后,t的值還是為1,然后再把t的值寫(xiě)回主存。
此時(shí),就出現(xiàn)了線程不安全問(wèn)題了。
Java中的線程安全問(wèn)題上面那種線程安全問(wèn)題,可能對(duì)于不同的操作系統(tǒng)會(huì)有不同的處理機(jī)制,例如Windows操作系統(tǒng)和Linux的操作系統(tǒng)的處理方法可能會(huì)不同。
我們都知道,Java是一種夸平臺(tái)的語(yǔ)言,因此Java這種語(yǔ)言在處理線程安全問(wèn)題的時(shí)候,會(huì)有自己的處理機(jī)制,例如volatile關(guān)鍵字,synchronized關(guān)鍵字,并且這種機(jī)制適用于各種平臺(tái)。
Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類(lèi)似于前面說(shuō)的物理內(nèi)存),每個(gè)線程都有自己的工作內(nèi)存(類(lèi)似于前面的高速緩存)。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主存進(jìn)行操作。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存。
由于java中的每個(gè)線程有自己的工作空間,這種工作空間相當(dāng)于上面所說(shuō)的高速緩存,因此多個(gè)線程在處理一個(gè)共享變量的時(shí)候,就會(huì)出現(xiàn)線程安全問(wèn)題。
這里簡(jiǎn)單解釋下共享變量,上面我們所說(shuō)的t就是一個(gè)共享變量,也就是說(shuō),能夠被多個(gè)線程訪問(wèn)到的變量,我們稱(chēng)之為共享變量。在java中共享變量包括實(shí)例變量,靜態(tài)變量,數(shù)組元素。他們都被存放在堆內(nèi)存中。volatile關(guān)鍵字
上面扯了一大堆,都沒(méi)提到volatile關(guān)鍵字的作用,下面開(kāi)始講解volatile關(guān)鍵字是如何保證線程安全問(wèn)題的。
可見(jiàn)性意思就是說(shuō),在多線程環(huán)境下,某個(gè)共享變量如果被其中一個(gè)線程給修改了,其他線程能夠立即知道這個(gè)共享變量已經(jīng)被修改了,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候,最終會(huì)去內(nèi)存中讀取,而不是從自己的工作空間中讀取
例如我們上面說(shuō)的,當(dāng)線程1對(duì)t進(jìn)行了加1操作并把數(shù)據(jù)寫(xiě)回到主存之后,線程2就會(huì)知道它自己工作空間內(nèi)的t已經(jīng)被修改了,當(dāng)它要執(zhí)行加1操作之后,就會(huì)去主存中讀取。這樣,兩邊的數(shù)據(jù)就能一致了。
假如一個(gè)變量被聲明為volatile,那么這個(gè)變量就具有了可見(jiàn)性的性質(zhì)了。這就是volatile關(guān)鍵的作用之一了。
當(dāng)一個(gè)變量被聲明為volatile時(shí),在編譯成會(huì)變指令的時(shí)候,會(huì)多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);
這句指令的意思就是在寄存器執(zhí)行一個(gè)加0的空操作。不過(guò)這條指令的前面有一個(gè)lock(鎖)前綴。
當(dāng)處理器在處理?yè)碛衛(wèi)ock前綴的指令時(shí):
在之前的處理中,lock會(huì)導(dǎo)致傳輸數(shù)據(jù)的總線被鎖定,其他處理器都不能訪問(wèn)總線,從而保證處理lock指令的處理器能夠獨(dú)享操作數(shù)據(jù)所在的內(nèi)存區(qū)域,而不會(huì)被其他處理所干擾。
但由于總線被鎖住,其他處理器都會(huì)被堵住,從而影響了多處理器的執(zhí)行效率。為了解決這個(gè)問(wèn)題,在后來(lái)的處理器中,處理器遇到lock指令時(shí)不會(huì)再鎖住總線,而是會(huì)檢查數(shù)據(jù)所在的內(nèi)存區(qū)域,如果該數(shù)據(jù)是在處理器的內(nèi)部緩存中,則會(huì)鎖定此緩存區(qū)域,處理完后把緩存寫(xiě)回到主存中,并且會(huì)利用緩存一致性協(xié)議來(lái)保證其他處理器中的緩存數(shù)據(jù)的一致性。
剛才我在說(shuō)可見(jiàn)性的時(shí)候,說(shuō)“如果一個(gè)共享變量被一個(gè)線程修改了之后,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候,最終會(huì)去內(nèi)存中讀取,而不是從自己的工作空間中讀取”,實(shí)際上是這樣的:
線程中的處理器會(huì)一直在總線上嗅探其內(nèi)部緩存中的內(nèi)存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內(nèi)存地址中的值,而該內(nèi)存地址剛好也在自己的內(nèi)部緩存中,那么處理器就會(huì)強(qiáng)制讓自己對(duì)該緩存地址的無(wú)效。所以當(dāng)該處理器要訪問(wèn)該數(shù)據(jù)的時(shí)候,由于發(fā)現(xiàn)自己緩存的數(shù)據(jù)無(wú)效了,就會(huì)去主存中訪問(wèn)。
有序性實(shí)際上,當(dāng)我們把代碼寫(xiě)好之后,虛擬機(jī)不一定會(huì)按照我們寫(xiě)的代碼的順序來(lái)執(zhí)行。例如對(duì)于下面的兩句代碼:
int a = 1; int b = 2;
對(duì)于這兩句代碼,你會(huì)發(fā)現(xiàn)無(wú)論是先執(zhí)行a = 1還是執(zhí)行b = 2,都不會(huì)對(duì)a,b最終的值造成影響。所以虛擬機(jī)在編譯的時(shí)候,是有可能把他們進(jìn)行重排序的。
為什么要進(jìn)行重排序呢?
你想啊,假如執(zhí)行 int a = 1這句代碼需要100ms的時(shí)間,但執(zhí)行int b = 2這句代碼需要1ms的時(shí)間,并且先執(zhí)行哪句代碼并不會(huì)對(duì)a,b最終的值造成影響。那當(dāng)然是先執(zhí)行int b = 2這句代碼了。
所以,虛擬機(jī)在進(jìn)行代碼編譯優(yōu)化的時(shí)候,對(duì)于那些改變順序之后不會(huì)對(duì)最終變量的值造成影響的代碼,是有可能將他們進(jìn)行重排序的。
更多代碼編譯優(yōu)化可以看我寫(xiě)的另一篇文章:
虛擬機(jī)在運(yùn)行期對(duì)代碼的優(yōu)化策略
那么重排序之后真的不會(huì)對(duì)代碼造成影響嗎?
實(shí)際上,對(duì)于有些代碼進(jìn)行重排序之后,雖然對(duì)變量的值沒(méi)有造成影響,但有可能會(huì)出現(xiàn)線程安全問(wèn)題的。具體請(qǐng)看下面的代碼
public class NoVisibility{ private static boolean ready; private static int number; private static class Reader extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); } } public static void main(String[] args){ new Reader().start(); number = 42; ready = true; } }
這段代碼最終打印的一定是42嗎?如果沒(méi)有重排序的話,打印的確實(shí)會(huì)是42,但如果number = 42和ready = true被進(jìn)行了重排序,顛倒了順序,那么就有可能打印出0了,而不是42。(因?yàn)閚umber的初始值會(huì)是0).
因此,重排序是有可能導(dǎo)致線程安全問(wèn)題的。
如果一個(gè)變量被聲明volatile的話,那么這個(gè)變量不會(huì)被進(jìn)行重排序,也就是說(shuō),虛擬機(jī)會(huì)保證這個(gè)變量之前的代碼一定會(huì)比它先執(zhí)行,而之后的代碼一定會(huì)比它慢執(zhí)行。
例如把上面中的number聲明為volatile,那么number = 42一定會(huì)比ready = true先執(zhí)行。
不過(guò)這里需要注意的是,虛擬機(jī)只是保證這個(gè)變量之前的代碼一定比它先執(zhí)行,但并沒(méi)有保證這個(gè)變量之前的代碼不可以重排序。之后的也一樣。
volatile關(guān)鍵字能夠保證代碼的有序性,這個(gè)也是volatile關(guān)鍵字的作用。
總結(jié)一下,一個(gè)被volatile聲明的變量主要有以下兩種特性保證保證線程安全。
可見(jiàn)性。
有序性。
volatile真的能完全保證一個(gè)變量的線程安全嗎?我們通過(guò)上面的講解,發(fā)現(xiàn)volatile關(guān)鍵字還是挺有用的,不但能夠保證變量的可見(jiàn)性,還能保證代碼的有序性。
那么,它真的能夠保證一個(gè)變量在多線程環(huán)境下都能被正確的使用嗎?
答案是否定的。原因是因?yàn)镴ava里面的運(yùn)算并非是原子操作。
原子操作:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
也就是說(shuō),處理器要嘛把這組操作全部執(zhí)行完,中間不允許被其他操作所打斷,要嘛這組操作不要執(zhí)行。
剛才說(shuō)Java里面的運(yùn)行并非是原子操作。我舉個(gè)例子,例如這句代碼
int a = b + 1;
處理器在處理代碼的時(shí)候,需要處理以下三個(gè)操作:
從內(nèi)存中讀取b的值。
進(jìn)行a = b + 1這個(gè)運(yùn)算
把a(bǔ)的值寫(xiě)回到內(nèi)存中
而這三個(gè)操作處理器是不一定就會(huì)連續(xù)執(zhí)行的,有可能執(zhí)行了第一個(gè)操作之后,處理器就跑去執(zhí)行別的操作的。
由于Java中的運(yùn)算并非是原子操作,所以導(dǎo)致volatile聲明的變量無(wú)法保證線程安全。
對(duì)于這句話,我給大家舉個(gè)例子。代碼如下:
public class Test{ public static volatile int t = 0; public static void main(String[] args){ Thread[] threads = new Thread[10]; for(int i = 0; i < 10; i++){ //每個(gè)線程對(duì)t進(jìn)行1000次加1的操作 threads[i] new Thread(new Runnable(){ @Override public void run(){ for(int j = 0; j < 1000; j++){ t = t + 1; } } }); threads[i].start(); } //等待所有累加線程都結(jié)束 while(Thread.activeCount() > 1){ Thread.yield(); } //打印t的值 System.out.println(t); } }
最終的打印結(jié)果會(huì)是1000 * 10 = 10000嗎?答案是否定的。
問(wèn)題就出現(xiàn)在t = t + 1這句代碼中。我們來(lái)分析一下
例如:
線程1讀取了t的值,假如t = 0。之后線程2讀取了t的值,此時(shí)t = 0。
然后線程1執(zhí)行了加1的操作,此時(shí)t = 1。但是這個(gè)時(shí)候,處理器還沒(méi)有把t = 1的值寫(xiě)回主存中。這個(gè)時(shí)候處理器跑去執(zhí)行線程2,注意,剛才線程2已經(jīng)讀取了t的值,所以這個(gè)時(shí)候并不會(huì)再去讀取t的值了,所以此時(shí)t的值還是0,然后線程2執(zhí)行了對(duì)t的加1操作,此時(shí)t =1 。
這個(gè)時(shí)候,就出現(xiàn)了線程安全問(wèn)題了,兩個(gè)線程都對(duì)t執(zhí)行了加1操作,但t的值卻是1。所以說(shuō),volatile關(guān)鍵字并不一定能夠保證變量的安全性。
什么情況下volatile能夠保證線程安全剛才雖然說(shuō),volatile關(guān)鍵字不一定能夠保證線程安全的問(wèn)題,其實(shí),在大多數(shù)情況下volatile還是可以保證變量的線程安全問(wèn)題的。所以,在滿(mǎn)足以下兩個(gè)條件的情況下,volatile就能保證變量的線程安全問(wèn)題:
運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
變量不需要與其他狀態(tài)變量共同參與不變約束。
講到這里,關(guān)于volatile關(guān)鍵字的就算講完了。如果有哪里講的不對(duì)的地方,非常歡迎你的指點(diǎn)。下篇應(yīng)該會(huì)講synchronize關(guān)鍵字。
完
參考書(shū)籍:
深入理解Java虛擬機(jī)(JVM高級(jí)特性與最佳實(shí)踐)。
Java并非編程實(shí)戰(zhàn)
關(guān)注公眾號(hào):苦逼的碼農(nóng),獲取更多原創(chuàng)文章,后臺(tái)回復(fù)"禮包"送你一份資源大禮包。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/76902.html
摘要:撤銷(xiāo)鎖偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。輕量級(jí)鎖線程在執(zhí)行同步代碼塊之前,會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的復(fù)制到鎖記錄中,官方稱(chēng)為。 前言 作者前面也寫(xiě)了幾篇關(guān)于Java并發(fā)編程,以及線程和volatil的基礎(chǔ)知識(shí),有興趣可以閱讀作者的原文博客,今天關(guān)于Java中的兩種鎖進(jìn)行詳解,希望對(duì)...
摘要:?jiǎn)卫J绞且环N常用的設(shè)計(jì)模式也可能是設(shè)計(jì)模式中代碼量最少的設(shè)計(jì)模式。簡(jiǎn)介單例模式屬于中設(shè)計(jì)模式中的創(chuàng)建型模式定義是確保某一個(gè)類(lèi)只有一個(gè)實(shí)例并提供一個(gè)全局的訪問(wèn)點(diǎn)。 單例模式是一種常用的設(shè)計(jì)模式、也可能是設(shè)計(jì)模式中代碼量最少的設(shè)計(jì)模式。但是少并不意味著簡(jiǎn)單、想要用好、用對(duì)單例、就的費(fèi)一番腦子了。因?yàn)樗锩嫔婕暗搅撕芏郕ava底層的知識(shí)如類(lèi)裝載機(jī)制、Java內(nèi)存模型、volatile等知識(shí)...
摘要:接著線程過(guò)來(lái)通過(guò)方式獲取鎖,獲取鎖的過(guò)程就是通過(guò)操作變量將其值從變?yōu)?。線程加鎖成功后還有一步重要的操作,就是將設(shè)置成為自己。線程屁顛屁顛的就去等待區(qū)小憩一會(huì)去了。 一、寫(xiě)在前面 這篇文章,我們聊一聊Java并發(fā)中的核武器, AQS底層實(shí)現(xiàn)。 不管是工作三四年、還是五六年的在工作或者面試中涉及到并發(fā)的是時(shí)候總是繞不過(guò)AQS這個(gè)詞。 首先,確實(shí)還有很多人連AQS是什么都不知道,甚至有的竟...
摘要:總結(jié)我們主要介紹到了以下幾種方式實(shí)現(xiàn)單例模式餓漢方式線程安全懶漢式非線程安全和關(guān)鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設(shè)計(jì)模式中文版第二版設(shè)計(jì)模式深入理解單例模式我是一個(gè)以架構(gòu)師為年之內(nèi)目標(biāo)的小小白。 初遇設(shè)計(jì)模式在上個(gè)寒假,當(dāng)時(shí)把每個(gè)設(shè)計(jì)模式過(guò)了一遍,對(duì)設(shè)計(jì)模式有了一個(gè)最初級(jí)的了解。這個(gè)學(xué)期借了幾本設(shè)計(jì)模式的書(shū)籍看,聽(tīng)了老師的設(shè)計(jì)模式課,對(duì)設(shè)計(jì)模式算是有個(gè)更進(jìn)一步的認(rèn)識(shí)。...
閱讀 2073·2021-08-21 14:09
閱讀 536·2019-08-30 15:44
閱讀 2175·2019-08-29 16:32
閱讀 1435·2019-08-29 15:36
閱讀 3561·2019-08-29 12:43
閱讀 2832·2019-08-29 11:14
閱讀 480·2019-08-28 18:26
閱讀 2300·2019-08-26 13:57