日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

Java并发编程(三)volatile域

發(fā)布時(shí)間:2025/3/19 java 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java并发编程(三)volatile域 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

相關(guān)文章
Java并發(fā)編程(一)線程定義、狀態(tài)和屬性
Java并發(fā)編程(二)同步
Android多線程(一)線程池
Android多線程(二)AsyncTask源代碼分析

前言

有時(shí)僅僅為了讀寫一個(gè)或者兩個(gè)實(shí)例域就使用同步的話,顯得開銷過大,volatile關(guān)鍵字為實(shí)例域的同步訪問提供了免鎖的機(jī)制。假設(shè)聲明一個(gè)域?yàn)関olatile,那么編譯器和虛擬機(jī)就知道該域是可能被還有一個(gè)線程并發(fā)更新的。

再講到volatile關(guān)鍵字之前我們須要了解一下內(nèi)存模型的相關(guān)概念以及并發(fā)編程中的三個(gè)特性:原子性,可見性和有序性。

1. java內(nèi)存模型與原子性。可見性和有序性

Java內(nèi)存模型規(guī)定全部的變量都是存在主存其中,每一個(gè)線程都有自己的工作內(nèi)存。線程對(duì)變量的全部操作都必須在工作內(nèi)存中進(jìn)行。而不能直接對(duì)主存進(jìn)行操作。而且每一個(gè)線程不能訪問其它線程的工作內(nèi)存。
在java中。運(yùn)行以下這個(gè)語句:

int i=3;

運(yùn)行線程必須先在自己的工作線程中對(duì)變量i所在的緩存行進(jìn)行賦值操作,然后再寫入主存其中。而不是直接將數(shù)值3寫入主存其中。
那么Java語言 本身對(duì) 原子性、可見性以及有序性提供了哪些保證呢?

原子性

對(duì)基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作。即這些操作是不可被中斷的,要么運(yùn)行。要么不運(yùn)行。
來看一下以下的代碼:

x = 10; //語句1 y = x; //語句2 x++; //語句3 x = x + 1; //語句4

僅僅有語句1是原子性操作,其它三個(gè)語句都不是原子性操作。
語句2實(shí)際上包括2個(gè)操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存,盡管讀取x的值以及 將x的值寫入工作內(nèi)存 這2個(gè)操作都是原子性操作,可是合起來就不是原子性操作了。
相同的,x++和 x = x+1包括3個(gè)操作:讀取x的值。進(jìn)行加1操作。寫入新的值。


也就是說,僅僅有簡(jiǎn)單的讀取、賦值(而且必須是將數(shù)字賦值給某個(gè)變量,變量之間的相互賦值不是原子操作)才是原子操作。


java.util.concurrent.atomic包中有非常多類使用了非常高效的機(jī)器級(jí)指令(而不是使用鎖)來保證其它操作的原子性。比如AtomicInteger類提供了方法incrementAndGet和decrementAndGet,它們分別以原子方式將一個(gè)整數(shù)自增和自減。能夠安全地使用AtomicInteger類作為共享計(jì)數(shù)器而無需同步。
另外這個(gè)包還包括AtomicBoolean。AtomicLong和AtomicReference這些原子類僅供開發(fā)并發(fā)工具的系統(tǒng)程序猿使用。應(yīng)用程序猿不應(yīng)該使用這些類。

可見性

可見性,是指線程之間的可見性,一個(gè)線程改動(dòng)的狀態(tài)對(duì)還有一個(gè)線程是可見的。也就是一個(gè)線程改動(dòng)的結(jié)果。還有一個(gè)線程馬上就能看到。
當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證改動(dòng)的值會(huì)馬上被更新到主存。所以對(duì)其它線程是可見的,當(dāng)有其它線程須要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘桓膭?dòng)之后。什么時(shí)候被寫入主存是不確定的,當(dāng)其它線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。

有序性

在Java內(nèi)存模型中,同意編譯器和處理器對(duì)指令進(jìn)行重排序。可是重排序過程不會(huì)影響到單線程程序的運(yùn)行,卻會(huì)影響到多線程并發(fā)運(yùn)行的正確性。


能夠通過volatile關(guān)鍵字來保證一定的“有序性”。

另外能夠通過synchronized和Lock來保證有序性,非常顯然,synchronized和Lock保證每一個(gè)時(shí)刻是有一個(gè)線程運(yùn)行同步代碼。相當(dāng)于是讓線程順序運(yùn)行同步代碼。自然就保證了有序性。

2. volatile關(guān)鍵字

一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

  • 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程改動(dòng)了某個(gè)變量的值,這新值對(duì)其它線程來說是馬上可見的。
  • 禁止進(jìn)行指令重排序。

先看一段代碼,假如線程1先運(yùn)行,線程2后運(yùn)行:
  

//線程1 boolean stop = false; while(!stop){doSomething(); }//線程2 stop = true;

非常多人在中斷線程時(shí)可能都會(huì)採用這樣的標(biāo)記辦法。可是其實(shí),這段代碼會(huì)全然運(yùn)行正確么?即一定會(huì)將線程中斷么?不一定。或許在大多數(shù)時(shí)候,這個(gè)代碼能夠把線程中斷。可是也有可能會(huì)導(dǎo)致無法中斷線程(盡管這個(gè)可能性非常小,可是僅僅要一旦發(fā)生這樣的情況就會(huì)造成死循環(huán)了)。
為何有可能導(dǎo)致無法中斷線程?每一個(gè)線程在運(yùn)行過程中都有自己的工作內(nèi)存。那么線程1在運(yùn)行的時(shí)候,會(huì)將stop變量的值拷貝一份放在自己的工作內(nèi)存其中。

那么當(dāng)線程2更改了stop變量的值之后,可是還沒來得及寫入主存其中。線程2轉(zhuǎn)去做其它事情了,那么線程1因?yàn)椴恢谰€程2對(duì)stop變量的更改,因此還會(huì)一直循環(huán)下去。
可是用volatile修飾之后就變得不一樣了:

  • 使用volatile關(guān)鍵字會(huì)強(qiáng)制將改動(dòng)的值馬上寫入主存。
  • 使用volatile關(guān)鍵字的話。當(dāng)線程2進(jìn)行改動(dòng)時(shí),會(huì)導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效;
  • 因?yàn)榫€程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時(shí)會(huì)去主存讀取。

volatile保證原子性嗎?

我們知道volatile關(guān)鍵字保證了操作的可見性,可是volatile能保證對(duì)變量的操作是原子性嗎?

public class Test {public volatile int inc = 0; public void increase() {inc++;}public static void main(String[] args) {final Test test = new Test();for(int i=0;i<10;i++){new Thread(){public void run() {for(int j=0;j<1000;j++)test.increase();};}.start();}//保證前面的線程都運(yùn)行完while(Thread.activeCount()>1) Thread.yield();System.out.println(test.inc);} }

這段代碼每次運(yùn)行結(jié)果都不一致,都是一個(gè)小于10000的數(shù)字,在前面已經(jīng)提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進(jìn)行加1操作、寫入工作內(nèi)存。那么就是說自增操作的三個(gè)子操作可能會(huì)切割開運(yùn)行。
假如某個(gè)時(shí)刻變量inc的值為10,線程1對(duì)變量進(jìn)行自增操作,線程1先讀取了變量inc的原始值,然后線程1被堵塞了;然后線程2對(duì)變量進(jìn)行自增操作,線程2也去讀取變量inc的原始值。因?yàn)榫€程1僅僅是對(duì)變量inc進(jìn)行讀取操作,而沒有對(duì)變量進(jìn)行改動(dòng)操作。所以不會(huì)導(dǎo)致線程2的工作內(nèi)存中緩存變量inc的緩存行無效。所以線程2會(huì)直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時(shí)10,然后進(jìn)行加1操作,并把11寫入工作內(nèi)存。最后寫入主存。然后線程1接著進(jìn)行加1操作,因?yàn)橐呀?jīng)讀取了inc的值,注意此時(shí)在線程1的工作內(nèi)存中inc的值仍然為10。所以線程1對(duì)inc進(jìn)行加1操作后inc的值為11,然后將11寫入工作內(nèi)存。最后寫入主存。

那么兩個(gè)線程分別進(jìn)行了一次自增操作后。inc僅僅添加了1。
自增操作不是原子性操作,而且volatile也無法保證對(duì)變量的不論什么操作都是原子性的。

volatile能保證有序性嗎?

在前面提到volatile關(guān)鍵字能禁止指令重排序。所以volatile能在一定程度上保證有序性。


volatile關(guān)鍵字禁止指令重排序有兩層意思:

  • 當(dāng)程序運(yùn)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見;在其后面的操作肯定還沒有進(jìn)行。
  • 在進(jìn)行指令優(yōu)化時(shí)。不能將在對(duì)volatile變量訪問的語句放在其后面運(yùn)行。也不能把volatile變量后面的語句放到其前面運(yùn)行。

3. 正確使用volatile關(guān)鍵字

synchronized關(guān)鍵字是防止多個(gè)線程同一時(shí)候運(yùn)行一段代碼。那么就會(huì)非常影響程序運(yùn)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,可是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因?yàn)関olatile關(guān)鍵字無法保證操作的原子性。

通常來說,使用volatile必須具備以下2個(gè)條件:

  • 對(duì)變量的寫操作不依賴于當(dāng)前值
  • 該變量沒有包括在具有其它變量的不變式中

第一個(gè)條件就是不能是自增自減等操作,上文已經(jīng)提到volatile不保證原子性。


第二個(gè)條件我們來舉個(gè)樣例它包括了一個(gè)不變式 :下界總是小于或等于上界

public class NumberRange {private volatile int lower, upper;public int getLower() { return lower; }public int getUpper() { return upper; }public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...);lower = value;}public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...);upper = value;} }

這樣的方式限制了范圍的狀態(tài)變量,因此將 lower 和 upper 字段定義為 volatile 類型不能夠充分實(shí)現(xiàn)類的線程安全,從而仍然須要使用同步。

否則,假設(shè)湊巧兩個(gè)線程在同一時(shí)間使用不一致的值運(yùn)行 setLower 和 setUpper 的話,則會(huì)使范圍處于不一致的狀態(tài)。

比如,假設(shè)初始狀態(tài)是 (0, 5),同一時(shí)間內(nèi),線程 A 調(diào)用 setLower(4) 而且線程 B 調(diào)用 setUpper(3),顯然這兩個(gè)操作交叉存入的值是不符合條件的,那么兩個(gè)線程都會(huì)通過用于保護(hù)不變式的檢查,使得最后的范圍值是 (4, 3),這顯然是不正確的。


其實(shí)就是要保證操作的原子性就能夠使用volatile,使用volatile主要有兩個(gè)場(chǎng)景:

狀態(tài)標(biāo)志

volatile boolean shutdownRequested; ... public void shutdown(){ shutdownRequested = true;} public void doWork() { while (!shutdownRequested) { // do stuff} }

非常可能會(huì)從循環(huán)外部調(diào)用 shutdown() 方法 —— 即在還有一個(gè)線程中 —— 因此,須要運(yùn)行某種同步來確保正確實(shí)現(xiàn) shutdownRequested 變量的可見性。然而,使用 synchronized 塊編寫循環(huán)要比使用volatile 狀態(tài)標(biāo)志編寫麻煩非常多。因?yàn)?volatile 簡(jiǎn)化了編碼,而且狀態(tài)標(biāo)志并不依賴于程序內(nèi)不論什么其它狀態(tài),因此此處非常適合使用 volatile。

雙重檢查模式 (DCL)

public class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized(this) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

在這里使用volatile會(huì)或多或少的影響性能,但考慮到程序的正確性。犧牲這點(diǎn)性能還是值得的。
DCL長(zhǎng)處是資源利用率高。第一次運(yùn)行g(shù)etInstance時(shí)單例對(duì)象才被實(shí)例化。效率高。缺點(diǎn)是第一次載入時(shí)反應(yīng)稍慢一些。在高并發(fā)環(huán)境下也有一定的缺陷。盡管發(fā)生的概率非常小。


DCL盡管在一定程度攻克了資源的消耗和多余的同步,線程安全等問題,可是他還是在某些情況會(huì)出現(xiàn)失效的問題,也就是DCL失效,在《java并發(fā)編程實(shí)踐》一書建議用以下的代碼(靜態(tài)內(nèi)部類單例模式)來替代DCL:

public class Singleton { private Singleton(){}public static Singleton getInstance(){ return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } }

關(guān)于雙重檢查能夠查看http://blog.csdn.net/dl88250/article/details/5439024

4. 總結(jié)

與鎖相比,Volatile 變量是一種非常easy但同一時(shí)候又非常脆弱的同步機(jī)制,它在某些情況下將提供優(yōu)于鎖的性能和伸縮性。假設(shè)嚴(yán)格遵循 volatile 的使用條件即變量真正獨(dú)立于其它變量和自己曾經(jīng)的值 ,在某些情況下能夠使用 volatile 取代 synchronized 來簡(jiǎn)化代碼。然而,使用 volatile 的代碼往往比使用鎖的代碼更加easy出錯(cuò)。本文介紹了能夠使用 volatile 取代 synchronized 的最常見的兩種用例,其它的情況我們不妨去使用synchronized 。

總結(jié)

以上是生活随笔為你收集整理的Java并发编程(三)volatile域的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。