Java并发编程——volatile
引
volatile可以看成是輕量級的低配版的Synchronized,他主要是作用于共享變量,保證共享變量的可見性。確保共享變量在主內(nèi)存中一致地準確的更新通知到各個線程,這是Volatile的可見性,同時由于它是低配版的Synchronized,所以他也沒有了Synchronized的一些功能,比如原子性。
Java內(nèi)存模型
在理解有關(guān)Java并發(fā)編程的時候,我們是非常有必要的先了解一下Java內(nèi)存模型的。
Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,線程之間的工作并不是直接去讀取操作主內(nèi)存的,而是每個工作線程首先會在主內(nèi)存中拷貝這些共享變量下來線程對變量的操作都是自己的工作線程中完成的,對于不同線程之間是具有不具備可見性的(各做各的),線程間變量值的傳遞均需要通過主內(nèi)存來完成。Volatile正是為了解決以上的問題而存在的。
工作線程操作變量與主內(nèi)存的交互
- read:從主內(nèi)存中讀取變量
- load:復(fù)制變量到工作線程本地內(nèi)存作為副本
- use:運算副本
- assign:給副本賦值
- store:副本寫入工作線程的內(nèi)存中
- write:線程內(nèi)存中的共享副本刷新主內(nèi)存中的共享變量
可見性
可見性是指線程之間變量的可見性,及時得到變量狀態(tài)變化的通知,一個線程修改了變量的狀態(tài)另一個就及時的知道變量的最新狀態(tài)。
舉個例子:A、B線程在主內(nèi)存中拷貝同一個Volatile修飾變量,A線程把這個變量的狀態(tài)由false改為了true,緊跟著B線程就會收到通知他剛剛拷貝的變量已經(jīng)過期失效,B線程就會更新這個變量,得到最新的狀態(tài)true,而不再是過期失效的狀態(tài)。
Volatile修飾的變量不允許線程內(nèi)部緩存和重排序,也就是說直接操作主內(nèi)存,這樣就對其他線程可見了。但是,但是,但是,Volatile修飾的變量只能保證變量的可見性,而不能保證變量的原子性,這樣就導致一些非原子性的操作仍然存在線程安全的問題。
普通的共享變量在進行操作之后,寫入主內(nèi)存的時機是不確定的,該線程可能在操作完變量并且還沒寫入主內(nèi)存的時候就去干別的事情了,這樣就導致在其他線程獲取這個變量的時候并不是最新的值,無法保證可見性,真是這樣的線程安全問題也就導致了程序運行結(jié)果并不是我們所期望的結(jié)果。
Volatile并不能保證原子性,而他的高配版——Synchronized完全應(yīng)付了這些問題。Synchronized既能保證可見性又能保證原子性。Synchronized在工作時只能有一個線程獲取鎖執(zhí)行同步代碼,并在釋放鎖的時候把變量寫入主內(nèi)存中。
上面的代碼使用退出標志終止線程關(guān)閉的代碼。看上去似乎是沒有問題,只要其他線程吧exit復(fù)制為true就能夠終止線程。但是這樣寫仍然會存在風險,有可能不是我們所期望的效果。上面說過,工作線程會各自在主線程中拷貝變量,然后自顧自的工作。
以實例來說,A、B線程會在主線程中各自拷貝exit變量到自己的工作內(nèi)存中,當B需要終止A線程的時候,B便會修改自己工作內(nèi)存中的exit變量,但是由于不確定性B在修改本地exit變量的時候可能還沒把修改后的變量exit寫入主內(nèi)存中就去了干別的事情了,導致A線程沒有終止。
給exit變量增加Volatile修飾后,B線程把本地變量exit賦值為true的時候,Volatile會強制把最新值寫到主內(nèi)存中并且會通知A線程告知其本地exit變量已過期失效,立即到主內(nèi)存中更新exit變量,這樣子便會使A線程的exit變量及時更新。也體現(xiàn)了Volatile在線程之間的可見性。
原子性
從Volatile可見性的問題中我們帶出了原子性這一名詞。原子性是指:一個操作或者多個操作(可以把它看成事務(wù))要么全執(zhí)行而且不會被中斷,要么全不執(zhí)行。在Java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是就原子性操作。原子就是不能再細分的意思。
舉個例子:int a = 8; 把8賦值給a這個操作已經(jīng)不能再細分或分割了,那么類似于這種操作就稱之為原子操作。
再舉個例子:i++; 這個操作就可以分解為 i = i + 1 ,那么類似于這從操作就稱之為非原子操作。
非原子操作帶來的是線程安全問題,使用同步技術(shù)Synchronized來把這堆操作變成一個原子操作。
上面代碼,我們直觀的認為新建了10個線程,每個線程都對inc變量自增1,那么10個線程最后輸出的結(jié)果自然是1000*10=10000,這是我們所期望的。但是通過輸出我們發(fā)現(xiàn)結(jié)果并不是我們所想要的。
前面提到過,Volatile只能保證變量的可見性,而不能保證原子性。
還是按實例來說,A、B線程創(chuàng)建后各自把inc拷貝到自己的本地內(nèi)存中。此時A、B都在自己本地線程中對inc++自增,然后A、B線程把運算后的結(jié)果寫入主內(nèi)存中。這樣,盡管A、B線程都進行了自增的運算,我們的期望是等于3,盡管進行了兩次自增,但是此時主內(nèi)存中的inc變量只是2。
例子也體現(xiàn)了Volatile并不能保證非原子操作,仍然會存在線程安全問題。
解決方案:可以通過synchronized或lock,進行加鎖,來保證操作的原子性。也可以通過AtomicInteger。(不在本文范圍)
有序性
有序性就是程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。?
處理器為了提高程序運行效率,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。處理器的重排序不會影響單個線程,但是面臨并發(fā)編程的時候就不能保證正確性了。
volatile關(guān)鍵字可以保證一定的“有序性”。synchronized既保證有序性同樣保證原子性。
Volatile原理
在對聲明了volatile變量進行寫操作時,JVM會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫會到系統(tǒng)內(nèi)存。 這一步確保了如果有其他線程對聲明了volatile變量進行修改,則立即更新主內(nèi)存中數(shù)據(jù)。
為了保證各個處理器的工作線程一致,每個處理會通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存是否過期,當處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改了,就會將當前處理器的緩存行設(shè)置成無效狀態(tài),當處理器要對這個數(shù)據(jù)進行修改操作時,會強制重新從系統(tǒng)內(nèi)存把數(shù)據(jù)讀到處理器緩存里。
小結(jié)
synchronized?是防止多個線程同時執(zhí)行一段代碼,這樣同步就會影響程序執(zhí)行效率,而volatile在某些情況下性能要優(yōu)于synchronized。Volatile只能保證變量的可見性,并不能保證變量的原子性。對于由于非原子操作而產(chǎn)生的線程安全問題,還是請使用synchronized。
最后使用Volatile的必備兩個條件:
- 對變量的操作不依賴于當前值,也就是原子操作
- 該變量沒有包含在具有其他變量的不變式中
That's all Thank you~
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?----- End -----
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?更多好文
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 請掃描下面二維碼
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?歡迎關(guān)注~
總結(jié)
以上是生活随笔為你收集整理的Java并发编程——volatile的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS逆向之旅(进阶篇) — 工具(LL
- 下一篇: oracle 日期格式转换 ‘ddMON