通过踩坑带你读透虚拟机的“锁粗化”
之前在學(xué)習(xí)volatile時(shí),踩過(guò)一些坑。通過(guò)這些坑,學(xué)習(xí)了一些jvm的鎖優(yōu)化機(jī)制。后來(lái)在面試的過(guò)程中,被問(wèn)到的概率還挺高。于是,我整理了這篇踩坑記錄。
1. java多線程內(nèi)存模型
在聊踩坑記錄前,先要了解下java多線程內(nèi)存模型。大家可通過(guò)“并發(fā)編程網(wǎng)”的一篇文章去學(xué)習(xí)這塊知識(shí),網(wǎng)址是http://ifeve.com/java-memory-model-1/。下面截取部分段落,先讓大家熟悉下。
在java中,所有實(shí)例域、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語(yǔ)代指實(shí)例域,靜態(tài)域和數(shù)組元素)。
局部變量(Local variables),方法定義參數(shù)(java語(yǔ)言規(guī)范稱(chēng)之為formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響。
Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱(chēng)為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。
從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本。
本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫(xiě)緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。
Java內(nèi)存模型的抽象示意圖如下:
多線程內(nèi)存模型
從上圖來(lái)看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
1、首先,線程A把本地內(nèi)存A中更新過(guò)的共享變量副本刷新到主內(nèi)存中去。
2、然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。
上面內(nèi)容可以總結(jié)如下:
1、多線程在運(yùn)行時(shí),會(huì)有主內(nèi)存和工作內(nèi)存的區(qū)分。 2、每個(gè)線程都有各自的工作內(nèi)存,工作內(nèi)存會(huì)復(fù)制一份主內(nèi)存的變量副本。 3、線程其后的運(yùn)行,都是修改工作內(nèi)存中的變量副本。然后在某個(gè)時(shí)間,再同步到主存中。 4、這種工作機(jī)制,可能使得多個(gè)線程在同一個(gè)時(shí)刻獲取到的變量值不同。2. volatile關(guān)鍵字的作用
2.1. volatile關(guān)鍵字語(yǔ)義
共享變量被volatile修飾之后,那么就具備了兩層語(yǔ)義:
1)保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的。
2)禁止進(jìn)行指令重排序。
2.2. volatile關(guān)鍵字如何保證線程間的可見(jiàn)性?
1、使用volatile關(guān)鍵字,線程會(huì)將修改的值立即同步至主內(nèi)存中
2、使用volatile關(guān)鍵字,線程會(huì)強(qiáng)制從主存中讀取值。
3、所以,這就保證了某個(gè)線程修改的值,會(huì)立即被其余線程獲得。
2.3. volatile關(guān)鍵字不保證原子性
volatile并不能代替synchronized關(guān)鍵字,因?yàn)樗荒鼙WC原子性。
下面給大家舉個(gè)例子:
1、多個(gè)線程對(duì)變量i進(jìn)行自增操作。 2、A線程從主存中獲得變量i的值,為6. 3、在A獲取主存的值后,B線程將運(yùn)算結(jié)果7同步至主存。 4、A線程對(duì)變量i進(jìn)行i++操作,然后同步至主存。主存結(jié)果依然為7。這時(shí)i++明顯小于預(yù)期結(jié)果。造成上述原因,就是因?yàn)関olatile關(guān)鍵字不能保證自增操作的原子性。
3. 踩坑之synchronized的可見(jiàn)性
看完java多線程模型和volatile關(guān)鍵字的作用,我們正式來(lái)聊踩坑記錄。
public class VolatileTest implements Runnable {public static String name = "dog";@Overridepublic void run() {while (true) {System.out.println(name);}}public static void main(String[] args) throws InterruptedException {VolatileTest volatileTest = new VolatileTest();Thread thread = new Thread(volatileTest);thread.start();// 讓主線程睡一段時(shí)間,保證子線程的開(kāi)啟。Thread.sleep(5000);VolatileTest.name = "wangcai";} }上述的name字段,我并沒(méi)有加volatile關(guān)鍵字。我還調(diào)用了Thread.sleep(5000);,以便讓子線程先開(kāi)啟。
按照多線程模型的描述,子線程里的name字段應(yīng)該是拷貝的變量副本“dog”。所以我在主線程修改name值為“wangcai”,并不對(duì)子線程可見(jiàn)。所以,按理來(lái)說(shuō),應(yīng)無(wú)限循環(huán)打印“dog”。但事實(shí)上,打印結(jié)果如下:
dog dog dog wangcai wangcai wangcai這和上面的原理不符啊,一度讓我十分困惑。后來(lái)我翻了下System.out.println的源碼,發(fā)現(xiàn)其源碼如下:
public void println(String x) {synchronized (this) {print(x);newLine();} }看到源碼,答案也就呼之欲出了。因?yàn)閜rintln方法添加了synchronized關(guān)鍵字。synchronized不僅能保證原子性,還能保證代碼塊里變量的可見(jiàn)性。所以,每次打印的值都是從主存中獲取的,自然也就變?yōu)榱恕皐angcai”。
4. 踩坑之我以為我懂了
發(fā)現(xiàn)上述原因后,我決定不再用System.out.println打印變量,這樣就不會(huì)觸發(fā)從主存中讀取數(shù)據(jù)。然而我還是太天真,事情的發(fā)展就是這么曲折。
我修改的代碼如下:
public class VolatileTest implements Runnable {public static String name = "dog";@Overridepublic void run() {for (; ; ) {if ("wangcai".equals(name)) {break;}System.out.println("我不是旺財(cái)");}}public static void main(String[] args) throws InterruptedException {VolatileTest volatileTest = new VolatileTest();Thread thread = new Thread(volatileTest);thread.start();Thread.sleep(5000);VolatileTest.name = "wangcai";} }這次我仍然沒(méi)有添加volatile關(guān)鍵字,更沒(méi)有打印name變量。按理說(shuō),這次應(yīng)該無(wú)限循環(huán)打印“我不是旺財(cái)”了吧。但是線程跳出循環(huán),并停止了。這時(shí),我已經(jīng)開(kāi)始對(duì)多線程模型產(chǎn)生動(dòng)搖了。經(jīng)過(guò)探索,我又知道了“鎖粗化”的概念。
5. 鎖粗化
下面,我們看看《深入理解java虛擬機(jī)》對(duì)鎖粗化的描述:
原則上,我們?cè)诰帉?xiě)代碼的時(shí)候,總是推薦將同步塊的作用范圍限制得盡量小-只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖。
大部分情況下,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)探測(cè)到有這樣零碎的操作都對(duì)統(tǒng)一對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部。
將原代碼生成的class文件進(jìn)行反編譯,得到如下代碼:
public void run() {while(!"wangcai".equals(name)) {System.out.println("我不是旺財(cái)");} }于是,while循環(huán)里的System.out.println("我不是旺財(cái)");具有同步代碼塊,每次都對(duì)PrintStream加鎖。于是,經(jīng)過(guò)虛擬機(jī)的鎖粗化,鎖擴(kuò)展到了外部,可見(jiàn)性也擴(kuò)展到了外部。所以子線程能看見(jiàn)主線程對(duì)name的改變,所以會(huì)讓線程跳出,并停止。
6. 守得云開(kāi)見(jiàn)月明
public class Test implements Runnable {private static String name = "dog";@Overridepublic void run() {while (true) {if ("wangcai".equals(name)) {System.out.println(name);break;}}}public static void main(String[] args) throws InterruptedException {Test test = new Test();Thread thread = new Thread(test);thread.start();Thread.sleep(5000);Test.name = "wangcai";} }最終,將代碼改成如上的樣式。不加volatile,主線程對(duì)name的改變,子線程不可見(jiàn)。所以線程會(huì)一直循環(huán),不退出。
加了volatile,主線程的對(duì)name的改變,子線程是可見(jiàn)的。所以會(huì)打出“wangcai”,并退出。
看到這里,如果你有某些疑問(wèn),我會(huì)覺(jué)得你好好研讀上面的內(nèi)容了。在while循環(huán)快中,我也加入了System.out.println函數(shù),為什么沒(méi)有進(jìn)行鎖粗化?這個(gè)依然是由反編譯后的代碼來(lái)決定的:
public void run() {while(!"wangcai".equals(name)) {;}System.out.println("我是旺財(cái)"); }通過(guò)反編譯得到的源碼,我們發(fā)現(xiàn)虛擬機(jī)對(duì)第二個(gè)代碼進(jìn)行了優(yōu)化,是將System.out.println("我是旺財(cái)");放在循環(huán)外的。而第一個(gè)優(yōu)化后的代碼,是將System.out.println("我不是旺財(cái)");放在循環(huán)里的。
所以,第二個(gè)不會(huì)進(jìn)行鎖粗化,而第一個(gè)會(huì)進(jìn)行鎖粗化。
7. 總結(jié)
上面就是我在學(xué)習(xí)volatile關(guān)鍵字時(shí),遇到的各種坑。但是通過(guò)踩坑,我不僅更加深入了解了volatile關(guān)鍵字,我也學(xué)會(huì)了虛擬機(jī)的鎖粗化機(jī)制。雖然我一開(kāi)始是茫然的,但是我沒(méi)有放棄思考。每一次的難題,都會(huì)讓我彌補(bǔ)知識(shí)上的短板。走出自己的知識(shí)舒適區(qū),你才能收獲成長(zhǎng)。
通過(guò)實(shí)戰(zhàn),你會(huì)更為扎實(shí)地掌握所學(xué)知識(shí)點(diǎn)。面試的時(shí)候,通過(guò)代碼向面試官闡述自己的思考過(guò)程,更能凸顯出你將理論融入實(shí)踐的能力,而不只是“紙上談兵”。
后面有機(jī)會(huì),我還會(huì)和大家分享volatile關(guān)于“防止指令重排序”的特性以及其他鎖優(yōu)化機(jī)制。
還是那句話,愿我們共同進(jìn)步!
作者:永不言Qi QQ: 591232672 e-mail:stephenqi@qq.com 版權(quán)聲明:轉(zhuǎn)載請(qǐng)保留此鏈接,不得用于商業(yè)用途。 雖然我不是最優(yōu)秀的程序員,但我還是想盡自己最大的努力,去分享一些學(xué)習(xí)心得。 如有錯(cuò)誤,歡迎指正。若有幸能博得您的喜愛(ài),歡迎關(guān)注及點(diǎn)贊哦。 愿我們共同進(jìn)步!
作者:永不言Qi
鏈接:https://www.jianshu.com/p/f05423a21e78
來(lái)源:簡(jiǎn)書(shū)
簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
總結(jié)
以上是生活随笔為你收集整理的通过踩坑带你读透虚拟机的“锁粗化”的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 锁优化:逃逸分析、自旋锁、锁消除、锁粗化
- 下一篇: 1.6的锁优化(适应性自旋/锁粗化/锁削