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