從計(jì)算機(jī)底層來說: 線程可以比作是輕量級(jí)的進(jìn)程,是程序執(zhí)行的最小單位,線程間的切換和調(diào)度的成本遠(yuǎn)遠(yuǎn)小于進(jìn)程。另外,多核 CPU 時(shí)代意味著多個(gè)線程可以同時(shí)運(yùn)行,這減少了線程上下文切換的開銷。
從當(dāng)代互聯(lián)網(wǎng)發(fā)展趨勢(shì)來說: 現(xiàn)在的系統(tǒng)動(dòng)不動(dòng)就要求百萬級(jí)甚至千萬級(jí)的并發(fā)量,而多線程并發(fā)編程正是開發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線程機(jī)制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。
總結(jié):并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度,充分的利用多核CPU資源。
原子性(Atomicity):在一次或多次操作中,要么所有的操作都執(zhí)行并且不會(huì)受其他因素干擾而中斷,要么所有的操作都不執(zhí)行
可見性:一個(gè)線程對(duì)共享變量的修改,其他線程能夠立刻看到。(synchronized,volatile)
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。(指令重排:處理器為了提高程序運(yùn)行效率,處理器根據(jù)指令之間的數(shù)據(jù)依賴性,可能會(huì)對(duì)指令進(jìn)行重排序,單線程下可以保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的,但是多線程下有可能出現(xiàn)問題)。
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
舉個(gè)簡單的例子,看下面這段代碼:
//線程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了,卻沒有立即寫入到主存當(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沒有立即看到線程1修改的值。在多線程環(huán)境下,一個(gè)線程對(duì)共享變量的操作對(duì)其他線程是不可見的。這就是可見性問題。
對(duì)于可見性,Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對(duì)變量的修改刷新到主存當(dāng)中。因此可以保證可見性。
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
舉個(gè)簡單的例子,看下面這段代碼:
int i = 0; ? ? ? ? ? ? ?
boolean flag = false;
i = 1; ? ? ? ? ? ? ? ?//語句1 ?
flag = true; ? ? ? ? ?//語句2
上面代碼定義了一個(gè)int型變量,定義了一個(gè)boolean類型變量,然后分別對(duì)兩個(gè)變量進(jìn)行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會(huì)保證語句1一定會(huì)在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會(huì)發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會(huì)保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對(duì)最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。那么它靠什么保證的呢?進(jìn)行重排序時(shí)是會(huì)考慮指令之間的數(shù)據(jù)依賴性。雖然重排序不會(huì)影響單個(gè)線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個(gè)例子:
//線程1:
context = loadContext(); ? //語句1
inited = true; ? ? ? ? ? ? //語句2
//線程2:
while(!inited ){
? ? sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會(huì)被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會(huì)以為初始化工作已經(jīng)完成,那么就會(huì)跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時(shí)context并沒有被初始化,就會(huì)導(dǎo)致程序出錯(cuò)。
從上面可以看出,在Java內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,
但是重排序過程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續(xù)吃飯,這說明你支持并發(fā)。 (不一定是同時(shí)的)
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。
并發(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)容易帶來線程安全問題。如線程的可見性、有序性、原子性問題,會(huì)導(dǎo)致程序出現(xiàn)的結(jié)果與預(yù)期結(jié)果不一致。
3)多線程容易造成死鎖、活鎖、線程饑餓等問題。此類問題往往只能通過手動(dòng)停止線程、甚至是進(jìn)程才能解決,影響嚴(yán)重。
4)對(duì)編程人員的技術(shù)要求較高,編寫出正確的并發(fā)程序并不容易。
5)并發(fā)程序易出問題,且難調(diào)試和排查;問題常常詭異地出現(xiàn),又詭異地消失。
Java 5.0 提供了java.util.concurrent(簡稱JUC)包,在此包中增加了在并發(fā)編程中很常見的實(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)存的訪問邏輯都有一定的差異,而這種差異會(huì)導(dǎo)致同一套代碼在不同操作系統(tǒng)或者硬件下,得到了不同的結(jié)果,而JMM的存在就是為了解決這個(gè)問題,通過JMM的規(guī)范,保證Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問都能得到一致的效果。
計(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ī)制是沒有任何問題的,可是隨著CPU的技術(shù)的發(fā)展,CPU的執(zhí)行速度和內(nèi)存的讀取速度差距越來越大,導(dǎo)致CPU每次操作內(nèi)存都要耗費(fèi)很多等待時(shí)間。
為了解決這個(gè)問題,初代程序員大佬們想到了一個(gè)的辦法,就是在CPU和物理內(nèi)存上新增高速緩存,這樣程序在運(yùn)行過程中,會(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開始出現(xiàn)了多核的概念,每個(gè)核都有一套自己的緩存,并且隨著計(jì)算機(jī)能力不斷提升,還開始支持多線程,最終演變成多個(gè)線程訪問進(jìn)程中的某個(gè)共享內(nèi)存,且這多個(gè)線程分別在不同的核心上執(zhí)行,則每個(gè)核心都會(huì)在各自的 Cache 中保留一份共享內(nèi)存的緩沖,我們知道多核是可以并行的,這樣就會(huì)出現(xiàn)多個(gè)線程同時(shí)寫各自的緩存的情況,導(dǎo)致各自的 Cache 之間的數(shù)據(jù)可能不同。
總結(jié)下來就是:在多核 CPU 中,每個(gè)核的自己的緩存,關(guān)于同一個(gè)數(shù)據(jù)的緩存內(nèi)容可能不一致。
重排序指的是在執(zhí)行程序時(shí),為了提高性能,從源代碼到最終執(zhí)行指令的過程中,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排的一種手段。
下圖為從源代碼到最終指令示意圖
重排序的分為3種
1)編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語義(as-if-serial)的的前提下,可以重新安排語句的執(zhí)行順序。
2)指令級(jí)并行的重排序:現(xiàn)在處理器采用指令級(jí)并行技術(shù)(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序:由于處理器使用了存儲(chǔ)和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去亂序執(zhí)行。
1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
2.指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
4.這些重排序?qū)τ趩尉€程沒問題,但是多線程都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。
數(shù)據(jù)依賴性:編譯器和處理器在重排序時(shí),針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
遵守as-if-serial 語義:不管編譯器和處理器為了提高并行度怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能被改變。
區(qū)別:
as-if-serial定義:無論編譯器和處理器如何進(jìn)行重排序,單線程程序的執(zhí)行結(jié)果不會(huì)改變。
happens-before定義:一個(gè)操作happens-before另一個(gè)操作,表示第一個(gè)的操作結(jié)果對(duì)第二個(gè)操作可見,并且第一個(gè)操作的執(zhí)行順序也在第二個(gè)操作之前。但這并不意味著Java虛擬機(jī)必須按照這個(gè)順序來執(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)類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。
不可變對(duì)象保證了對(duì)象的內(nèi)存可見性,對(duì)不可變對(duì)象的讀取不需要進(jìn)行額外的同步手段,提升了代碼執(zhí)行效率。
1.保證變量寫操作的可見性;
2.保證變量前后代碼的執(zhí)行順序;
不能。volatile不能保證原子性,只能保證線程可見性,可見性表現(xiàn)在當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。
從實(shí)踐角度而言,volatile的一個(gè)重要作用就是和CAS結(jié)合,保證了原子性,詳細(xì)的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。
被volatile修飾的變量被修改時(shí),會(huì)將修改后的變量直接寫入主存中,并且將其他線程中該變量的緩存置為無效,從而讓其它線程對(duì)該變量的引用直接從主存中獲取數(shù)據(jù),這樣就保證了變量的可見性。
但是volatile修飾的變量在自增時(shí)由于該操作分為讀寫兩個(gè)步驟,所以當(dāng)一個(gè)線程的讀操作被阻塞時(shí),另一個(gè)線程同時(shí)也進(jìn)行了自增操作,此時(shí)由于第一個(gè)線程的寫操作沒有進(jìn)行所以主存中仍舊是之前的原數(shù)據(jù),所以當(dāng)兩個(gè)線程自增完成后,該變量可能只加了1。因而volatile是無法保證對(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ī)制打斷的操作,這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程上下文切換。原子操作可以是一個(gè)步驟,也可以是多個(gè)操作步驟,但是其順序不可以被打亂,也不可以被切割而只執(zhí)行其中的一部分,將整個(gè)操作視作一個(gè)整體是原子性的核心特征。
而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執(zhí)行添加、遞增、遞減等操作。比如之前多線程下的線程不安全的 i++ 問題,到了原子類這里,就可以用功能相同且線程安全的 getAndIncrement 方法來優(yōu)雅地解決。
原子類的作用和鎖有類似之處,是為了保證并發(fā)情況下線程安全。不過原子類相比于鎖,有一定的優(yōu)勢(shì):
粒度更細(xì):原子變量可以把競爭范圍縮小到變量級(jí)別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會(huì)比使用同步互斥鎖的效率更高,因?yàn)樵宇惖讓永昧?CAS 操作,不會(huì)阻塞線程。原子類的作用和鎖有類似之處,是為了保證并發(fā)情況下線程安全。不過原子類相比于鎖,有一定的優(yōu)勢(shì):
粒度更細(xì):原子變量可以把競爭范圍縮小到變量級(jí)別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會(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開頭的類,如AtomicBoolean、AtomicInteger、AtomicLong等都是用原子的方式來實(shí)現(xiàn)指定類型的值的更新,它的底層通過CAS原理解決并發(fā)情況下原子性的問題,在jdk中CAS是Unsafe類中的api來實(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ì)某條線程來說,永遠(yuǎn)是一個(gè)不會(huì)變的值A(chǔ),只要某次CAS操作失敗,永遠(yuǎn)都不可能成功。
以AtomicInteger為例,說明CAS的使用與原理。首先atomicIngeter初始化為5,調(diào)用對(duì)象的compareAndSet方法來對(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,通過對(duì)比相等更新為2019,輸出 true current 2019,第二次調(diào)用時(shí),內(nèi)存重點(diǎn)的值已經(jīng)更新為2019,不相等不更新內(nèi)存中的值,輸出 false current 2019。
1)CAS存在一個(gè)很明顯的問題,即ABA問題。
如果變量V初次讀取的時(shí)候是A,并且在準(zhǔn)備賦值的時(shí)候檢查到它仍然是A,那能說明它的值沒有被其他線程修改過了嗎?如果在這段期間曾經(jīng)被改成B,然后又改回A,那CAS操作就會(huì)誤認(rèn)為它從來沒有被修改過。針對(duì)這種情況,java并發(fā)包中提供了一個(gè)帶有標(biāo)記的原子引用類AtomicStampedReference,它可以通過控制變量值的版本來保證CAS的正確性。
2)只能保證一個(gè)共享變量的原子性。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無 法保證操作的原子性,這個(gè)時(shí)候就可以使用鎖來保證原子性。
多線程訪問共享資源的時(shí)候,避免不了資源競爭而導(dǎo)致數(shù)據(jù)錯(cuò)亂的問題,所以我們通常為了解決這一問題,都會(huì)在訪問共享資源之前加鎖。不同種類有不同的成本開銷,不同的鎖適用于不同的場景。
從資源已被鎖定,線程是否阻塞可以分為 自旋鎖(spinlock)和互斥鎖(mutexlock);
從線程是否需要對(duì)資源加鎖可以分為 悲觀鎖 和 樂觀鎖;
從多個(gè)線程并發(fā)訪問資源,也就是 Synchronized 可以分為 無鎖、偏向鎖、 輕量級(jí)鎖 和 重量級(jí)鎖;
從鎖的公平性進(jìn)行區(qū)分,可以分為公平鎖 和 非公平鎖;
從根據(jù)鎖是否重復(fù)獲取可以分為可重入鎖(自己獲得鎖以后,自己還可以進(jìn)入鎖之中) 和 不可重入鎖;
從那個(gè)多個(gè)線程能否獲取同一把鎖分為共享鎖和 排他鎖;
互斥鎖是在訪問共享資源之前對(duì)其進(jìn)行加鎖操作,在訪問完成之后進(jìn)行解鎖操作。加鎖后,任何其它試圖再次加鎖的線程都會(huì)被阻塞,直到當(dāng)前線程解鎖。在這種方式下,只有一個(gè)線程能夠訪問被互斥鎖保護(hù)的資源。如synchronized/Lock 這些方式都是互斥鎖,不同線程不能同時(shí)進(jìn)入 synchronized Lock 設(shè)定鎖的范圍
自旋鎖是一種特殊的互斥鎖,當(dāng)資源被加鎖后,其它線程想要再次加鎖,此時(shí)該線程不會(huì)被阻塞睡眠而是陷入循環(huán)等待狀態(tài)(CPU不能做其它事情),循環(huán)檢查資源持有者是否已經(jīng)釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗,但會(huì)一直占用CPU的資源。
區(qū)別:互斥鎖的起始開銷要高于自旋鎖,但是基本上是一勞永逸,臨界區(qū)持鎖時(shí)間的大小并不會(huì)對(duì)互斥鎖的開銷造成影響,而自旋鎖是死循環(huán)檢測(cè),加鎖全程消耗cpu,起始開銷雖然低于互斥鎖,但是隨著持鎖時(shí)間,加鎖的開銷是線性增長。
讀寫鎖將對(duì)一個(gè)資源(比如文件)的訪問分成了2個(gè)鎖,一個(gè)讀鎖和一個(gè)寫鎖。
ReadWriteLock就是讀寫鎖,它是一個(gè)接口,ReentrantReadWriteLock實(shí)現(xiàn)了這個(gè)接口。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
讀寫鎖也叫共享鎖。其共享是在讀數(shù)據(jù)的時(shí)候,可以讓多個(gè)線程同時(shí)進(jìn)行讀操作的。在寫的時(shí)候具有排他性,其他讀或者寫操作都要被阻塞。
1. 悲觀鎖
線程對(duì)一個(gè)共享變量進(jìn)行訪問,它就自動(dòng)加鎖,所以只能有一個(gè)線程訪問它
悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
缺點(diǎn):只有一個(gè)線程對(duì)它操作時(shí),沒有必要加鎖,造成了性能浪費(fèi)
2.樂觀鎖
線程訪問共享變量時(shí)不加鎖,當(dāng)執(zhí)行完后,同步值到內(nèi)存時(shí),使用舊值和內(nèi)存中的值進(jìn)行判斷,如果相同,那么寫入,如果不相同,重新使用新值執(zhí)行。樂觀鎖適合讀操作多的場景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。
缺點(diǎn):值相同的情況,可能被其他線程執(zhí)行過;操作變量頻繁時(shí),重新執(zhí)行次數(shù)多,造成性能浪費(fèi);完成比較后,寫入前,被其他線程修改了值,導(dǎo)致不同步問題
1)synchronized 同步語句塊的情況
public class SynchronizedDemo {
?? ?public void method() {
?? ??? ?synchronized (this) {
?? ??? ??? ?System.out.println("synchronized 代碼塊");
?? ??? ?}
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對(duì)應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class。
從上面我們可以看出:synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。當(dāng)執(zhí)行 monitorenter 指令時(shí),線程試圖獲取鎖也就是獲取 對(duì)象監(jiān)視器 monitor 的持有權(quán)。
在 Java 虛擬機(jī)(HotSpot)中,Monitor 是基于 C++實(shí)現(xiàn)的,由ObjectMonitor實(shí)現(xiàn)的。每個(gè)對(duì)象中都內(nèi)置了一個(gè) ObjectMonitor對(duì)象。另外,wait/notify等方法也依賴于monitor對(duì)象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會(huì)拋出java.lang.IllegalMonitorStateException的異常的原因。
在執(zhí)行monitorenter時(shí),會(huì)嘗試獲取對(duì)象的鎖,如果鎖的計(jì)數(shù)器為 0 則表示鎖可以被獲取,獲取后將鎖計(jì)數(shù)器設(shè)為 1 也就是加 1。
在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為 0,表明鎖被釋放。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止。
2) synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
?? ?public synchronized void method() {
?? ??? ?System.out.println("synchronized 方法");
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對(duì)應(yīng)目錄執(zhí)行 javac SynchronizedDemo2.java 命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo2.class。
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法。JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
public class Singleton {
// 這里為什么需要加上volatile 后面會(huì)講解
private volatile static Singleton uniqueInstance;
// 私有化構(gòu)造方法
private Singleton() {
}
// 提供getInstance方法
public static Singleton getInstance() {
//先判斷對(duì)象是否已經(jīng)實(shí)例過,沒有實(shí)例化過才進(jìn)入加鎖代碼
if (uniqueInstance == null) {
//類對(duì)象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
其中uniqueInstance 變量采用 volatile 關(guān)鍵字修飾,分析如下:
uniqueInstance = new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:
1.為 uniqueInstance 分配內(nèi)存空間
2.初始化 uniqueInstance
3.將 uniqueInstance 指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會(huì)出現(xiàn)問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例。例如,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時(shí) uniqueInstance 還未被初始化。
可重入原理即加鎖次數(shù)計(jì)數(shù)器。一個(gè)線程拿到鎖之后,可以繼續(xù)地持有鎖,如果想再次進(jìn)入由這把鎖控制的方法,那么它可以直接進(jìn)入。它的原理是利用加鎖次數(shù)計(jì)數(shù)器來實(shí)現(xiàn)的。
1.每重入一次,計(jì)數(shù)器+1
每個(gè)對(duì)象自動(dòng)含有一把鎖,JVM負(fù)責(zé)跟蹤對(duì)象被加鎖的次數(shù)。
線程第一次給對(duì)象加鎖的時(shí)候,計(jì)數(shù)器=0+1=1,每當(dāng)這個(gè)相同的線程在此對(duì)象上再次獲得鎖時(shí),計(jì)數(shù)器再+1。只有首先獲取這把鎖的線程,才能繼續(xù)在這個(gè)對(duì)象上多次地獲取這把鎖
2.計(jì)數(shù)器-1
每當(dāng)任務(wù)結(jié)束離開時(shí),計(jì)數(shù)遞減,當(dāng)計(jì)數(shù)器減為0,鎖被完全釋放。
利用這個(gè)計(jì)數(shù)器可以得知這把鎖是被當(dāng)前多次持有,還是如果=0的話就是完全釋放了。
不能。其它線程只能訪問該對(duì)象的非同步方法,同步方法則不能進(jìn)入。因?yàn)榉庆o態(tài)方法上的 synchronized 修飾符要求執(zhí)行方法時(shí)要獲得對(duì)象的鎖,如果已經(jīng)進(jìn)入A 方法說明對(duì)象鎖已經(jīng)被取走,那么試圖進(jìn)入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對(duì)象的鎖。
1)lock是一個(gè)接口,而synchronized是java的一個(gè)關(guān)鍵字。
2)synchronized在發(fā)生異常時(shí)會(huì)自動(dòng)釋放占有的鎖,因此不會(huì)出現(xiàn)死鎖;而lock發(fā)生異常時(shí),不會(huì)主動(dòng)釋放占有的鎖,必須手動(dòng)來釋放鎖,可能引起死鎖的發(fā)生。
synchronized是和if、else、for、while一樣的關(guān)鍵字,ReentrantLock是類,這是二者的本質(zhì)區(qū)別。既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴(kuò)展性體現(xiàn)在幾點(diǎn)上:
(1)ReentrantLock可以對(duì)獲取鎖的等待時(shí)間進(jìn)行設(shè)置,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的信息
(3)ReentrantLock可以靈活地實(shí)現(xiàn)多路通知
另外,二者的鎖機(jī)制其實(shí)也是不一樣的。ReentrantLock底層調(diào)用的是Unsafe的park方法加鎖,synchronized操作的應(yīng)該是對(duì)象頭中mark word,這點(diǎn)我不能確定。
1)synchronized保證內(nèi)存可見性和操作的原子性
2)volatile只能保證內(nèi)存可見性
3)volatile不需要加鎖,比Synchronized更輕量級(jí),并不會(huì)阻塞線程(volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。)
4)volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化,而synchronized標(biāo)記的變量可以被編譯器優(yōu)化(如編譯器重排序的優(yōu)化).
5)volatile是變量修飾符,僅能用于變量,而synchronized是一個(gè)方法或塊的修飾符。
volatile本質(zhì)是在告訴JVM當(dāng)前變量在寄存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實(shí)現(xiàn)可見性。而對(duì)n=n+1,n++等操作時(shí),volatile關(guān)鍵字將失效,不能起到像synchronized一樣的線程同步(原子性)的效果。
1. volatile 修飾變量
2. synchronized 修飾修改變量的方法
3. wait/notify
4. 輪詢
監(jiān)視器和鎖在Java虛擬機(jī)中是一塊使用的。監(jiān)視器監(jiān)視一塊同步代碼塊,確保一次只有一個(gè)線程執(zhí)行同步代碼塊。每一個(gè)監(jiān)視器都和一個(gè)對(duì)象引用相關(guān)聯(lián)。線程在獲取鎖之前不允許執(zhí)行同步代碼。
在 java 虛擬機(jī)中, 每個(gè)對(duì)象( Object 和 class )通過某種邏輯關(guān)聯(lián)監(jiān)視器,每個(gè)監(jiān)視器和一個(gè)對(duì)象引用相關(guān)聯(lián), 為了實(shí)現(xiàn)監(jiān)視器的互斥功能, 每個(gè)對(duì)象都關(guān)聯(lián)著一把鎖.一旦方法或者代碼塊被 synchronized 修飾, 那么這個(gè)部分就放入了監(jiān)視器的監(jiān)視區(qū)域, 確保一次只能有一個(gè)線程執(zhí)行該部分的代碼, 線程在獲取鎖之前不允許執(zhí)行該部分的代碼。另外 java 還提供了顯式監(jiān)視器( Lock )和隱式監(jiān)視器( synchronized )兩種鎖方案。
死鎖 : 指多個(gè)線程在運(yùn)行過程中因爭奪資源而造成的一種僵局。比如有一個(gè)線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時(shí)又有另外一個(gè)線程B,按照先鎖b再鎖a的順序獲得鎖。
死鎖發(fā)生的必要條件
(1) 互斥,同一時(shí)刻只能有一個(gè)線程訪問。
(2) 持有且等待,當(dāng)線程持有資源A時(shí),再去競爭資源B并不會(huì)釋放資源A。
(3) 不可搶占,線程T1占有資源A,其他線程不能強(qiáng)制搶占。
(4) 循環(huán)等待,線程T1占有資源A,再去搶占資源B如果沒有搶占到會(huì)一直等待下去。
想要破壞死鎖那么上訴條件只要不滿足一個(gè)即可,那么分析如下
(1) 互斥條件,不可破壞,如果破壞那么并發(fā)安全就不存在了。
(2) 持有且等待,可以破壞,可以一次性申請(qǐng)所有的資源。
(3) 不可搶占,當(dāng)線程T1持有資源A再次獲取資源B時(shí),發(fā)現(xiàn)資源B被占用那么主動(dòng)釋放資源A。
(4) 循環(huán)等待,可以將資源排序,可以按照排序順序的資源申請(qǐng),這樣就不會(huì)存在環(huán)形資源申請(qǐng)了。
活鎖:是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個(gè)線程都無法使用資源。
就類似馬路中間有條小橋,只能容納一輛車經(jīng)過,橋兩頭開來兩輛車A和B,A比較禮貌,示意B先過,B也比較禮貌,示意A先過,結(jié)果兩人一直謙讓誰也過不去。
饑餓:是指如果線程T1占用了資源R,線程T2又請(qǐng)求封鎖R,于是T2等待。T3也請(qǐng)求資源R,當(dāng)T1釋放了R上的封鎖后,系統(tǒng)首先批準(zhǔn)了T3的請(qǐng)求,T2仍然等待。然后T4又請(qǐng)求封鎖R,當(dāng)T3釋放了R上的封鎖之后,系統(tǒng)又批準(zhǔn)了T4的請(qǐng)求…,T2可能永遠(yuǎn)等待。
類似有兩條道A和B上都堵滿了車輛,其中A道堵的時(shí)間最長,B相對(duì)相對(duì)堵的時(shí)間較短,這時(shí),前面道路已疏通,交警按照最佳分配原則,示意B道上車輛先過,B道路上過了一輛又一輛,A道上排隊(duì)時(shí)間最長的確沒法通過,只能等B道上沒有車輛通過的時(shí)候再等交警發(fā)指令讓A道依次通過。
活鎖和死鎖類似,不同之處在于處于活鎖的線程或進(jìn)程的狀態(tài)是不斷改變的,活鎖可以認(rèn)為是一種特殊的饑餓。一個(gè)現(xiàn)實(shí)的活鎖例子是兩個(gè)人在狹小的走廊碰到,兩個(gè)人都試著避讓對(duì)方好讓彼此通過,但是因?yàn)楸茏尩姆较蚨家粯訉?dǎo)致最后誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區(qū)別是前者進(jìn)程的狀態(tài)可以改變但是卻不能繼續(xù)執(zhí)行。
饑餓與死鎖有一定聯(lián)系:二者都是由于競爭資源而引起的,但又有明顯差別,主要表現(xiàn)在如下幾個(gè)方面:
(1)從進(jìn)程狀態(tài)考慮,死鎖進(jìn)程都處于等待狀態(tài),忙式等待(處于運(yùn)行或就緒狀態(tài))的進(jìn)程并非處于等待狀態(tài),但卻可能被餓死;
(2)死鎖進(jìn)程等待永遠(yuǎn)不會(huì)被釋放的資源,餓死進(jìn)程等待會(huì)被釋放但卻不會(huì)分配給自己的資源,表現(xiàn)為等待時(shí)限沒有上界(排隊(duì)等待或忙式等待);
(3)死鎖一定發(fā)生了循環(huán)等待,而餓死則不然。這也表明通過資源分配圖可以檢測(cè)死鎖存在與否,但卻不能檢測(cè)是否有進(jìn)程餓死;
(4)死鎖一定涉及多個(gè)進(jìn)程,而饑餓或被餓死的進(jìn)程可能只有一個(gè)。饑餓和餓死與資源分配策略有關(guān),因而防止饑餓與餓死可從公平性考慮,確保所有進(jìn)程不被忽視,如FCFS分配算法。