并行程序基礎(chǔ)
本文為《實(shí)戰(zhàn)java高并發(fā)程序設(shè)計(jì)》電子筆記罕袋,供個(gè)人查閱及裝逼浴讯,不具有參考性榆纽。
https://legacy.gitbook.com/book/jiapengcai/effective-java/details
1.2 幾個(gè)重要的概念
- 并發(fā)偏重于多個(gè)任務(wù)交替執(zhí)行,而多個(gè)任務(wù)之間有可能還是串行的饥侵。而并行是真正意義上的“同時(shí)執(zhí)行
- 臨界區(qū)
- 阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞:一個(gè)線程占用了臨界區(qū)的資源躏升,其他所有需要這個(gè)資源的線程就必須在這個(gè)臨界區(qū)中進(jìn)行等待狼忱。等待會(huì)導(dǎo)致線程掛起钻弄,這種情況就是阻塞窘俺。
非阻塞:沒有一個(gè)線程可以妨礙其他線程執(zhí)行批销。所有的線程都會(huì)嘗試不斷前向執(zhí)行 - 死鎖(Deadlock)染坯、饑餓(Starvation)和活鎖(Livelock)
1.3 并發(fā)級(jí)別
由于臨界區(qū)的存在单鹿,多線程之間的并發(fā)必須受到控制。根據(jù)控制并發(fā)的策略,我們可以把并發(fā)的級(jí)別進(jìn)行分類劲妙,大致上可以分為阻塞湃鹊、無饑餓、無障礙镣奋、無鎖币呵、無等待幾種。
- 阻塞(Blocking)
在其他線程釋放資源之前侨颈,當(dāng)前線程無法繼續(xù)執(zhí)行余赢。當(dāng)我們使用synchronized關(guān)鍵字,或者重入鎖時(shí)妻柒,我們得到的就是阻塞的線程。
無論是synchronized或者重入鎖耘分,都會(huì)試圖在執(zhí)行后續(xù)代碼前举塔,得到臨界區(qū)的鎖,如果得不到求泰,線程就會(huì)被掛起等待央渣,直到占有了所需資源為止。
- 無饑餓(Starvation-Free)
不同線程的優(yōu)先級(jí)相等 - 無障礙(Obstruction-Free)
一種最弱的非阻塞調(diào)度拜秧。兩個(gè)線程無障礙運(yùn)行痹屹,他們不會(huì)因?yàn)榕R界區(qū)的問題導(dǎo)致一方掛起。但數(shù)據(jù)出現(xiàn)異常枉氮,則立即回滾自己的修改志衍,確保數(shù)據(jù)正確。 - 無鎖(Lock-Free)
無鎖的并行都是無障礙的聊替。在無鎖的情況下楼肪,所有的線程都能嘗試對(duì)臨界區(qū)進(jìn)行訪問,但不同的是惹悄,無鎖的并發(fā)保證必然有一個(gè)線程能夠在有限步內(nèi)完成操作離開臨界區(qū)春叫。 - 無等待(Wait-Free)
無鎖只要求有一個(gè)線程可以在有限步內(nèi)完成操作,而無等待則在無鎖的基礎(chǔ)上更進(jìn)一步進(jìn)行擴(kuò)展泣港。它要求所有的線程都必須在有限步內(nèi)完成暂殖,這樣就不會(huì)引起饑餓問題。
一種典型的無等待結(jié)構(gòu)就是RCU(Read-Copy-Update)当纱。它的基本思想是呛每,對(duì)數(shù)據(jù)的讀可以不加控制。因此坡氯,所有的讀線程都是無等待的晨横,它們既不會(huì)被鎖定等待也不會(huì)引起任何沖突洋腮。但在寫數(shù)據(jù)的時(shí)候,先取得原始數(shù)據(jù)的副本手形,接著只修改副本數(shù)據(jù)(這就是為什么讀可以不加控制)啥供,修改完成后,在合適的時(shí)機(jī)回寫數(shù)據(jù)库糠。
1.4關(guān)于并行的兩個(gè)重要定律
1.5 JMM
我們需要在深入了解并行機(jī)制的前提下伙狐,再定義一種規(guī)則,保證多個(gè)線程間可以有效地瞬欧、正確地協(xié)同工作鳞骤。而JMM也就是為此而生的。
JMM的關(guān)鍵技術(shù)點(diǎn)都是圍繞著多線程的原子性黍判、可見性和有序性來建立的
- 原子性(Atomicity)
原子性是指一個(gè)操作是不可中斷的豫尽。即使是在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開始顷帖,就不會(huì)被其他線程干擾美旧。 - 可見性(Visibility)
出現(xiàn)一個(gè)線程的修改不會(huì)立即被其他線程察覺的情況
2.2 線程的基本操作
1.新建線程
通過繼承Thread類或?qū)崿F(xiàn)Runnable接口
start()方法就新建一個(gè)線程并讓這個(gè)線程執(zhí)行run()方法
2.停止線程
一般無需手動(dòng)關(guān)閉線程
Thread.stop()方法在結(jié)束線程時(shí),會(huì)直接終止線程贬墩,并且會(huì)立即釋放這個(gè)線程所持有的鎖榴嗅。而這些鎖恰恰是用來維持對(duì)象一致性的。故stop方法不使用
3.線程中斷
public void Thread.interrupt() // 中斷線程
public boolean Thread.isInterrupted() // 判斷是否被中斷
public static boolean Thread.interrupted() // 判斷是否被中斷陶舞,并清除當(dāng)前中斷狀態(tài)
- Thead.sleep()方法:讓當(dāng)前線程休眠若干時(shí)間
Thread.sleep()方法會(huì)讓當(dāng)前線程休眠若干時(shí)間嗽测,它會(huì)拋出一個(gè)InterruptedException中斷異常。InterruptedException不是運(yùn)行時(shí)異常肿孵,也就是說程序必須捕獲并且處理它唠粥,當(dāng)線程在sleep()休眠時(shí),如果被中斷停做,這個(gè)異常就會(huì)產(chǎn)生
01 public static void main(String[] args) throws InterruptedException {
02 Thread t1=new Thread(){
03 @Override
04 public void run(){
05 while(true){
06 if(Thread.currentThread().isInterrupted()){
07 System.out.println("Interruted!");
08 break;
09 }
10 try {
11 Thread.sleep(2000);
12 } catch (InterruptedException e) {
13 System.out.println("Interruted When Sleep");
14 //設(shè)置中斷狀態(tài)
15 Thread.currentThread().interrupt();
16 }
17 Thread.yield();
18 }
19 }
20 };
21 t1.start();
22 Thread.sleep(2000);
23 t1.interrupt();
24 }
Thread.sleep()方法由于中斷而拋出異常晤愧,此時(shí),它會(huì)清除中斷標(biāo)記蛉腌,如果不加處理官份,那么在下一次循環(huán)開始時(shí),就無法捕獲這個(gè)中斷烙丛,故在異常處理中舅巷,再次設(shè)置中斷標(biāo)記位。
4.等待(wait)和通知(notify)
這兩個(gè)方法并不是在Thread類中的河咽,而是輸出Object類钠右。這也意味著任何對(duì)象都可以調(diào)用這兩個(gè)方法。
線程A中库北,調(diào)用了obj.wait()方法爬舰,那么線程A就會(huì)停止繼續(xù)執(zhí)行,而轉(zhuǎn)為等待狀態(tài)寒瓦。線程A會(huì)一直等到其他線程調(diào)用了obj.notify()方法為止情屹。這時(shí),obj對(duì)象就儼然成為多個(gè)線程之間的有效通信手段杂腰。
注意:
必須在包含synchronzied的語(yǔ)句中才可以調(diào)用 wait方法
6.等待線程結(jié)束(join)和謙讓(yield)
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一個(gè)join()方法表示無限等待垃你,它會(huì)一直阻塞當(dāng)前線程,直到目標(biāo)線程執(zhí)行完畢喂很。第二個(gè)方法給出了一個(gè)最大等待時(shí)間惜颇,超過最大時(shí)間線程就繼續(xù)執(zhí)行。
public volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at=new AddThread();
at.start();
at.join();
//主線程等待子線程執(zhí)行后再執(zhí)行
System.out.println(i);
}
join()的本質(zhì)是讓調(diào)用線程wait()在當(dāng)前線程對(duì)象實(shí)例
public static native void yield();
//讓出當(dāng)前占用cpu少辣,但接下來會(huì)繼續(xù)參與搶奪
2.3volatile與Java內(nèi)存模型(JMM)
當(dāng)你用volatile去申明一個(gè)變量時(shí)凌摄,就等于告訴了虛擬機(jī),這個(gè)變量極有可能會(huì)被某些程序或者線程修改漓帅。為了確保這個(gè)變量被修改后锨亏,應(yīng)用程序范圍內(nèi)的所有線程都能夠“看到”這個(gè)改動(dòng),虛擬機(jī)就必須采用一些特殊的手段忙干,保證這個(gè)變量的可見性等特點(diǎn)器予。
- volatile并不能代替鎖,它也無法保證一些復(fù)合操作的原子性捐迫。比如下面的例子乾翔,通過volatile是無法保證i++的原子性操作的:
01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03 @Override
04 public void run() {
05 for(int k=0;k<10000;k++)
06 i++;
07 }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11 Thread[] threads=new Thread[10];
12 for(int i=0;i<10;i++){
13 threads[i]=new Thread(new PlusTask());
14 threads[i].start();
15 }
16 for(int i=0;i<10;i++){
17 threads[i].join();
18 }
19
20 System.out.println(i);
21 }
執(zhí)行上述代碼,如果第6行i++是原子性的施戴,那么最終的值應(yīng)該是100000(10個(gè)線程各累加10000次)反浓。但實(shí)際上,上述代碼的輸出總是會(huì)小于100000赞哗。
- volatile能保證數(shù)據(jù)的可見性和有序性
2.6 線程優(yōu)先級(jí)
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
2.7 線程安全的概念與synchronized
volatile并不能真正的保證線程安全勾习。它只能確保一個(gè)線程修改了數(shù)據(jù)后,其他線程能夠看到這個(gè)改動(dòng)懈玻。但當(dāng)兩個(gè)線程同時(shí)修改某一個(gè)數(shù)據(jù)時(shí)巧婶,卻依然會(huì)產(chǎn)生沖突
01 public class AccountingVol implements Runnable{
02 static AccountingVol instance=new AccountingVol();
03 static volatile int i=0;
04 public static void increase(){
05 i++;
06 }
07 @Override
08 public void run() {
09 for(int j=0;j<10000000;j++){
10 increase();
11 }
12 }
13 public static void main(String[] args) throws InterruptedException {
14 Thread t1=new Thread(instance);
15 Thread t2=new Thread(instance);
16 t1.start();t2.start();
17 t1.join();t2.join();
18 System.out.println(i);
19 }
20 }
很多時(shí)候,i的最終值會(huì)小于20000000涂乌。這就是因?yàn)閮蓚€(gè)線程同時(shí)對(duì)i進(jìn)行寫入時(shí)艺栈,其中一個(gè)線程的結(jié)果會(huì)覆蓋另外一個(gè)(雖然這個(gè)時(shí)候i被聲明為volatile變量)。線程1和線程2同時(shí)讀取i為0湾盒,并各自計(jì)算得到i=1湿右,并先后寫入這個(gè)結(jié)果,因此罚勾,雖然i++被執(zhí)行了2次毅人,但是實(shí)際i的值只增加了1吭狡。
要從根本上解決這個(gè)問題,我們就必須保證多個(gè)線程在對(duì)i進(jìn)行操作時(shí)完全同步丈莺,使用關(guān)鍵字synchronized來實(shí)現(xiàn)這個(gè)功能
關(guān)鍵字synchronized的作用是實(shí)現(xiàn)線程間的同步划煮。它的工作是對(duì)同步的代碼加鎖,使得每一次缔俄,只能有一個(gè)線程進(jìn)入同步塊弛秋,從而保證線程間的安全性
關(guān)鍵字synchronized可以有多種用法。這里做一個(gè)簡(jiǎn)單的整理俐载。
- 指定加鎖對(duì)象:對(duì)給定對(duì)象加鎖蟹略,進(jìn)入同步代碼前要獲得給定對(duì)象的鎖。
- 直接作用于實(shí)例方法:相當(dāng)于對(duì)當(dāng)前實(shí)例加鎖遏佣,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖挖炬。
- 直接作用于靜態(tài)方法:相當(dāng)于對(duì)當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類的鎖状婶。
01 public class AccountingSync2 implements Runnable{
02 static AccountingSync2 instance=new AccountingSync2();
03 static int i=0;
04 public synchronized void increase(){
05 i++;
06 }
07 @Override
08 public void run() {
09 for(int j=0;j<10000000;j++){
10 increase();
11 }
12 }
13 public static void main(String[] args) throws InterruptedException {
14 Thread t1=new Thread(instance);
15 Thread t2=new Thread(instance);
16 t1.start();t2.start();
17 t1.join();t2.join();
18 System.out.println(i);
19 }
20 }
這兩個(gè)線程都指向同一個(gè)Runnable接口實(shí)例(instance對(duì)象)茅茂,這樣才能保證兩個(gè)線程在工作時(shí),能夠關(guān)注到同一個(gè)對(duì)象鎖上去太抓,從而保證線程安全空闲。
一個(gè)錯(cuò)誤的示例
01 public class AccountingSyncBad implements Runnable{
02 static int i=0;
03 public synchronized void increase(){
04 i++;
05 }
06 @Override
07 public void run() {
08 for(int j=0;j<10000000;j++){
09 increase();
10 }
11 }
12 public static void main(String[] args) throws InterruptedException {
13 Thread t1=new Thread(new AccountingSyncBad());
14 Thread t2=new Thread(new AccountingSyncBad());
15 t1.start();t2.start();
16 t1.join();t2.join();
17 System.out.println(i);
18 }
19 }
但我們只要簡(jiǎn)單地修改上述代碼,就能使其正確執(zhí)行走敌。那就是使用synchronized的第三種用法,將其作用于靜態(tài)方法掉丽。將increase()方法修改如下:
這樣跌榔,即使兩個(gè)線程指向不同的Runnable對(duì)象,但由于方法塊需要請(qǐng)求的是當(dāng)前類的鎖,而非當(dāng)前實(shí)例锭部,因此,線程間還是可以正確同步匪傍。
public static synchronized void increase(){
i++;
}
被synchronized限制的多個(gè)線程是串行執(zhí)行的
2.8 程序中的幽靈:隱蔽的錯(cuò)誤
- 并發(fā)下的ArrayList
注意:改進(jìn)的方法很簡(jiǎn)單,使用線程安全的Vector代替ArrayList即可帽撑。
- 并發(fā)下詭異的HashMap
最簡(jiǎn)單的解決方案就是使用ConcurrentHashMap代替HashMap。
- 錯(cuò)誤的加鎖
比如加在不可變的int上