Java中的synchronized与volatile关键字
原文出處:http://hukai.me/android-training-course-in-chinese/performance/smp/index.html
Java中的”synchronized”與”volatile”關(guān)鍵字
“synchronized”關(guān)鍵字提供了Java一種內(nèi)置的鎖機(jī)制。每一個(gè)對象都有一個(gè)相對應(yīng)的“monitor”,這個(gè)監(jiān)聽器可以提供互斥的訪問。
“synchronized”代碼段的實(shí)現(xiàn)機(jī)制與自旋鎖(spin lock)有著相同的基礎(chǔ)結(jié)構(gòu): 他們都是從獲取到CAS開始,以釋放CAS結(jié)束。這意味著編譯器(compilers)與代碼優(yōu)化器(code optimizers)可以輕松的遷移代碼到“synchronized”代碼段中。一個(gè)實(shí)踐結(jié)果是:你不能判定synchronized代碼段是執(zhí)行在這段代碼下面一部分的前面,還是這段代碼上面一部分的后面。更進(jìn)一步,如果一個(gè)方法有兩個(gè)synchronized代碼段并且鎖住的是同一個(gè)對象,那么在這兩個(gè)操作的中間代碼都無法被其他的線程所檢測到,編譯器可能會(huì)執(zhí)行“鎖粗化lock coarsening”并且把這兩者綁定到同一個(gè)代碼塊上。
另外一個(gè)相關(guān)的關(guān)鍵字是“volatile”。在Java 1.4以及之前的文檔中是這樣定義的:volatile聲明和對應(yīng)的C語言中的一樣可不靠。從Java 1.5開始,提供了更有力的保障,甚至和synchronization一樣具備強(qiáng)同步的機(jī)制。
volatile的訪問效果可以用下面這個(gè)例子來說明。如果線程1給volatile字段做了賦值操作,線程2緊接著讀取那個(gè)字段的值,那么線程2是被確保能夠查看到之前線程1的任何寫操作。更通常的情況是,任何線程對那個(gè)字段的寫操作對于線程2來說都是可見的。實(shí)際上,寫volatile就像是釋放監(jiān)聽器,讀volatile就像是獲取監(jiān)聽器。
非volatile的訪問有可能因?yàn)檎疹檝olatile的訪問而需要做順序的調(diào)整。例如編譯器可能會(huì)往上移動(dòng)一個(gè)非volatile加載操作,但是不會(huì)往下移動(dòng)。Volatile之間的訪問不會(huì)因?yàn)楸舜硕龀鲰樞虻恼{(diào)整。虛擬機(jī)會(huì)注意處理如何的內(nèi)存柵欄(memory barriers)。
當(dāng)加載與保存大多數(shù)的基礎(chǔ)數(shù)據(jù)類型,他們都是原子的atomic, 對于long以及double類型的數(shù)據(jù)則不具備原子型,除非他們被聲明為volatile。即使是在單核處理器上,并發(fā)多線程更新非volatile字段值也還是不確定的。
Examples
下面是一個(gè)錯(cuò)誤實(shí)現(xiàn)的單步計(jì)數(shù)器(monotonic counter)的示例: (Java theory and practice: Managing volatility).
class Counter {private int mValue;public int get() {return mValue;}public void incr() {mValue++;} }假設(shè)get()與incr()方法是被多線程調(diào)用的。然后我們想確保當(dāng)get()方法被調(diào)用時(shí),每一個(gè)線程都能夠看到當(dāng)前的數(shù)量。最引人注目的問題是mValue++實(shí)際上包含了下面三個(gè)操作。
reg = mValue reg = reg + 1 mValue = reg如果兩個(gè)線程同時(shí)在執(zhí)行incr()方法,其中的一個(gè)更新操作會(huì)丟失。為了確保正確的執(zhí)行++的操作,我們需要把incr()方法聲明為“synchronized”。這樣修改之后,這段代碼才能夠在單核多線程的環(huán)境中正確的執(zhí)行。
然而,在SMP的系統(tǒng)下還是會(huì)執(zhí)行失敗。不同的線程通過get()方法獲取到得值可能是不一樣的。因?yàn)槲覀兪鞘褂猛ǔ5募虞d方式來讀取這個(gè)值的。我們可以通過聲明get()方法為synchronized的方式來修正這個(gè)錯(cuò)誤。通過這些修改,這樣的代碼才是正確的了。
不幸的是,我們有介紹過有可能發(fā)生的鎖競爭(lock contention),這有可能會(huì)傷害到程序的性能。除了聲明get()方法為synchronized之外,我們可以聲明mValue為“volatile”. (請注意incr()必須使用synchronize) 現(xiàn)在我們知道volatile的mValue的寫操作對于后續(xù)的讀操作都是可見的。incr()將會(huì)稍稍有點(diǎn)變慢,但是get()方法將會(huì)變得更加快速。因此讀操作多于寫操作時(shí),這會(huì)是一個(gè)比較好的方案。(請參考AtomicInteger.)
下面是另外一個(gè)示例,和之前的C示例有點(diǎn)類似:
class MyGoodies {public int x, y; } class MyClass {static MyGoodies sGoodies;void initGoodies() { // runs in thread 1MyGoodies goods = new MyGoodies();goods.x = 5;goods.y = 10;sGoodies = goods;}void useGoodies() { // runs in thread 2if (sGoodies != null) {int i = sGoodies.x; // could be 5 or 0....}} }這段代碼同樣存在著問題,sGoodies = goods的賦值操作有可能在goods成員變量賦值之前被察覺到。如果你使用volatile聲明sGoodies變量,你可以認(rèn)為load操作為atomic_acquire_load(),并且把store操作認(rèn)為是atomic_release_store()。
(請注意僅僅是sGoodies的引用本身為volatile,訪問它的內(nèi)部字段并不是這樣的。賦值語句z = sGoodies.x會(huì)執(zhí)行一個(gè)volatile load MyClass.sGoodies的操作,其后會(huì)伴隨一個(gè)non-volatile的load操作::sGoodies.x。如果你設(shè)置了一個(gè)本地引用MyGoodies localGoods = sGoodies, z = localGoods.x,這將不會(huì)執(zhí)行任何volatile loads.)
另外一個(gè)在Java程序中更加常用的范式就是臭名昭著的“double-checked locking”:
class MyClass {private Helper helper = null;public Helper getHelper() {if (helper == null) {synchronized (this) {if (helper == null) {helper = new Helper();}}}return helper;} }上面的寫法是為了獲得一個(gè)MyClass的單例。我們只需要?jiǎng)?chuàng)建一次這個(gè)實(shí)例,通過getHelper()這個(gè)方法。為了避免兩個(gè)線程會(huì)同時(shí)創(chuàng)建這個(gè)實(shí)例。我們需要對創(chuàng)建的操作加synchronize機(jī)制。然而,我們不想要為了每次執(zhí)行這段代碼的時(shí)候都為“synchronized”付出額外的代價(jià),因此我們僅僅在helper對象為空的時(shí)候加鎖。
在單核系統(tǒng)上,這是不能正常工作的。JIT編譯器會(huì)破壞這件事情。請查看4)Appendix的“‘Double Checked Locking is Broken’ Declaration”獲取更多的信息, 或者是Josh Bloch’s Effective Java書中的Item 71 (“Use lazy initialization judiciously”)。
在SMP系統(tǒng)上執(zhí)行這段代碼,引入了一個(gè)額外的方式會(huì)導(dǎo)致失敗。把上面那段代碼換成C的語言實(shí)現(xiàn)如下:
if (helper == null) {// acquire monitor using spinlockwhile (atomic_acquire_cas(&this.lock, 0, 1) != success);if (helper == null) {newHelper = malloc(sizeof(Helper));newHelper->x = 5;newHelper->y = 10;helper = newHelper;}atomic_release_store(&this.lock, 0); }此時(shí)問題就更加明顯了: helper的store操作發(fā)生在memory barrier之前,這意味著其他的線程能夠在store x/y之前觀察到非空的值。
你應(yīng)該嘗試確保store helper執(zhí)行在atomic_release_store()方法之后。通過重新排序代碼進(jìn)行加鎖,但是這是無效的,因?yàn)橥弦苿?dòng)的代碼,編譯器可以把它移動(dòng)回原來的位置:在atomic_release_store()前面。 (這里沒有讀懂,下次再回讀)
有2個(gè)方法可以解決這個(gè)問題:
- 刪除外層的檢查。這確保了我們不會(huì)在synchronized代碼段之外做任何的檢查。
- 聲明helper為volatile。僅僅這樣一個(gè)小小的修改,在前面示例中的代碼就能夠在Java 1.5及其以后的版本中正常工作。
下面的示例演示了使用volatile的2各重要問題:
class MyClass {int data1, data2;volatile int vol1, vol2;void setValues() { // runs in thread 1data1 = 1;vol1 = 2;data2 = 3;}void useValues1() { // runs in thread 2if (vol1 == 2) {int l1 = data1; // okayint l2 = data2; // wrong}}void useValues2() { // runs in thread 2int dummy = vol2;int l1 = data1; // wrongint l2 = data2; // wrong}請注意useValues1(),如果thread 2還沒有察覺到vol1的更新操作,那么它也無法知道data1或者data2被設(shè)置的操作。一旦它觀察到了vol1的更新操作,那么它也能夠知道data1的更新操作。然而,對于data2則無法做任何猜測,因?yàn)閟tore操作是在volatile store之后發(fā)生的。
useValues2()使用了第2個(gè)volatile字段:vol2,這會(huì)強(qiáng)制VM生成一個(gè)memory barrier。這通常不會(huì)發(fā)生。為了建立一個(gè)恰當(dāng)?shù)摹癶appens-before”關(guān)系,2個(gè)線程都需要使用同一個(gè)volatile字段。在thread 1中你需要知道vol2是在data1/data2之后被設(shè)置的。(The fact that this doesn’t work is probably obvious from looking at the code; the caution here is against trying to cleverly “cause” a memory barrier instead of creating an ordered series of accesses.)
總結(jié)
以上是生活随笔為你收集整理的Java中的synchronized与volatile关键字的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ContentProviderOpera
- 下一篇: Java基础:String类