談這兩個的區別,首先須要理解線程安全的兩個方面:執行控制和內存可見。緩存
執行控制的目的是控制代碼執行(順序)及是否能夠併發執行。安全
內存可見控制的是線程執行結果在內存中對其它線程的可見性。根據Java內存模型的實現,線程在具體執行時,會先拷貝主存數據到線程本地(CPU緩存),操做完成後再把結果從線程本地刷到主存。併發
synchronized關鍵字解決的是執行控制的問題,它會阻止其它線程獲取當前對象的監控鎖,這樣就使得當前對象中被synchronized
關鍵字保護的代碼塊沒法被其它線程訪問,也就沒法併發執行。更重要的是,synchronized
還會建立一個內存屏障,內存屏障指令保證了全部CPU操做結果都會直接刷到主存中,從而保證了操做的內存可見性,同時也使得先得到這個鎖的線程的全部操做,都happens-before於隨後得到這個鎖的線程的操做。app
volatile關鍵字解決的是內存可見性的問題,會使得全部對volatile
變量的讀寫都會直接刷到主存,即保證了變量的可見性。這樣就能知足一些對變量可見性有要求而對讀取順序沒有要求的需求。高併發
使用volatile
關鍵字僅能實現對原始變量(如boolen、 short 、int 、long等)操做的原子性,但須要特別注意, volatile
不能保證複合操做的原子性,即便只是i++
,實際上也是由多個原子操做組成:read i; inc; write i
,假如多個線程同時執行i++
,volatile
只能保證他們操做的i
是同一塊內存,但依然可能出現寫入髒數據的狀況。性能
由於它其實是三個操做組成的一個複合操做。線程
一個簡單的例子:code
若是兩個線程在volatile讀階段都拿到的是a=1,那麼後續在線程對應的CPU核心上進行自增固然都獲得的是a=2,最後兩個寫操做無論怎麼保證原子性,結果最終都是a=2。每一個操做自己都沒啥問題,可是合在一塊兒,從總體上看就是一個線程不安全的操做:發生了兩次自增操做,然而最終結果卻不是3。對象
在第一步操做的指令後,會增長兩個內存屏障:排序
所以第一個指令和它後續的普通讀寫操做會被保證沒有重排序來搗亂。一般是去內存中去讀。
那麼問題又來了,爲何一般去內存中讀?
其實這個問題要說細的話能夠很細,大概就兩個關鍵點吧:
具體看下面第三步的分析。
這個步驟沒什麼特別的,就是在CPU自身的高速緩存(寄存器,L1-L3 Cache)中完成。不涉及到緩存和內存的交互。
volatile寫算是一個重點。
根據JMM對於volatile變量類型的語義規範:volatile在編譯以後,會在變量寫操做時添加LOCK前綴指令。這個LOCK前綴指令在多核處理器的環境中,有這樣的做用:
另外,內存屏障在volatile的寫操做中起到了很大的做用,來保證上面兩點可以實現:
那麼爲了解決volatile++這類複合操做的原子性,有什麼方案呢?其實方案也比較多的,這裏提供兩種典型的:
synchronized是比較原始的同步手段。它本質上是一個獨佔的,可重入的鎖。當一個線程嘗試獲取它的時候,可能會被阻塞住,因此高併發的場景下性能存在一些問題。
在某些場景下,使用synchronized關鍵字和volatile是等價的:
加鎖能夠同時保證可見性和原子性,而volatile只保證變量值的可見性。
使用上的區別
volatile只能修飾變量,synchronized只能修飾方法和語句塊;
對原子性的保證
synchronized能夠保證原子性,volatile不能保證原子性;
對可見性的保證
均可以保證可見性,但實現原理不一樣,volatile對變量加了lock,synchronized使用monitorEnter和monitorExit;
對有序性的保證
均可以保證有序性,可是synchronized併發退化到串行;
其餘 synchronized引發阻塞;volatile不會引發阻塞;