在Java多線程編程-(2)中提及到了一段使用Synchronized關(guān)鍵字實現(xiàn)的單利模式--雙重校驗鎖,代碼如下: 慧眼的小伙伴,已經(jīng)發(fā)現(xiàn)了其中的問題,并給了及時的回復(fù): 這也是我今天準備和大家一起學習的內(nèi)容。上述的代碼是錯誤的寫法,之所以是錯誤的,這是因為:指令重排優(yōu)化,可能會導(dǎo)致初始化單利對象和將該對象地址賦值給instance字段的順序與上面Java代碼中書寫的順序不同。 例如:線程A在創(chuàng)建單例對象時,在構(gòu)造方法被調(diào)用之前,就為該對象分配了內(nèi)存空間并將對象設(shè)置為默認值。此時線程A就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對象可能還沒有完成初始化操作。線程B來調(diào)用newInstance()方法,得到的就是為初始化完全的單例對象,這就會導(dǎo)致系統(tǒng)出現(xiàn)異常行為。 為了解決上述的問題,可以使用volatile關(guān)鍵字進行修飾instance字段。volatile關(guān)鍵字在這里的含義就是禁止指令的重排序優(yōu)化(另一個作用是提供內(nèi)存可見性),從而保證instance字段被初始化時,單例對象已經(jīng)被完全初始化。 最終代碼如下: 那么問題來了,為什么volatile關(guān)鍵字可以實現(xiàn)禁止指令的重排序優(yōu)化以及什么是指令重排序優(yōu)化哪? 在Java內(nèi)存模型中我們都是圍繞著原子性、有序性和可見性進行討論的。為了確保線程間的原子性、有序性和可見性,Java中使用了一些特殊的關(guān)鍵字申明或者是特殊的操作來告訴虛擬機,在這個地方,要注意一下,不能隨意變動優(yōu)化目標指令。關(guān)鍵字volatile就是其中之一。 指令重排序是JVM為了優(yōu)化指令,提高程序運行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度(比如:將多條指定并行執(zhí)行或者是調(diào)整指令的執(zhí)行順序)。編譯器、處理器也遵循這樣一個目標。注意是單線程??娠@而知,多線程的情況下指令重排序就會給程序員帶來問題。 最重要的一個問題就是程序執(zhí)行的順序可能會被調(diào)整,另一個問題是對修改的屬性無法及時的通知其他線程,已達到所有線程操作該屬性的可見性。 根據(jù)編譯器的優(yōu)化規(guī)則,如果不使用volatile關(guān)鍵字對變量進行修飾的,那么這個變量被修改后,其他線程可能并不會被通知到,甚至在別的想愛你城中,看到變量修改順序都會是反的。一旦使用volatile關(guān)鍵字進行修飾的話,虛擬機就會特別小心的處理這種情況。 因此,如何正確的使用雙重校驗鎖,以及為什么使用關(guān)鍵字volatile這里我們應(yīng)該很清楚了。 上述也提到了volatile關(guān)鍵字的另一個作用就是:變量在多個線程之間可見。 volatile可見性 首先我們先看一下段代碼: 執(zhí)行結(jié)果: 可以看出 在單線程的情況下,程序會一直執(zhí)行下去,即一直執(zhí)行while循環(huán),導(dǎo)致程序不能正常執(zhí)行下邊的代碼。解決的方法可以使用多線程。多線程示例代碼如下: 執(zhí)行結(jié)果如下: 可以看出使用多線程的技術(shù)實現(xiàn),但是有一個問題就是在一些平臺上執(zhí)行的時候會出現(xiàn)死鎖的情況,解決的方法就是使用volatile關(guān)鍵字。即變量用volatile關(guān)鍵字修飾。 volatile關(guān)鍵字的作用就是強制從公共堆棧中取得變量的值,而不是線程私有的數(shù)據(jù)棧中取得變量的值。 volatile與synchronized的區(qū)別 1、關(guān)鍵字volatile是線程同步的輕量級實現(xiàn),性能比synchronized要好,并且volatile只能修于變量,而synchronized可以修飾方法,代碼塊等。 2、多線程訪問volatile不會發(fā)生阻塞,而synchronized會發(fā)生阻塞。 3、volatile可以保證數(shù)據(jù)的可見性,但不可以保證原子性,而synchronized可以保證原子性,也可以間接保證可見性,因為他會將私有內(nèi)存和公共內(nèi)存中的數(shù)據(jù)做同步。 4、volatile解決 的是變量在多個線程之間的可見性,而synchronized解決的是多個線程之間訪問資源的同步性。 |
|