做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
你是個能吃苦的人嗎?
java
從前的能吃苦大多指的體力勞動的苦,但如今的能吃苦已經包括太多維度,包括:讀書學習&寂寞的苦
、深度思考&腦力的苦
、自律習慣&修行的苦
、自控能力&放棄的苦
、低頭作人&尊嚴的苦
。面試
雖然這些苦擺在眼前,但大多數人仍是喜歡吃簡單的苦。熬夜加班、日復一日、重複昨天、CRUD,最後身體發胖、體質降低、能力不足、自抱自泣!因此有些苦能不吃就不吃,要吃就吃那些有成長價值的苦。緩存
今天你寫博客了嗎?
sass
若是一件小事能堅持5年以上,那你必定是很了不得的人。是的,很了不得。人最難的就是想清楚了但作不到,或者偶爾作到長期作不到。微信
其實大多數走在研發路上的夥伴們,都知道本身該努力,但明明下好了的決心就是堅持不了多久。就像你是否也想過要寫技術博客,作技術積累。直到有一天被瓶頸限制在困局中才會着急,但這時候在想破局就真的很難了!jvm
謝飛機,小記
,飛機趁着週末,吃完火鍋。又去約面試官喝茶了!ide
謝飛機:嗨,我在這,這邊,這邊。函數
面試官:你怎麼又來了,最近學的不錯了?學習
謝飛機:仍是想來大廠,別害羞,面我吧!
面試官:我好像是你補課老師... 既然來了,就問問你吧!volatile 是幹啥的?
謝飛機:啊,volatile 是保證變量對全部線程的可見性的。
面試官:那 volatile 能夠解決原子性問題嗎?
謝飛機:不能夠!
面試官:那 volatile 的底層原理是如何實現的呢?
謝飛機:...,這!面試官,剛問兩個題就甩雷,你是不家裏有事要忙?
面試官:你管我!
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) 結束!"); } }); Thread01.start(); Thread02.start(); } } class VT implements Runnable { public boolean sign = false; public void run() { while (!sign) { } System.out.println("你壞"); } }
這段代碼,是兩個線程操做一個變量,程序指望當 sign
在線程 Thread01 被操做 vt.sign = true
時,Thread02 輸出 你壞。
但實際上這段代碼永遠不會輸出 你壞,而是一直處於死循環。這是爲何呢?接下來咱們就一步步講解和驗證。
咱們把 sign 關鍵字加上 volatitle 描述,以下:
class VT implements Runnable { public volatile boolean sign = false; public void run() { while (!sign) { } System.out.println("你壞"); } }
測試結果
vt.sign = true 通知 while (!sign) 結束! 你壞 Process finished with exit code 0
volatile關鍵字是Java虛擬機提供的的最輕量級的同步機制,它做爲一個修飾符出現,用來修飾變量,可是這裏不包括局部變量哦
在添加 volatile 關鍵字後,程序就符合預期的輸出了 你壞。從咱們對 volatile 的學習認知能夠知道。volatile關鍵字是 JVM 提供的最輕量級的同步機制,用來修飾變量,用來保證變量對全部線程可見性。
正在修飾後可讓字段在線程見可見,那麼這個屬性被修改值後,能夠及時的在另外的線程中作出相應的反應。
首先是當 sign 沒有 volatitle 修飾時 public boolean sign = false;
,線程01對變量進行操做,線程02並不會拿到變化的值。因此程序也就不會輸出結果 「你壞」
當咱們把變量使用 volatile 修飾時 public volatile boolean sign = false;
,線程01對變量進行操做時,會把變量變化的值強制刷新的到主內存。當線程02獲取值時,會把本身的內存裏的 sign 值過時掉,以後從主內存中讀取。因此添加關鍵字後程序如預期輸出結果。
相似這樣有深度的技術知識,最佳的方式就是深刻理解原理,看看它到底作了什麼才保證的內存可見性操做。
指令: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."<init>":()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指令碼中只會發現多了,ACC_VOLATILE
,並無什麼其餘的點。因此,也不能看出是怎麼實現的可見性。
經過Class文件查看彙編,須要下載 hsdis-amd64.dll 文件,複製到 JAVA_HOME\jre\bin\server目錄下
。下載資源以下:
另外是執行命令,包括:
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黑窗口中使用
另外,爲了更簡單的使用,咱們把指令能夠配置到idea的 VM options 裏,以下圖:
配置完成後,不出意外的運行結果以下:
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 ...
運行結果就是彙編指令,比較多這裏就不都放了。咱們只觀察🕵重點部分:
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關鍵字和沒有volatile關鍵字,主要差異在於多了一個 lock addl $0x0,(%rsp)
,也就是lock的前綴指令。
lock指令至關於一個內存屏障,它保證以下三點:
那麼,這裏的一、3就是用來保證被修飾的變量,保證內存可見性。
有質疑就要有驗證
咱們如今再把例子修改下,在 while (!sign)
循環體中添加一段執行代碼,以下;
class VT implements Runnable { public boolean sign = false; public void run() { while (!sign) { System.out.println("你好"); } System.out.println("你壞"); } }
修改後去掉了 volatile
關鍵字,並在while循環中添加一段代碼。如今的運行結果是:
... 你好 你好 你好 vt.sign = true 通知 while (!sign) 結束! 你壞 Process finished with exit code 0
咋樣,又可見了吧!
這是由於在沒 volatile 修飾時,jvm也會盡可能保證可見性。有 volatile 修飾的時候,必定保證可見性。