摘要:并發設計的三大原則原子性原子性對共享變量的操作相對于其他線程是不可干擾的,即其他線程的執行只能在該原子操作完成后或開始前執行。發現兩個線程運行結束后的值為。這就是在多線程情況下要求程序執行的順序按照代碼的先后順序執行的原因之一。
并發設計的三大原則 原子性
原子性:對共享變量的操作相對于其他線程是不可干擾的,即其他線程的執行只能在該原子操作完成后或開始前執行。
通過一個小例子理解
public class Main { private static Integer a = 0; public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) { pool.submit(() -> { a = a + 1; }); } pool.shutdown(); //等待線程全部結束 while(!pool.isTerminated()); System.out.println(a); } }
這里創建了一個包含50個線程的線程池,并讓每個線程執行一次自增的操作,最后等待全部線程執行結束之后打印a的值。
理論上,這個a的值應該是50吧,但實際運行發現并不是如此,而且多次運行的結果不一樣。
分析一下原因,在多線程的情況下,a = a + 1這一條語句是可能被多個線程同時執行或交替執行的,而這條語句本身分為3個步驟,讀取a的值,a的值+1,寫回a。
假設現在a的值為1,線程A和線程B正在執行。線程A讀取a得值為1,并將a得值+1(線程A內a的值目前依舊為1),此時線程B讀取a得值為1,將a值+1,寫回a,此時a為2,線程A再次運行,將剛才+1后的a值(2)寫回a。
發現兩個線程運行結束后a的值為2。
以一個表格描述運行的過程。
線程A | 線程B | a |
---|---|---|
讀取a | 讀取a | 1 |
a + 1 | a + 1,寫回結果 | 2 |
寫回結果 | 2 |
這一現象發生的原因,正是因為a = a + 1其實是由多個步驟所構成的,在一個線程操作的過程中,其他線程也可以進行操作,所以發生了非預期的錯誤結果。
因此,若能保證一個線程在執行操作共享變量的時候,其他線程不能操作,即不能干擾的情況下,就能保證程序正常的運行了,這就是原子性。
可見性可見性:當一個線程修改了狀態,其他的線程能夠看到改變。
了解過計算機組成原理的應該知道,為了緩解CPU過高的執行速度和內存過低的讀取速度的矛盾,CPU內置了緩存功能,能夠存儲近期訪問過的數據,若需要再次操作這些數據,只需要從緩存中讀取即可,大大減少了內存I/O的時間。
(此處應當有JVM的內存結構分析,待添加)
但此時就產生了一個問題,在多處理器的情況下,若對同一個內存區域進行操作,就會在多個處理器緩存中存在該內存區域的拷貝。但每個處理器對結果的操作并不能對其他處理器可見,因為各個處理器都在讀取自己的緩存區域,這就造成了緩存不一致的情況。
同樣以一個小例子理解
public class Main { private static Boolean ready = false; private static Integer number = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!ready) ; System.out.println(number); }).start(); Thread.sleep(100); number = 42; ready = true; System.out.println("Main Thread Over !"); } }
這里ready初始化為false,創建一個線程,持續監測ready的值,直到為true后打印number的結果。
主線程則在創建完線程后給ready和number重新賦值。
運行之后發現,程序打印出了Main Thread Over !意味著主線程結束,此時ready和number應該已經被賦值,但等待很久之后發現還是沒有正常打印出number的值。
因為這里在主線程讓線程暫停了一段時間,保證子線程先運行,此時子線程讀到的內存中的ready為false,并拷貝至自身的緩存,當主線程運行時,修改了ready的值,而子線程并不知道這一事件的發生,依舊在使用緩沖中的值。這正是因為多線程下緩存的不一致,即可見性問題。
如果有興趣的朋友可以將Thread.sleep(100);這句取消,看看結果,分析一下原因。有序性
有序性:程序執行的順序按照代碼的先后順序執行。
可能有同學看到這一條不是很理解,而且這個相關的例子也很難給出,因為存在很大的隨機性。
首先理解一下,為什么會有這一條,難道程序的執行順序還不是按照我寫的代碼的順序嗎?
其實還真不一定是。
上面講到,每個處理器都會有一個高速緩存,在程序運行中,更多次數的命中緩存,往往意味著更高效率的運行,而緩存的空間實際是很小的,可能時常需要讓出空間為新變量使用。針對這一點,很多編譯器內置了一個優化,通過不影響程序的運行結果,調整部分代碼的位置,使得高速緩存的利用率提升。
例如
Integer a,b; a = a + 1; //(1) b = b - 3; //(2) a = a + 1; //(3)
如果處理器的緩存空間很小,只能存下一個變量,那么將第(3)句放置(1),(2)句之間,是不是緩存多使用了一次,而且沒有改變程序的運行結果。這就是重排序問題,當然重排序提升的不僅僅是緩存利用率,還有其他很多的方面。
到這里,可能會有疑問,不是說保證不影響程序運行結果才會有重排序發生嗎,為什么還要考慮這一點。
重排序遵守一個happens-before原則,而這個原則實則并沒有對多線程交替的情況進行考慮,因為這太復雜,考慮多線程的交替性還要進行重排序而不影響運行結果的最好辦法,就是不排序 :-)
happens-before原則
同一個線程中的每個Action都happens-before于出現在其后的任何一個Action。
對一個監視器的解鎖happens-before于每一個后續對同一個監視器的加鎖。
對volatile字段的寫入操作happens-before于每一個后續的同一個字段的讀操作。
Thread.start()的調用會happens-before于啟動線程里面的動作。
Thread中的所有動作都happens-before于其他線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。
一個線程A調用另一個另一個線程B的interrupt()都happens-before于線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。
一個對象構造函數的結束happens-before與該對象的finalizer的開始
如果A動作happens-before于B動作,而B動作happens-before與C動作,那么A動作happens-before于C動作。
那么,多線程下的重排序會怎么樣影響程序的結果呢?還是拿上一個例子來講
public class Main { private static volatile Boolean ready = false; private static volatile Integer number = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!ready) ; System.out.println(number); }).start(); number = 42; //(1) ready = true; //(2) System.out.println("Main Thread Over !"); } }
注意此處刪除了線程休眠的代碼。
這里我們假設理想的情況,現在整個程序已經滿足了可見性(此處使用了volatile,具體原理可見續文),而此時發生了重排序,將(1)(2)兩行的內容進行了交換,子線程開始了運行,并持續檢測ready中。主線程執行,由于發生了重排序,(2)將先會執行,此時子線程看到ready變為了true,之后打印出number的值,此時,number的值為0,而預期的結果應該是42。
這就是在多線程情況下要求程序執行的順序按照代碼的先后順序執行的原因之一。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73996.html
摘要:作為面試官,我是如何甄別應聘者的包裝程度語言和等其他語言的對比分析和主從復制的原理詳解和持久化的原理是什么面試中經常被問到的持久化與恢復實現故障恢復自動化詳解哨兵技術查漏補缺最易錯過的技術要點大掃盲意外宕機不難解決,但你真的懂數據恢復嗎每秒 作為面試官,我是如何甄別應聘者的包裝程度Go語言和Java、python等其他語言的對比分析 Redis和MySQL Redis:主從復制的原理詳...
摘要:作為面試官,我是如何甄別應聘者的包裝程度語言和等其他語言的對比分析和主從復制的原理詳解和持久化的原理是什么面試中經常被問到的持久化與恢復實現故障恢復自動化詳解哨兵技術查漏補缺最易錯過的技術要點大掃盲意外宕機不難解決,但你真的懂數據恢復嗎每秒 作為面試官,我是如何甄別應聘者的包裝程度Go語言和Java、python等其他語言的對比分析 Redis和MySQL Redis:主從復制的原理詳...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 3279·2021-10-11 11:08
閱讀 4423·2021-09-22 15:54
閱讀 911·2019-08-30 15:56
閱讀 864·2019-08-30 15:55
閱讀 3540·2019-08-30 15:52
閱讀 1352·2019-08-30 15:43
閱讀 1937·2019-08-30 11:14
閱讀 2502·2019-08-29 16:11