并發(fā)概述
學(xué)過單片機(jī),微機(jī)原理的同學(xué)都知道。在單片機(jī)構(gòu)成的最小系統(tǒng)中辟拷,程序是按照從上往下執(zhí)行的,任何時(shí)間點(diǎn)环凿,任何時(shí)間點(diǎn)執(zhí)行單元中只會(huì)存在一條指令,這樣帶來最直接的問題就是資源利用率低下放吩。例如程序因需要等待IO輸入而進(jìn)行循環(huán)等待智听,此時(shí)CPU本應(yīng)空閑下來卻因?yàn)樾枰却脩糨斎攵鵁o(wú)法真正去執(zhí)行其他程序邏輯塊,造成CPU資源浪費(fèi)渡紫。為此CPU提供了中斷機(jī)制(此中斷非Java的中斷)來使得CPU可以在等待IO輸入時(shí)可以執(zhí)行其他指令到推,而在IO輸入時(shí)利用中斷機(jī)制打斷現(xiàn)有的程序邏輯進(jìn)入中斷,執(zhí)行完中斷程序后再跳回原先的程序邏輯惕澎,從而提高了CPU的資源利用率莉测。這是相對(duì)底層的做法,在操作系統(tǒng)層面唧喉,一般通過粗粒的的時(shí)間分片捣卤,控制CPU在不同的時(shí)間段(很小)內(nèi)執(zhí)行不同的程序邏輯八孝,以達(dá)到在用戶無(wú)法感知的情況下“同時(shí)”運(yùn)行多個(gè)程序董朝,這就是我們說的多進(jìn)程/多線程。雖然這樣提高了系統(tǒng)資源的利用率干跛,但若在進(jìn)程或線程間存在數(shù)據(jù)交換子姜,就會(huì)隨之帶來讓人頭疼的并發(fā)問題。
從一只Java程序猿觸發(fā)楼入,本文將介紹著重介紹Java中的線程安全概念哥捕。
1.Java中并發(fā)為什么會(huì)帶來安全性問題
Java采用多線程的形式為程序提供并發(fā)執(zhí)行方案。在同一時(shí)間可能存在多條線程同時(shí)訪問同一個(gè)變量并對(duì)其進(jìn)行值修改嘉熊。我們知道Java在編譯后是以字節(jié)碼的形式供JVM執(zhí)行遥赚,而一般情況下在對(duì)變量進(jìn)行操作時(shí)會(huì)經(jīng)歷入棧,運(yùn)算阐肤,出棧的操作鸽捻。此時(shí)如果有兩個(gè)線程同時(shí)進(jìn)行入棧,再運(yùn)算之后將結(jié)果出棧推回變量泽腮,就會(huì)造成計(jì)算結(jié)果無(wú)法預(yù)估御蒲,因?yàn)榻Y(jié)果取決于執(zhí)行較慢的那個(gè)線程。超減問題便是在高并發(fā)情況下的安全問題之一诊赊。
public class IdentifyGenerator{
private int v;
public int next(){
//這里不使用return ++v或 return v++主要是怕難以理解,注意兩者的區(qū)別
v = v + 1;
return v;
}
}
上述代碼在多線程情境下厚满,會(huì)存在線程安全問題,原因在于v=v+1并不是一步操作碧磅。而是先讀取v的值碘箍,再+1遵馆,再寫回v中。使用命令javap -c 查看如下:
public class IdentifyGenerator{
public IdentifyGenerator();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int get();
Code:
0: aload_0
1: aload_0
2: getfield #2 // 讀取變量v的值入棧
5: iconst_1 // 常量1
6: iadd // 棧頂加法運(yùn)算
7: putfield #2 // 寫回變量v
10: aload_0
11: getfield #2 // Field v:I
14: ireturn
}
如上圖丰榴,在線程交替執(zhí)行的情況下货邓,我們的唯一標(biāo)示生成器可能返回兩次1,這種情況下四濒,就認(rèn)為我們寫的這個(gè)類不是線程安全的换况。
2.什么是線程安全
當(dāng)多個(gè)線程同時(shí)訪問某個(gè)資源(類)時(shí),不管JVM對(duì)線程采用何種調(diào)度方式和交替執(zhí)行順序盗蟆,并且在主調(diào)代碼中不需要額外的同步或協(xié)同戈二,這個(gè)類都能表現(xiàn)出正確的行為,那么就說該類是線程安全的喳资。
3.如何實(shí)現(xiàn)線程安全
3.1原子性
在1節(jié)中我們知道造成線程安全問題觉吭,是由線程交替執(zhí)行導(dǎo)致我們的關(guān)鍵性操作被拆分導(dǎo)致的。所以如果若我們能將關(guān)鍵性操作打包成一個(gè)操作仆邓,那么就可以規(guī)避該問題了鲜滩。我們稱這種打包后的,不可拆分的操作為原子操作节值。
1節(jié)中這種由于線程交替執(zhí)行而出現(xiàn)不正確結(jié)果绒北,我們稱之為競(jìng)態(tài)條件。常見的競(jìng)態(tài)條件類型有:先檢查后操作察署,延遲初始化(其實(shí)和先檢查后操作差不多闷游。例子:懶漢單例模式)。
3.2可見性
可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí)贴汪,一個(gè)線程修改了這個(gè)變量的值脐往,其他線程能夠立即看得到修改的值。
Java線程在讀取變量時(shí)扳埂,會(huì)先從主存將變量讀入到CPU高速緩存中业簿,線程在對(duì)變量進(jìn)行操作時(shí),都是先在高速緩存上的這個(gè)副本進(jìn)行操作后阳懂,再寫回主存梅尤,從而導(dǎo)致了可見性問題。Java采用volatile關(guān)鍵字來保證可見性岩调。當(dāng)一個(gè)共享變量被volatile修飾時(shí)巷燥,它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí)号枕,它會(huì)去內(nèi)存中讀取新值缰揪。
4.Java中的加鎖機(jī)制
4.1Java同步代碼塊
Java提供了同步代碼塊的加鎖機(jī)制來實(shí)現(xiàn)原子操作,同步代碼塊包括兩個(gè)部分葱淳,一個(gè)是作為鎖的對(duì)象引用钝腺,一個(gè)是被該鎖保護(hù)的代碼塊抛姑。Java同步鎖相當(dāng)于一種互斥體,同一時(shí)間只能有一個(gè)線程可以持有同一對(duì)象的鎖艳狐,未持有該對(duì)象鎖的線程會(huì)被阻塞而不能進(jìn)入代碼塊定硝。
值得注意的是,即使我們采用了同步代碼塊毫目,我們的類或方法并不一定是線程安全的蔬啡。要保證線程安全,我們需要在同步代碼塊(即單個(gè)原子操作)中更新所有相關(guān)的狀態(tài)變量蒜茴。
4.2可重入
Java同步鎖是一種可重入鎖星爪,可重入意味著獲取了鎖的線程在試圖重新獲取該鎖時(shí)浆西,是成功的粉私。即鎖的操作粒度是線程而不是代碼塊。重入鎖的一種實(shí)現(xiàn)方式是近零,給每個(gè)鎖增加一個(gè)獲取計(jì)數(shù)值和一個(gè)所有線程诺核,當(dāng)有線程進(jìn)入成功獲取鎖進(jìn)入同步代碼時(shí),便是計(jì)數(shù)器加1久信,退出代碼塊則減1.當(dāng)計(jì)數(shù)器為0時(shí)認(rèn)為沒有線程獲取到該鎖窖杀。
4.3活躍性問題和性能問題
同步鎖保證了Java在多線程環(huán)境下的安全性,但同時(shí)也降低了程序的并發(fā)能力裙士。倘若濫用同步鎖入客,由于同步代碼塊在同一時(shí)間只能有一個(gè)線程在執(zhí)行,那么極端情況下可能造成即使采用了多線程腿椎,實(shí)際上JVM中只存在一個(gè)線程在執(zhí)行桌硫。
public class Server extends Thread{
...
public synchronized void doSomething(){
...
}
public void run(){
doSomething();
}
public static void main(String[] args){
new Server().start();
new Server().start();
new Server().start();
}
}