java多線程3:原子性,可見性,有序性
概念
在了解線程安全問題之前,必須先知道為什麼需要並發,並發給我們帶來什麼問題。
為什麼需要並發,多線程?
- 時代的召喚,為了更充分的利用多核CPU的計算能力,多個線程程序可通過提高處理器的資源利用率來提升程序性能。
- 方便業務拆分,異步處理業務,提高應用性能。
多線程並發產生的問題?
- 大量的線程讓CPU頻繁上下文切換帶來的系統開銷。
- 臨界資源線程安全問題(共享,可變)。
- 容易造成死鎖。
注意:當多個線程執行一個方法時,該方法內部的局部變量並不是臨界資源,因為這些局部變量是在每個線程的私有棧中,因此不具有共享性質,不會導致線程安全問題。
可見性
多線程訪問同一個變量時,如果有一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。這是因為為了保證多個CPU之間的高速緩存是一致的,操作系統會有一個緩存一致性協議,volatile就是通過OS的緩存一致性協議策略來保證了共享變量在多個線程之間的可見性。
public class ThreadDemo2 { private static boolean flag = false; public void thread_1(){ flag = true; System.out.println("線程1已對flag做出改變"); } public void thread_2(){ while (!flag){ } System.out.println("線程2->flag已被修改,成功打斷循環"); } public static void main(String[] args) { ThreadDemo2 threadDemo2 = new ThreadDemo2(); Thread thread2 = new Thread(()->{ threadDemo2.thread_2(); }); Thread thread1= new Thread(()->{ threadDemo2.thread_1(); }); thread2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread1.start(); } }
執行結果
線程1已對flag做出改變
代碼無論執行多少次,線程2的輸出語句都不會被打印。為flag添加volatile修飾後執行,線程2執行的語句被打印
執行結果
線程1已對flag做出改變
線程2->flag已被修改,成功打斷循環
局限:volatile只是保證共享變量的可見性,無法保證其原子性。多個線程並發時,執行共享變量i的i++操作<==> i = i + 1,這是分兩步執行,並不是一個原子性操作。根據緩存一致性協議,多個線程讀取i並對i進行改變時,其中一個線程搶先獨佔i進行修改,會通知其他CPU我已經對i進行修改,把你們高速緩存的值設為無效並重新讀取,在並發情況下是可能出現數據丟失的情況的。
public class ThreadDemo3 { private volatile static int count = 0; public static void main(String[] args) { for (int i = 0; i < 10; ++i){ Thread thread = new Thread(()->{ for (int j = 0; j < 1000; ++j){ count++; } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count執行的結果為->" + count); } }
執行結果
count執行的結果為->9561
注意:這個結果是不固定的,有時10000,有時少於10000。
原子性
就像戀人一樣同生共死,表現在多線程代碼中程序一旦開始執行,就不會被其他線程干擾要嘛一起成功,要嘛一起失敗,一個操作不可被中斷。在上文的例子中,為什麼執行結果不一定等於10000,就是因為在count++是多個操作,1.讀取count值,2.對count進行加1操作,3.計算的結果再賦值給count。這幾個操作無法構成原子操作的,在一個線程讀取完count值時,另一個線程也讀取他並給它賦值,根據緩存一致性協議通知其他線程把本次讀取的值置為無效,所以本次循環操作是無效的,我們看到的值不一定等於10000,如何進行更正—->synchronized關鍵字
public class ThreadDemo3 { private volatile static int count = 0; private static Object object = new Object(); public static void main(String[] args) { for (int i = 0; i < 10; ++i){ Thread thread = new Thread(()->{ for (int j = 0; j < 1000; ++j){ synchronized (object){ count++; } } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count執行的結果為->" + count); } }
執行結果
count執行的結果為->10000
加鎖後,線程在爭奪執行權就必須獲取到鎖,當前線程就不會被其他線程所干擾,保證了count++的原子性,至於synchronized為什麼能保證原子性,篇幅有限,下一篇在介紹。
有序性
jmm內存模型允許編譯器和CPU在單線程執行結果不變的情況下,會對代碼進行指令重排(遵守規則的前提下)。但在多線程的情況下卻會影響到並發執行的正確性。
public class ThreadDemo4 { private static int x = 0,y = 0; private static int a = 0,b = 0; private static int i = 0; public static void main(String[] args) throws InterruptedException { for (;;){ i++; x = 0;y = 0; a = 0;b = 0; Thread thread1 = new Thread(new Runnable() { @Override public void run() { waitTime(10000); a = 1; x = b; } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { b = 1; y = a; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("第" + i + "次執行結果(" + x + "," + y + ")"); if (x == 0 && y == 0){ System.out.println("在第" + i + "次發生指令重排,(" + x + "," + y + ")"); break; } } } public static void waitTime(int time){ long start = System.nanoTime(); long end; do { end = System.nanoTime(); }while (start + time >= end); } }
執行結果
第1次執行結果(0,1) 第2次執行結果(1,0) .... 第35012次執行結果(0,1) 第35013次執行結果(0,0) 在第35013次發生指令重排,(0,0)
如何解決上訴問題哪?volatile的另一個作用就是禁止指令重排優化,它的底層是內存屏障,其實就是一個CPU指令,一個標識,告訴CPU和編譯器,禁止在這個標識前後的指令執行重排序優化。內存屏障的作用有兩個,一個就是上文所講的保證變量的內存可見性,第二個保證特定操作的執行順序。
補充
指令重排序:Java語言規範規定JVM線程內部維持順序化語義,程序的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以和代碼順序不一致。JVM根據處理器特性,適當的堆機器指令進行重排序,使機器指令更符號CPU的執行特性,最大限度發揮機器性能。
as-if-serial語義:不管怎麼重排序,單線程程序的執行結果不能被改變,編譯器和處理器都必須遵守這個原則。
happens-before原則:輔助保證程序執行的原子性,可見性和有序性的問題,判斷數據是否存在競爭,線程是否安全的依據(JDK5)
1. 程序順序原則,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行。
2. 鎖規則 解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說, 如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)。
3. volatile規則 volatile變量的寫,先發生於讀,這保證了volatile變量的可見性,簡單 的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當 該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能 夠看到該變量的最新值。
4. 線程啟動規則 線程的start()方法先於它的每一個動作,即如果線程A在執行線程B的 start方法之前修改了共享變量的值,那麼當線程B執行start方法時,線程A對共享變量 的修改對線程B可見
5. 傳遞性 A先於B ,B先於C 那麼A必然先於C
6. 線程終止規則 線程的所有操作先於線程的終結,Thread.join()方法的作用是等待當前 執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法 成功返回後,線程B對共享變量的修改將對線程A可見。
7. 線程中斷規則 對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中 斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。