自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

既生Synchronized,何生Volatile?!

開(kāi)發(fā) 開(kāi)發(fā)工具
Java語(yǔ)言為了解決并發(fā)編程中存在的原子性、可見(jiàn)性和有序性問(wèn)題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如synchronized、volatile、final、concurren包等。

 [[274130]]

在我的博客和公眾號(hào)中,發(fā)表過(guò)很多篇關(guān)于并發(fā)編程的文章,之前的文章中我們介紹過(guò)了兩個(gè)在Java并發(fā)編程中比較重要的兩個(gè)關(guān)鍵字:synchronized和volatile

我們簡(jiǎn)單回顧一下相關(guān)內(nèi)容:

1、Java語(yǔ)言為了解決并發(fā)編程中存在的原子性、可見(jiàn)性和有序性問(wèn)題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如synchronized、volatile、final、concurren包等。

2、synchronized通過(guò)加鎖的方式,使得其在需要原子性、可見(jiàn)性和有序性這三種特性的時(shí)候都可以作為其中一種解決方案,看起來(lái)是“萬(wàn)能”的。的確,大部分并發(fā)控制操作都能使用synchronized來(lái)完成。

3、volatile通過(guò)在volatile變量的操作前后插入內(nèi)存屏障的方式,保證了變量在并發(fā)場(chǎng)景下的可見(jiàn)性和有序性。

4、volatile關(guān)鍵字是無(wú)法保證原子性的,而synchronized通過(guò)monitorenter和monitorexit兩個(gè)指令,可以保證被synchronized修飾的代碼在同一時(shí)間只能被一個(gè)線程訪問(wèn),即可保證不會(huì)出現(xiàn)CPU時(shí)間片在多個(gè)線程間切換,即可保證原子性。

那么,我們知道,synchronized和volatile兩個(gè)關(guān)鍵字是Java并發(fā)編程中經(jīng)常用到的兩個(gè)關(guān)鍵字,而且,通過(guò)前面的回顧,我們知道synchronized可以保證并發(fā)編程中不會(huì)出現(xiàn)原子性、可見(jiàn)性和有序性問(wèn)題,而volatile只能保證可見(jiàn)性和有序性,那么,既生synchronized、何生volatile?

接下來(lái),本文就來(lái)論述一下,為什么Java中已經(jīng)有了synchronized關(guān)鍵字,還要提供volatile關(guān)鍵字。

1.synchronized的問(wèn)題

我們都知道synchronized其實(shí)是一種加鎖機(jī)制,那么既然是鎖,天然就具備以下幾個(gè)缺點(diǎn):

1、有性能損耗

雖然在JDK 1.6中對(duì)synchronized做了很多優(yōu)化,如如適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等(深入理解多線程(五)—— Java虛擬機(jī)的鎖優(yōu)化技術(shù)),但是他畢竟還是一種鎖。

以上這幾種優(yōu)化,都是盡量想辦法避免對(duì)Monitor(深入理解多線程(四)—— Moniter的實(shí)現(xiàn)原理)進(jìn)行加鎖,但是,并不是所有情況都可以優(yōu)化的,況且就算是經(jīng)過(guò)優(yōu)化,優(yōu)化的過(guò)程也是有一定的耗時(shí)的。

所以,無(wú)論是使用同步方法還是同步代碼塊,在同步操作之前還是要進(jìn)行加鎖,同步操作之后需要進(jìn)行解鎖,這個(gè)加鎖、解鎖的過(guò)程是要有性能損耗的。

關(guān)于二者的性能對(duì)比,由于虛擬機(jī)對(duì)鎖實(shí)行的許多消除和優(yōu)化,使得我們很難量化這兩者之間的性能差距,但是我們可以確定的一個(gè)基本原則是:volatile變量的讀操作的性能小號(hào)普通變量幾乎無(wú)差別,但是寫操作由于需要插入內(nèi)存屏障所以會(huì)慢一些,即便如此,volatile在大多數(shù)場(chǎng)景下也比鎖的開(kāi)銷要低。

2、產(chǎn)生阻塞

我們?cè)谏钊肜斫舛嗑€程(一)——Synchronized的實(shí)現(xiàn)原理中介紹過(guò)關(guān)于synchronize的實(shí)現(xiàn)原理,無(wú)論是同步方法還是同步代碼塊,無(wú)論是ACC_SYNCHRONIZED還是monitorenter、monitorexit都是基于Monitor實(shí)現(xiàn)的。

基于Monitor對(duì)象,當(dāng)多個(gè)線程同時(shí)訪問(wèn)一段同步代碼時(shí),首先會(huì)進(jìn)入Entry Set,當(dāng)有一個(gè)線程獲取到對(duì)象的鎖之后,才能進(jìn)行The Owner區(qū)域,其他線程還會(huì)繼續(xù)在Entry Set等待。并且當(dāng)某個(gè)線程調(diào)用了wait方法后,會(huì)釋放鎖并進(jìn)入Wait Set等待。

所以,synchronize實(shí)現(xiàn)的鎖本質(zhì)上是一種阻塞鎖,也就是說(shuō)多個(gè)線程要排隊(duì)訪問(wèn)同一個(gè)共享對(duì)象。

而volatile是Java虛擬機(jī)提供的一種輕量級(jí)同步機(jī)制,他是基于內(nèi)存屏障實(shí)現(xiàn)的。說(shuō)到底,他并不是鎖,所以他不會(huì)有synchronized帶來(lái)的阻塞和性能損耗的問(wèn)題。

2.volatile的附加功能

除了前面我們提到的volatile比synchronized性能好以外,volatile其實(shí)還有一個(gè)很好的附加功能,那就是禁止指令重排。

我們先來(lái)舉一個(gè)例子,看一下如果只使用synchronized而不使用volatile會(huì)發(fā)生什么問(wèn)題,就拿我們比較熟悉的單例模式來(lái)看。

我們通過(guò)雙重校驗(yàn)鎖的方式實(shí)現(xiàn)一個(gè)單例,這里不使用volatile關(guān)鍵字:

  1. public class Singleton {   
  2.        private static Singleton singleton;   
  3.         private Singleton (){}   
  4.         public static Singleton getSingleton() {   
  5.         if (singleton == null) {   
  6.             synchronized (Singleton.class) {   
  7.                 if (singleton == null) {   
  8.                     singleton = new Singleton();   
  9.                 }   
  10.             }   
  11.         }   
  12.        return singleton;   
  13.        }   
  14.     }   

以上代碼,我們通過(guò)使用synchronized對(duì)Singleton.class進(jìn)行加鎖,可以保證同一時(shí)間只有一個(gè)線程可以執(zhí)行到同步代碼塊中的內(nèi)容,也就是說(shuō)singleton = new Singleton()這個(gè)操作只會(huì)執(zhí)行一次,這就是實(shí)現(xiàn)了一個(gè)單例。

但是,當(dāng)我們?cè)诖a中使用上述單例對(duì)象的時(shí)候有可能發(fā)生空指針異常。這是一個(gè)比較詭異的情況。

我們假設(shè)Thread1 和 Thread2兩個(gè)線程同時(shí)請(qǐng)求Singleton.getSingleton方法的時(shí)候:

  • Step1 ,Thread1執(zhí)行到第8行,開(kāi)始進(jìn)行對(duì)象的初始化。
  • Step2 ,Thread2執(zhí)行到第5行,判斷singleton == null。
  • Step3 ,Thread2經(jīng)過(guò)判斷發(fā)現(xiàn)singleton != null,所以執(zhí)行第12行,返回singleton。
  • Step4 ,Thread2拿到singleton對(duì)象之后,開(kāi)始執(zhí)行后續(xù)的操作,比如調(diào)用singleton.call()。

以上過(guò)程,看上去并沒(méi)有什么問(wèn)題,但是,其實(shí),在Step4,Thread2在調(diào)用singleton.call()的時(shí)候,是有可能拋出空指針異常的。

之所有會(huì)有NPE拋出,是因?yàn)樵赟tep3,Thread2拿到的singleton對(duì)象并不是一個(gè)完整的對(duì)象。

什么叫做不完整對(duì)象,這個(gè)怎么理解呢?

我們這里來(lái)先來(lái)看一下,singleton = new Singleton();這行代碼到底做了什么事情,大致過(guò)程如下:

1、虛擬機(jī)遇到new指令,到常量池定位到這個(gè)類的符號(hào)引用。

2、檢查符號(hào)引用代表的類是否被加載、解析、初始化過(guò)。

3、虛擬機(jī)為對(duì)象分配內(nèi)存。

4、虛擬機(jī)將分配到的內(nèi)存空間都初始化為零值。

5、虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置。

6、執(zhí)行方法,成員變量進(jìn)行初始化。

7、將對(duì)象的引用指向這個(gè)內(nèi)存區(qū)域。

我們把這個(gè)過(guò)程簡(jiǎn)化一下,簡(jiǎn)化成3個(gè)步驟:

a、JVM為對(duì)象分配一塊內(nèi)存M

b、在內(nèi)存M上為對(duì)象進(jìn)行初始化

c、將內(nèi)存M的地址復(fù)制給singleton變量

如下圖:

因?yàn)閷?nèi)存的地址賦值給singleton變量是最后一步,所以Thread1在這一步驟執(zhí)行之前,Thread2在對(duì)singleton==null進(jìn)行判斷一直都是true的,那么他會(huì)一直阻塞,直到Thread1將這一步驟執(zhí)行完。

但是,問(wèn)題就出在以上過(guò)程并不是一個(gè)原子操作,并且編譯器可能會(huì)進(jìn)行重排序,如果以上步驟被重排成:

  • a、JVM為對(duì)象分配一塊內(nèi)存M
  • c、將內(nèi)存的地址復(fù)制給singleton變量
  • b、在內(nèi)存M上為對(duì)象進(jìn)行初始化

如下圖:

這樣的話,Thread1會(huì)先執(zhí)行內(nèi)存分配,在執(zhí)行變量賦值,最后執(zhí)行對(duì)象的初始化,那么,也就是說(shuō),在Thread1還沒(méi)有為對(duì)象進(jìn)行初始化的時(shí)候,Thread2進(jìn)來(lái)判斷singleton==null就可能提前得到一個(gè)false,則會(huì)返回一個(gè)不完整的sigleton對(duì)象,因?yàn)樗€未完成初始化操作。

這種情況一旦發(fā)生,我們拿到了一個(gè)不完整的singleton對(duì)象,當(dāng)嘗試使用這個(gè)對(duì)象的時(shí)候就極有可能發(fā)生NPE異常。

那么,怎么解決這個(gè)問(wèn)題呢?因?yàn)橹噶钪嘏艑?dǎo)致了這個(gè)問(wèn)題,那就避免指令重排就行了。

所以,volatile就派上用場(chǎng)了,因?yàn)関olatile可以避免指令重排。只要將代碼改成以下代碼,就可以解決這個(gè)問(wèn)題:

  1. public class Singleton {   
  2.      private volatile static Singleton singleton;   
  3.       private Singleton (){}   
  4.       public static Singleton getSingleton() {   
  5.       if (singleton == null) {   
  6.           synchronized (Singleton.class) {   
  7.               if (singleton == null) {   
  8.                   singleton = new Singleton();   
  9.               }   
  10.            }   
  11.        }   
  12.        return singleton;   
  13.        }   
  14.    }   

對(duì)singleton使用volatile約束,保證他的初始化過(guò)程不會(huì)被指令重排。這樣就可以保Thread2 要不然就是拿不到對(duì)象,要不然就是拿到一個(gè)完整的對(duì)象。

3.synchronized的有序性保證呢?

看到這里可能有朋友會(huì)問(wèn)了,說(shuō)到底上面問(wèn)題是發(fā)生了指令重排,其實(shí)還是個(gè)有序性的問(wèn)題,不是說(shuō)synchronized是可以保證有序性的么,這里為什么就不行了呢?

首先,可以明確的一點(diǎn)是:synchronized是無(wú)法禁止指令重排和處理器優(yōu)化的。那么他是如何保證的有序性呢?

這就要再把有序性的概念擴(kuò)展一下了。Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是天然有序的。如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無(wú)序的。

以上這句話也是《深入理解Java虛擬機(jī)》中的原句,但是怎么理解呢?周志明并沒(méi)有詳細(xì)的解釋。這里我簡(jiǎn)單擴(kuò)展一下,這其實(shí)和as-if-serial語(yǔ)義有關(guān)。

as-if-serial語(yǔ)義的意思指:不管怎么重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。編譯器和處理器無(wú)論如何優(yōu)化,都必須遵守as-if-serial語(yǔ)義。

這里不對(duì)as-if-serial語(yǔ)義詳細(xì)展開(kāi)了,簡(jiǎn)單說(shuō)就是,as-if-serial語(yǔ)義保證了單線程中,不管指令怎么重排,最終的執(zhí)行結(jié)果是不能被改變的。

那么,我們回到剛剛那個(gè)雙重校驗(yàn)鎖的例子,站在單線程的角度,也就是只看Thread1的話,因?yàn)榫幾g器會(huì)遵守as-if-serial語(yǔ)義,所以這種優(yōu)化不會(huì)有任何問(wèn)題,對(duì)于這個(gè)線程的執(zhí)行結(jié)果也不會(huì)有任何影響。

但是,Thread1內(nèi)部的指令重排卻對(duì)Thread2產(chǎn)生了影響。

那么,我們可以說(shuō),synchronized保證的有序性是多個(gè)線程之間的有序性,即被加鎖的內(nèi)容要按照順序被多個(gè)線程執(zhí)行。但是其內(nèi)部的同步代碼還是會(huì)發(fā)生重排序,只不過(guò)由于編譯器和處理器都遵循as-if-serial語(yǔ)義,所以我們可以認(rèn)為這些重排序在單線程內(nèi)部可忽略。

4.總結(jié)

本文從兩方面論述了volatile的重要性以及不可替代性:

一方面是因?yàn)閟ynchronized是一種鎖機(jī)制,存在阻塞問(wèn)題和性能問(wèn)題,而volatile并不是鎖,所以不存在阻塞和性能問(wèn)題。

另外一方面,因?yàn)関olatile借助了內(nèi)存屏障來(lái)幫助其解決可見(jiàn)性和有序性問(wèn)題,而內(nèi)存屏障的使用還為其帶來(lái)了一個(gè)禁止指令重排的附件功能,所以在有些場(chǎng)景中是可以避免發(fā)生指令重排的問(wèn)題的。

所以,在日后需要做并發(fā)控制的時(shí)候,如果不涉及到原子性的問(wèn)題,可以優(yōu)先考慮使用volatile關(guān)鍵字。

【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號(hào)Hollis(ID:hollischuang)】

 

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專欄
相關(guān)推薦

2012-08-21 15:52:48

2018-06-14 10:56:52

電視顯示器數(shù)碼

2024-08-28 11:34:37

2013-08-20 09:25:05

微信易信微信故障

2019-01-03 12:46:19

多云云存儲(chǔ)風(fēng)險(xiǎn)

2010-11-15 10:46:57

簡(jiǎn)歷

2015-10-28 14:32:27

大數(shù)據(jù)幸福

2011-06-30 17:21:56

2023-08-10 17:33:06

元宇宙人工智能

2020-11-02 08:54:29

JMMVolatileSynchronize

2017-01-03 15:08:31

自適應(yīng)安全應(yīng)用安全RASP

2015-04-23 08:51:53

2020-01-10 17:21:09

應(yīng)屆生月薪薪資

2016-08-24 11:13:30

2016-04-13 14:29:42

云計(jì)算

2017-03-27 16:44:07

戴爾服務(wù)器

2016-11-15 14:31:31

大數(shù)據(jù)航運(yùn)大數(shù)據(jù)

2020-03-10 10:25:38

volatileJava編程語(yǔ)言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)