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