一 并發(fā)編程的挑戰(zhàn)
1蒲赂、上下文切換
- 即使是單核處理器也支持多線程執(zhí)行代碼水醋,CPU通過給每個(gè)線程分配CPU時(shí)間片來實(shí)現(xiàn)
這個(gè)機(jī)制葛碧。時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間片非常短粪狼,所以CPU通過不停地切
換線程執(zhí)行,讓我們感覺多個(gè)線程是同時(shí)執(zhí)行的任岸,時(shí)間片一般是幾十毫秒(ms)再榄。 - CPU通過時(shí)間片分配算法來循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個(gè)時(shí)間片后會(huì)切換到下一個(gè)
任務(wù)享潜。但是困鸥,在切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài),以便下次切換回這個(gè)任務(wù)時(shí)剑按,可以再加載這
個(gè)任務(wù)的狀態(tài)窝革。所以任務(wù)從保存到再加載的過程就是一次上下文切換 - 這就像我們同時(shí)讀兩本書购城,當(dāng)我們?cè)谧x一本英文的技術(shù)書時(shí),發(fā)現(xiàn)某個(gè)單詞不認(rèn)識(shí)虐译,于是
便打開中英文字典瘪板,但是在放下英文技術(shù)書之前,大腦必須先記住這本書讀到了多少頁的第
多少行漆诽,等查完單詞之后侮攀,能夠繼續(xù)讀這本書。這樣的切換是會(huì)影響讀書效率的厢拭,同樣上下文
切換也會(huì)影響多線程的執(zhí)行速度兰英。
1.1如何減少上下文切換
減少上下文切換的方法有無鎖并發(fā)編程供鸠、CAS算法畦贸、使用最少線程和使用協(xié)程。
- 無鎖并發(fā)編程楞捂。多線程競(jìng)爭(zhēng)鎖時(shí)薄坏,會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí)寨闹,可以用一
些辦法來避免使用鎖胶坠,如將數(shù)據(jù)的ID按照Hash算法取模分段,不同的線程處理不同段的數(shù)據(jù)繁堡。 - CAS算法沈善。Java的Atomic包使用CAS算法來更新數(shù)據(jù),而不需要加鎖椭蹄。
- 使用最少線程闻牡。避免創(chuàng)建不需要的線程,比如任務(wù)很少绳矩,但是創(chuàng)建了很多線程來處理澈侠,這
樣會(huì)造成大量線程都處于等待狀態(tài)。 - 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度埋酬,并在單線程里維持多個(gè)任務(wù)間的切換哨啃。
實(shí)戰(zhàn)
- 第一步:用jstack命令dump線程信息,看看pid為3117的進(jìn)程里的線程都在做什么写妥。
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17 - 第二步:統(tǒng)計(jì)所有線程分別處于什么狀態(tài)拳球,發(fā)現(xiàn)300多個(gè)線程處于WAITING(onobject-
monitor)狀態(tài)。
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
- 第三步:打開dump文件查看處于WAITING(onobjectmonitor)的線程在做什么珍特。發(fā)現(xiàn)這些線
程基本全是JBOSS的工作線程祝峻,在await。說明JBOSS線程池里線程接收到的任務(wù)太少,大量線
程都閑著莱找。
2 酬姆、 死鎖
避免死鎖的幾個(gè)常見方法。
- 避免一個(gè)線程同時(shí)獲取多個(gè)鎖
- ·避免一個(gè)線程在鎖內(nèi)同時(shí)占用多個(gè)資源奥溺,盡量保證每個(gè)鎖只占用一個(gè)資源辞色。
- ·嘗試使用定時(shí)鎖,使用lock.tryLock(timeout)來替代使用內(nèi)部鎖機(jī)制浮定。
- ·對(duì)于數(shù)據(jù)庫鎖相满,加鎖和解鎖必須在一個(gè)數(shù)據(jù)庫連接里,否則會(huì)出現(xiàn)解鎖失敗的情況桦卒。
3立美、資源限制的挑戰(zhàn)
什么是資源限制?
- 服務(wù)器的帶寬只有2Mb/s方灾,某個(gè)資源的下載速度是1Mb/s每秒建蹄,系統(tǒng)啟動(dòng)10個(gè)線程下載資
源,下載速度不會(huì)變成10Mb/s - 在進(jìn)行并發(fā)編程時(shí)裕偿,要考慮這些資源的限制洞慎。硬件資源限
制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU的處理速度击费。軟件資源限制有數(shù)據(jù)庫的連接
數(shù)和socket連接數(shù)等。
資源限制引發(fā)的問題
- 將代碼中串行執(zhí)行的部分變成并發(fā)執(zhí)行桦他,但是如果將某段串行的代碼并發(fā)執(zhí)行蔫巩,因?yàn)槭芟抻谫Y源,仍然在串行執(zhí)行快压,這時(shí)候程序不僅不
會(huì)加快執(zhí)行圆仔,反而會(huì)更慢,因?yàn)樵黾恿松舷挛那袚Q和資源調(diào)度的時(shí)間蔫劣。
如何解決資源限制的問題
-
硬件資源限制 坪郭,既然單機(jī)的資源有限制,那么就讓
程序在多機(jī)上運(yùn)行脉幢。 使用集群歪沃。不同的機(jī)器處理不同
的數(shù)據(jù)∠铀桑可以通過“數(shù)據(jù)ID%機(jī)器數(shù)”沪曙,計(jì)算得到一個(gè)機(jī)器編號(hào),然后由對(duì)應(yīng)編號(hào)的機(jī)器處理這
筆數(shù)據(jù)萎羔。 -
軟件資源限制液走,可以考慮使用資源池將資源復(fù)用。比如使用連接池將數(shù)據(jù)庫和Socket
連接復(fù)用,或者在調(diào)用對(duì)方webservice接口獲取數(shù)據(jù)時(shí)缘眶,只建立一個(gè)連接嘱根。
二 、Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理
Java代碼在編譯后會(huì)變成Java字節(jié)碼巷懈,字節(jié)碼被類加載器加載到JVM里该抒,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行砸喻,Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令柔逼。
volatile的應(yīng)用
- volatile的定義與實(shí)現(xiàn)原理
- 定義: Java語言規(guī)范第3版中對(duì)volatile的定義如下:Java編程語言允許線程訪問共享變量,為了
確保共享變量能被準(zhǔn)確和一致地更新割岛,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量愉适。Java語言
提供了volatile,在某些情況下比鎖要更加方便癣漆。如果一個(gè)字段被聲明成volatile维咸,Java線程內(nèi)存
模型確保所有線程看到這個(gè)變量的值是一致的
- 定義: Java語言規(guī)范第3版中對(duì)volatile的定義如下:Java編程語言允許線程訪問共享變量,為了
-
CPU的術(shù)語定義
volatile是如何來保證可見性的呢? 看下反編譯之后的
Java代碼如下。
instance = new Singleton(); // instance是volatile變量
轉(zhuǎn)變成匯編代碼惠爽,如下癌蓖。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出第二行匯編代碼
,Lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事情
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存婚肆。
- 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效
- JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令租副,將這個(gè)變量所在緩存行的數(shù)據(jù)
寫回到系統(tǒng)內(nèi)存。但是较性,就算寫回到內(nèi)存用僧,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操
作就會(huì)有問題赞咙。所以责循,在多處理器下,為了保證各個(gè)處理器的緩存是一致的攀操,就會(huì)實(shí)現(xiàn)緩存一
致性協(xié)議院仿,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)
處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改速和,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀
態(tài)歹垫,當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存
里颠放。
volatile的兩條實(shí)現(xiàn)原則
- Lock前綴指令會(huì)引起處理器緩存回寫到內(nèi)存
- 一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無效(例如县钥,在Pentium和P6 family處理器中,如果通過嗅探一個(gè)處理
器來檢測(cè)其他處理器打算寫內(nèi)存地址慈迈,而這個(gè)地址當(dāng)前處于共享狀態(tài)若贮,那么正在嗅探的處理
器將使它的緩存行無效省有,在下次訪問相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充谴麦。)
synchronized的實(shí)現(xiàn)原理與應(yīng)用
Java中的每一個(gè)對(duì)象都可以作為鎖蠢沿。具體表現(xiàn)為以下3種形式。
- 對(duì)于普通同步方法匾效,鎖是當(dāng)前實(shí)例對(duì)象舷蟀。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象面哼。
- 對(duì)于同步方法塊野宜,鎖是Synchonized括號(hào)里配置的對(duì)象。
代碼塊同步是使用monitorenter
和monitorexit指令實(shí)現(xiàn)的
任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián)魔策,當(dāng)且一個(gè)monitor被持有后匈子,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí)闯袒,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán)虎敦,即嘗試獲得對(duì)象的鎖
鎖的升級(jí)與對(duì)比
在
Java SE 1.6中,鎖一共有4種狀態(tài)政敢,級(jí)別從低到高依次是:無鎖狀態(tài)其徙、偏向鎖狀態(tài)、輕量級(jí)鎖狀
態(tài)和重量級(jí)鎖狀態(tài)喷户,這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)唾那。鎖可以升級(jí)但不能降級(jí),意味著偏
向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖褪尝。這種鎖升級(jí)卻不能降級(jí)的策略闹获,目的是為了提高
獲得鎖和釋放鎖的效率
1.偏向鎖
- 大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng)恼五,而且總是由同
一線程多次獲得昌罩,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖哭懈。當(dāng)一個(gè)線程訪問同步塊并
獲取鎖時(shí)灾馒,會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出
同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖遣总,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否
存儲(chǔ)著指向當(dāng)前線程的偏向鎖睬罗。如果測(cè)試成功,表示線程已經(jīng)獲得了鎖 - 如果測(cè)試失敗旭斥,則需
要再測(cè)試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置容达,則
使用CAS競(jìng)爭(zhēng)鎖;如果設(shè)置了垂券,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程花盐。
偏向鎖在Java 6和Java 7里是默認(rèn)啟用的羡滑,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0算芯。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài)柒昏,可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)熙揍。
2.輕量級(jí)鎖
- 輕量級(jí)鎖加鎖
線程在執(zhí)行同步塊之前职祷,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并
將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中届囚,官方稱為Displaced Mark Word有梆。然后線程嘗試使用
CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針。如果成功意系,當(dāng)前線程獲得鎖泥耀,如果失
敗,表示其他線程競(jìng)爭(zhēng)鎖昔字,當(dāng)前線程便嘗試使用自旋來獲取鎖
- 輕量級(jí)鎖解鎖
輕量級(jí)解鎖時(shí)爆袍,會(huì)使用原子的CAS操作將Displaced Mark Word替換回到對(duì)象頭,如果成
功作郭,則表示沒有競(jìng)爭(zhēng)發(fā)生陨囊。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng)夹攒,鎖就會(huì)膨脹成重量級(jí)鎖蜘醋。
因?yàn)樽孕龝?huì)消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了)咏尝,一旦鎖升級(jí)成重量級(jí)鎖压语,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下编检,其他線程試圖獲取鎖時(shí)胎食,都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些線程允懂,被喚醒的線程就會(huì)進(jìn)行新一輪的奪鎖之爭(zhēng)厕怜。
原子操作的實(shí)現(xiàn)原理
java 實(shí)現(xiàn) 原子操作
-
(1)使用循環(huán)CAS實(shí)現(xiàn)原子操作
JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)的。自旋CAS實(shí)現(xiàn)的基本
思路就是循環(huán)進(jìn)行CAS操作直到成功為止蕾总,以下代碼實(shí)現(xiàn)了一個(gè)基于CAS線程安全的計(jì)數(shù)器
方法safeCount和一個(gè)非線程安全的計(jì)數(shù)器count粥航。
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有線程執(zhí)行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis() - start);
}
/**
* 使用CAS實(shí)現(xiàn)線程安全計(jì)數(shù)器
*/
private void safeCount() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非線程安全計(jì)數(shù)器
*/
private void count() {
i++;
}
}
-
(2)CAS實(shí)現(xiàn)原子操作的三大問題
1)ABA問題。
因?yàn)镃AS需要在操作值的時(shí)候生百,檢查值有沒有發(fā)生變化递雀,如果沒有發(fā)生變化
則更新,但是如果一個(gè)值原來是A蚀浆,變成了B缀程,又變成了A搜吧,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它
的值沒有發(fā)生變化,但是實(shí)際上卻變化了杨凑。ABA問題的解決思路就是使用版本號(hào)赎败。在變量前面
追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1蠢甲,那么A→B→A就會(huì)變成1A→2B→3A僵刮。從
Java 1.5開始,JDK的Atomic包里提供了一個(gè)類AtomicStampedReference來解決ABA問題鹦牛。這個(gè)
類的compareAndSet方法的作用是首先檢查當(dāng)前引用是否等于預(yù)期引用搞糕,并且檢查當(dāng)前標(biāo)志是
否等于預(yù)期標(biāo)志,如果全部相等曼追,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值窍仰。
public boolean compareAndSet(
V expectedReference, // 預(yù)期引用
V newReference, // 更新后的引用
int expectedStamp, // 預(yù)期標(biāo)志
int newStamp // 更新后的標(biāo)志
)
2)循環(huán)時(shí)間長(zhǎng)開銷大。
自旋CAS如果長(zhǎng)時(shí)間不成功礼殊,會(huì)給CPU帶來非常大的執(zhí)行開銷驹吮。如
果JVM能支持處理器提供的pause指令,那么效率會(huì)有一定的提升晶伦。pause指令有兩個(gè)作用:第
一碟狞,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會(huì)消耗過多的執(zhí)行資源婚陪,延遲的時(shí)間
取決于具體實(shí)現(xiàn)的版本族沃,在一些處理器上延遲時(shí)間是零;第二泌参,它可以避免在退出循環(huán)的時(shí)候
因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush)脆淹,從而
提高CPU的執(zhí)行效率。
3)只能保證一個(gè)共享變量的原子操作
當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí)沽一,我們可以使用循
環(huán)CAS的方式來保證原子操作盖溺,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無法保證操作的原子
性铣缠,這個(gè)時(shí)候就可以用鎖烘嘱。還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來
操作攘残。比如拙友,有兩個(gè)共享變量i=2为狸,j=a歼郭,合并一下ij=2a,然后用CAS來操作ij辐棒。從Java 1.5開始病曾,
JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性牍蜂,就可以把多個(gè)變量放在一個(gè)對(duì)
象里來進(jìn)行CAS操作。
-
(3)使用鎖機(jī)制實(shí)現(xiàn)原子操作
鎖機(jī)制保證了只有獲得鎖的線程才能夠操作鎖定的內(nèi)存區(qū)域泰涂。JVM內(nèi)部實(shí)現(xiàn)了很多種鎖
機(jī)制鲫竞,有偏向鎖、輕量級(jí)鎖和互斥鎖逼蒙。有意思的是除了偏向鎖从绘,JVM實(shí)現(xiàn)鎖的方式都用了循環(huán)
CAS,即當(dāng)一個(gè)線程想進(jìn)入同步塊的時(shí)候使用循環(huán)CAS的方式來獲取鎖是牢,當(dāng)它退出同步塊的時(shí)
候使用循環(huán)CAS釋放鎖僵井。