Java-Review-Note——4.多線程
標(biāo)簽: JavaStudy
PS:本來是分開三篇的,后來想想還是整理成一篇了定嗓,多線程看得夠嗆蜕琴,只能說,很多東西還需要實(shí)踐...
Σ(⊙▽⊙"a
程序宵溅,進(jìn)程奸绷,線程及多線程的理解
- 程序:為了完成特定任務(wù),用某種語言編寫的一組指令集合(一組靜態(tài)代碼)
-
進(jìn)程:運(yùn)行中的程序层玲,系統(tǒng)調(diào)度與資源分配的一個獨(dú)立單位,操作系統(tǒng)會為每個進(jìn)程分配
一段內(nèi)存空間;程序的依次動態(tài)執(zhí)行辛块,經(jīng)歷代碼的加載畔派,執(zhí)行,執(zhí)行完畢的完整過程润绵。 -
線程:進(jìn)程的子集线椰,比進(jìn)程更小的執(zhí)行單位,每個進(jìn)程可能有多條線程尘盼,
線程需要放在一個進(jìn)行中才能執(zhí)行憨愉;線程由程序負(fù)責(zé)管理,而進(jìn)程則由系統(tǒng)進(jìn)行調(diào)度卿捎。 -
多線程的理解:并行執(zhí)行多條指令配紫,將CPU時間片按照調(diào)度算法分配給各個線程,
實(shí)際上是分時執(zhí)行午阵,只是切換的時間很短躺孝,用戶感覺到"同時"而已。
線程的生命周期
注意:可以調(diào)isAlive()判斷線程是否死亡底桂,如果對已死亡的線程調(diào)start()方法會拋出
IllegalThreadStateException異常植袍!
創(chuàng)建線程的三種方式
1.繼承Thread類創(chuàng)建
流程:繼承Thread類,重寫run()方法籽懦,實(shí)例化線程對象于个,調(diào)start()方法從而啟動run()方法。
示例代碼:
class MyThread extends Thread {
public MyThread() {
super("MyThread"); //標(biāo)識進(jìn)程名
System.out.println("新建了線程:" + getName()); //getName()方法可獲得線程名
}
public void run() {
for(int i = 1;i <= 3;i++) {
System.out.println(Thread.currentThread().getName() +" : " + i );
try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
//測試類
public class ThreadTest1 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
MyThread mt = new MyThread();
mt.start();
//這里演示的是線程運(yùn)行完猴才打印主線程中的語句暮顺,join()讓異步執(zhí)行的線程
//改成同步執(zhí)行厅篓,直到這個線程退出,程序才會繼續(xù)執(zhí)行
try { mt.join(); }catch(InterruptedException e){ e.printStackTrace(); }
System.out.println(t.getName() + " 打印完畢!");
}
}
運(yùn)行結(jié)果:
2.實(shí)現(xiàn)Runnable接口創(chuàng)建
流程:實(shí)現(xiàn)Runnable接口拖云,覆蓋run()方法贷笛,實(shí)例化自定義線程類對象,實(shí)例化Thread對象宙项,
將自定義線程對象作為參數(shù)傳給Thread對象乏苦,調(diào)用Thread對象的start()方法啟動run()方法。
示例代碼:
class MyThread implements Runnable {
public void run() {
for(int i = 1;i <= 3;i++) {
System.out.println(Thread.currentThread().getName() +" : " + i );
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread(),"MyThread");
thread.start();
System.out.println(Thread.currentThread().getName() + " 打印完畢尤筐!");
}
}
運(yùn)行結(jié)果:
3.實(shí)現(xiàn)Callable泛型接口創(chuàng)建
流程:實(shí)現(xiàn)Callable<T>泛型接口汇荐,重寫call()方法,然后使用Future或FutureTask來創(chuàng)建盆繁。
簡述:Callable和Future是jdk 1.5后引入的掀淘,引入目的是:解決兩種普通創(chuàng)建
Thread無法直接獲取執(zhí)行結(jié)果的缺陷;Callable接口中只聲明了一個call()的方法油昂,
返回類型就是Callable傳進(jìn)來的V類型革娄,一般情況下是配合ExecutorService來使用倾贰。
Future接口是對于具體的Runnable或Callable任務(wù)的執(zhí)行結(jié)果進(jìn)行取消(cancel),
查詢是否取消成功(isCancelled)拦惋,查詢是否完成(isDone)匆浙,獲取查詢結(jié)果(get)
該方法會阻塞直到任務(wù)返回結(jié)果,另外get還有一個方法get(long timeout, TimeUnit unit)厕妖,
如果在指定時間內(nèi)沒獲取到結(jié)果首尼,就會返回null,也就是說Future提供三種功能:
判斷任務(wù)是否完成言秸,能否中斷任務(wù)软能,能夠獲取任務(wù)的執(zhí)行結(jié)果。
因?yàn)镕uture是一個接口举畸,無法直接用來創(chuàng)建對象查排,因此有了FutureTask。
FutureTask俱恶,實(shí)現(xiàn)了RunnableFuture<V>接口雹嗦,而RunnableFuture<V>接口接口繼承了
Runnable和Future<V>接口,所以FutureTask既可以作為Runnable被線程執(zhí)行合是,又可以作為
Future得到Callable的返回值了罪。
使用示例:
實(shí)現(xiàn)Callable接口:
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子線程正在計(jì)算...");
int sum = 0;
for(int i = 0;i < 100;i++) { sum += i; }
return sum;
}
}
Future :
ExecutorService executor = Executors.newCachedThreadPool();
MyThread myThread = new MyThread();
Future<Integer> result = executor.submit(myThread);
executor.shutdown(); //如果不調(diào)shutdown方法,executor會一直等待運(yùn)行聪全,即使沒線程
try {
System.out.println("result運(yùn)行結(jié)果:" + result.get());
} catch (InterruptedException e) { e.printStackTrace();
} catch (ExecutionException e) { e.printStackTrace();
}
FutureTask
//第一種ExecutorService
ExecutorService executor = Executors.newCachedThreadPool();
MyThread thread = new MyThread();
FutureTask<Integer> result = new FutureTask<>(thread);
executor.submit(result);
executor.shutdown();
//第二種Thread
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println("result運(yùn)行結(jié)果:" + result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
運(yùn)行結(jié)果:
多線程集錦
1.線程執(zhí)行的順序
多個線程同時執(zhí)行又叫線程并發(fā)泊藕,如果當(dāng)前有多個線程在并發(fā)執(zhí)行的話,多次運(yùn)行
的結(jié)果可能是不唯一的难礼,Java對于線程啟動后唯一能保證的就是:
每個線程都被啟動并且結(jié)束娃圆,但對于哪個線程先執(zhí)行,何時執(zhí)行蛾茉,都沒保證讼呢。
2.線程的優(yōu)先級
操作系統(tǒng)中的線程是具有優(yōu)先級,Java運(yùn)行環(huán)境采用的是固定優(yōu)先級調(diào)度算法
(Fixed Priority Scheduling)某一時刻有多個線程在運(yùn)行谦炬,JVM會選擇優(yōu)先級最高
的線程運(yùn)行悦屏,優(yōu)先級較高的線程會搶占cpu時間,相同優(yōu)先級的線程可能順序執(zhí)行键思,
也可能分時執(zhí)行础爬,取決于本地操作系統(tǒng)的線程調(diào)度策略喧枷,雖然說在任意時刻牲蜀,應(yīng)該
是最高優(yōu)先級的線程在運(yùn)行,但是這樣是不能保證的甩鳄,調(diào)度程序有可能選擇優(yōu)先級
較低的線程以避免饑餓(starvation)赔桌。搶占(pre-empt)策略:
當(dāng)一個優(yōu)先級更高的進(jìn)程進(jìn)行可運(yùn)行狀態(tài)時發(fā)生搶占供炎,終止當(dāng)正在運(yùn)行的進(jìn)程而立即
去執(zhí)行優(yōu)先級更高的線程渴逻,而兩個相同優(yōu)先級的線程則采用循環(huán)執(zhí)行策略(round-robin);
3.Java中的線程優(yōu)先級
Java中線程的優(yōu)先級從0-10,整數(shù)碱茁,數(shù)值越大裸卫,優(yōu)先級越大,默認(rèn)線程優(yōu)先級為5纽竣,
可調(diào)用:setPriority()改變線程優(yōu)先級,或調(diào)用:getPriority()獲得線程的優(yōu)先級茧泪,
另外Java還提供了幾個線程優(yōu)先級的字段供使用:
MIN_PRIORITY(最低優(yōu)先級0)蜓氨;MAX_PRIORITY(最高優(yōu)先級10);NORM_PRIORITY(默認(rèn)優(yōu)先級5)
另外队伟,只能說優(yōu)先級高的線程更有可能獲得CPU穴吹,但也不能說優(yōu)先級較低的線程就
永遠(yuǎn)最后執(zhí)行,這不是絕對的嗜侮,可以理解成設(shè)置進(jìn)程優(yōu)先級只是給系統(tǒng)提供一個參考港令。
4.Java提供的進(jìn)程協(xié)作相關(guān)的方法
使用優(yōu)先級不能保證并發(fā)執(zhí)行的線程順序,但是Java也給我們提供了一些線程協(xié)作相關(guān)的方法:
Thread類中:
- run():線程執(zhí)行業(yè)務(wù)邏輯的地方锈颗,想讓線程執(zhí)行的代碼就寫在這里顷霹。
-
start():線程開始執(zhí)行,調(diào)用線程對象中的run()方法击吱,只能調(diào)用一次淋淀,多次啟動
同一個線程,會拋出IllegalThreadStateException異常覆醇。 -
sleep():讓當(dāng)前線程休眠(堵塞)一段時間朵纷,但不釋放持有資源(對象鎖),如果此時
的線程已經(jīng)被別的經(jīng)常中斷的話永脓,會拋出InterruptedException異常袍辞,另外調(diào)用該方法時
需要對異常進(jìn)行捕獲。 -
join():在一個線程中調(diào)用第二個線程的join方法常摧,會導(dǎo)致當(dāng)前線程堵塞搅吁,直到第二
個線程執(zhí)行完畢或者超過設(shè)置的等待毫秒數(shù)。 -
yield():暫停當(dāng)前線程排宰,把執(zhí)行機(jī)會讓給優(yōu)先級相同或更高的線程執(zhí)行似芝,不會進(jìn)入
堵塞狀態(tài),直接強(qiáng)制切換為就緒狀態(tài)板甘,因此可能剛切換完yield方法党瓮,線程又獲得了處理器
資源,然后又繼續(xù)執(zhí)行了盐类,該方法沒有聲明拋出任何異常寞奸。另外呛谜,如果沒有優(yōu)先級相同或者
更高的線程,yield()方法什么都不做枪萄。
Object類中:
疑問:為何這三個方法在Object類中隐岛?
答:每個對象都擁有一個monitor(鎖),讓當(dāng)前線程等待某個對象的鎖瓷翻,應(yīng)該通過這個對象操作聚凹,
而不是用當(dāng)前線程來操作,因?yàn)楫?dāng)前線程可能等待的事多個線程的鎖齐帚,用線程來操作妒牙,會非常復(fù)雜。
-
wait():讓當(dāng)前線程等待对妄,直到通過notify()和notifyAll()或等待時間結(jié)束湘今,當(dāng)前
線程進(jìn)入等待隊(duì)列時,當(dāng)前的線程必須獲得該對象的內(nèi)部鎖剪菱,因此調(diào)用wait()方法必須在同
步塊或同步方法中進(jìn)行摩瞎,如果該線程沒獲得對象鎖的話,是會拋出IllegalMonitorStateException
異常的孝常,另外和sleep()不同旗们,wait()方法會讓出對象鎖,然后進(jìn)入等待狀態(tài)茫因; -
notify():通知隨機(jī)喚醒一個在等待隊(duì)列中等待鎖的線程蚪拦,使該線程從堵塞狀態(tài)
切換到就緒狀態(tài);同樣調(diào)用該方法的線程需要獲得對象鎖冻押,所以也需要些在同步塊/方法中進(jìn)行驰贷。 -
notifyAll():喚醒等待隊(duì)列中等待對象鎖的所有線程,但是也是只有一個會拿到對象鎖洛巢,
至于是誰拿到鎖就看系統(tǒng)的調(diào)度了括袒。
幾個不安全,不推薦的方法:
- stop():停止線程稿茉,可能導(dǎo)致ThreadDeath異常锹锰,導(dǎo)致程序崩潰。
- interrupt():終端線程漓库。
- suspend()/resume():和wait恃慧,notify差不多,但容易引起死鎖渺蒿,不建議使用痢士。
Java 1.5 新增Condition接口:
用來代替wait(),notify()茂装,notifyAll()實(shí)現(xiàn)實(shí)現(xiàn)線程間的協(xié)作怠蹂,使用await()善延,signal(),
signalAll()來實(shí)現(xiàn)線程間的協(xié)作更加安全高效城侧;Condition依賴于Lock接口易遣,調(diào)用Lock對象的
newCondition()可以生成Condition,Condition的await()嫌佑,signal()和signalAll()方法調(diào)用豆茫,
需要在lock()和unlock()之間使用。
5.線程同步安全問題
當(dāng)有兩個或以上線程在同一時刻訪問操作同一資源屋摇,可能會帶來一些問題澜薄,比如:
數(shù)據(jù)庫表中不允許插入重復(fù)數(shù)據(jù),線程1,2都得到了數(shù)據(jù)X摊册,然后線程1,2同時查詢了
數(shù)據(jù)庫,發(fā)現(xiàn)沒有數(shù)據(jù)X颊艳,接著兩線程都往數(shù)據(jù)庫中插入X茅特,然后就GG了,這就是線程
安全問題棋枕,而這里的數(shù)據(jù)庫資源我們又稱為:臨界資源(共享資源)
6.如何解決線程安全問題
基本所有并發(fā)模式在解決線程安全問題時白修,都采用"系列化訪問臨界資源"的方式,
就是:同一時刻重斑,只能有一個線程訪問臨界資源兵睛,也成"同步互斥訪問"。通常就是
在操作臨界資源的代碼前加一個鎖窥浪,當(dāng)有線程訪問資源祖很,獲取這個所,其他線程無法
訪問漾脂,只能等待(堵塞)假颇,等這個線程訪問完臨界資源,然后釋放鎖骨稿,供其他線程繼續(xù)訪問笨鸡。
而Java中提供兩種方案來實(shí)現(xiàn)同步互斥訪問:synchronized和Lock,等下詳細(xì)講坦冠。
7.與鎖相關(guān)的特殊情況:死鎖形耗,饑餓與活鎖
每個對象都擁有一個鎖,當(dāng)多個線程辙浑,操作涉及到多個鎖激涤,就可能會出現(xiàn)這三種情況:
死鎖(DeadLock)
兩個或以上進(jìn)程(線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象例衍,
如果無外力作用昔期,他們將繼續(xù)這樣僵持下去已卸;簡單點(diǎn)說:兩個人互相持有對方想要的資源,
然后每一方都不愿意放棄自己手上的資源硼一,就一直那樣僵持著累澡。
死鎖發(fā)生的條件:
互斥條件(臨界資源);
請求和保持條件(請求資源但不釋放自己暫用的資源)般贼;
不剝奪條件(線程獲得的資源只有線程使用完后自己釋放愧哟,不能被其他線程剝奪);
環(huán)路等待條件:在死鎖發(fā)生時哼蛆,必然存在一個"進(jìn)程-資源環(huán)形鏈"蕊梧,t1等t2,t2等t1腮介;
如何避免死鎖:
破壞四個條件中的一個或多個條件肥矢,常見的預(yù)防方法有如下兩種:
有序資源分配法:資源按某種規(guī)則統(tǒng)一編號,申請時必須按照升序申請:
1.屬于同一類的資源要一次申請完叠洗;2.申請不同類資源按照一定的順序申請甘改。
銀行家算法:就是檢查申請者對資源的最大需求量,如果當(dāng)前各類資源都可以滿足的
申請者的請求灭抑,就滿足申請者的請求十艾,這樣申請者就可很快完成其計(jì)算,然后釋放它占用
的資源腾节,從而保證了系統(tǒng)中的所有進(jìn)程都能完成忘嫉,所以可避免死鎖的發(fā)生。
理論上能夠非常有效的避免死鎖案腺,但從某種意義上說庆冕,缺乏使用價值,因?yàn)楹苌儆羞M(jìn)程
能夠知道所需資源的最大值救湖,而且進(jìn)程數(shù)目也不是固定的愧杯,往往是不斷變化的,
況且原本可用的資源也可能突然間變得不可用(比如打印機(jī)損壞)鞋既。
饑餓(starvation)與餓死(starve to death)
資源分配策略有可能是不公平的力九,即不能保證等待時間上界的存在,即使沒有發(fā)生死鎖邑闺,
某些進(jìn)程可能因長時間的等待跌前,對進(jìn)程推進(jìn)與相應(yīng)帶來明顯影響,此時的進(jìn)程就是
發(fā)生了進(jìn)程饑餓(starvation)陡舅,當(dāng)饑餓達(dá)到一定程序即此時進(jìn)程即使完成了任務(wù)也
沒有實(shí)際意義時抵乓,此時稱該進(jìn)程被餓死(starve to death),典型的例子:
文件打印,采用短文件優(yōu)先策略灾炭,如果短文件太多茎芋,長文件會一直推遲,那還打印個毛蜈出。
活鎖(LiveLock)
特殊的饑餓田弥,一系列進(jìn)程輪詢等待某個不可能為真的條件為真,此時進(jìn)程不會進(jìn)入blocked狀態(tài)铡原,
但會占用CPU資源偷厦,,活鎖還有幾率能自己解開燕刻,而死鎖則無法自己解開只泼。(例子:都覺得對方
優(yōu)先級比自己搞,相互謙讓卵洗,導(dǎo)致無法使用某資源)请唱,簡單避免死鎖的方法:先來先服務(wù)策略。
8.守護(hù)線程
也叫后臺線程过蹂,是一種為其他線程提供服務(wù)的一種線程籍滴;當(dāng)虛擬機(jī)檢測到?jīng)]有用戶進(jìn)程可服務(wù)的
時候,就會退出當(dāng)前應(yīng)用程序的運(yùn)行榴啸,比如JVM的gc(垃圾回收線程),當(dāng)我們程序中不再有任何
Thread的時候晚岭,垃圾回收就沒事做了鸥印,即使還有其他后臺線程,但是此時的JVM還是會退出坦报!
將一個線程設(shè)置為后臺線程:setDaemon(boolean)
判斷一個線程是否為后臺線程:isDaemon()
9.線程并發(fā)的問題
開始的問題:臨時數(shù)據(jù)存放在內(nèi)存中库说,而CPU執(zhí)行指令比內(nèi)存讀寫快太多!
解決方法:CPU中使用高速緩存片择,將需要數(shù)據(jù)從內(nèi)存復(fù)制一份到緩存中潜的,運(yùn)算時CPU直接讀緩存,
運(yùn)算結(jié)束后字管,再將緩存中的數(shù)據(jù)刷新回內(nèi)存中啰挪。
又有問題:單線程倒沒什么,多線程的話嘲叔,切線程處于不同的CPU中亡呵,每個線程都擁有自己的
高速緩存,這樣可能會出現(xiàn)緩存不一致的問題硫戈,比如內(nèi)存中的i = 0锰什,然后在兩個線程中都執(zhí)行
i = i + 1;分別在兩個線程中執(zhí)行,最后的結(jié)果可能是1汁胆,而不是2梭姓。
硬件層次的解決方案:在總線加LOCK#鎖的方式 和 緩存一致性協(xié)議(Intel的MESI協(xié)議)
10.并發(fā)編程的三個概念
-
原子性:一個操作或多個操作,要么全部執(zhí)行嫩码,并且執(zhí)行過程不會被任何因素打斷誉尖;
要么就都不執(zhí)行,比如:銀行轉(zhuǎn)賬的栗子谢谦。 -
可見性:當(dāng)多個線程訪問同一個變量時释牺,一個線程修改了這個變量的值,其他線程能夠
立即看到修改后的值回挽。 -
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行没咙。可能你覺得自己程序怎么寫的執(zhí)行
順序就是怎樣千劈,只能說naive祭刚!有個名詞叫:指令重排序!一般來說處理器為了提高程序
的運(yùn)行效率墙牌,可能會對輸入的代碼進(jìn)行優(yōu)化涡驮,它不保證程序中每個語句的執(zhí)行先后順序一致,
但是他會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的喜滨!另外處理器在重排序的
時候還會考慮指令的數(shù)據(jù)依賴性捉捅,如果指令2需要用到指令1的結(jié)果,那么處理器會保證
指令1會在指令2之前執(zhí)行虽风,指令重排序不會音箱單線程的執(zhí)行棒口,但是在多線程并發(fā)的時候可能
會影響結(jié)果的正確性。
綜上:想要保證并發(fā)程序能夠正確執(zhí)行辜膝,必須保持原子性无牵,可見性,有序性厂抖,只有有一個沒
保證茎毁,就有可能會導(dǎo)致運(yùn)行不正確。
11.Java中對并發(fā)編程的保證與8條先行發(fā)生原則
Java內(nèi)存模型規(guī)定所有變量都是存儲在主存中忱辅,每個線程都有自己的工作內(nèi)存(類似于前面的高速
緩存)七蜘,線程對變量的所有操作必須在工作內(nèi)存中,而不能直接對主存進(jìn)行操作墙懂,且每個線程不能
訪問其他線程的工作內(nèi)存崔梗。
Java中語言本身對原子性,可見性垒在,有序性提供了哪些保證蒜魄?
-
原子性:Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作扔亥,如果要實(shí)現(xiàn)
更大范圍操作的原子性,可以通過synchronized和Lock來實(shí)現(xiàn)谈为。 -
可見性:Java提供了volatile關(guān)鍵字來保證可見性旅挤,當(dāng)一個共享變量被
volatile修飾時,它會保證修改的值會立即被更新到主存伞鲫,當(dāng)有其他線程需要讀取時粘茄,
他會去內(nèi)存中讀取新值。另外秕脓,通過synchronized和Lock也能夠保證可見性柒瓣。 -
有序性:Java內(nèi)存模型中,允許編譯器和處理器對指令進(jìn)行重排序吠架,以通過volatile
關(guān)鍵字來保證一定的"有序性"芙贫,通過synchronized和Lock也能夠保證有序性。
另外傍药,Java內(nèi)存模型具有一些先天的"有序性"磺平,即不需要通過任何手段就能夠得到保證的有序性,
這個通常也稱為happens-before原則拐辽,如果兩個操作的執(zhí)行次序無法從happens-before原則
推導(dǎo)出來拣挪,那么它們就不能保證它們的有序性,虛擬機(jī)可以隨意地對它們進(jìn)行重排序俱诸。
8條happens-before原則(先行發(fā)生原則):
-
程序次序規(guī)則:一個線程內(nèi)菠劝,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作睁搭。
該規(guī)則用來保證程序在單線程中執(zhí)行結(jié)果的正確性闸英,但無法保證程序在多線程中執(zhí)行的正確性目尖。 -
鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作纫谅。
就是同一個鎖如果處于被鎖定狀態(tài)蛮粮,那么必須先對鎖進(jìn)行釋放操作,后面才能繼續(xù)進(jìn)行l(wèi)ock操作遇伞。 -
volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
直觀解釋:若一個線程先去寫一個變量捶牢,然后一個線程去讀取鸠珠,那么寫入操作肯定先與讀操作執(zhí)行。 -
傳遞規(guī)則:如果操作A先行發(fā)生于操作B秋麸,而操作B又先行發(fā)生于操作C渐排,則可以得出操作A先
行發(fā)生于操作C。說明happens-before原則具備傳遞性灸蟆。 - 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作驯耻。
- 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
-
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()
方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行 - 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
12.線程并發(fā)的經(jīng)典問題:生產(chǎn)者消費(fèi)者問題
問題概述:
兩個共享固定緩沖區(qū)大小的線程可缚,生產(chǎn)者線程負(fù)責(zé)生產(chǎn)一定量的數(shù)據(jù)放入緩沖區(qū)霎迫,
而消費(fèi)者線程則負(fù)責(zé)消耗緩沖區(qū)中的數(shù)據(jù),關(guān)鍵問題是需要保證:
1.緩沖區(qū)滿的時候帘靡,生產(chǎn)者不再往緩沖區(qū)中填充數(shù)據(jù)
2.緩存區(qū)空的時候知给,消費(fèi)者不在消耗緩沖區(qū)中的數(shù)據(jù)
可以用簡單的兩種套路來模擬這個問題:
synchronized + wait() + notify()方式 或 Lock + Condition接口的await()與signal()實(shí)現(xiàn)
等下細(xì)講。
13.同步容器
Java集合容器中有四類:List(數(shù)組)描姚,Set(集合)涩赢,Queue(隊(duì)列),Map轩勘,前三個都繼承Collection接口筒扒,
Map本身是一個接口。我們平常使用的ArrayList赃阀,LinkedList霎肯,HashMap這些容器都是非線程安全的,并發(fā)訪問的時候可能會
有問題榛斯,然后Java給我們提供了兩類的同步容器:
- Vector观游,Stack,HashTable(普通集合加了同步措施而已)
-
Collections類中提供的靜態(tài)工廠方法創(chuàng)建的類(不是Connection類M运住!王凑!提供了靜態(tài)工廠方法來創(chuàng)建同步容器搪柑,還提供
了對集合或容器進(jìn)行排序工碾,查找等的操作。)
相比起非同步容器百姓,同步容器因?yàn)殒i的關(guān)系渊额,性能會稍弱。
另外同步容器也不一定是安全的垒拢,只能保證每個時刻只能有一個線程在訪問他旬迹,集合刪元素遍歷的例子,
有時為了保證線程安全求类,還需要在方法調(diào)用端做額外的同步措施奔垦!
另外,在對Vector等容器并發(fā)地進(jìn)行迭代修改時尸疆,會報(bào)ConcurrentModificationException異常椿猎!
原因是:Iterator執(zhí)行next方法時惶岭,調(diào)用checkForComodification()時expectedModCount與modCount不相等,
由于執(zhí)行remove()鸵贬,modCount每次循環(huán)值都會改變俗他,而expectedModCount并沒改變,所以拋異常阔逼!
解決方法:
- 1.使用Iterator提供的remove方法來刪除當(dāng)前元素兆衅。
- 2.另外創(chuàng)建一個集合來存放要刪除的元素,然后調(diào)用removeAll統(tǒng)一刪除嗜浮。
- 3.自己通過索引來刪除羡亩,要保證索引是正常的,比如刪除了4危融,你的索引要變回3畏铆;
另外這些可能不是線程安全的,想保證多線程執(zhí)行也安全的話吉殃,可以: - 1.使用Iterator迭代的時候辞居,加同步鎖
- 2.使用并發(fā)容器CopyOnWriteArrayList代替ArrayList和Vector。
14.并發(fā)容器
同步容器因?yàn)槭褂肧ynchronized進(jìn)行同步蛋勺,執(zhí)行讀寫都需要去獲取鎖瓦灶,并發(fā)的時候效率較低,
Jdk 1.5新增的concurrent提供了這些并發(fā)容器:
-
BlockingQueue接口:線程安全的堵塞式隊(duì)列抱完,線程安全的阻塞式隊(duì)列贼陶;當(dāng)隊(duì)列已滿時,向隊(duì)列
添加會阻塞巧娱;當(dāng)隊(duì)列空時碉怔,取數(shù)據(jù)會阻塞。(非常適合消費(fèi)者-生產(chǎn)者模式)
阻塞方式:put()禁添、take()撮胧。
非阻塞方式:offer()、poll()老翘。
實(shí)現(xiàn)類:基于數(shù)組的固定元素個數(shù)的ArrayBolockingQueue和基于鏈表結(jié)構(gòu)的不固定元素個
數(shù)的LinkedBlockQueue類芹啥。 -
BlockingDeque接口: 與BlockingQueue相似,但可以對頭尾進(jìn)行添加和刪除操作的雙向隊(duì)列酪捡;
方法分為兩類,分別在隊(duì)首和對尾進(jìn)行操作纳账。
實(shí)現(xiàn)類:標(biāo)準(zhǔn)庫值提供了一個基于鏈表的實(shí)現(xiàn)逛薇,LinkedBlockgingDeque。 -
ConcurrentMap接口: 繼承自java.util.Map接口
putIfAbsent():只有在散列表不包含給定鍵時疏虫,才會把給定的值放入永罚。
remove():刪除條目啤呼。
replace(key,value):把value 替換到給定的key上。
replace(key, oldvalue, newvalue):CAS的實(shí)現(xiàn)呢袱。
實(shí)現(xiàn)類:ConcurrentHashMap
創(chuàng)建時官扣,如果可以預(yù)估可能包含的條目個數(shù),可以優(yōu)化性能羞福。(因?yàn)閯討B(tài)調(diào)整所能包含的數(shù)目操作比較耗時
這個HashMap也一樣惕蹄,只是多線程下更耗時)。創(chuàng)建時治专,預(yù)估進(jìn)行更新操作的線程數(shù)卖陵,這樣實(shí)現(xiàn)中會根據(jù)這
個數(shù)把內(nèi)部空間劃分為對應(yīng)數(shù)量的部分。(默認(rèn)是16张峰,如果只有一個線程進(jìn)行寫操作泪蔫,其他都是讀取,
那么把值設(shè)為1 可以提高性能)喘批。
注:當(dāng)從集合中創(chuàng)建出迭代器遍歷Map元素時撩荣,不一定能看到正在添加的數(shù)據(jù),只能和集合保證弱一致性饶深。
(當(dāng)然使用迭代器不會因?yàn)椴榭凑诟淖兊腗ap餐曹,而拋出java.util.ConcurrentModifycationException) -
CopyOnWriteArrayList/CopyOnWriteArraySet:當(dāng)往容器中添加數(shù)據(jù)時,不是直接添加粥喜,而是將當(dāng)前容器進(jìn)行Copy
然后往新容器里添加凸主,添加完后,再把舊容器的引用只想新容器额湘,讀寫分離的思想卿吐。可以參考CopyOnWriteArrayList寫個
CopyOnWriteMap锋华,套路:在put和putAll的時候加synchronized鎖嗡官,創(chuàng)建新集合放值,然后舊的指向新的集合就好毯焕。
注意:創(chuàng)建時初始化好大小衍腥,避免擴(kuò)容開銷;批量添加纳猫,減少容器的復(fù)制次數(shù)婆咸;只保證數(shù)據(jù)的最終一致性,不保證
數(shù)據(jù)的實(shí)時一致性芜辕!
15.阻塞隊(duì)列
在解決生產(chǎn)者與消費(fèi)者問題時尚骄,我們的套路一般是對容器加鎖,然后判斷容器中數(shù)據(jù)滿和空的情況侵续,
然后喚醒或者阻塞生產(chǎn)者或者消費(fèi)者線程倔丈,有些麻煩憨闰,如果使用阻塞隊(duì)列,我們就不用關(guān)心那么多需五,
阻塞隊(duì)列會對訪問線程產(chǎn)生堵塞鹉动,比如當(dāng)?shù)仃?duì)列滿了,此時生產(chǎn)者線程會被阻塞宏邮,直到消費(fèi)者消費(fèi)
了隊(duì)列中的元素泽示,被堵塞的線程會自動喚醒。其實(shí)就是把wait()蜀铲,notify()這些集成到隊(duì)列中實(shí)現(xiàn)边琉。
幾種主要的阻塞隊(duì)列:
同樣是java.util.concurrent包下提供的若干個阻塞隊(duì)列:
-
ArrayBlockingQueue:基于數(shù)組實(shí)現(xiàn)的,創(chuàng)建時需指定容量大小记劝,也可以指定公平性與非公平性变姨,
默認(rèn)非公平,即不保證等待時間最長的隊(duì)列最優(yōu)先能夠訪問隊(duì)列厌丑。 -
LinkedBlockingQueue:基于鏈表實(shí)現(xiàn)的定欧,創(chuàng)建時不指定容量大小的話,默認(rèn)大小為Integer.MAX_VALUE怒竿。
PriorityBlockingQueue:前面兩個都是先進(jìn)先出隊(duì)列砍鸠,而PriorityBlockingQueue是按照元素優(yōu)先級對
元素進(jìn)行排序,按照優(yōu)先級順序出隊(duì)耕驰,即每次出隊(duì)的都是優(yōu)先級最高的元素爷辱;另外,該隊(duì)列是無界阻塞隊(duì)列朦肘,
即容量沒有上線饭弓,而前兩種是有界隊(duì)列。
DelayQueue:基于PriorityQueue媒抠,延時阻塞隊(duì)列弟断,隊(duì)列中的元素只有當(dāng)其延時時間到了,才能夠從隊(duì)列中獲取到
該元素趴生,同樣是無界隊(duì)列阀趴,因此往隊(duì)列里插入元素(生產(chǎn)者)永遠(yuǎn)不會被阻塞,而只有獲取數(shù)據(jù)(消費(fèi)者)才
會被阻塞苍匆。
堵塞隊(duì)列除了對非阻塞隊(duì)列的下述五個方法進(jìn)行了同步:
- add(E e):添加元素到隊(duì)尾刘急,插入成功返回true,若插入失敗(隊(duì)滿)浸踩,拋出異常叔汁。
- remove():移除隊(duì)首元素,移除成功返回true,若移除失敗(隊(duì)空)攻柠,拋出異常。
- offer(E e):添加元素到隊(duì)尾后裸,插入成功返回true瑰钮,若插入失敗(隊(duì)滿),返回false微驶。
- poll():移除并獲取隊(duì)首元素浪谴,成功返回元素,否則返回null因苹。
- peek():獲取隊(duì)首元素苟耻,成功返回隊(duì)首元素,否則返回null扶檐。
還提供了另外4個非常有用的方法:
- put(E e):向隊(duì)尾存入元素凶杖,隊(duì)滿則等待。
- take():取隊(duì)首元素款筑,隊(duì)空則等待智蝠。
-
offer(E e,long timeout,TimeUnit unit):往隊(duì)尾存元素,隊(duì)滿等待一定時間奈梳,時間到后杈湾,若沒有插入成功,
返回false攘须,否則返回true漆撞; -
poll(long timeout,TimeUnit unit):取隊(duì)首元素,隊(duì)空等待一定時間于宙,時間到后浮驳,沒取到返回null,否則
返回取得的元素限煞。
阻塞隊(duì)列適用于生產(chǎn)者-消費(fèi)者問題抹恳,因?yàn)椴恍枰賳为?dú)考慮同步和線程間通信的問題。
16.線程組
我們可以通過java.lang.ThreadGroup對線程進(jìn)行組操作署驻,每個線程都?xì)w屬于某個線程組管理的一員奋献。
在創(chuàng)建Thread實(shí)例時,如果沒有制定線程組參數(shù)旺上,則默認(rèn)屬于創(chuàng)建者線程所隸屬的線程組瓶蚂,這種隸屬
關(guān)系在創(chuàng)建新線程時指定,在線程的整個生命周期里都不能改變宣吱!比如我們在main()中創(chuàng)建的新線程窃这,
這個線程屬于main這個線程管理中的一員!
作用:
簡化對多個線程的管理征候,對若干線程同時操作杭攻,比如:調(diào)用線程組的方法設(shè)置所有線程的優(yōu)先級祟敛,
調(diào)用線程組的犯法啟動或堵塞組中所有線程等;其實(shí)兆解,線程組的最重要意義是線程安全馆铁,Java默認(rèn)
創(chuàng)建的線程都是屬于系統(tǒng)線程組,而處于同一線程組中的線程可以互相修改對方數(shù)據(jù)锅睛,當(dāng)如果在不
同的線程組中埠巨,那么就不能"跨線程組"修改數(shù)據(jù),從一定程度上保證了數(shù)據(jù)的安全现拒。
線程組提供的操作:
- 集合管理方法:用于管理包含在線程組中的線程與子線程組
- 組操作方法:設(shè)置或獲取線程對象的屬性
- 組中所有偶線程的操作方法辣垒,針對組中所有線程與子線程執(zhí)行某一操作,比如:線程啟動印蔬,恢復(fù)
- 訪問限制方法:基于線程組的成員關(guān)系
常見用法
- 獲得當(dāng)前線程的線程組對象:ThreadGroup tGroup = Thread.currentThread().getThreadGroup();
- 獲得線程組的名字:tGroup.getName();
- 獲得線程中活動線程的數(shù)目:tGroup.activeCount();
- 將線程組中每個活動線程復(fù)制到線程數(shù)組中:tGroup.enumerate(Thread list[])
- 獲得本線程組的父線程組:tGroup.getParent();
- 判斷某個線程組是否為守護(hù)線程組:tGroup.isDaemon();
使用示例:
public class ThreadGroupTest {
public static void main(String[] args) {
ThreadGroup tGroup = new ThreadGroup("自定義線程組");
Thread t1 = new Thread(tGroup,"自定義線程1");
Thread t2 = new Thread(tGroup,"自定義線程2");
System.out.println("線程組的初始最大優(yōu)先級:" + tGroup.getMaxPriority());
System.out.println(t1.getName() + "的初始優(yōu)先級:" + t1.getPriority());
System.out.println(t2.getName() + "的初始優(yōu)先級:" + t2.getPriority());
t1.setPriority(9); //設(shè)置t1的優(yōu)先級為9
tGroup.setMaxPriority(8); //設(shè)置線程組的優(yōu)先級為8
System.out.println("線程組的新最大優(yōu)先級:" + tGroup.getMaxPriority());
System.out.println(t1.getName() + "的新優(yōu)先級" + t1.getPriority());
t2.setPriority(10); //設(shè)置t2的優(yōu)先級為10
System.out.println(t2.getName() + "的新優(yōu)先級" + t2.getPriority());
System.out.println(tGroup.toString());
}
}
運(yùn)行結(jié)果:
17.線程池
引入:
直接創(chuàng)建的線程在調(diào)用完start()方法結(jié)束后勋桶,線程就結(jié)束了,而線程的創(chuàng)建與結(jié)束都需要耗費(fèi)
一定的系統(tǒng)時間侥猬,如果并發(fā)的線程數(shù)量很多的話哥遮,不停的創(chuàng)建和銷毀線程會消耗大量的時間,
效率就有些低了陵究,我們想讓線程在執(zhí)行完任務(wù)以后并不銷毀眠饮,而是讓他進(jìn)入休眠狀態(tài),然后
繼續(xù)執(zhí)行其他任務(wù)铜邮,當(dāng)需要用到這個線程再喚醒仪召,Java中使用線程池可以達(dá)到這樣的效果。
ThreadPoolExecutor類
繼承AbstractExecutorService類松蒜,并提供四種構(gòu)造方法(前三其實(shí)都是調(diào)用第四個進(jìn)行初始化
工作的):
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { }
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { }
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { }
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }
參數(shù)解析:
-
corePoolSize:核心池大小扔茅,創(chuàng)建線程池后,默認(rèn)池中沒有任何線程秸苗,而是等待有任務(wù)來才去
創(chuàng)建線程執(zhí)行任務(wù)召娜,除非調(diào)用prestartAllCoreThreads()或prestartCoreThread()方法預(yù)創(chuàng)建線
程,就是在沒有任務(wù)來之前就創(chuàng)建corePollSize個線程或者一個線程惊楼,當(dāng)池中線程數(shù)目達(dá)到
corePoolSize后玖瘸,會把后到達(dá)的任務(wù)放到緩存隊(duì)列中。 - maximumPoolSize:線程池最大線程數(shù)檀咙,表示線程池中最多創(chuàng)建多少個線程雅倒。
-
keepAliveTime:線程沒有任務(wù)執(zhí)行最多保持多久時間會終止,只有當(dāng)池中線程數(shù)大于
corePoolSize才會起作用弧可,直到池中線程數(shù)不超過corePoolSize蔑匣。但是如果調(diào)用了 -
allowCoreThreadTimeOut(boolean)方法,即使池中線程不大于CorePoolsize,
該參數(shù)也會起作用裁良,知道池中線程數(shù)為0凿将。 - unit:keepAliveTime的時間單位,比如毫秒:TimeUnit.MILLISECONDS 等价脾。
-
workQueue:堵塞隊(duì)列丸相,用來存儲等待執(zhí)行的任務(wù),一般來說有以下幾種選擇:
ArrayBlockingQueue(基于數(shù)組的并發(fā)堵塞隊(duì)列)
LinkedBlockingQueue(基于鏈表的FIFO堵塞隊(duì)列)
PriorityBlockingQueue(帶優(yōu)先級的無界堵塞隊(duì)列)
SynchronousQueue(并發(fā)同步堵塞隊(duì)列) - threadFactory:線程工廠彼棍,用于創(chuàng)建線程。
-
handler:表示拒絕處理任務(wù)時的策略膳算,有四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常座硕。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務(wù),但是不拋出異常涕蜂。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù)华匾,然后重新嘗試執(zhí)行任務(wù)(重復(fù)此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線程處理該任務(wù)
線程池相關(guān)的幾個類之間的關(guān)系:
使用示例:
MyThread.java:
public class MyThread implements Runnable {
private int threadNum;
public MyThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
System.out.println("線程:" + threadNum + "開始執(zhí)行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程:" + threadNum + "執(zhí)行完畢...");
}
}
ThreadTest.java 測試類:
public class ThreadTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,
TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5));
for (int i = 0; i < 15; i++) {
MyThread myThread = new MyThread(i);
executor.execute(myThread);
System.out.println("線程池中線程數(shù)目:" + executor.getPoolSize() + " 等待執(zhí)行的任務(wù)數(shù)目:" + executor.getQueue().size());
}
executor.shutdown();
}
}
另外,Java文檔中并不提倡我們直接使用ThreadPoolExecutor机隙,而是使用Executors提供的幾個靜態(tài)方法來創(chuàng)建線程池:
- Executors.newSingleThreadExecutor():創(chuàng)建容量為1的線程池蜘拉,可以理解為串行執(zhí)行所有任務(wù),任務(wù)執(zhí)行順序與提交順序相同有鹿。
- Executors.newFixedThreadPool():創(chuàng)建固定容量的線程池
- Executors.newCachedThreadPool():創(chuàng)建可以緩存的線程池旭旭,緩沖池容量大小為Inter.MAX_VALUE,可以靈活的往池中添加線程葱跋,
如果線程空閑超過了指定時間持寄,該工作線程會終止,終止后如果你提交了任務(wù)娱俺,線程池會重新創(chuàng)建一個工作線程稍味。 - Executors.newScheduledThreadPool():創(chuàng)建不限容量的線程池,支持定時及周期性執(zhí)行任務(wù)的需求荠卷,多了個schedule(thread,time)
延時執(zhí)行的方法模庐。
當(dāng)然,如果這個幾個線程池都滿足不了你的話油宜,你可以繼承ThreadPoolExecutor類重寫掂碱。
18.Timer和TimerTask
Timer是Jdk提供的定時器類,延時或者重復(fù)執(zhí)行任務(wù)慎冤,使用時候會主線程外開啟
單獨(dú)的線程來執(zhí)行定時任務(wù)顶吮,可以指定執(zhí)行一次或重復(fù)多次。TimerTask是實(shí)現(xiàn)了
Runnable接口的抽象類粪薛,代表一個可以被Timer執(zhí)行的任務(wù)难审。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() { ... timer.cancle(); }
},延時時間)
終止Timer的幾個方式:
- 1.調(diào)用timer的cancle();
- 2.將timer線程設(shè)置為守護(hù)線程蝶防;
- 3.所有任務(wù)執(zhí)行完后谓松,刪除timer對象的引用凤薛,線程也會被終止;
- 4.System.exit()終止程序昏名;
另外,schedule保證每次延遲間隔固定,而scheduleAtFixedRate則可能因?yàn)槟硞€調(diào)度
時間太長息罗,而縮短間隔的方式,保證下一次調(diào)度在預(yù)定時間執(zhí)行才沧,比如迈喉,每隔3s調(diào)度一次:
正常都是:0,3,6,9,假如第二次調(diào)度花了2s温圆,前者會變成:0,3+2,8,11挨摸,而后者會壓縮間隔,
保證調(diào)度時間岁歉,變成:0,3+2,6,9得运,另外還要注意Timer不保證任務(wù)執(zhí)行的十分準(zhǔn)確!
19.三個并發(fā)輔助類:CountDownLatch锅移,CyclicBarrier和Semaphore
CountDownLatch(類似于計(jì)時器熔掺,比如有任務(wù)A,需等其他4個任務(wù)執(zhí)行完畢才執(zhí)行非剃,就可以用上)
使用方法如下:
CountDownLatch latch = new CountDownLatch[2]置逻; //初始值
latch.await(); //調(diào)用了這個方法的那個線程會被掛起,直到count = 0才繼續(xù)執(zhí)行备绽,也可以設(shè)置時間诽偷,
//超時count還沒變0就會繼續(xù)執(zhí)行
latch.countDown(); //count值減1
CyclicBarrier(回環(huán)柵欄,讓一組線程等待到某個狀態(tài)再全部同時執(zhí)行疯坤,所有等待線程被釋放后报慕,CyclicBarrier可以重用)
比如:若干個線程需要進(jìn)行寫操作,并且想所有線程都達(dá)到某個狀態(tài)才能執(zhí)行后續(xù)任務(wù)压怠,此時可以用CyclicBarrier眠冈。
使用方法如下:
CyclicBarrier barrier = new CyclicBarrier(parties); //參數(shù)是指定多個線程達(dá)到某狀態(tài)
//如果你想執(zhí)行完任務(wù)后,做其他操作可加個Runnable的參數(shù)菌瘫。
barrier.await(); //掛起當(dāng)前線程蜗顽,直到到達(dá)某個狀態(tài)再同時執(zhí)行,同樣可以設(shè)置時間雨让,超時直接
//執(zhí)行已經(jīng)到達(dá)這個狀態(tài)的線程
PS:個人簡單理解:調(diào)了await()會掛起這個線程雇盖,然后+1,直到結(jié)果等于parties栖忠,再繼續(xù)執(zhí)行掛起線程的后續(xù)部分崔挖。
Semaphore(信號量贸街,控制某資源同時被幾個線程訪問的類,與鎖類似)
使用方法如下:
Semaphore semaphore = new Semaphore(5); //設(shè)置多少個線程同時訪問狸相,可選參數(shù)boolean fair表示
//是否公平薛匪,即等待越久越先獲得許可
semaphore.acquire(); //獲取一個許可
semaphore.acquire(int); //獲取多個許可
semaphore.release() //釋放一個許可
semaphore.release(int) //釋放多個許可
//acquire獲取許可的方式會堵塞,就是沒有拿到的話會一直等待脓鹃,如果想立即得到結(jié)果
可調(diào)用:tryAcquire()
semaphore.availablePermits() //獲得可用許可數(shù)目
20.ThreadLocal(線程本地存儲)
作用:ThreadLocal的作用是提供線程內(nèi)的局部變量逸尖,這種變量在線程的生命周期內(nèi)起作用,
減少同一個線程內(nèi)多個函數(shù)或者組件之間一些公共變量傳遞的復(fù)雜度瘸右,同時隔離其他線程娇跟。
你可以:
- 重寫initialValue()方法來設(shè)置ThreadLocal的初始值
- get():獲得當(dāng)前線程的ThreadLocal的值
- set():設(shè)置當(dāng)前線程的ThreadLocal的值
- remove():刪除當(dāng)前線程的ThreadLocal綁定的值,某些情況下需要手動調(diào)用該函數(shù)太颤,防止內(nèi)存泄露苞俘。
用法示例:
public class ThreadTest {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i =0;i < 5;i++) {
ThreadTest test = new ThreadTest();
new Thread(test.new MyThread(i)).start();
}
}
class MyThread implements Runnable {
private int index;
public MyThread(int index) {
this.index = index;
}
@Override
public void run() {
System.out.println("線程" + index + "的初始value:" + value.get());
for (int i = 0; i < 10; i++) {
value.set(value.get() + i);
}
System.out.println("線程" + index + "的累加value:" + value.get());
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運(yùn)行結(jié)果:
ThreadLocal的實(shí)現(xiàn)原理:
每個Thread維護(hù)一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實(shí)例本身栋齿,
value是真正需要存儲的Object。
更多詳細(xì)內(nèi)容可見:[Java并發(fā)包學(xué)習(xí)七]解密ThreadLocal
細(xì)講與代碼實(shí)現(xiàn)
1.synchronized同步方法或代碼塊
在Java中每個對象都擁有一個monitor(互斥鎖標(biāo)記)襟诸,也稱為監(jiān)視器瓦堵,當(dāng)多個線程訪問某
個對象時,線程只有獲得該對象的鎖才能訪問歌亲。使用synchronized關(guān)鍵字來獲取對象上的鎖菇用,
可應(yīng)用在方法級別(粗粒度鎖)或代碼塊級別(細(xì)粒度鎖)。
同步方法:
比如: public synchronized void save(){}
同步代碼塊:
比如:synchronized(資源對象){ }
類鎖:
每個類都有一個類鎖陷揪,用于類的靜態(tài)方法或者一個類的class對象上的(單例)惋鸥,類的對象實(shí)例
可以有很多個,但是每個類只有一個class對象悍缠,所以不同對象實(shí)例的對象鎖是互不干擾
的卦绣,但是每個類只有一個類鎖。類鎖與對象鎖是兩種不同的鎖飞蚓,控制著不同區(qū)域滤港,互不干擾,
統(tǒng)一趴拧,線程獲得對象鎖的同時溅漾,也可以獲取類鎖,同時獲得兩個鎖是允許的著榴。
用法比如:public static synchronized insert() {}; synchronized(類.class)
注意事項(xiàng):
- 1.當(dāng)有線程在訪問對象的synchronized方法添履,其他線程不能訪問該對象的其他
synchronized方法!但可以訪問非synchronized方法脑又。 - 2.對于synchronized方法或者synchronized代碼塊暮胧,當(dāng)出現(xiàn)異常時锐借,JVM會自動
釋放當(dāng)前線程占用的鎖,因此不會由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象叔壤。 - 3.一個線程可以獲得多個鎖標(biāo)記瞎饲,一個對象最多只能把鎖標(biāo)記給一個線程,
synchronized是以犧牲程序效率為代價的炼绘,因此應(yīng)該盡量控制互斥代碼塊的范圍嗅战。 - 4.方法的Synchronized特性本身不會被繼承,只能覆蓋俺亮。
- 5.線程因未拿到鎖標(biāo)記而發(fā)生的堵塞不同于基本狀態(tài)中的堵塞驮捍,等待的線程
會進(jìn)入該對象的鎖池(放置等待獲取鎖標(biāo)記的線程)中,鎖池中哪個線程拿到鎖
標(biāo)記由系統(tǒng)決定脚曾。
2.Lock(鎖)
Synchronized的缺陷:
使用Synchronized獲取鎖的線程釋放鎖的兩種情況
- 1.獲取鎖的線程執(zhí)行完該代碼塊东且,然后線程釋放對鎖的占有;
- 2.線程執(zhí)行發(fā)生異常本讥,此時JVM會讓線程自動釋放鎖珊泳;
如果是IO等待或其他原因(調(diào)sleep方法)被堵塞了,但又沒釋放鎖拷沸,只能一直等待色查;
另外當(dāng)多個線程讀寫文件時,讀和寫會沖突撞芍,寫和寫會沖突秧了,但是讀與讀不該沖突。
而使用Lock可以解決上述問題序无,而且Lock還可以知道有沒有獲得鎖验毡,Lock類可以實(shí)
現(xiàn)同步訪問,另外synchronized不需要用戶自己手動去釋放鎖帝嗡,而Lock需由用戶去
手動釋放鎖晶通,若果沒有主動釋放的話,就有可能導(dǎo)致出現(xiàn)死鎖現(xiàn)象哟玷。
Lock源碼解析:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock():獲取鎖录择,如果鎖已被其他線程獲取,則進(jìn)行等待
- unlock():釋放鎖碗降,Lock必須主動去釋放鎖隘竭,所以Lock必須在try-catch中進(jìn)行,在finally中釋放讼渊。
-
tryLock():嘗試獲取鎖动看,獲取成功返回true,獲取失敗返回false爪幻,無論如何會立即返回菱皆,而不會
在因拿不到鎖就一直在那里等待须误;如果是有參的那個,拿不到鎖會等待一段時間仇轻,時間到了也會立即返回京痢。 -
lockInterruptibly():當(dāng)通過該方法來獲取鎖時,如果已經(jīng)有某個進(jìn)程持有了這個鎖篷店,而另一線
程需要等待祭椰,那么對另一個線程調(diào)用interrupt方法中斷等待過程。
ReentrantLock(可重入鎖疲陕,獨(dú)占鎖):唯一實(shí)現(xiàn)Lock接口的類
示例代碼:
public class Main {
private ArrayList<Integer> arrayList = new ArrayList<>();
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Main main = new Main();
for(int i = 0;i < 2;i++) {
new Thread(){
@Override
public void run() {
super.run();
main.lock(Thread.currentThread());
}
}.start();
}
}
/**lock()*/
public void lock(Thread thread) {
lock.lock();
duplicated(thread);
}
/**tryLock()*/
public void tryLock(Thread thread) {
if(lock.tryLock()) {
duplicated(thread);
} else {
System.out.println(thread.getName()+"獲取鎖失敗");
}
}
//相同代碼
public void duplicated(Thread thread) {
try {
System.out.println(thread.getName() + "得到了鎖");
for(int i=0;i<5;i++) { arrayList.add(i); }
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
運(yùn)行結(jié)果:
ReadWriteLock接口:
讀寫操作分開成兩個鎖方淤,一個資源能夠被多個讀線程訪問,或者被一個寫線程訪問蹄殃,
但是不能同時存在讀寫線程携茂,使用場合:共享資源被大量讀取操作,只有少量寫操作(修改數(shù)據(jù))
ReentrantReadWriteLock(讀寫鎖):
readLock()和writeLock()用來獲取讀鎖和寫鎖诅岩,讀鎖是共享鎖讳苦,能同時被多個線程獲取吩谦;
寫入鎖是獨(dú)占鎖鸳谜,只能被一個線程鎖獲取。
3.鎖的相關(guān)概念
- 1.可重入鎖:synchronized和ReentrantLock都是可重鎖逮京,比如線程執(zhí)行某個synchronized方法
在這個方法里會調(diào)該類中的另一個synchronized方法卿堂,此時不用重復(fù)申請鎖束莫,可以直接執(zhí)行該方法懒棉。
- 2.可中斷鎖:可以中斷的鎖,在Java中览绿,synchronized就不是可中斷鎖策严,而Lock是可中斷鎖。
- 3.公平鎖:盡量以請求鎖的順序來獲取鎖饿敲,當(dāng)這個所釋放時妻导,等待時間最久的線程(最先請求)
會獲得該鎖,這就是公平鎖怀各,非公平鎖無法保證按順序倔韭,就可能導(dǎo)致某個/一些線程永遠(yuǎn)獲取不到鎖。
synchronized就是非公平鎖瓢对,而對于ReentrantLock和ReentrantReadWriteLock寿酌,它默認(rèn)情況下是非
公平鎖,但是可以設(shè)置為公平鎖硕蛹,構(gòu)建的時候傳參(true表示公平鎖醇疼,false為非公平鎖硕并,用無參構(gòu)
造方法,則是非公平鎖)秧荆,另外記住一點(diǎn):ReentrantReadWriteLock并未實(shí)現(xiàn)Lock接口倔毙,它實(shí)現(xiàn)的是
ReadWriteLock接口。 - 4.讀寫鎖:將對一個資源(比如文件)的訪問分成了2個鎖乙濒,一個讀鎖和一個寫鎖陕赃,
ReadWriteLock就是讀寫鎖,它是一個接口琉兜,ReentrantReadWriteLock實(shí)現(xiàn)了這個接口凯正。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖豌蟋。
4.生產(chǎn)者與消費(fèi)者的幾種代碼實(shí)現(xiàn)
synchronized + wait() + notify()方式
實(shí)現(xiàn)核心:定義一個倉庫類廊散,對于生產(chǎn)和消耗方法加synchronized鎖;
定義兩個線程,生產(chǎn)者和消費(fèi)者梧疲,對于滿或空的情況進(jìn)行判斷允睹,wait()和notify();
產(chǎn)品類:Product.java
public class Product {
private int productId = 0;
public Product() { }
public Product(int productId) {
this.productId = productId;
}
public int getProductId() {
return productId;
}
public void setProductId(int productId) {
this.productId = productId;
}
@Override
public String toString() {
return "Product{" +
"productId=" + productId +
'}';
}
}
倉庫類:WareHouse.java
public class WareHouse {
private int base = 0;
private int top = 0;
private Product[] products = new Product[10];
public synchronized void produce(Product product) {
notify();
while (top == products.length) {
try {
System.out.println("倉庫已滿幌氮,暫停生產(chǎn)缭受,等待消費(fèi)者消費(fèi)...");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
products[top] = product;
top ++;
}
public synchronized Product consume() {
Product product = null;
while (top == base) {
notify();
try {
System.out.println("倉庫已空,暫停消費(fèi)该互,等待生產(chǎn)者生產(chǎn)...");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
top--;
product = products[top];
products[top] = null;
return product;
}
}
生產(chǎn)者線程:Producer.java:
public class Producer implements Runnable {
private String produceName;
private WareHouse wareHouse;
public Producer() { }
public Producer(String produceName, WareHouse wareHouse) {
this.produceName = produceName;
this.wareHouse = wareHouse;
}
public String getProduceName() {
return produceName;
}
public void setProduceName(String produceName) {
this.produceName = produceName;
}
@Override
public void run() {
int i = 0;
int j = 0;
while (j < 100) {
i++;
j++;
Product product = new Product(i);
wareHouse.produce(product);
System.out.println(getProduceName() + "生產(chǎn)了" + product);
try {
Thread.sleep(200l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消費(fèi)者線程:Consumer.java
public class Consumer implements Runnable{
private String consumerName = null;
private WareHouse wareHouse = null;
public Consumer() { }
public Consumer(String consumerName, WareHouse wareHouse) {
this.consumerName = consumerName;
this.wareHouse = wareHouse;
}
public String getConsumerName() {
return consumerName;
}
public void setConsumerName(String consumerName) {
this.consumerName = consumerName;
}
@Override
public void run() {
int j = 0;
while (j < 100) {
j++;
System.out.println(getConsumerName() + "消費(fèi)了" + wareHouse.consume());
try {
Thread.sleep(300l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
測試類:Test.java
public class Test {
public static void main(String[] args) {
WareHouse wareHouse = new WareHouse();
Producer producer = new Producer("生產(chǎn)者",wareHouse);
Consumer consumer = new Consumer("消費(fèi)者",wareHouse);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
Lock + Condition接口的await()與signal()實(shí)現(xiàn)
和上面代碼沒太大區(qū)別米者,只是改了下倉庫類:ConditionWareHouse.java:
這里可以只用一個Condition,把signal改成signalAll()即可宇智。
public class ConditionWareHouse {
private LinkedList<Product> products = new LinkedList<>();
private static final int MAX_SIZE = 10; //倉庫容量
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public void produce(Product product) {
lock.lock();
try {
while (products.size() == MAX_SIZE) {
System.out.println("倉庫已滿蔓搞,暫停生產(chǎn),等待消費(fèi)者消費(fèi)...");
notFull.await();
}
products.add(product);
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public Product consume() {
lock.lock();
Product product = null;
try {
while (products.size() == 0) {
System.out.println("倉庫已空随橘,暫停消費(fèi)喂分,等待生產(chǎn)者生產(chǎn)...");
notEmpty.await();
}
product = products.removeLast();
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return product;
}
}
使用堵塞隊(duì)列實(shí)現(xiàn)
public class ThreadTest {
private static final int MAX_SIZE = 10; //倉庫容量
private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(MAX_SIZE);
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
new Thread(producer).start();
new Thread(consumer).start();
}
class Producer implements Runnable {
@Override
public void run() {
for(int i =0;i < 100;i++) {
try {
queue.put(i);
System.out.println("往倉庫中放入元素(" + i + ")剩余容量為:" + (MAX_SIZE - queue.size()));
Thread.sleep(50l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
for(int i =0;i < 100;i++) {
try {
queue.take();
System.out.println("從倉庫中取走元素(" + i + ")剩下元素:" + queue.size());
Thread.sleep(100l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5.volatile關(guān)鍵字詳解
深入剖析volatile關(guān)鍵字
-
volatile關(guān)鍵字的兩層語義:可見性,禁止進(jìn)行指令重排序机蔗。
使用volatile修飾的變量蒲祈,會強(qiáng)制將修改的值立即寫入主存,當(dāng)寫入的時候萝嘁,
會導(dǎo)致工作內(nèi)存緩存的變量的緩存行無效梆掸,線程會再去主存中讀取變量。 -
volatile保證原子性嗎牙言?
volatile無法保證對變量的任何操作都是原子性的酸钦,比如自增操作不是原子性的,
可以通過synchronized和Lock來實(shí)現(xiàn)嬉挡,另外jdk 1.5在java.util.concurrent.atomic包下
提供了一些原子操作類钝鸽,比如自增AtomicInteger汇恤。 -
volatile能保證有序性嗎?volatile關(guān)鍵字禁止指令重排序有兩層意思:
1.當(dāng)程序執(zhí)行到volatile變量的讀寫操作拔恰,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行因谎,
且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進(jìn)行颜懊;
2.在進(jìn)行指令優(yōu)化時财岔,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,
也不能把volatile變量后面的語句放到其前面執(zhí)行河爹。
volatile的原理與實(shí)現(xiàn)機(jī)制
摘自:《深入理解Java虛擬機(jī)》:
"觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時所生成的匯編代碼發(fā)現(xiàn)匠璧,加入volatile關(guān)鍵
字時,會多出一個lock前綴指令” lock前綴指令實(shí)際上相當(dāng)于一個內(nèi)存屏障(也成內(nèi)存柵欄)咸这,
內(nèi)存屏障會提供3個功能:
1)它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置夷恍,也不會把前面
的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時媳维,在它前面的操作已經(jīng)全部完成酿雪;
2)它會強(qiáng)制將對緩存的修改操作立即寫入主存;
3)如果是寫操作侄刽,它會導(dǎo)致其他CPU中對應(yīng)的緩存行無效指黎。
volatile關(guān)鍵字的使用場景
狀態(tài)量標(biāo)記:
//狀態(tài)量標(biāo)記
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
//多線程
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
雙重校驗(yàn)鎖
class Singleton{
private volatile static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
MusicTime:
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="http://music.163.com/outchain/player?type=2&id=344719&auto=1&height=66"></iframe>
本文內(nèi)容部分摘自: