Java中的synchronized与volatile关键字
原文出處:http://hukai.me/android-training-course-in-chinese/performance/smp/index.html
Java中的”synchronized”與”volatile”關鍵字
“synchronized”關鍵字提供了Java一種內置的鎖機制。每一個對象都有一個相對應的“monitor”,這個監聽器可以提供互斥的訪問。
“synchronized”代碼段的實現機制與自旋鎖(spin lock)有著相同的基礎結構: 他們都是從獲取到CAS開始,以釋放CAS結束。這意味著編譯器(compilers)與代碼優化器(code optimizers)可以輕松的遷移代碼到“synchronized”代碼段中。一個實踐結果是:你不能判定synchronized代碼段是執行在這段代碼下面一部分的前面,還是這段代碼上面一部分的后面。更進一步,如果一個方法有兩個synchronized代碼段并且鎖住的是同一個對象,那么在這兩個操作的中間代碼都無法被其他的線程所檢測到,編譯器可能會執行“鎖粗化lock coarsening”并且把這兩者綁定到同一個代碼塊上。
另外一個相關的關鍵字是“volatile”。在Java 1.4以及之前的文檔中是這樣定義的:volatile聲明和對應的C語言中的一樣可不靠。從Java 1.5開始,提供了更有力的保障,甚至和synchronization一樣具備強同步的機制。
volatile的訪問效果可以用下面這個例子來說明。如果線程1給volatile字段做了賦值操作,線程2緊接著讀取那個字段的值,那么線程2是被確保能夠查看到之前線程1的任何寫操作。更通常的情況是,任何線程對那個字段的寫操作對于線程2來說都是可見的。實際上,寫volatile就像是釋放監聽器,讀volatile就像是獲取監聽器。
非volatile的訪問有可能因為照顧volatile的訪問而需要做順序的調整。例如編譯器可能會往上移動一個非volatile加載操作,但是不會往下移動。Volatile之間的訪問不會因為彼此而做出順序的調整。虛擬機會注意處理如何的內存柵欄(memory barriers)。
當加載與保存大多數的基礎數據類型,他們都是原子的atomic, 對于long以及double類型的數據則不具備原子型,除非他們被聲明為volatile。即使是在單核處理器上,并發多線程更新非volatile字段值也還是不確定的。
Examples
下面是一個錯誤實現的單步計數器(monotonic counter)的示例: (Java theory and practice: Managing volatility).
class Counter {private int mValue;public int get() {return mValue;}public void incr() {mValue++;} }假設get()與incr()方法是被多線程調用的。然后我們想確保當get()方法被調用時,每一個線程都能夠看到當前的數量。最引人注目的問題是mValue++實際上包含了下面三個操作。
reg = mValue reg = reg + 1 mValue = reg如果兩個線程同時在執行incr()方法,其中的一個更新操作會丟失。為了確保正確的執行++的操作,我們需要把incr()方法聲明為“synchronized”。這樣修改之后,這段代碼才能夠在單核多線程的環境中正確的執行。
然而,在SMP的系統下還是會執行失敗。不同的線程通過get()方法獲取到得值可能是不一樣的。因為我們是使用通常的加載方式來讀取這個值的。我們可以通過聲明get()方法為synchronized的方式來修正這個錯誤。通過這些修改,這樣的代碼才是正確的了。
不幸的是,我們有介紹過有可能發生的鎖競爭(lock contention),這有可能會傷害到程序的性能。除了聲明get()方法為synchronized之外,我們可以聲明mValue為“volatile”. (請注意incr()必須使用synchronize) 現在我們知道volatile的mValue的寫操作對于后續的讀操作都是可見的。incr()將會稍稍有點變慢,但是get()方法將會變得更加快速。因此讀操作多于寫操作時,這會是一個比較好的方案。(請參考AtomicInteger.)
下面是另外一個示例,和之前的C示例有點類似:
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變量,你可以認為load操作為atomic_acquire_load(),并且把store操作認為是atomic_release_store()。
(請注意僅僅是sGoodies的引用本身為volatile,訪問它的內部字段并不是這樣的。賦值語句z = sGoodies.x會執行一個volatile load MyClass.sGoodies的操作,其后會伴隨一個non-volatile的load操作::sGoodies.x。如果你設置了一個本地引用MyGoodies localGoods = sGoodies, z = localGoods.x,這將不會執行任何volatile loads.)
另外一個在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;} }上面的寫法是為了獲得一個MyClass的單例。我們只需要創建一次這個實例,通過getHelper()這個方法。為了避免兩個線程會同時創建這個實例。我們需要對創建的操作加synchronize機制。然而,我們不想要為了每次執行這段代碼的時候都為“synchronized”付出額外的代價,因此我們僅僅在helper對象為空的時候加鎖。
在單核系統上,這是不能正常工作的。JIT編譯器會破壞這件事情。請查看4)Appendix的“‘Double Checked Locking is Broken’ Declaration”獲取更多的信息, 或者是Josh Bloch’s Effective Java書中的Item 71 (“Use lazy initialization judiciously”)。
在SMP系統上執行這段代碼,引入了一個額外的方式會導致失敗。把上面那段代碼換成C的語言實現如下:
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); }此時問題就更加明顯了: helper的store操作發生在memory barrier之前,這意味著其他的線程能夠在store x/y之前觀察到非空的值。
你應該嘗試確保store helper執行在atomic_release_store()方法之后。通過重新排序代碼進行加鎖,但是這是無效的,因為往上移動的代碼,編譯器可以把它移動回原來的位置:在atomic_release_store()前面。 (這里沒有讀懂,下次再回讀)
有2個方法可以解決這個問題:
- 刪除外層的檢查。這確保了我們不會在synchronized代碼段之外做任何的檢查。
- 聲明helper為volatile。僅僅這樣一個小小的修改,在前面示例中的代碼就能夠在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被設置的操作。一旦它觀察到了vol1的更新操作,那么它也能夠知道data1的更新操作。然而,對于data2則無法做任何猜測,因為store操作是在volatile store之后發生的。
useValues2()使用了第2個volatile字段:vol2,這會強制VM生成一個memory barrier。這通常不會發生。為了建立一個恰當的“happens-before”關系,2個線程都需要使用同一個volatile字段。在thread 1中你需要知道vol2是在data1/data2之后被設置的。(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.)
總結
以上是生活随笔為你收集整理的Java中的synchronized与volatile关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ContentProviderOpera
- 下一篇: Android性能优化典范 - 第6季