這篇文章將為大家詳細(xì)講解有關(guān) Java中Volatile關(guān)鍵字怎么使用,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
創(chuàng)新互聯(lián)建站長期為上1000家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為雞西梨樹企業(yè)提供專業(yè)的成都做網(wǎng)站、成都網(wǎng)站制作,雞西梨樹網(wǎng)站改版等技術(shù)服務(wù)。擁有十年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
Java volatile關(guān)鍵字保證了跨線程更改線程間共享變量的可見性。這可能聽起來有點(diǎn)抽象,讓我們?cè)敿?xì)說明一下。
在多線程應(yīng)用程序中,線程對(duì) non-volatile 變量進(jìn)行操作,出于性能原因,每個(gè)線程在處理變量時(shí),可以將它們從主內(nèi)存復(fù)制到CPU緩存中。如果你的計(jì)算機(jī)包含一個(gè)以上的CPU,每個(gè)線程可以在不同的CPU上運(yùn)行。這意味著,每個(gè)線程可以將同一個(gè)變量復(fù)制到不同CPU的CPU緩存中。這就和計(jì)算機(jī)的組成和工作原理息息相關(guān)了,之所以在每一個(gè) CPU 中都含有緩存模塊是因?yàn)槌鲇谛阅芸紤]。因?yàn)?CPU 的執(zhí)行速度要比內(nèi)存(這里的內(nèi)存指的是 Main Memory)快很多,因?yàn)?CPU 要對(duì)數(shù)據(jù)進(jìn)行讀、寫的操作,如果每次都和內(nèi)存進(jìn)行交互那么 CPU 在等待 I/O 這個(gè)過程中就消耗了大量時(shí)間,大部分時(shí)間都是在停滯等待而沒有真正投入工作當(dāng)中。所以為了解決這個(gè)問題就引入了CPU緩存。如下圖所示:
這樣就導(dǎo)致了一個(gè)問題同一個(gè)變量會(huì)被不同的 CPU 放在自己的緩存中,對(duì)該變量的讀、寫操作在緩存中進(jìn)行。當(dāng)然對(duì)于非共享數(shù)據(jù)來說這一點(diǎn)問題也沒有,就比如函數(shù)內(nèi)部的變量,但是對(duì)于共享數(shù)據(jù)來說就會(huì)造成多個(gè) CPU 之間對(duì)該數(shù)據(jù)進(jìn)行了操作但是別的 CPU 不知道這個(gè)數(shù)據(jù)發(fā)生了改變 ,依然使用舊的數(shù)據(jù),最終導(dǎo)致程序不符合我們的預(yù)期。因?yàn)?CPU 是不知道你的程序內(nèi)哪些數(shù)據(jù)是多線程共享數(shù)據(jù),而那些數(shù)據(jù)不是,如果你不告訴 CPU 那么它默認(rèn)都會(huì)認(rèn)為這些數(shù)據(jù)都是不共享的,而各自在自己的緩存中隨意操作。比如這個(gè)代碼:
public class VolatileCase0 { public int counter = 0; }
這個(gè)代碼在多線程執(zhí)行的環(huán)境下是不安全的,counter 是共享變量。假設(shè)兩個(gè) CPU 共同操作同一個(gè) VolatileCase0 對(duì)象,如下圖所示:
目前這個(gè)情況下 counter 在兩個(gè) CPU 緩存中都存在,但是每個(gè) CPU 對(duì) counter 的操作對(duì)其他 CPU 來說是不可見的。因?yàn)榇藭r(shí)我們并沒有告知 CPU 和 CPU 緩存這個(gè) counter 是一個(gè)共享內(nèi)存變量。要解決多個(gè) CPU 緩存之間變量寫操作可見性的問題,就需要用 volatile 關(guān)鍵字來修飾這個(gè) counter 。代碼如下:
public class VolatileCase0 { public volatile int counter = 0; }
接下來看一個(gè)例子程序:
public class VolatileCase1 { volatile boolean running = true; public void run() { while (running) { } System.out.println(Thread.currentThread().getName() + " end of execution "); } public void stop() { running = false; System.out.println(Thread.currentThread().getName() + " thread Modified running to false"); } public static void main(String[] args) throws Exception { VolatileCase1 vc = new VolatileCase1(); Thread t1 = new Thread(vc::run , "Running-Thread"); Thread t2 = new Thread(vc::stop , "Stop-Thread"); t1.start(); TimeUnit.SECONDS.sleep(1); t2.start(); } }
如果對(duì) running 變量不加 volatile 關(guān)鍵字,程序就會(huì)陷在 “Running-Thread”中一直執(zhí)行而無法結(jié)束。加上了 volatile 關(guān)鍵字之后 “Running-Thread”會(huì)讀取到被修改后的 running 值,這時(shí)就可以執(zhí)行結(jié)束了。
首先需要解釋一下什么是“指令重排序”。所謂指令重排序也就是 CPU 對(duì)程序指令進(jìn)行執(zhí)行的時(shí)候,會(huì)按照自己制定的順序,并不是完全嚴(yán)格按照程序代碼編寫的順序執(zhí)行。這樣做的原因也是出于性能因素考慮,CPU對(duì)一些可以執(zhí)行的指令先執(zhí)行可以提供總體的運(yùn)行效率,而不是讓CPU把時(shí)間都浪費(fèi)在停滯等待上面。感興趣的讀者可以參考這篇文章:
感興趣的讀者也可以閱讀 64-ia-32-architectures-software-developer-vol-3a-part-1-manual 這個(gè)開發(fā)手冊(cè)。以下是該手冊(cè)中對(duì)于指令重排序的一些描述:
譯文:術(shù)語Memory Ordering 是指處理器通過系統(tǒng)總線向系統(tǒng)內(nèi)存發(fā)出讀(裝入)和寫(存儲(chǔ))的順序。Intel 64和IA-32體系結(jié)構(gòu)支持多種內(nèi)存排序模型,具體取決于體系結(jié)構(gòu)的實(shí)現(xiàn)。例如,Intel386處理器強(qiáng)制執(zhí)行程序排序(通常稱為強(qiáng)排序),在任何情況下,讀寫都是按指令流中發(fā)生的順序在系統(tǒng)總線上發(fā)出的。
為了優(yōu)化指令執(zhí)行的性能,IA-32體系結(jié)構(gòu)允許在Pentium 4、Intel Xeon和P6系列處理器中偏離稱為處理器排序的強(qiáng)排序模型。這些處理器排序變體(在這里稱為內(nèi)存排序模型)允許性能增強(qiáng)操作,比如允許讀優(yōu)先于緩沖寫。這些變化的目的是提高指令執(zhí)行速度,同時(shí)保持內(nèi)存一致性,即使在多處理器系統(tǒng)中也是如此。我們通過一個(gè)代碼來證實(shí)CPU對(duì)指令的重排序:
public class MemoryOrderingCase1 { static int x = 0 , y = 0 , a = 0 , b = 0; public static void main(String[] args) throws Exception { while (true) { CountDownLatch latch = new CountDownLatch(2); x = 0; y = 0; a = 0; b = 0; Thread t1 = new Thread(() -> { a = 1; x = b; latch.countDown(); }); Thread t2 = new Thread(() -> { b = 1; y = a; latch.countDown(); }); t1.start(); t2.start(); latch.await(); if (x == 0 && y == 0) { System.out.println("x = " + x + " , y = " + y + " , a = " + a + " , b = " + b); break; } } } }
當(dāng) x = 0 同時(shí) y = 0 的時(shí)候說明CPU在寫指令完成之前執(zhí)行了讀指令。
另一個(gè)例子 Java Double checking locking 單例模式,代碼如下:
public class MemoryOrderingCase2 { private static volatile MemoryOrderingCase2 INSTANCE; int a; int b; private MemoryOrderingCase2() { a = 1; b = 2; } public static MemoryOrderingCase2 getInstance() { if (MemoryOrderingCase2.INSTANCE == null) { synchronized (MemoryOrderingCase2.class) { if (MemoryOrderingCase2.INSTANCE == null) { MemoryOrderingCase2.INSTANCE = new MemoryOrderingCase2(); } } } return MemoryOrderingCase2.INSTANCE; } }
在這個(gè)例子中如果 INSTANCE 取除掉 volidate 關(guān)鍵字就會(huì)導(dǎo)致問題的發(fā)生。假設(shè)有兩個(gè)線程在訪問 getInstance() 函數(shù),執(zhí)行序列如下:
1. 線程 1 進(jìn)入 getInstance 函數(shù) , INSTANCE 為 null ,并切當(dāng)前沒有線程持有鎖定。
2. 線程 1 再次判斷 INSTANCE 是否為 null ,結(jié)果為 true 。
3. 線程 1 執(zhí)行 INSTANCE = new MemoryOrderingCase2() 。
4. 線程 1 執(zhí)行 new MemoryOrderingCase2() 。
5. 線程 1 在堆內(nèi)存中為對(duì)象分配了空間。
6. 線程 1 INSTANCE 指向了該對(duì)象,此時(shí) INSTANCE 已經(jīng)不為 null。
7. 線程 1 new MemoryOrderingCase2() 對(duì)象開始執(zhí)行初始化過程,調(diào)用父類構(gòu)造函數(shù),給一些屬性賦值等。
8. 線程 2 進(jìn)入 getInstance 函數(shù) ,判斷 INSTANCE 不為 null ,將 INSTANCE 返回。
這里的問題在于 MemoryOrderingCase2 對(duì)象還沒有完成全部的初始化過程,就被線程2暴漏給了外界。也就是說讀操作在寫操作還沒有完成之前就發(fā)生了。
查看 getInstance() 函數(shù)的部分匯編代碼:
0x0000000003a663f4: movabs $0x7c0060828,%rdx ; {metadata('org/blackhat/concurrent/date20200312/MemoryOrderingCase2')} 0x0000000003a663fe: mov 0x60(%r15),%rax 0x0000000003a66402: lea 0x18(%rax),%rdi 0x0000000003a66406: cmp 0x70(%r15),%rdi 0x0000000003a6640a: ja 0x0000000003a66557 0x0000000003a66410: mov %rdi,0x60(%r15) 0x0000000003a66414: mov 0xa8(%rdx),%rcx 0x0000000003a6641b: mov %rcx,(%rax) 0x0000000003a6641e: mov %rdx,%rcx 0x0000000003a66421: shr $0x3,%rcx 0x0000000003a66425: mov %ecx,0x8(%rax) 0x0000000003a66428: xor %rcx,%rcx 0x0000000003a6642b: mov %ecx,0xc(%rax) 0x0000000003a6642e: xor %rcx,%rcx 0x0000000003a66431: mov %rcx,0x10(%rax) ;*new ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@17 (line 24) 0x0000000003a66435: movl $0x1,0xc(%rax) ;*putfield a ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::@6 (line 16) ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24) 0x0000000003a6643c: movl $0x2,0x10(%rax) ;*putfield b ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2:: @11 (line 17) ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24) 0x0000000003a66443: movabs $0x76b907160,%rsi ; {oop(a 'java/lang/Class' = 'org/blackhat/concurrent/date20200312/MemoryOrderingCase2')} 0x0000000003a6644d: mov %rax,%r10 0x0000000003a66450: shr $0x3,%r10 0x0000000003a66454: mov %r10d,0x68(%rsi) 0x0000000003a66458: shr $0x9,%rsi 0x0000000003a6645c: movabs $0xf6fd000,%rax 0x0000000003a66466: movb $0x0,(%rsi,%rax,1) 0x0000000003a6646a: lock addl $0x0,(%rsp) ;*putstatic INSTANCE ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@24 (line 24)
關(guān)于 Java中Volatile關(guān)鍵字怎么使用就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。