可見性:
可見性是一種復雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變量a具有可見性,但是a++ 依然是一個非原子操作,也就這這個操作同樣存在線程安全問題。
原子性:
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
下面內容摘錄自《Java Concurrency in Practice》:
下面一段代碼在多線程環境下,將存在問題。
/** * @author zhengbinMac */ public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { while(!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
NoVisibility可能會持續循環下去,因為讀線程可能永遠都看不到ready的值。甚至NoVisibility可能會輸出0,因為讀線程可能看到了寫入ready的值,但卻沒有看到之後寫入number的值,這種現象被稱為“重排序”。只要在某個線程中無法檢測到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那麼就無法確保線程中的操作將按照程序中指定的順序來執行。當主線程首先寫入number,然後在沒有同步的情況下寫入ready,那麼讀線程看到的順序可能與寫入的順序完全相反。
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行春旭進行判斷,無法得到正確的結論。
這個看上去像是一個失敗的設計,但卻能使JVM充分地利用現代多核處理器的強大性能。例如,在缺少同步的情況下,Java內存模型允許編譯器對操作順序進行重排序,並將數值緩存在寄存器中。此外,它還允許CPU對操作順序進行重排序,並將數值緩存在處理器特定的緩存中。
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會講該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。
當對非volatile變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以考慮到不同的CPU cache中。
而聲明變量是volatile的,JVM保證了每次讀變量都從內存中讀,跳過CPU cache這一步。