摘要:一前言我們知道在多線程的場景下,線程安全是必須要著重考慮的。變量可用于提供線程安全,但是只能應(yīng)用于非常有限的一組用例多個變量之間或者某個變量的當(dāng)前值與修改后值之間沒有約束。
一、前言
我們知道在多線程的場景下,線程安全是必須要著重考慮的。Java語言包含兩種內(nèi)在的同步機制:同步塊(synchronize關(guān)鍵字)和 volatile 變量。但是其中 Volatile 變量雖然使用簡單,有時候開銷也比較低,但是同時它的同步性較差,而且其使用也更容易出錯。下面我們先使用一個例子來展示下volatile有可能出現(xiàn)線程不安全的情況:
public class ShareDataVolatile { //同時創(chuàng)建十個線程,每個線程自增100次 //主程序等待3秒讓所有線程全部運行完畢后輸出最后的count值 //使用volatile修飾計數(shù)變量count public volatile static int count=0; public static void main(String[] args){ final ShareDataVolatile data = new ShareDataVolatile(); for(int i=0;i<10;i++){ new Thread( new Runnable(){ public void run(){ try{ Thread.sleep(1); }catch(InterruptedException e){ e.printStackTrace(); } for(int j=0;j<100;j++){ data.addCount(); } System.out.print(count+" "); } } ).start(); } try{ Thread.sleep(3000); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println(); System.out.print("count="+count); } public void addCount(){ count++; } }
運行結(jié)果:
200 200 416 585 755 742 513 513 501 855
count=855
多次運行結(jié)果最后的count都不是預(yù)計的1000,這說明使用volatile變量并不能保證線程安全。
鎖提供了兩種主要特性:互斥(mutual exclusion)和可見性(visibility)。
互斥即一次只允許一個線程持有某個特定的鎖,因此可使用該特性實現(xiàn)對共享數(shù)據(jù)的協(xié)調(diào)訪問協(xié)議,這樣,一次就只有一個線程能夠使用該共享數(shù)據(jù)??梢娦砸訌?fù)雜一些,它必須確保釋放鎖之前對共享數(shù)據(jù)做出的更改對于隨后獲得該鎖的另一個線程是可見的 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發(fā)許多嚴(yán)重問題。
Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發(fā)現(xiàn) volatile 變量的最新值。Volatile 變量可用于提供線程安全,但是只能應(yīng)用于非常有限的一組用例:多個變量之間或者某個變量的當(dāng)前值與修改后值之間沒有約束。因此,多帶帶使用 volatile 還不足以實現(xiàn)計數(shù)器、互斥鎖或任何具有與多個變量相關(guān)的不變式(Invariants)的類(例如 “start <=end”)。
所以例子中雖然增量操作(count++)看上去類似一個多帶帶操作,實際上它是一個由讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執(zhí)行,而 volatile 不能對組合操作提供必須的原子特性。實現(xiàn)正確的操作需要使 count 的值在操作期間保持不變,而 volatile 變量無法實現(xiàn)這點。
在 java 垃圾回收整理一文中,描述了jvm運行時刻內(nèi)存的分配。其中有一個內(nèi)存區(qū)域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當(dāng)線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應(yīng)在堆內(nèi)存的變量的值,然后把堆內(nèi)存變量的具體值load到線程本地內(nèi)存中,建立一個變量副本,之后線程就不再和對象在堆內(nèi)存變量值有任何關(guān)系,而是直接修改副本變量的值,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產(chǎn)生變化了。下面一幅圖描述這寫交互:
read and load 從主存復(fù)制變量到當(dāng)前工作內(nèi)存
use and assign 執(zhí)行代碼,改變共享變量值
store and write 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容
其中use and assign 可以多次出現(xiàn),但是這一些操作并不是原子性,也就是 在read load之后,如果主內(nèi)存count變量發(fā)生修改之后,線程工作內(nèi)存中的值由于已經(jīng)加載,不會產(chǎn)生對應(yīng)的變化,所以計算出來的結(jié)果會和預(yù)期不一樣對于volatile修飾的變量,jvm虛擬機只是保證從主內(nèi)存加載到線程工作內(nèi)存的值是最新的,例如:
假如線程1,線程2 在進行ead,load 操作中,發(fā)現(xiàn)主內(nèi)存中count的值都是5,那么都會加載這個最新的值四、Volatile的優(yōu)勢與使用條件
在線程1堆count進行修改之后,會write到主內(nèi)存中,主內(nèi)存中的count變量就會變?yōu)?
線程2由于已經(jīng)進行read,load操作,在進行運算之后,也會更新主內(nèi)存count的變量值為6
導(dǎo)致兩個線程及時用volatile關(guān)鍵字修改之后,還是會存在并發(fā)的情況。
看了上面的,大家可能已經(jīng)對volatile表示十分失望,不打算使用它了,然后volatile的存在肯定有它存在的意義:
1.簡易性:在某些情形下,使用 volatile 變量要比使用相應(yīng)的鎖簡單得多。
2.性能:某些情況下,volatile 變量同步機制的性能要優(yōu)于鎖。
對 JVM 內(nèi)在的操作而言,我們難以抽象地比較 volatile 和 synchronized 的開銷。但是大部分情況下,在目前大多數(shù)的處理器架構(gòu)上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現(xiàn)內(nèi)存界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低。
volatile 操作不會像鎖一樣造成阻塞,因此,在能夠安全使用 volatile 的情況下,volatile 可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠遠超過寫操作,與鎖相比,volatile 變量通常能夠減少同步的性能開銷。
所以我們需要明確可以使用volatile的條件有兩點:
1.對變量的寫操作不依賴于當(dāng)前值。五、結(jié)束語
2.該變量沒有包含在具有其他變量的不變式中。
與鎖相比,Volatile 變量是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優(yōu)于鎖的性能和伸縮性。如果嚴(yán)格遵循 volatile 的使用條件 —— 即變量真正獨立于其他變量和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼往往比使用鎖的代碼更加容易出錯。
另外如果不是很在意性能方面,并且希望實現(xiàn)簡潔明了的技術(shù)器功能,可以參考我博客內(nèi)的另一篇介紹AtomicInteger類的文章,該類可以實現(xiàn)原子性操作,從而保證線程安全:http://blog.csdn.net/roy_70/a...
參考文章:
Java 理論與實踐: 正確使用 Volatile 變量:
https://www.ibm.com/developer...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/73042.html
摘要:本文對多線程基礎(chǔ)知識進行梳理,主要包括多線程的基本使用,對象及變量的并發(fā)訪問,線程間通信,的使用,定時器,單例模式,以及線程狀態(tài)與線程組。源碼采用構(gòu)建,多線程這部分源碼位于模塊中。通知可能等待該對象的對象鎖的其他線程。 本文對多線程基礎(chǔ)知識進行梳理,主要包括多線程的基本使用,對象及變量的并發(fā)訪問,線程間通信,lock的使用,定時器,單例模式,以及線程狀態(tài)與線程組。 寫在前面 花了一周時...
摘要:使用雙檢查機制來實現(xiàn)多線程環(huán)境中的延遲加載單例設(shè)計模式。類主要負責(zé)日期的轉(zhuǎn)換與格式化,但在多線程環(huán)境中,使用此類容易造成數(shù)據(jù)轉(zhuǎn)換及處理的不準(zhǔn)確,因為類并不是線程安全的。 立即加載就是使用類的時候已經(jīng)將對象創(chuàng)建完畢,常見的實現(xiàn)辦法就是直接new實例化。而立即加載從中文的語境來看,有著急、急迫的含義,所以也稱為餓漢模式。 package com.zxf.demo.singleton_0; ...
摘要:起因及介紹在處理原始對賬文件的時候,我將數(shù)據(jù)歸類后批量存入相應(yīng)的表中。結(jié)論事務(wù)只能管著開啟事務(wù)的線程,其他子線程出了問題都感知不到,所以在多線程環(huán)境操作要慎重。高頻容易搞死服務(wù)器,低頻會阻塞自身程序。重試次數(shù)和超時時間根據(jù)業(yè)務(wù)情況設(shè)置。 起因及介紹 在處理原始對賬文件的時候,我將數(shù)據(jù)歸類后批量存入相應(yīng)的表中。在持久化的時候,用了parallelStream(),想著同時存入很多表這樣可...
摘要:的多線程機制可彌補拋出未檢查的異常,將終止線程執(zhí)行,此時會錯誤的認(rèn)為任務(wù)都取消了。如果想要不保留,則需要設(shè)置,此時最小的就是線程池最大的線程數(shù)。 提供Executor的工廠類showImg(https://segmentfault.com/img/bVbj3Ei?w=2890&h=1480); 忽略了自定義的ThreadFactory、callable和unconfigurable相關(guān)...
摘要:在一個進程內(nèi)部,要同時干多件事,就需要同時運行多個子任務(wù),我們把進程內(nèi)的這些子任務(wù)稱為線程??偨Y(jié)一下,多任務(wù)的實現(xiàn)方式有三種多進程模式多線程模式多進程多線程模式線程是最小的執(zhí)行單元,而進程由至少一個線程組成。 進程與線程 很多同學(xué)都聽說過,現(xiàn)代操作系統(tǒng)比如Mac OS X,UNIX,Linux,Windows等,都是支持多任務(wù)的操作系統(tǒng)。 什么叫多任務(wù)呢?簡單地說,就是操作系統(tǒng)可以同時...
閱讀 3550·2021-11-22 15:22
閱讀 3338·2019-08-30 15:54
閱讀 2732·2019-08-30 15:53
閱讀 823·2019-08-29 11:22
閱讀 3543·2019-08-29 11:14
閱讀 2084·2019-08-26 13:46
閱讀 2219·2019-08-26 13:24
閱讀 2283·2019-08-26 12:22