上文說到了 synchronized,那麼就不得不說下 volatile關鍵字了,它們兩者經常協同處理多線程的安全問題。
volatile保證可見性
那麼volatile的作用是什麼呢?
在jvm運行時刻內存的分配中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,
線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,
然後把堆內存變量的具體值load到線程本地內存中,建立一個變量副本,之後線程就不再和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,
在修改完之後的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。
注意這些操作並不是原子性,也就是 在read load之後,如果主內存count變量發生修改之後,線程工作內存中的值由於已經加載,
不會產生對應的變化,所以計算出來的結果會和預期不一樣,對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的。
如何解決緩存不一致的問題?
在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。
因爲CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),
從而使得只能有一個CPU能使用這個變量的內存。比如一個線程在執行 i = i +1,如果在執行這段代碼的過程中,在總線上發出了LCOK#鎖的信號,
那麼只有等待這段代碼完全執行完畢之後,其他CPU才能從變量i所在的內存讀取變量,然後進行相應的操作。
這樣就解決了緩存不一致的問題。但是上面的方式會有一個問題,由於在鎖住總線期間,其他CPU無法訪問內存,導致效率低下。
所以就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。
它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,
因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。
看一個例子:
public class MyThread15 extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } public void run() { System.out.println("進入run了"); while (isRunning == true){} System.out.println("線程被停止了"); } }
@Test public void test15() throws InterruptedException { try{ MyThread15 a = new MyThread15(); a.start(); Thread.sleep(1000); a.setRunning(false); System.out.println("已賦值爲false"); } catch (InterruptedException e){ e.printStackTrace(); } a.join(); }
執行結果:
進入run了 已賦值爲false
可以看待,明明已經將 isRunning設置爲false了,但是“線程被停止了”還是沒有被執行。
當我們給isRunning加上關鍵字volatile後,執行結果:
進入run了 已賦值爲false 線程被停止了
可見volatile關鍵字可以保證共享變量在多線程之間的可見性。
volatile不保證原子性
下面看個demo,定義一個線程類,將count值++ 10000次。
public class MyDomain16 { private volatile int count = 0; public MyDomain16() { } public void count() { for (int i=0; i<10000; i++) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } System.out.println(Thread.currentThread().getName() + ": count=" + count); } }
public class Mythread16 extends Thread { private MyDomain16 myDomain16; public Mythread16(MyDomain16 myDomain16) { this.myDomain16 = myDomain16; } public void run() { myDomain16.count(); } } @Test public void test16() throws InterruptedException { MyDomain16 myDomain16 = new MyDomain16(); Mythread16 a = new Mythread16(myDomain16); Mythread16 b = new Mythread16(myDomain16); a.start(); b.start(); a.join(); b.join(); }
執行結果:
Thread-1: count=17913 Thread-0: count=17919
可以看出兩個線程總共應該增加2萬次,但是count值並沒有等於20000,說明自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
當我們給count方法加上synchronized之後
public class MyDomain16 { private volatile int count = 0; public MyDomain16() { } public synchronized void count() { for (int i=0; i<10000; i++) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } System.out.println(Thread.currentThread().getName() + ": count=" + count); } }
執行結果:
Thread-0: count=10000 Thread-1: count=20000
總結:
關鍵字synchronized和volatile進行一下比較:
1)關鍵字volatile是線程同步的輕量級實現,所以volatile性能肯定比synchronized要好,並且volatile只能修飾於變量,而synchronized可以修飾方法,以及代碼塊。
隨着JDK新版本的發佈,synchronized關鍵字在執行效率上得到很大提升,在開發中使用synchronized關鍵字的比率還是比較大的。
2)多線程訪問volatile不會發生阻塞,而synchronized會出現阻塞。
3)volatile能保證數據的可見性,但不能保證原子性;而synchronized可以保證原子性,也可以間接保證可見性,因爲它會將私有內存和公共內存中的數據做同步。
4)關鍵字volatile解決的是變量在多個線程之間的可見性;而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。
線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。
參考文獻
1:《Java併發編程的藝術》
2:《Java多線程編程核心技術》