從計(jì)算機(jī)底層來(lái)說(shuō): 線程可以比作是輕量級(jí)的進(jìn)程,是程序執(zhí)行的最小單位,線程間的切換和調(diào)度的成本遠(yuǎn)遠(yuǎn)小于進(jìn)程。另外,多核 CPU 時(shí)代意味著多個(gè)線程可以同時(shí)運(yùn)行,這減少了線程上下文切換的開(kāi)銷。
從當(dāng)代互聯(lián)網(wǎng)發(fā)展趨勢(shì)來(lái)說(shuō): 現(xiàn)在的系統(tǒng)動(dòng)不動(dòng)就要求百萬(wàn)級(jí)甚至千萬(wàn)級(jí)的并發(fā)量,而多線程并發(fā)編程正是開(kāi)發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線程機(jī)制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。
總結(jié):并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度,充分的利用多核CPU資源。
原子性(Atomicity):在一次或多次操作中,要么所有的操作都執(zhí)行并且不會(huì)受其他因素干擾而中斷,要么所有的操作都不執(zhí)行
可見(jiàn)性:一個(gè)線程對(duì)共享變量的修改,其他線程能夠立刻看到。(synchronized,volatile)
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。(指令重排:處理器為了提高程序運(yùn)行效率,處理器根據(jù)指令之間的數(shù)據(jù)依賴性,可能會(huì)對(duì)指令進(jìn)行重排序,單線程下可以保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的,但是多線程下有可能出現(xiàn)問(wèn)題)。
可見(jiàn)性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
舉個(gè)簡(jiǎn)單的例子,看下面這段代碼:
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2。由上面的分析可知,當(dāng)線程1執(zhí)行 i = 10這句時(shí),會(huì)先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒(méi)有立即寫入到主存當(dāng)中。此時(shí)線程2執(zhí)行 j = i,它會(huì)先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會(huì)使得j的值為0,而不是10。線程1對(duì)變量i修改了之后,線程2沒(méi)有立即看到線程1修改的值。在多線程環(huán)境下,一個(gè)線程對(duì)共享變量的操作對(duì)其他線程是不可見(jiàn)的。這就是可見(jiàn)性問(wèn)題。
對(duì)于可見(jiàn)性,Java提供了volatile關(guān)鍵字來(lái)保證可見(jiàn)性。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。而普通的共享變量不能保證可見(jiàn)性,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來(lái)的舊值,因此無(wú)法保證可見(jiàn)性。
另外,通過(guò)synchronized和Lock也能夠保證可見(jiàn)性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見(jiàn)性。
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
舉個(gè)簡(jiǎn)單的例子,看下面這段代碼:
int i = 0; ? ? ? ? ? ? ?
boolean flag = false;
i = 1; ? ? ? ? ? ? ? ?//語(yǔ)句1 ?
flag = true; ? ? ? ? ?//語(yǔ)句2
上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對(duì)兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語(yǔ)句1是在語(yǔ)句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語(yǔ)句1一定會(huì)在語(yǔ)句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來(lái)說(shuō),處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語(yǔ)句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語(yǔ)句1和語(yǔ)句2誰(shuí)先執(zhí)行對(duì)最終的程序結(jié)果并沒(méi)有影響,那么就有可能在執(zhí)行過(guò)程中,語(yǔ)句2先執(zhí)行而語(yǔ)句1后執(zhí)行。那么它靠什么保證的呢?進(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性。雖然重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個(gè)例子:
//線程1:
context = loadContext(); ? //語(yǔ)句1
inited = true; ? ? ? ? ? ? //語(yǔ)句2
//線程2:
while(!inited ){
? ? sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語(yǔ)句1和語(yǔ)句2沒(méi)有數(shù)據(jù)依賴性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過(guò)程中先執(zhí)行語(yǔ)句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒(méi)有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。
從上面可以看出,在Java內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,
但是重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
你吃飯吃到一半,電話來(lái)了,你一直到吃完了以后才去接,這就說(shuō)明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來(lái)了,你停了下來(lái)接了電話,接完后繼續(xù)吃飯,這說(shuō)明你支持并發(fā)。 (不一定是同時(shí)的)
你吃飯吃到一半,電話來(lái)了,你一邊打電話一邊吃飯,這說(shuō)明你支持并行。
并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力,不一定要同時(shí)。
并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力。
1)Java 中的線程對(duì)應(yīng)是操作系統(tǒng)級(jí)別的線程,線程數(shù)量控制不好,頻繁的創(chuàng)建、銷毀線程和線程間的切換,比較消耗內(nèi)存和時(shí)間。
2)容易帶來(lái)線程安全問(wèn)題。如線程的可見(jiàn)性、有序性、原子性問(wèn)題,會(huì)導(dǎo)致程序出現(xiàn)的結(jié)果與預(yù)期結(jié)果不一致。
3)多線程容易造成死鎖、活鎖、線程饑餓等問(wèn)題。此類問(wèn)題往往只能通過(guò)手動(dòng)停止線程、甚至是進(jìn)程才能解決,影響嚴(yán)重。
4)對(duì)編程人員的技術(shù)要求較高,編寫出正確的并發(fā)程序并不容易。
5)并發(fā)程序易出問(wèn)題,且難調(diào)試和排查;問(wèn)題常常詭異地出現(xiàn),又詭異地消失。
Java 5.0 提供了java.util.concurrent(簡(jiǎn)稱JUC)包,在此包中增加了在并發(fā)編程中很常見(jiàn)的實(shí)用工具類,用于定義類似于編程的自定義子系統(tǒng),包括線程池、異步IO和輕量級(jí)任務(wù)框架。提供可調(diào)的、靈活的線程池。還提供了設(shè)計(jì)用于多線程上下文的Collection實(shí)現(xiàn)等。
JMM其實(shí)并不像JVM內(nèi)存模型一樣是真實(shí)存在的,它只是一個(gè)抽象的規(guī)范。在不同的硬件或者操作系統(tǒng)下,對(duì)內(nèi)存的訪問(wèn)邏輯都有一定的差異,而這種差異會(huì)導(dǎo)致同一套代碼在不同操作系統(tǒng)或者硬件下,得到了不同的結(jié)果,而JMM的存在就是為了解決這個(gè)問(wèn)題,通過(guò)JMM的規(guī)范,保證Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問(wèn)都能得到一致的效果。
計(jì)算機(jī)在執(zhí)行程序的時(shí)候,每條指令都是在 CPU 中執(zhí)行的,而執(zhí)行的時(shí)候,又免不了和數(shù)據(jù)打交道,而計(jì)算機(jī)上面的數(shù)據(jù),是存放在計(jì)算機(jī)的物理內(nèi)存上的。當(dāng)內(nèi)存的讀取速度和CPU的執(zhí)行速度相比差別不大的時(shí)候,這樣的機(jī)制是沒(méi)有任何問(wèn)題的,可是隨著CPU的技術(shù)的發(fā)展,CPU的執(zhí)行速度和內(nèi)存的讀取速度差距越來(lái)越大,導(dǎo)致CPU每次操作內(nèi)存都要耗費(fèi)很多等待時(shí)間。
為了解決這個(gè)問(wèn)題,初代程序員大佬們想到了一個(gè)的辦法,就是在CPU和物理內(nèi)存上新增高速緩存,這樣程序在運(yùn)行過(guò)程中,會(huì)將運(yùn)算所需要的數(shù)據(jù)從主內(nèi)存復(fù)制一份到CPU的高速緩存中,當(dāng)CPU進(jìn)行計(jì)算時(shí)就可以直接從高速緩存中讀數(shù)據(jù)和寫數(shù)據(jù)了,當(dāng)運(yùn)算結(jié)束再將數(shù)據(jù)刷新到主內(nèi)存就可以了。
隨著時(shí)代的變遷,CPU開(kāi)始出現(xiàn)了多核的概念,每個(gè)核都有一套自己的緩存,并且隨著計(jì)算機(jī)能力不斷提升,還開(kāi)始支持多線程,最終演變成多個(gè)線程訪問(wèn)進(jìn)程中的某個(gè)共享內(nèi)存,且這多個(gè)線程分別在不同的核心上執(zhí)行,則每個(gè)核心都會(huì)在各自的 Cache 中保留一份共享內(nèi)存的緩沖,我們知道多核是可以并行的,這樣就會(huì)出現(xiàn)多個(gè)線程同時(shí)寫各自的緩存的情況,導(dǎo)致各自的 Cache 之間的數(shù)據(jù)可能不同。
總結(jié)下來(lái)就是:在多核 CPU 中,每個(gè)核的自己的緩存,關(guān)于同一個(gè)數(shù)據(jù)的緩存內(nèi)容可能不一致。
重排序指的是在執(zhí)行程序時(shí),為了提高性能,從源代碼到最終執(zhí)行指令的過(guò)程中,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排的一種手段。
下圖為從源代碼到最終指令示意圖
重排序的分為3種
1)編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語(yǔ)義(as-if-serial)的的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
2)指令級(jí)并行的重排序:現(xiàn)在處理器采用指令級(jí)并行技術(shù)(Instruction-Level Parallelism, ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序:由于處理器使用了存儲(chǔ)和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去亂序執(zhí)行。
1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
2.指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
4.這些重排序?qū)τ趩尉€程沒(méi)問(wèn)題,但是多線程都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。
數(shù)據(jù)依賴性:編譯器和處理器在重排序時(shí),針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
遵守as-if-serial 語(yǔ)義:不管編譯器和處理器為了提高并行度怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能被改變。
區(qū)別:
as-if-serial定義:無(wú)論編譯器和處理器如何進(jìn)行重排序,單線程程序的執(zhí)行結(jié)果不會(huì)改變。
happens-before定義:一個(gè)操作happens-before另一個(gè)操作,表示第一個(gè)的操作結(jié)果對(duì)第二個(gè)操作可見(jiàn),并且第一個(gè)操作的執(zhí)行順序也在第二個(gè)操作之前。但這并不意味著Java虛擬機(jī)必須按照這個(gè)順序來(lái)執(zhí)行程序。如果重排序的后的執(zhí)行結(jié)果與按happens-before關(guān)系執(zhí)行的結(jié)果一致,Java虛擬機(jī)也會(huì)允許重排序的發(fā)生。
happens-before關(guān)系保證了同步的多線程程序的執(zhí)行結(jié)果不被改變,as-if-serial保證了單線程內(nèi)程序的執(zhí)行結(jié)果不被改變。
相同點(diǎn):happens-before和as-if-serial的作用都是在不改變程序執(zhí)行結(jié)果的前提下,提高程序執(zhí)行的并行度。
不可變對(duì)象即對(duì)象一旦被創(chuàng)建,它的狀態(tài)(對(duì)象屬性值)就不能改變。
不可變對(duì)象的類即為不可變類。Java 平臺(tái)類庫(kù)中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。
不可變對(duì)象保證了對(duì)象的內(nèi)存可見(jiàn)性,對(duì)不可變對(duì)象的讀取不需要進(jìn)行額外的同步手段,提升了代碼執(zhí)行效率。
1.保證變量寫操作的可見(jiàn)性;
2.保證變量前后代碼的執(zhí)行順序;
不能。volatile不能保證原子性,只能保證線程可見(jiàn)性,可見(jiàn)性表現(xiàn)在當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
從實(shí)踐角度而言,volatile的一個(gè)重要作用就是和CAS結(jié)合,保證了原子性,詳細(xì)的可以參見(jiàn)java.util.concurrent.atomic包下的類,比如AtomicInteger。
被volatile修飾的變量被修改時(shí),會(huì)將修改后的變量直接寫入主存中,并且將其他線程中該變量的緩存置為無(wú)效,從而讓其它線程對(duì)該變量的引用直接從主存中獲取數(shù)據(jù),這樣就保證了變量的可見(jiàn)性。
但是volatile修飾的變量在自增時(shí)由于該操作分為讀寫兩個(gè)步驟,所以當(dāng)一個(gè)線程的讀操作被阻塞時(shí),另一個(gè)線程同時(shí)也進(jìn)行了自增操作,此時(shí)由于第一個(gè)線程的寫操作沒(méi)有進(jìn)行所以主存中仍舊是之前的原數(shù)據(jù),所以當(dāng)兩個(gè)線程自增完成后,該變量可能只加了1。因而volatile是無(wú)法保證對(duì)變量的任何操作都是原子性的。
能,Java 中可以創(chuàng)建 volatile 類型數(shù)組,但如果多個(gè)線程改變引用指向的數(shù)組,將會(huì)受到 volatile 的保護(hù),如果多個(gè)線程改變數(shù)組的元素內(nèi)容,volatile 標(biāo)示符就不能起到之前的保護(hù)作用了。
volatile變量可以確保先行關(guān)系,即寫操作會(huì)發(fā)生在后續(xù)的讀操作之前,但它并不能保證原子性。例如用volatile修飾count變量那么count++操作就不是原子性的。
而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement( )方法會(huì)原子性的進(jìn)行增量操作把當(dāng)前值加- ,其它數(shù)據(jù)類型和引用變量也可以進(jìn)行相似操作。
原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作,這種操作一旦開(kāi)始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程上下文切換。原子操作可以是一個(gè)步驟,也可以是多個(gè)操作步驟,但是其順序不可以被打亂,也不可以被切割而只執(zhí)行其中的一部分,將整個(gè)操作視作一個(gè)整體是原子性的核心特征。
而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執(zhí)行添加、遞增、遞減等操作。比如之前多線程下的線程不安全的 i++ 問(wèn)題,到了原子類這里,就可以用功能相同且線程安全的 getAndIncrement 方法來(lái)優(yōu)雅地解決。
原子類的作用和鎖有類似之處,是為了保證并發(fā)情況下線程安全。不過(guò)原子類相比于鎖,有一定的優(yōu)勢(shì):
粒度更細(xì):原子變量可以把競(jìng)爭(zhēng)范圍縮小到變量級(jí)別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競(jìng)爭(zhēng)的情況之外,使用原子類的效率通常會(huì)比使用同步互斥鎖的效率更高,因?yàn)樵宇惖讓永昧?CAS 操作,不會(huì)阻塞線程。原子類的作用和鎖有類似之處,是為了保證并發(fā)情況下線程安全。不過(guò)原子類相比于鎖,有一定的優(yōu)勢(shì):
粒度更細(xì):原子變量可以把競(jìng)爭(zhēng)范圍縮小到變量級(jí)別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競(jìng)爭(zhēng)的情況之外,使用原子類的效率通常會(huì)比使用同步互斥鎖的效率更高,因?yàn)樵宇惖讓永昧?CAS 操作,不會(huì)阻塞線程。
AtomicInteger與AtomicLong:它們的底層實(shí)現(xiàn)使用了CAS鎖,不同點(diǎn)在于AtomicInteger包裝了一個(gè)Integer型變量,而AtomicLong包裝了一個(gè)Long型變量。
LongAdder:它的底層實(shí)現(xiàn)是分段鎖+CAS鎖。
atomic代表的是concurrent包下Atomic開(kāi)頭的類,如AtomicBoolean、AtomicInteger、AtomicLong等都是用原子的方式來(lái)實(shí)現(xiàn)指定類型的值的更新,它的底層通過(guò)CAS原理解決并發(fā)情況下原子性的問(wèn)題,在jdk中CAS是Unsafe類中的api來(lái)實(shí)現(xiàn)的。
CAS,全稱為Compare and Swap,即比較-替換,實(shí)現(xiàn)并發(fā)算法時(shí)常用到的一種技術(shù)。假設(shè)有三個(gè)操作數(shù):內(nèi)存值V、舊的預(yù)期值A(chǔ)、要修改的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),才會(huì)將內(nèi)存值修改為B并返回true,否則什么都不做并返回false。當(dāng)然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內(nèi)存中最新的那個(gè)值,否則舊的預(yù)期值A(chǔ)對(duì)某條線程來(lái)說(shuō),永遠(yuǎn)是一個(gè)不會(huì)變的值A(chǔ),只要某次CAS操作失敗,永遠(yuǎn)都不可能成功。
以AtomicInteger為例,說(shuō)明CAS的使用與原理。首先atomicIngeter初始化為5,調(diào)用對(duì)象的compareAndSet方法來(lái)對(duì)比當(dāng)前值與內(nèi)存中的值,是否相等,相等則更新為2019,不相等則不會(huì)更新,compareAndSet方法返回的是boolean類型。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019)+" \t current "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,2014)+" \t current "+atomicInteger.get());
}
}
分析:第一次調(diào)用,內(nèi)存中的值是5,通過(guò)對(duì)比相等更新為2019,輸出 true current 2019,第二次調(diào)用時(shí),內(nèi)存重點(diǎn)的值已經(jīng)更新為2019,不相等不更新內(nèi)存中的值,輸出 false current 2019。
1)CAS存在一個(gè)很明顯的問(wèn)題,即ABA問(wèn)題。
如果變量V初次讀取的時(shí)候是A,并且在準(zhǔn)備賦值的時(shí)候檢查到它仍然是A,那能說(shuō)明它的值沒(méi)有被其他線程修改過(guò)了嗎?如果在這段期間曾經(jīng)被改成B,然后又改回A,那CAS操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有被修改過(guò)。針對(duì)這種情況,java并發(fā)包中提供了一個(gè)帶有標(biāo)記的原子引用類AtomicStampedReference,它可以通過(guò)控制變量值的版本來(lái)保證CAS的正確性。
2)只能保證一個(gè)共享變量的原子性。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú) 法保證操作的原子性,這個(gè)時(shí)候就可以使用鎖來(lái)保證原子性。