推薦學(xué)習(xí)并發(fā)編程基礎(chǔ)篇:高并發(fā)一瞥,「線程」和「線程池」,細(xì)說一番 真真香!耗時(shí)大半個(gè)月收整全套「Java架構(gòu)進(jìn)階pdf」沒白費(fèi) 一、碼場(chǎng)心得 你是個(gè)能吃苦的人嗎? 從前的能吃苦大多指的體力勞動(dòng)的苦,但現(xiàn)在的能吃苦已經(jīng)包括太多維度,包括:讀書學(xué)習(xí)&寂寞的苦、深度思考&腦力的苦、自律習(xí)慣&修行的苦、自控能力&放棄的苦、低頭做人&尊嚴(yán)的苦。 雖然這些苦擺在眼前,但大多數(shù)人還是喜歡吃簡(jiǎn)單的苦。熬夜加班、日復(fù)一日、重復(fù)昨天、CRUD,最后身體發(fā)胖、體質(zhì)下降、能力不足、自抱自泣!所以有些苦能不吃就不吃,要吃就吃那些有成長(zhǎng)價(jià)值的苦。 今天你堅(jiān)持了嗎? 如果一件小事能堅(jiān)持5年以上,那你一定是很了不起的人。是的,很了不起。人最難的就是想清楚了但做不到,或者偶爾做到長(zhǎng)期做不到。 其實(shí)大多數(shù)走在研發(fā)路上的伙伴們,都知道自己該努力,但明明下好了的決心就是堅(jiān)持不了多久。直到有一天被瓶頸限制在困局中才會(huì)著急,但這時(shí)候在想破局就真的很難了! 二、面試題 謝飛機(jī),小記,飛機(jī)趁著周末,吃完火鍋。又去約面試官喝茶了! 謝飛機(jī):嗨,我在這,這邊,這邊。 面試官:你怎么又來了,最近學(xué)的不錯(cuò)了? 謝飛機(jī):還是想來大廠,別害羞,面我吧! 面試官:我好像是你補(bǔ)課老師... 既然來了,就問問你吧!volatile 是干啥的? 謝飛機(jī):啊,volatile 是保證變量對(duì)所有線程的可見性的。 面試官:那 volatile 可以解決原子性問題嗎? 謝飛機(jī):不可以! 面試官:那 volatile 的底層原理是如何實(shí)現(xiàn)的呢? 謝飛機(jī):...,這!面試官,剛問兩個(gè)題就甩雷,你是不家里有事要忙? 面試官:你管我! 三、volatile 講解1. 可見性案例 public class ApiTest { public static void main(String[] args) { final VT vt=new VT(); Thread Thread01=new Thread(vt); Thread Thread02=new Thread(new Runnable() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException ignore) { } vt.sign=true; System.out.println("vt.sign=true 通知 while (!sign) 結(jié)束!"); } }); Thread01.start(); Thread02.start(); } } class VT implements Runnable { public boolean sign=false; public void run() { while (!sign) { } System.out.println("你壞"); } } 這段代碼,是兩個(gè)線程操作一個(gè)變量,程序期望當(dāng) sign 在線程 Thread01 被操作 vt.sign=true 時(shí),Thread02 輸出 你壞。 但實(shí)際上這段代碼永遠(yuǎn)不會(huì)輸出 你壞,而是一直處于死循環(huán)。這是為什么呢?接下來我們就一步步講解和驗(yàn)證。 2. 加上volatile關(guān)鍵字 我們把 sign 關(guān)鍵字加上 volatitle 描述,如下: class VT implements Runnable { public volatile boolean sign=false; public void run() { while (!sign) { } System.out.println("你壞"); } } 測(cè)試結(jié)果 vt.sign=true 通知 while (!sign) 結(jié)束! 你壞 Process finished with exit code 0 volatile關(guān)鍵字是Java虛擬機(jī)提供的的最輕量級(jí)的同步機(jī)制,它作為一個(gè)修飾符出現(xiàn),用來修飾變量,但是這里不包括局部變量哦 在添加 volatile 關(guān)鍵字后,程序就符合預(yù)期的輸出了 你壞。從我們對(duì) volatile 的學(xué)習(xí)認(rèn)知可以知道。volatile關(guān)鍵字是 JVM 提供的最輕量級(jí)的同步機(jī)制,用來修飾學(xué)歷提升變量,用來保證變量對(duì)所有線程可見性。 正在修飾后可以讓字段在線程見可見,那么這個(gè)屬性被修改值后,可以及時(shí)的在另外的線程中做出相應(yīng)的反應(yīng)。 3. volatile怎么保證的可見性3.1 無volatile時(shí),內(nèi)存變化 無volatile時(shí),內(nèi)存變化 首先是當(dāng) sign 沒有 volatitle 修飾時(shí) public boolean sign=false;,線程01對(duì)變量進(jìn)行操作,線程02并不會(huì)拿到變化的值。所以程序也就不會(huì)輸出結(jié)果 “你壞” 3.2 有volatile時(shí),內(nèi)存變化 有volatile時(shí),內(nèi)存變化 當(dāng)我們把變量使用 volatile 修飾時(shí) public volatile boolean sign=false;,線程01對(duì)變量進(jìn)行操作時(shí),會(huì)把變量變化的值強(qiáng)制刷新的到主內(nèi)存。當(dāng)線程02獲取值時(shí),會(huì)把自己的內(nèi)存里的 sign 值過期掉,之后從主內(nèi)存中讀取。所以添加關(guān)鍵字后程序如預(yù)期輸出結(jié)果。 4. 反編譯解毒可見性 類似這樣有深度的技術(shù)知識(shí),最佳的方式就是深入理解原理,看看它到底做了什么才保證的內(nèi)存可見性操作。 4.1 查看JVM指令 指令:javap -v -p VT public volatile boolean sign; descriptor: Z flags: ACC_PUBLIC, ACC_VOLATILE org.itstack.interview.test.VT(); descriptor: ()V flags: Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field sign:Z 9: return LineNumberTable: line 35: 0 line 37: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lorg/itstack/interview/test/VT; public void run(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field sign:Z 4: ifne 10 7: goto 0 10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 13: ldc #4 // String 你壞 15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: return LineNumberTable: line 40: 0 line 42: 10 line 43: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 this Lorg/itstack/interview/test/VT; StackMapTable: number_of_entries = 2 frame_type = 0 /* same */ frame_type = 9 /* same */ } 從JVM指令碼中只會(huì)發(fā)現(xiàn)多了,ACC_VOLATILE,并沒有什么其他的點(diǎn)。所以,也不能看出是怎么實(shí)現(xiàn)的可見性。 4.2 查看匯編指令 通過Class文件查看匯編,需要下載 hsdis-amd64.dll 文件,復(fù)制到 JAVA_HOME\jre\bin\server目錄下。下載資源如下: vorboss.dl.sourceforge/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zipvorboss.dl.sourceforge/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-i386.zip 另外是執(zhí)行命令,包括:基礎(chǔ)指令:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly指定打?。?XX:CompileCommand=dontinline,類名.方法名指定打?。?XX:CompileCommand=compileonly,類名.方法名輸出位置:> xxx 最終使用:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=dontinline,ApiTest.main -XX:CompileCommand=compileonly,ApiTest.mian 指令可以在IDEA中的 Terminal 里使用,也可以到 DOS黑窗口中使用 另外,為了更簡(jiǎn)單的使用,我們把指令可以配置到idea的 VM options 里,如下圖:
Idea VM options 配置編譯指令 配置完成后,不出意外的運(yùn)行結(jié)果如下: Loaded disassembler from C:\Program Files\Java\jdk1.8.0_161\jre\bin\server\hsdis-amd64.dll Decoding compiled method 0x0000000003744990: Code: Argument 0 is unknown.RIP: 0x3744ae0 Code size: 0x00000110 [Disassembling for mach='amd64'] [Entry Point] [Constants] # {method} {0x000000001c853d18} 'getSnapshotTransformerList' '()[Lsun/instrument/TransformerManager$TransformerInfo;' in 'sun/instrument/TransformerManager' # [sp+0x40] (sp of caller) 0x0000000003744ae0: mov r10d,dword ptr [rdx+8h] 0x0000000003744ae4: shl r10,3h 0x0000000003744ae8: cmp r10,rax 0x0000000003744aeb: jne 3685f60h ; {runtime_call} 0x0000000003744af1: nop word ptr [rax+rax+0h] 0x0000000003744afc: nop [Verified Entry Point] 0x0000000003744b00: mov dword ptr [rsp+0ffffffffffffa000h],eax 0x0000000003744b07: push rbp 0x0000000003744b08: sub rsp,30h ;*aload_0 ; - sun.instrument.TransformerManager::getSnapshotTransformerList@0 (line 166) 0x0000000003744b0c: mov eax,dword ptr [rdx+10h] 0x0000000003744b0f: shl rax,3h ;*getfield mTransformerList ; - sun.instrument.TransformerManager::getSnapshotTransformerList@1 (line 166) 0x0000000003744b13: add rsp,30h ... 運(yùn)行結(jié)果就是匯編指令,比較多這里就不都放了。我們只觀察重點(diǎn)部分: 0x0000000003324cda: mov 0x74(%r8),%edx ;*getstatic state ; - VT::run@28 (line 27) 0x0000000003324cde: inc %edx 0x0000000003324ce0: mov %edx,0x74(%r8) 0x0000000003324ce4: lock addl $0x0,(%rsp) ;*putstatic state ; - VT::run@33 (line 27) 編譯后的匯編指令中,有volatile關(guān)鍵字和沒有volatile關(guān)鍵字,主要差別在于多了一個(gè) lock addl $0x0,(%rsp),也就是lock的前綴指令。 lock指令相當(dāng)于一個(gè)內(nèi)存屏障,它保證如下三點(diǎn): 將本處理器的緩存寫入內(nèi)存。重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置。如果是寫入動(dòng)作會(huì)導(dǎo)致其他處理器中對(duì)應(yīng)的內(nèi)存無效。 那么,這里的1、3就是用來保證被修飾的變量,保證內(nèi)存可見性。 5. 不加volatile也可見嗎 有質(zhì)疑就要有驗(yàn)證 我們現(xiàn)在再把例子修改下,在 while (!sign) 循環(huán)體中添加一段執(zhí)行代碼,如下; class VT implements Runnable { public boolean sign=false; public void run() { while (!sign) { System.out.println("你好"); } System.out.println("你壞"); } } 修改后去掉了 volatile 關(guān)鍵字,并在while循環(huán)中添加一段代碼?,F(xiàn)在的運(yùn)行結(jié)果是: ... 你好 你好 你好 vt.sign=true 通知 while (!sign) 結(jié)束! 你壞 Process finished with exit code 0 咋樣,又可見了吧! 這是因?yàn)樵跊] volatile 修飾時(shí),jvm也會(huì)盡量保證可見性。有 volatile 修飾的時(shí)候,一定保證可見性。 四、總結(jié)最后我們?cè)倏偨Y(jié)下 volatile,它呢,會(huì)控制被修飾的變量在內(nèi)存操作上主動(dòng)把值刷新到主內(nèi)存,JMM 會(huì)把該線程對(duì)應(yīng)的CPU內(nèi)存設(shè)置過期,從主內(nèi)存中讀取最新值。那么,volatile 如何防止指令重排也是內(nèi)存屏障,volatile 的內(nèi)存屏障是(寫操作前插入StoreStore、寫操作后添加StoreLoad、讀操作前添加LoadLoad、讀操作后添加LoadStore),也就是四個(gè)位置,來保證重排序時(shí)不能把內(nèi)存屏障后面的指令重排序到內(nèi)存屏障之前的位置。另外 volatile 并不能解決原子性,如果需要解決原子性問題,需要使用 synchronzied 或者 lock。 |
|