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