??一個(gè)任務(wù)通常就是一個(gè)程序似扔,每個(gè)運(yùn)行中的程序就是一個(gè)進(jìn)程逛尚。當(dāng)一個(gè)程序運(yùn)行時(shí)垄惧,內(nèi)部可能包含了多個(gè)順序執(zhí)行流,每個(gè)順序執(zhí)行流就是一個(gè)線程绰寞。
??
進(jìn)程
定義:
??當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí)到逊,即變成一個(gè)進(jìn)程。進(jìn)程是處于運(yùn)行過程中的程序滤钱,并且具有一定的獨(dú)立功能觉壶,進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。
進(jìn)程的特點(diǎn):
獨(dú)立性:是系統(tǒng)獨(dú)立存在的實(shí)體菩暗,擁有自己獨(dú)立的資源掰曾,有自己私有的地址空間。在沒有經(jīng)過進(jìn)程本身允許的情況下停团,一個(gè)用戶的進(jìn)程不可以直接訪問其他進(jìn)程的地址空間旷坦。
動(dòng)態(tài)性:進(jìn)程與程序的區(qū)別在于:程序只是一個(gè)靜態(tài)的指令集合,而進(jìn)程是一個(gè)正在系統(tǒng)中活動(dòng)的指令集和佑稠,進(jìn)程中加入了時(shí)間的概念秒梅。進(jìn)程具有自己的生命周期和不同的狀態(tài),這些都是程序不具備的舌胶。
并發(fā)性:多個(gè)進(jìn)程可以在單個(gè)處理器上并發(fā)執(zhí)行捆蜀,多個(gè)進(jìn)程之間不會(huì)相互影響。
??
并行性和并發(fā)性
??并行:指在同一時(shí)刻幔嫂,有多條指令在多個(gè)處理上同時(shí)執(zhí)行辆它。(多核同時(shí)工作)
??并發(fā):指在同一時(shí)刻只能有一條指令執(zhí)行,但多個(gè)進(jìn)程指令被快速輪換執(zhí)行履恩,使得在宏觀上具有多個(gè)進(jìn)程同時(shí)執(zhí)行的效果锰茉。(單核在工作,單核不停輪詢)
??
線程
??多線程擴(kuò)展了多進(jìn)程的概念切心,使得同一個(gè)進(jìn)程可以同時(shí)并發(fā)處理多個(gè)任務(wù)飒筑。
??線程(Thread)也被成為輕量級的進(jìn)程片吊,線程是進(jìn)程執(zhí)行的單元,線程在程序中是獨(dú)立的协屡、并發(fā)的執(zhí)行流
??當(dāng)進(jìn)程被初始化后俏脊,主線程就被創(chuàng)建了。絕大數(shù)應(yīng)用程序只需要有一個(gè)主線程肤晓,但也可以在進(jìn)程內(nèi)創(chuàng)建多條的線程爷贫,每個(gè)線程也是相互獨(dú)立的。
??一個(gè)進(jìn)程可以擁有多個(gè)線程补憾,一個(gè)線程必須有一個(gè)父進(jìn)程沸久。
??線程可以擁有自己的堆棧、自己的程序計(jì)數(shù)器和自己的局部變量余蟹,但不擁有系統(tǒng)資源卷胯,它與父進(jìn)程的其他線程共享該進(jìn)程所擁有的全部資源,因此編程更加方便威酒。
??線程是獨(dú)立運(yùn)行的窑睁,它并不知道進(jìn)程中是否還有其他的線程存在。線程的執(zhí)行是搶占式的葵孤,即:當(dāng)前運(yùn)行的線程在任何時(shí)候都有可能被掛起担钮,以便另外一個(gè)線程可以運(yùn)行。
??一個(gè)線程可以創(chuàng)建和撤銷另一個(gè)線程尤仍,同一個(gè)進(jìn)程中多個(gè)線程之間可以并發(fā)執(zhí)行箫津。
??線程的調(diào)度和管理由進(jìn)程本身負(fù)責(zé)完成。
??歸納而言:操作系統(tǒng)可以同時(shí)執(zhí)行多個(gè)任務(wù)宰啦,每個(gè)任務(wù)就是進(jìn)程苏遥;進(jìn)程可以同時(shí)執(zhí)行多個(gè)任務(wù),每個(gè)任務(wù)就是線程
??
多線程的優(yōu)點(diǎn):
進(jìn)程之間不能共享內(nèi)存赡模,但線程之間共享內(nèi)存非常容易
系統(tǒng)創(chuàng)建進(jìn)程要為該進(jìn)程重新分配系統(tǒng)資源田炭,但創(chuàng)建線程的代價(jià)則小得多。因此多線程實(shí)現(xiàn)多任務(wù)并發(fā)比多線程的效率高漓柑。
Java語言內(nèi)置了多線程功能支撐教硫,簡化了多線程的編程。
??
??
線程的創(chuàng)建和啟動(dòng)
一辆布、繼承Thread類創(chuàng)建線程類
步驟:
① 定義Thread類的子類瞬矩,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務(wù)锋玲,稱為線程執(zhí)行體
② 創(chuàng)建Thread子類的實(shí)例景用,即創(chuàng)建了線程對象
③ 調(diào)用線程對象的start()方法來啟動(dòng)該線程
示例:
// 通過繼承Thread類來創(chuàng)建線程類
public class FirstThread extends Thread
{
private int i ;
// 重寫run方法,run方法的方法體就是線程執(zhí)行體
public void run()
{
for ( ; i < 100 ; i++ )
{
// 當(dāng)線程類繼承Thread類時(shí)嫩絮,直接使用this即可獲取當(dāng)前線程
// Thread對象的getName()返回當(dāng)前該線程的名字
// 因此可以直接調(diào)用getName()方法返回當(dāng)前線程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 調(diào)用Thread的currentThread方法獲取當(dāng)前線程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
// 創(chuàng)建丛肢、并啟動(dòng)第一條線程
new FirstThread().start();
// 創(chuàng)建、并啟動(dòng)第二條線程
new FirstThread().start();
}
}
}
}
注意點(diǎn):
① 當(dāng)Java程序開始運(yùn)行后剿干,程序至少會(huì)創(chuàng)建一個(gè)主線程蜂怎,main()方法的方法體代表主線程的線程執(zhí)行體
② 當(dāng)線程類繼承Tread類時(shí),直接使用this即可以獲取當(dāng)前線程
③ 繼承Thread類創(chuàng)建線程類置尔,多個(gè)線程之間無法共享線程類的實(shí)例變量
??
二杠步、實(shí)現(xiàn)Runnable接口創(chuàng)建線程類
步驟:
① 定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法
② 創(chuàng)建Runnable實(shí)現(xiàn)類的實(shí)例榜轿,并以此實(shí)例作為Thread的target來創(chuàng)建Tread對象幽歼,該Tread對象才是真正的線程對象
// 通過實(shí)現(xiàn)Runnable接口來創(chuàng)建線程類
public class SecondThread implements Runnable
{
private int i ;
// run方法同樣是線程執(zhí)行體
public void run()
{
for ( ; i < 100 ; i++ )
{
// 當(dāng)線程類實(shí)現(xiàn)Runnable接口時(shí),
// 如果想獲取當(dāng)前線程谬盐,只能用Thread.currentThread()方法甸私。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
// 通過new Thread(target , name)方法創(chuàng)建新線程
new Thread(st , "新線程1").start();
new Thread(st , "新線程2").start();
}
}
}
}
注意點(diǎn):
① 實(shí)現(xiàn)Runnable接口創(chuàng)建線程類,必須通過Thread.currentThread()方法來獲得當(dāng)前線程對象
② 實(shí)現(xiàn)Runnable接口創(chuàng)建線程類飞傀,多個(gè)線程可以共享線程類的實(shí)例變量
??
三皇型、使用Callable和Future創(chuàng)建線程
Callable接口提供了一個(gè)call()方法,call()方法比run()方法更強(qiáng)大:
① call()方法可以由返回值
② call()方法可以聲明拋出異常
步驟:
① 創(chuàng)建Callable接口的實(shí)現(xiàn)類砸烦,并實(shí)現(xiàn)call()方法弃鸦,該call()方法作為線程執(zhí)行體,且該call()方法有返回值
② 使用FutureTask類來包裝Callable對象幢痘,該FutureTask對象封裝了該Callable對象的call()方法的返回值
③ 調(diào)用FutureTask對象的get()方法獲得子線程執(zhí)行結(jié)束的返回值
示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable接口和Future來創(chuàng)建線程
public class ThreadFuture {
//拋出異常
public static void main(String[] args) throws InterruptedException, ExecutionException {
//創(chuàng)建FutureTask對象唬格,包裝 Callable接口實(shí)例
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int sum = 0;
for(int i = 0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
//注意看這里有返回值
return sum;
});
//使用task作為 Thread類的target 來創(chuàng)建一個(gè)線程
Thread instance = new Thread(task);
//啟動(dòng)線程
instance.start();
//sleep一段時(shí)間,讓上面的線程執(zhí)行完畢
Thread.sleep(1000);
//這里可以調(diào)用task.get() 獲取上面的那個(gè)線程的返回值
System.out.println("線程返回值:"+task.get());
}
}
??
??
創(chuàng)建線程三種方式的對比:
實(shí)現(xiàn)Runnable接口颜说、Callable接口創(chuàng)建線程
優(yōu)點(diǎn):
①實(shí)現(xiàn)的是接口购岗,還可以繼承其他類
② 多個(gè)線程可以共享同一個(gè)target對象,適合多個(gè)相同的線程來處理同一份資源的情況
缺點(diǎn):
① 編程稍微復(fù)雜
② 獲取當(dāng)前線程必須用Thread.currentThread()方法來獲得
繼承Tread類創(chuàng)建線程
優(yōu)點(diǎn):
①編程簡單
② 獲取當(dāng)前線程门粪,可以直接使用this來獲得
缺點(diǎn):
① 已經(jīng)繼承了Thread類藕畔,不能繼承其他類
??
??
線程的生命周期
線程的生命周期要經(jīng)歷新建(New)、就緒(Runnable)庄拇、運(yùn)行(Running)注服、阻塞(Blocke)和死亡(Dead)5種狀態(tài)。
尤其是當(dāng)線程啟動(dòng)以后措近,它不可能一直“霸占”著CPU獨(dú)自運(yùn)行溶弟,所以CPU需要在多條線程之間切換,于是線程狀態(tài)也會(huì)多次在運(yùn)行瞭郑、阻塞之間切換辜御。
1、新建和就緒狀態(tài)
當(dāng)程序使用new
關(guān)鍵字創(chuàng)建了一個(gè)線程之后屈张,該線程就處于新建狀態(tài)擒权,此時(shí)它僅僅由Java虛擬機(jī)為其分配內(nèi)存袱巨,并且初始化其成員變量的值。此時(shí)的線程對象沒有表現(xiàn)出任何線程隊(duì)動(dòng)態(tài)特征碳抄,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體愉老。
當(dāng)線程對象調(diào)用了start()
方法之后,該線程處于就緒狀態(tài)剖效,Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器嫉入,處于這個(gè)狀態(tài)中的線程并沒有開始運(yùn)行,只是表示該線程可以運(yùn)行了璧尸,至于該線程何時(shí)開始運(yùn)行咒林,取決于JVM里線程調(diào)度器的調(diào)度。
tips:
啟動(dòng)線程使用
start()
方法爷光,而不是run()
方法垫竞,如果調(diào)用run()
方法,則run()
方法立即就會(huì)被執(zhí)行蛀序,而且在run()
方法返回之前件甥,其他線程無法并發(fā)執(zhí)行,也就是說哼拔,如果直接調(diào)用線程對象的run()
方法引有,系統(tǒng)把線程對象當(dāng)成一個(gè)普通對象,而run()
方法也是一個(gè)普通方法倦逐,而不是線程執(zhí)行體譬正。如果直接調(diào)用線程對象的
run()
方法,則run()
方法里不能直接通過getName()
方法來獲得當(dāng)前執(zhí)行線程的名字檬姥,而是需要使用Thread.currentThread()
方法先獲得當(dāng)前線程曾我,再調(diào)用線程對象的getName()
方法來獲得線程的名字。啟動(dòng)線程的正確方法是調(diào)用Thread
對象的start()
方法健民,而不是直接調(diào)用run()
方法抒巢,否則就變成單線程程序了。調(diào)用了線程的
run()
方法之后秉犹,該線程已經(jīng)不再處于新建狀態(tài)蛉谜,不要再次調(diào)用線程對象的start()
方法。
2崇堵、運(yùn)行和阻塞狀態(tài)
如果處于就緒狀態(tài)的線程獲得了CPU型诚,開始執(zhí)行run()
方法的線程執(zhí)行體,則該線程處于運(yùn)行狀態(tài)鸳劳。
但線程不可能一直處于運(yùn)行狀態(tài)狰贯,它在運(yùn)行過程中會(huì)被中斷,從而進(jìn)入一個(gè)阻塞的狀態(tài)
當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):
1涵紊、線程調(diào)用sleep()
方法主動(dòng)放棄所占用的處理器資源傍妒。
2、線程調(diào)用了一個(gè)阻塞式IO方法摸柄,在該方法返回之前颤练,該線程被阻塞。
3塘幅、線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有尿贫。
4电媳、線程在等待某個(gè)通知(notify)。
5庆亡、程序調(diào)用了線程的suspend()
方法將該線程掛起匾乓。但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法又谋。
針對上面幾種情況拼缝,當(dāng)發(fā)生如下特定的情況時(shí)可以解除上面的阻塞,讓該線程重新進(jìn)入就緒狀態(tài)彰亥。
1咧七、調(diào)用sleep()
方法的線程經(jīng)過了指定時(shí)間。
2任斋、線程調(diào)用的阻塞式IO方法已經(jīng)返回继阻。
3、 線程成功地獲得了試圖取得的同步監(jiān)視器废酷。
4瘟檩、 線程正在等待某個(gè)通知時(shí),其他線程發(fā)出了一個(gè)通知澈蟆。
5墨辛、處于掛起狀態(tài)的線程被調(diào)用了resume()
恢復(fù)方法。
從圖中可以看出趴俘,線程從阻塞狀態(tài)只能進(jìn)入就緒狀態(tài)睹簇,無法直接進(jìn)入運(yùn)行狀態(tài)。
而就緒和運(yùn)行狀態(tài)之間的轉(zhuǎn)換通常不受程序控制寥闪,而是由系統(tǒng)線程調(diào)度所決定带膀。
當(dāng)處于就緒狀態(tài)的線程獲得處理器資源時(shí),該線程進(jìn)入運(yùn)行狀態(tài)橙垢;當(dāng)處于運(yùn)行狀態(tài)的線程失去處理器資源時(shí)垛叨,該線程進(jìn)入就緒狀態(tài)。
但有一個(gè)方法例外,調(diào)用yield()
方法可以讓運(yùn)行狀態(tài)的線程轉(zhuǎn)入就緒狀態(tài)嗽元。
線程死亡
線程會(huì)以如下三種方式結(jié)束敛纲,結(jié)束后就處于死亡狀態(tài)。
run()
或call()
方法執(zhí)行完成剂癌,線程正常結(jié)束淤翔。線程拋出一個(gè)未捕獲的
Exception
或Error
。直接調(diào)用該線程的
stop()
方法來結(jié)束該線程——該方法容易導(dǎo)致死鎖佩谷,通常不推薦使用旁壮。
tips:
1、當(dāng)主線程結(jié)束時(shí)谐檀,其他線程不受任何影響抡谐,并不會(huì)隨之結(jié)束。一旦子線程啟動(dòng)起來后桐猬,它就擁有和主線程相同的地位麦撵,它不會(huì)受主線程的影響。
2溃肪、為了測試某個(gè)線程是否已經(jīng)死亡免胃,可以調(diào)用線程對象的isAlive()
方法,當(dāng)線程處于就緒惫撰、運(yùn)行羔沙、阻塞三種狀態(tài)時(shí),該方法將返回true
厨钻;當(dāng)線程處于新建撬碟、死亡兩種狀態(tài)時(shí),該方法將返回false
莉撇。
3呢蛤、不要試圖對一個(gè)已經(jīng)死亡的線程調(diào)用start()
方法使它重新啟動(dòng),死亡就是死亡棍郎,該線程將不可再次作為線程執(zhí)行其障。在線程已經(jīng)死亡的情況下再次調(diào)用start()
方法將會(huì)引發(fā)IIIegalThreadException
異常。
4涂佃、不能對死亡的線程調(diào)用start()
方法励翼,程序只能對新建狀態(tài)的線程調(diào)用start()方法,對新建的線程兩次調(diào)用start()方法也是錯(cuò)誤的辜荠,會(huì)引發(fā)IIIegalThreadStateException
異常汽抚。
??
控制線程
1、join
線程
Thread
提供了讓一個(gè)線程等待另一個(gè)線程完成的方法:join()
方法伯病。當(dāng)在某個(gè)程序執(zhí)行流中調(diào)用其他線程的join()
方法時(shí)造烁,調(diào)用線程將被阻塞,直到被join()
方法加入的join
線程執(zhí)行完為止。
join()
方法通常由使用線程的程序調(diào)用惭蟋,以將大問題劃分成許多小問題苗桂,每個(gè)小問題分配一個(gè)線程。當(dāng)所有的小問題都得到處理后告组,再調(diào)用主線程來進(jìn)一步操作煤伟。
代碼示例:
public class JoinThread extends Thread
{
// 提供一個(gè)有參數(shù)的構(gòu)造器,用于設(shè)置該線程的名字
public JoinThread(String name)
{
super(name);
}
// 重寫run()方法木缝,定義線程執(zhí)行體
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception
{
// 啟動(dòng)子線程
new JoinThread("新線程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的線程");
jt.start();
// main線程調(diào)用了jt線程的join()方法便锨,main線程必須等jt執(zhí)行結(jié)束才會(huì)向下執(zhí)行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
2解取、后臺(tái)線程
有一種線程杠输,它是在后臺(tái)運(yùn)行的,它的任務(wù)是為其他的線程提供服務(wù)帚稠,這種線程被稱為“后臺(tái)線程(Daemon Thread)”怎囚,又稱為“守護(hù)線程”或“精靈線程”卿叽。JVM的垃圾回收線程就是典型的后臺(tái)線程桥胞。
后臺(tái)線程有個(gè)特征:如果所有的前臺(tái)線程都死亡恳守,后臺(tái)線程會(huì)自動(dòng)死亡。
調(diào)用Thread
對象的setDaemon(true)
方法可將指定線程設(shè)置成后臺(tái)線程贩虾。
tips:
1催烘、Thread
類還提供了一個(gè)isDaemon()
方法,用于判斷指定線程是否為后臺(tái)線程缎罢。
2伊群、前臺(tái)線程創(chuàng)建的子線程默認(rèn)是前臺(tái)線程,后臺(tái)線程子線程默認(rèn)是后臺(tái)線程策精。
3舰始、前臺(tái)線程死亡后,JVM會(huì)通知后臺(tái)線程死亡咽袜,但從它接收指令到做出響應(yīng)丸卷,需要一定時(shí)間。
而且要將某個(gè)線程設(shè)置為后臺(tái)線程询刹,必須在該線程啟動(dòng)之前設(shè)置谜嫉,也就是說,setDaemon(true)
必須在start()
方法之前調(diào)用凹联,否則會(huì)引發(fā)llegalThreadStateException
異常沐兰。
3、線程睡眠:sleep
如果需要讓當(dāng)前正在執(zhí)行的線程暫停一段時(shí)間蔽挠,并進(jìn)入阻塞狀態(tài)住闯,則可以通過調(diào)用Thread
類的靜態(tài)sleep()
方法來實(shí)現(xiàn)。
4、線程讓步yield()
yield()
方法是一個(gè)和sleep()
方法有點(diǎn)相似的方法寞秃,它也是Thread
類提供的一個(gè)靜態(tài)方法斟叼,它也可以讓當(dāng)前正在執(zhí)行的線程暫停,但它不會(huì)阻塞該線程春寿,它只是將該線程轉(zhuǎn)入就緒狀態(tài)朗涩。
yield()
只是讓當(dāng)前線程暫停一下,讓系統(tǒng)的線程調(diào)度器重新調(diào)度一次绑改,完全可能的情況是:當(dāng)某個(gè)線程調(diào)用了yield()
方法暫停之后谢床,線程調(diào)度器又將其調(diào)度出來重新執(zhí)行。
實(shí)際上厘线,當(dāng)某個(gè)線程調(diào)用了yield()
方法暫停之后识腿,只有優(yōu)先級與當(dāng)前線程相同,或者優(yōu)先級比當(dāng)前線程更高的處于就緒狀態(tài)的線程才會(huì)獲得執(zhí)行的機(jī)會(huì)造壮。
關(guān)于sleep()
方法和yield()
方法的區(qū)別如下
sleep()
方法暫停當(dāng)前線程后渡讼,會(huì)給其他線程執(zhí)行機(jī)會(huì),不會(huì)理會(huì)其他線程的優(yōu)先級耳璧;但yield()
方法只會(huì)給優(yōu)先級相同成箫,或優(yōu)先級更高的線程執(zhí)行機(jī)會(huì)。sleep()
方法會(huì)將線程轉(zhuǎn)入阻塞狀態(tài)旨枯,直到經(jīng)過阻塞時(shí)間才會(huì)轉(zhuǎn)入就緒狀態(tài)蹬昌;而yield()
不會(huì)將線程轉(zhuǎn)入阻塞狀態(tài),它只是強(qiáng)制當(dāng)前線程進(jìn)入就緒狀態(tài)攀隔。因此完全有可能某個(gè)線程調(diào)用yield()
方法暫停之后皂贩,立即再次獲得處理器資源被執(zhí)行。sleep()
方法聲明拋出了InterruptedException
異常昆汹,所以調(diào)用sleep()
方法時(shí)要么捕捉該異常明刷,要么顯式聲明拋出該異常;而yield()
方法則沒有聲明拋出任何異常满粗。sleep()
方法比yield()
方法有更好的可移植性辈末,通常不建議使用yield()
方法來控制并發(fā)線程的執(zhí)行。
5败潦、改變線程優(yōu)先級
通過Thread
類提供的setPriority(int newPriority)
本冲、getPriority()
方法來設(shè)置和返回指定線程的優(yōu)先級。
setPriority()
方法的參數(shù)可以是一個(gè)整數(shù)劫扒,范圍是1~10之間檬洞,也可以使用Thread
類的如下三個(gè)靜態(tài)常量。
MAXPRIORITY:其值是10沟饥。
MIN PRIORITY:其值是1添怔。
NORM_PRIORITY:其值是5湾戳。
??
線程同步
為了解決多個(gè)線程訪問同一個(gè)數(shù)據(jù)時(shí),會(huì)出現(xiàn)問題广料,因此需要進(jìn)行線程同步砾脑。就像前面介紹的文件并發(fā)訪問,當(dāng)有兩個(gè)進(jìn)程并發(fā)修改同一個(gè)文件時(shí)就有可能造成異常艾杏。
1韧衣、同步代碼塊
為了解決線程同步問題,Java的多線程支持引入了同步監(jiān)視器來解決這個(gè)問題购桑,使用同步監(jiān)視器的通用方法就是同步代碼塊畅铭。同步代碼塊的語法格式如下:
synchronized(obj)
{.....
//此處的代碼就是同步代碼塊
}
上面語法格式中synchronized后括號里的obj就是同步監(jiān)視器,上面代碼的含義是:線程開始執(zhí)行同步代碼塊之前勃蜘,必須先獲得對同步監(jiān)視器的鎖定硕噩。
任何時(shí)刻只能有一個(gè)線程可以獲得對同步監(jiān)視器的鎖定,當(dāng)同步代碼塊執(zhí)行完成后缭贡,該線程會(huì)釋放對該同步監(jiān)視器的鎖定炉擅。
通常推薦使用可能被并發(fā)訪問的共享資源充當(dāng)同步監(jiān)視器,代碼示例如下:
public class DrawThread extends Thread
{
// 模擬用戶賬戶
private Account account;
// 當(dāng)前取錢線程所希望取的錢數(shù)
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 當(dāng)多條線程修改同一個(gè)共享數(shù)據(jù)時(shí)阳惹,將涉及數(shù)據(jù)安全問題谍失。
public void run()
{
// 使用account作為同步監(jiān)視器,任何線程進(jìn)入下面同步代碼塊之前穆端,
// 必須先獲得對account賬戶的鎖定——其他線程無法獲得鎖袱贮,也就無法修改它
// 這種做法符合:“加鎖 → 修改 → 釋放鎖”的邏輯
synchronized (account)
{
// 賬戶余額大于取錢數(shù)目
if (account.getBalance() >= drawAmount)
{
// 吐出鈔票
System.out.println(getName()
+ "取錢成功仿便!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余額為: " + account.getBalance());
}
else
{
System.out.println(getName() + "取錢失斕鍐!余額不足嗽仪!");
}
}
// 同步代碼塊結(jié)束荒勇,該線程釋放同步鎖
}
}
??
2、同步方法
同步方法就是使用synchronized
關(guān)鍵字來修飾某個(gè)方法闻坚,則該方法稱為同步方法沽翔。
對于synchronized
修飾的實(shí)例方法(非static方法)而言,無須顯式指定同步監(jiān)視器窿凤,同步方法的同步監(jiān)視器是this
仅偎,也就是調(diào)用該方法的對象。
通過使用同步方法可以非常方便地實(shí)現(xiàn)線程安全的類雳殊,線程安全的類具有如下特征橘沥。
該類的對象可以被多個(gè)線程安全地訪問。
每個(gè)線程調(diào)用該對象的任意方法之后都將得到正確結(jié)果夯秃。
每個(gè)線程調(diào)用該對象的任意方法之后座咆,該對象狀態(tài)依然保持合理狀態(tài)痢艺。
代碼示例:
public class Account
{
// 封裝賬戶編號、賬戶余額兩個(gè)成員變量
private String accountNo;
private double balance;
public Account(){}
// 構(gòu)造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改介陶,所以只為balance提供getter方法堤舒,
public double getBalance()
{
return this.balance;
}
// 提供一個(gè)線程安全draw()方法來完成取錢操作
public synchronized void draw(double drawAmount)
{
// 賬戶余額大于取錢數(shù)目
if (balance >= drawAmount)
{
// 吐出鈔票
System.out.println(Thread.currentThread().getName()
+ "取錢成功!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
balance -= drawAmount;
System.out.println("\t余額為: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取錢失敳肝亍舌缤!余額不足!");
}
}
// 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
上面程序中增加了一個(gè)代表取錢的draw()
方法某残,并使用了synchronized
關(guān)鍵字修飾該方法友驮,把該方法變成同步方法。
該同步方法的同步監(jiān)視器是this
驾锰,因此對于同一個(gè)Account
賬戶而言卸留,任意時(shí)刻只能有一個(gè)線程獲得對Account
對象的鎖定,然后進(jìn)入draw()
方法執(zhí)行取錢操作椭豫,這樣也可以保證多個(gè)線程并發(fā)取錢的線程安全耻瑟。
3、釋放同步監(jiān)視器的鎖定
程序無法顯式釋放對同步監(jiān)視器的鎖定赏酥,線程會(huì)在如下情況下釋放對同步監(jiān)視器的鎖定喳整。
當(dāng)前線程的同步方法、同步代碼塊執(zhí)行結(jié)束裸扶,當(dāng)前線程即釋放同步監(jiān)視器框都。
當(dāng)前線程在同步代碼塊、同步方法中遇到
break
呵晨、return
終止了該代碼塊魏保、該方法的繼續(xù)執(zhí)行,當(dāng)前線程將會(huì)釋放同步監(jiān)視器摸屠。當(dāng)前線程在同步代碼塊谓罗、同步方法中出現(xiàn)了未處理的Error 或Exception,導(dǎo)致了該代碼塊季二、該方法異常結(jié)束時(shí)檩咱,當(dāng)前線程將會(huì)釋放同步監(jiān)視器。
當(dāng)前線程執(zhí)行同步代碼塊或同步方法時(shí)胯舷,程序執(zhí)行了同步監(jiān)視器對象的wait0方法刻蚯,則當(dāng)前線程暫停,并釋放同步監(jiān)視器桑嘶。
??
在如下所示的情況下炊汹,線程不會(huì)釋放同步監(jiān)視器:
線程執(zhí)行同步代碼塊或同步方法時(shí),程序調(diào)用
Thread.sleep()
不翩、Thread.yield()
方法來暫停當(dāng)前線程的執(zhí)行兵扬,當(dāng)前線程不會(huì)釋放同步監(jiān)視器麻裳。線程執(zhí)行同步代碼塊時(shí),其他線程調(diào)用了該線程的
suspend()
方法將該線程掛起器钟,該線程不會(huì)釋放同步監(jiān)視器津坑。當(dāng)然,程序應(yīng)該盡量避免使用suspend()
和resume()
方法來控制線程傲霸。
4疆瑰、同步鎖(Lock)
Lock、ReadWriteLock是Java5提供的兩個(gè)根接口昙啄,并為Lock提供ReentrantLock(可重入鎖)實(shí)現(xiàn)類穆役,為ReadWriteLock提供了ReentrantReadWriteLock 實(shí)現(xiàn)類。
Java8新增了新型的StampedLock類梳凛,在大多數(shù)場景中它可以替代傳統(tǒng)的ReentrantReadWriteLock耿币。
ReentrantReadWriteLock為讀寫操作提供了三種鎖模式:Writing、ReadingOptimistic韧拒、Reading淹接。
在實(shí)現(xiàn)線程安全的控制中,比較常用的是ReentrantLock(可重入鎖)叛溢。使用該Lock對象可以顯式地加鎖塑悼、釋放鎖,通常使用ReentrantLock的代碼格式如下:
public class Account
{
// 定義鎖對象
private final ReentrantLock lock = new ReentrantLock();
// 封裝賬戶編號楷掉、賬戶余額的兩個(gè)成員變量
private String accountNo;
private double balance;
public Account(){}
// 構(gòu)造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改厢蒜,所以只為balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一個(gè)線程安全draw()方法來完成取錢操作
public void draw(double drawAmount)
{
// 加鎖
lock.lock();
try
{
// 賬戶余額大于取錢數(shù)目
if (balance >= drawAmount)
{
// 吐出鈔票
System.out.println(Thread.currentThread().getName()
+ "取錢成功烹植!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
balance -= drawAmount;
System.out.println("\t余額為: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取錢失敯哐弧!余額不足刊橘!");
}
}
finally
{
// 修改完成鄙才,釋放鎖
lock.unlock();
}
}
// 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
同步方法或同步代碼塊使用與競爭資源相關(guān)的颂鸿、隱式的同步監(jiān)視器促绵,并且強(qiáng)制要求加鎖和釋放鎖要出現(xiàn)在一個(gè)塊結(jié)構(gòu)中,而且當(dāng)獲取了多個(gè)鎖時(shí)嘴纺,它們必須以相反的順序釋放败晴,且必須在與所有鎖被獲取時(shí)相同的范圍內(nèi)釋放所有鎖。
Lock提供了同步方法和同步代碼塊所沒有的其他功能栽渴,包括用于非塊結(jié)構(gòu)的tryLock()
方法尖坤,以及試圖獲取可中斷鎖的lockInterruptibly()
方法,還有獲取超時(shí)失效鎖的tryLock(long闲擦,TimeUnit)
方法慢味。
ReentrantLock
鎖具有可重入性场梆,也就是說,一個(gè)線程可以對已被加鎖的ReentrantLock
鎖再次加鎖纯路,ReentrantLock
對象會(huì)維持一個(gè)計(jì)數(shù)器來追蹤lock()
方法的嵌套調(diào)用或油,線程在每次調(diào)用lock()
加鎖后,必須顯式調(diào)用unlock()
來釋放鎖驰唬,所以一段被鎖保護(hù)的代碼可以調(diào)用另一個(gè)被相同鎖保護(hù)的方法顶岸。
死鎖
當(dāng)兩個(gè)線程相互等待對方釋放同步監(jiān)視器時(shí)就會(huì)發(fā)生死鎖一旦出現(xiàn)死鎖,整個(gè)程序既不會(huì)發(fā)生任何異常叫编,也不會(huì)給出任何提示辖佣,只是所有線程處于阻塞狀態(tài),無法繼續(xù)搓逾。
死鎖示例:
有兩個(gè)類 A 和 B 卷谈,這兩個(gè)類每個(gè)類都各含有兩個(gè)同步方法,利用兩個(gè)線程來進(jìn)行操作霞篡。
首先線程1調(diào)用 A 類的同步方法 A1雏搂,然后休眠,此時(shí)線程2會(huì)開始工作寇损,它會(huì)調(diào)用 B 類的同步方法 B1凸郑,然后也休眠。
此時(shí)線程1休眠結(jié)束矛市,它繼續(xù)執(zhí)行方法 A1 芙沥,A1的下一步操作是調(diào)用 B 中的同步方法 B2,因?yàn)榇藭r(shí) B 的對象示例正被線程2所占據(jù)浊吏,因此線程1只能等待對 B 的鎖的釋放而昨。
此時(shí)線程2又蘇醒了,它繼續(xù)執(zhí)行方法 B1找田,B1的下一步操作是調(diào)用 A 中的同步方法 A2歌憨,因此是 A 類的對象也被線程1給鎖住了,因此線程2也只能等待墩衙,這樣就造成了線程1和線程2相互等待务嫡,從而導(dǎo)致了死鎖的發(fā)生。
代碼示例:
//A類
class A
{
public synchronized void foo( B b )
{
System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
+ " 進(jìn)入了A實(shí)例的foo()方法" ); // ①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
+ " 企圖調(diào)用B實(shí)例的last()方法"); // ③
b.last();
}
public synchronized void last()
{
System.out.println("進(jìn)入了A類的last()方法內(nèi)部");
}
}
//B類
class B
{
public synchronized void bar( A a )
{
System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
+ " 進(jìn)入了B實(shí)例的bar()方法" ); // ②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("當(dāng)前線程名: " + Thread.currentThread().getName()
+ " 企圖調(diào)用A實(shí)例的last()方法"); // ④
a.last();
}
public synchronized void last()
{
System.out.println("進(jìn)入了B類的last()方法內(nèi)部");
}
}
//線程類
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主線程");
// 調(diào)用a對象的foo方法
a.foo(b);
System.out.println("進(jìn)入了主線程之后");
}
public void run()
{
Thread.currentThread().setName("副線程");
// 調(diào)用b對象的bar方法
b.bar(a);
System.out.println("進(jìn)入了副線程之后");
}
//主函數(shù)
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl為target啟動(dòng)新線程
new Thread(dl).start();
// 調(diào)用init()方法
dl.init();
}
}
??
線程通信
1漆改、傳統(tǒng)的線程通信——通過Object
類提供的方法實(shí)現(xiàn)
借助于Object
類提供的wait()
心铃、notify()
和notifyAll()
三個(gè)方法。
這三個(gè)方法并不屬于Thread
類挫剑,而是屬于Object
類去扣。但這三個(gè)方法必須由同步監(jiān)視器對象來調(diào)用,這可分成以下兩種情況樊破。
對于使用synchronized修飾的同步方法愉棱,因?yàn)樵擃惖哪J(rèn)實(shí)例(this)就是同步監(jiān)視器唆铐,所以可以在同步方法中直接調(diào)用這三個(gè)方法。
對于使用synchronized修飾的同步代碼塊奔滑,同步監(jiān)視器是synchronized后括號里的對象或链,所以必須使用該對象調(diào)用這三個(gè)方法。
關(guān)于這三個(gè)方法的解釋如下:
wait()
:導(dǎo)致當(dāng)前線程等待档押,直到其他線程調(diào)用該同步監(jiān)視器的notify()
方法或notifyAll()
方法來喚醒該線程澳盐。notify()
:喚醒在此同步監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此同步監(jiān)視器上等待令宿,則會(huì)選擇喚醒其中一個(gè)線程叼耙。選擇是任意性的。只有當(dāng)前線程放棄對該同步監(jiān)視器的鎖定后(使用wait()
方法)粒没,才可以執(zhí)行被喚醒的線程筛婉。notifyAll
:喚醒在此同步監(jiān)視器上等待的所有線程。只有當(dāng)前線程放棄對該同步監(jiān)視器的鎖定后癞松,才可以執(zhí)行被喚醒的線程爽撒。
使用Condition
控制線程通信
如果程序不使用synchronized 關(guān)鍵字來保證同步,而是直接便用Lock對象采保證同步响蓉,則系統(tǒng)中下存在隱式的同步監(jiān)視器硕勿,也就不能使用wait()
、notify()
枫甲、notifyAll()
方法進(jìn)行線程通信了源武。
當(dāng)使用Lock 對象來保證同步時(shí),Java提供了一個(gè)Condition
類來保持協(xié)調(diào)想幻,使用Condition
可以讓那些已經(jīng)得到Lock對象卻無法繼續(xù)執(zhí)行的線程釋放Lock
對象粱栖,Condition
對象也可以喚醒其他處于等待的線程。
Condition
實(shí)例被綁定在一個(gè)Lock
對象上脏毯。要獲得特定Lock
實(shí)例的Condition
實(shí)例闹究,調(diào)用Lock
對象的newCondition()
方法即可。Condition
類提供了如下三個(gè)方法:
await()
:類似于隱式同步監(jiān)視器上的wait()
方法食店,導(dǎo)致當(dāng)前線程等待渣淤,直到其他線程調(diào)用該Condition
的signal()
方法或signalAll()
方法來喚醒該線程。signal()
:喚醒在此Lock
對象上等待的單個(gè)線程叛买。如果所有線程都在該Lock
對象上等待砂代,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的率挣。只有當(dāng)前線程放棄對該Lock
對象的鎖定后(使用await()
方法),才可以執(zhí)行被喚醒的線程露戒。signalAIl()
:喚醒在此Lock
對象上等待的所有線程椒功。只有當(dāng)前線程放棄對該Lock
對象的鎖定后捶箱,才可以執(zhí)行被喚醒的線程。
public class Account
{
// 顯式定義Lock對象
private final Lock lock = new ReentrantLock();
// 獲得指定Lock對象對應(yīng)的Condition
private final Condition cond = lock.newCondition();
// 封裝賬戶編號动漾、賬戶余額的兩個(gè)成員變量
private String accountNo;
private double balance;
// 標(biāo)識(shí)賬戶中是否已有存款的旗標(biāo)
private boolean flag = false;
public Account(){}
// 構(gòu)造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改丁屎,所以只為balance提供getter方法,
public double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)
{
// 加鎖
lock.lock();
try
{
// 如果flag為假旱眯,表明賬戶中還沒有人存錢進(jìn)去晨川,取錢方法阻塞
if (!flag)
{
cond.await();
}
else
{
// 執(zhí)行取錢
System.out.println(Thread.currentThread().getName()
+ " 取錢:" + drawAmount);
balance -= drawAmount;
System.out.println("賬戶余額為:" + balance);
// 將標(biāo)識(shí)賬戶是否已有存款的旗標(biāo)設(shè)為false。
flag = false;
// 喚醒其他線程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally塊來釋放鎖
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
// 如果flag為真删豺,表明賬戶中已有人存錢進(jìn)去共虑,則存錢方法阻塞
if (flag) // ①
{
cond.await();
}
else
{
// 執(zhí)行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("賬戶余額為:" + balance);
// 將表示賬戶是否已有存款的旗標(biāo)設(shè)為true
flag = true;
// 喚醒其他線程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally塊來釋放鎖
finally
{
lock.unlock();
}
}
// 下面兩個(gè)方法根據(jù)accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
使用阻塞隊(duì)列(BlockingQueue)控制線程通信
Java5提供了一個(gè)BlockingQueue
接口,雖然BlockingQueue
也是Queue
的子接口呀页,但它的主要用途并不是作為容器妈拌,而是作為線程同步的工具。
BlockingQueue
具有一個(gè)特征:
當(dāng)生產(chǎn)者線程試圖向BlockingOueue
中放入元素時(shí)蓬蝶,如果該隊(duì)列已滿尘分,則該線程被阻塞;
當(dāng)消費(fèi)者線程試圖從BlockingQueue
中取出元素時(shí)丸氛,如果該隊(duì)列已空培愁,則該線程被阻塞。
BlockingQueue
提供如下兩個(gè)支持阻塞的方法缓窜。
put(E e):嘗試把E元素放入
BlockingQueue
中竭钝,如果該隊(duì)列的元素已滿,則阻塞該線程雹洗。take()
:嘗試從BlockingQueue
的頭部取出元素香罐,如果該隊(duì)列的元素已空,則阻塞該線程时肿。
BlockingQueue
繼承了Queue
接口庇茫,當(dāng)然也可使用Queue
接口中的方法。這些方法歸納起來可分為如下三組螃成。
在隊(duì)列尾部插入元素旦签。包括
add(E e)
、offer(E e)
和put(Ee)
方法寸宏,當(dāng)該隊(duì)列已滿時(shí)宁炫,這三個(gè)方法分別會(huì)拋出異常、返回false氮凝、阻塞隊(duì)列羔巢。在隊(duì)列頭部刪除并返回刪除的元素。包括
remove()
、poll()
和take()
方法竿秆。當(dāng)該隊(duì)列已空時(shí)启摄,這三個(gè)方法分別會(huì)拋出異常、返回false幽钢、阻塞隊(duì)列歉备。在隊(duì)列頭部取出但不刪除元素。包括
element()
和peek()
方法匪燕,當(dāng)隊(duì)列已空時(shí)蕾羊,這兩個(gè)方法分別拋出異常、返回false帽驯。
使用阻塞隊(duì)列(BlockingQueue)來實(shí)現(xiàn)線程通信龟再,以消費(fèi)者生產(chǎn)者為例:
//生產(chǎn)者類
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生產(chǎn)者準(zhǔn)備生產(chǎn)集合元素!");
try
{
Thread.sleep(200);
// 嘗試放入元素界拦,如果隊(duì)列已滿吸申,線程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生產(chǎn)完成:" + bq);
}
}
}
//消費(fèi)者類
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消費(fèi)者準(zhǔn)備消費(fèi)集合元素!");
try
{
Thread.sleep(200);
// 嘗試取出元素享甸,如果隊(duì)列已空截碴,線程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消費(fèi)完成:" + bq);
}
}
}
//主程序
public class BlockingQueueTest2
{
public static void main(String[] args)
{
// 創(chuàng)建一個(gè)容量為1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 啟動(dòng)3條生產(chǎn)者線程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 啟動(dòng)一條消費(fèi)者線程
new Consumer(bq).start();
}
}
??
線程池
系統(tǒng)啟動(dòng)一個(gè)新線程的成本是比較高的,因?yàn)樗婕芭c操作系統(tǒng)交互蛉威。在這種情形下斋否,使用線程池可以很好地提高性能浙宜,尤其是當(dāng)程序中需要?jiǎng)?chuàng)建大量生存期很短暫的線程時(shí),更應(yīng)該考慮使用線程池。
與數(shù)據(jù)庫連接池類似的是是目,線程池在系統(tǒng)啟動(dòng)時(shí)即創(chuàng)建大量空閑的線程谚攒,程序?qū)⒁粋€(gè)Runnable
對象或Callable
對象傳給線程池令境,線程池就會(huì)啟動(dòng)一個(gè)線程來執(zhí)行它們的run()
或call()
方法挣菲。
當(dāng)run()
或call()
方法執(zhí)行結(jié)束后,該線程并不會(huì)死亡栅盲,而是再次返回線程池中成為空閑狀態(tài)汪诉,等待執(zhí)行下一個(gè)Runnable
對象的run()
或call()
方法。
除此之外谈秫,使用線程池可以有效地控制系統(tǒng)中并發(fā)線程的數(shù)量扒寄,當(dāng)系統(tǒng)中包含大量并發(fā)線程時(shí),會(huì)導(dǎo)致系統(tǒng)性能劇烈下降拟烫,甚至導(dǎo)致JVM崩潰该编,而線程池的最大線程數(shù)參數(shù)可以控制系統(tǒng)中并發(fā)線程數(shù)不超過此數(shù)。
創(chuàng)建線程池
在Java5以前硕淑,開發(fā)者必須手動(dòng)實(shí)現(xiàn)自己的線程池课竣;從Java5開始嘉赎,Java內(nèi)建支持線程池。
Java5新增了一個(gè)Executors
工廠類來產(chǎn)生線程池稠氮,該工廠類包含如下幾個(gè)靜態(tài)工廠方法來創(chuàng)建線程池曹阔。
newCachedThreadPool()
:創(chuàng)建一個(gè)具有緩存功能的線程池半开,系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建線程隔披,這些線程將會(huì)被緩存在線程池中。newFixedThreadPool(int nThreads)
:創(chuàng)建一個(gè)可重用的寂拆、具有固定線程數(shù)的線程池奢米。newSingle ThreadExecutor()
:創(chuàng)建一個(gè)只有單線程的線程池,它相當(dāng)于調(diào)用newFixedThread Pool()
方法時(shí)傳入?yún)?shù)為1纠永。newScheduledThreadPool(int corePoolSize)
:創(chuàng)建具有指定線程數(shù)的線程池鬓长,它可以在指定延遲后執(zhí)行線程任務(wù)。corePoolSize
指池中所保存的線程數(shù)尝江,即使線程是空閑的也被保存在線程池內(nèi)涉波。newSingle ThreadScheduledExecutor)
:創(chuàng)建只有一個(gè)線程的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)炭序。ExecutorService new WorkStealingPool(int parallelism)
:創(chuàng)建持有足夠的線程的線程池來支持給定的并行級別啤覆,該方法還會(huì)使用多個(gè)隊(duì)列來減少競爭。ExecutorService new WorkStealingPool)
:該方法是前一個(gè)方法的簡化版本惭聂。如果當(dāng)前機(jī)器有4個(gè)CPU窗声,則目標(biāo)并行級別被設(shè)置為4,也就是相當(dāng)于為前一個(gè)方法傳入4作為參數(shù)辜纲。
上面7個(gè)方法中的前三個(gè)方法返回一個(gè)ExecutorService
對象笨觅,該對象代表一個(gè)線程池,它可以執(zhí)行Runnable
對象或Callable
對象所代表的線程耕腾;
而中間兩個(gè)方法返回一個(gè)ScheduledExecutorService
線程池见剩,它是ExecutorService
的子類,它可以在指定延遲后執(zhí)行線程任務(wù)扫俺;
最后兩個(gè)方法則是Java8新增的苍苞,這兩個(gè)方法可充分利用多CPU并行的能力。這兩個(gè)方法生成的work stealing
池牵舵,都相當(dāng)于后臺(tái)線程池柒啤,如果所有的前臺(tái)線程都死亡了,work stealing
池中的線程會(huì)自動(dòng)死亡畸颅。
ExecutorService
代表盡快執(zhí)行線程的線程池(只要線程池中有空閑線程担巩,就立即執(zhí)行線程任務(wù))
程序只要將一個(gè)Runnable
對象或Callable
對象(代表線程任務(wù))提交給該線程池,該線程池就會(huì)盡快執(zhí)行該任務(wù)没炒。
ExecutorService里提供了如下三個(gè)方法涛癌。
-
Future<?>submit(Runnable task)
:將一個(gè)Runnable
對象提交給指定的線程池,線程池將在有空閑線程時(shí)執(zhí)行Runnable
對象代表的任務(wù)。其中Future
對象代表Runnable
任務(wù)的返回值拳话,但run()
方法沒有返回值先匪,所以Future
對象將在run()
方法執(zhí)行結(jié)束后返回null
。
但可以調(diào)用Future
的isDone()
弃衍、isCancelled()
方法來獲得Runnable
對象的執(zhí)行狀態(tài)呀非。
<T>Future-T>submit(Runnable task,T result)
:將一個(gè)Runnable
對象提交給指定的線程池,線程池將在有空閑線程時(shí)執(zhí)行Runnable對象代表的任務(wù)镜盯。其中result
顯式指定線程執(zhí)行結(jié)束后的返回值岸裙,所以Future
對象將在run()
方法執(zhí)行結(jié)束后返回result
。<T>Future-T>submit(Callable<T>task)
:將一個(gè)Callable
對象提交給指定的線程池速缆,線程池將在有空閑線程時(shí)執(zhí)行Callable
對象代表的任務(wù)降允。其中Future
代表Callable
對象里call()
方法的返回值。
ScheduledExecutorService
代表可在指定延遲后或周期性地執(zhí)行線程任務(wù)的線程池艺糜,它提供了如下4個(gè)方法剧董。
ScheduledFuture<V> schedule(Callable-V> callable,long delay,TimeUnit unit)
:指定callable
任務(wù)將在delay
延遲后執(zhí)行。ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit)
:指定command
任務(wù)將在delay
延遲后執(zhí)行破停。ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
:指定command
任務(wù)將在delay延遲后執(zhí)行翅楼,而且以設(shè)定頻率重復(fù)執(zhí)行。也就是說辱挥,在initialDelay
后開始執(zhí)行犁嗅,依次在initialDelay+period、initialDelay+2*period…
處重復(fù)執(zhí)行晤碘,依此類推褂微。ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
:創(chuàng)建并執(zhí)行一個(gè)在給定初始延遲后首次啟用的定期操作,隨后在每一次執(zhí)行終止和下一次執(zhí)行開始之間都存在給定的延遲园爷。如果任務(wù)在任一次執(zhí)行時(shí)遇到異常宠蚂,就會(huì)取消后續(xù)執(zhí)行;否則童社,只能通過程序來顯式取消或終止該任務(wù)求厕。
用完一個(gè)線程池后,應(yīng)該調(diào)用該線程池的shutdown0方法扰楼,該方法將啟動(dòng)線程池的關(guān)閉序列呀癣,調(diào)用shutdown()
方法后的線程池不再接收新任務(wù),但會(huì)將以前所有已提交任務(wù)執(zhí)行完成弦赖。當(dāng)線程池中的所有任務(wù)都執(zhí)行完成后项栏,池中的所有線程都會(huì)死亡;
另外也可以調(diào)用線程池的shutdownNow()
方法來關(guān)閉線程池蹬竖,該方法試圖停止所有正在執(zhí)行的活動(dòng)任務(wù)沼沈,暫停處理正在等待的任務(wù)流酬,并返回等待執(zhí)行的任務(wù)列
表。
使用線程池來執(zhí)行線程任務(wù)的步驟如下列另。
①調(diào)用Executors
類的靜態(tài)工廠方法創(chuàng)建一個(gè)ExecutorService
對象芽腾,該對象代表一個(gè)線程池。
②創(chuàng)建Runnable
實(shí)現(xiàn)類或Callable
實(shí)現(xiàn)類的實(shí)例页衙,作為線程執(zhí)行任務(wù)摊滔。
③調(diào)用ExecutorService
對象的submit()
方法來提交Runnable
實(shí)例或Callable
實(shí)例。
④當(dāng)不想提交任何任務(wù)時(shí)拷姿,調(diào)用ExecutorService
對象的shutdown()
方法來關(guān)閉線程池惭载。
代碼示例:
public class ThreadPoolTest
{
public static void main(String[] args)
throws Exception
{
// 創(chuàng)建足夠的線程來支持4個(gè)CPU并行的線程池
// 創(chuàng)建一個(gè)具有固定線程數(shù)(6)的線程池
ExecutorService pool = Executors.newFixedThreadPool(6);
// 使用Lambda表達(dá)式創(chuàng)建Runnable對象
Runnable target = () -> {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值為:" + i);
}
};
// 向線程池中提交兩個(gè)線程
pool.submit(target);
pool.submit(target);
// 關(guān)閉線程池
pool.shutdown();
}
}
??
Java8增強(qiáng)的ForkJoinPool
Java7提供了ForkJoinPool
來支持將一個(gè)任務(wù)拆分成多個(gè)“小任務(wù)”并行計(jì)算旱函,再把多個(gè)“小任務(wù)”的結(jié)果合并成總的計(jì)算結(jié)果响巢。ForkJoinPool
是ExecutorService
的實(shí)現(xiàn)類,因此是一種特殊的線程池棒妨。
ForkJoinPool
提供了如下兩個(gè)常用的構(gòu)造器踪古。
ForkJoinPool(int parallelism)
:創(chuàng)建一個(gè)包含parallelism
個(gè)并行線程的ForkJoinPool
。ForkJoinPool()
:以Runtime.availableProcessors()
方法的返回值作為parallelism
參數(shù)來創(chuàng)建ForkJoinPool
券腔。
Java8進(jìn)一步擴(kuò)展了ForkJoinPool
的功能伏穆,Java8為ForkJoinPool
增加了通用池功能。
ForkJoinPool
通過如下兩個(gè)靜態(tài)方法提供通用池功能纷纫。
-
ForkJoinPool commonPool()
:該方法返回一個(gè)通用池枕扫。
通用池的運(yùn)行狀態(tài)不會(huì)受shutdown()
或shutdownNow()
方法的影響。當(dāng)然辱魁,如果程序直接執(zhí)行System.exit(0)
烟瞧;來終止虛擬機(jī),通用池以及通用池中正在執(zhí)行的任務(wù)都會(huì)被自動(dòng)終止染簇。
-
int getCommonPoolParallelism()
:該方法返回通用池的并行級別参滴。
創(chuàng)建了ForkJoinPool
實(shí)例之后,就可調(diào)用ForkJoinPool
的submit(ForkJoin Task task)
或invoke(ForkJoinTask task)
方法來執(zhí)行指定任務(wù)了锻弓。
其中ForkJoinTask
代表一個(gè)可以并行砾赔、合并的任務(wù)。
ForkJoinTask
是一個(gè)抽象類青灼,它還有兩個(gè)抽象子類:RecursiveAction
和Recursive Task
暴心。
其中Recursive Task
代表有返回值的任務(wù),而RecursiveAction
代表沒有返回值的任務(wù)杂拨。
下面以執(zhí)行沒有返回值的“大任務(wù)”(簡單地打印0-300的數(shù)值)為例专普,程序?qū)⒁粋€(gè)“大任務(wù)”拆分成多個(gè)“小任務(wù)”,并將任務(wù)交給ForkJoinPool
來執(zhí)行扳躬。
// 繼承RecursiveAction來實(shí)現(xiàn)"可分解"的任務(wù)
class PrintTask extends RecursiveAction
{
// 每個(gè)“小任務(wù)”只最多只打印50個(gè)數(shù)
private static final int THRESHOLD = 50;
private int start;
private int end;
// 打印從start到end的任務(wù)
public PrintTask(int start, int end)
{
this.start = start;
this.end = end;
}
@Override
protected void compute()
{
// 當(dāng)end與start之間的差小于THRESHOLD時(shí)脆诉,開始打印
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值:" + i);
}
}
else
{
// 如果當(dāng)end與start之間的差大于THRESHOLD時(shí)甚亭,即要打印的數(shù)超過50個(gè)
// 將大任務(wù)分解成兩個(gè)小任務(wù)。
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行執(zhí)行兩個(gè)“小任務(wù)”
left.fork();
right.fork();
}
}
}
/**
* description: 主函數(shù)
**/
public class ForkJoinPoolTest
{
public static void main(String[] args)
throws Exception
{
ForkJoinPool pool = new ForkJoinPool();
// 提交可分解的PrintTask任務(wù)
pool.submit(new PrintTask(0 , 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
// 關(guān)閉線程池
pool.shutdown();
}
}
上面定義的任務(wù)是一個(gè)沒有返回值的打印任務(wù)击胜,如果大任務(wù)是有返回值的任務(wù)亏狰,則可以讓任務(wù)繼承Recursive Task<T>
,其中泛型參數(shù)T就代表了該任務(wù)的返回值類型偶摔。下面程序示范了使用Recursive Task
對一個(gè)長度為100的數(shù)組的元素值進(jìn)行累加暇唾。
// 繼承RecursiveTask來實(shí)現(xiàn)"可分解"的任務(wù)
class CalTask extends RecursiveTask<Integer>
{
// 每個(gè)“小任務(wù)”只最多只累加20個(gè)數(shù)
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加從start到end的數(shù)組元素
public CalTask(int[] arr , int start, int end)
{
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute()
{
int sum = 0;
// 當(dāng)end與start之間的差小于THRESHOLD時(shí),開始進(jìn)行實(shí)際累加
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
sum += arr[i];
}
return sum;
}
else
{
// 如果當(dāng)end與start之間的差大于THRESHOLD時(shí)辰斋,即要累加的數(shù)超過20個(gè)時(shí)
// 將大任務(wù)分解成兩個(gè)小任務(wù)策州。
int middle = (start + end) / 2;
CalTask left = new CalTask(arr , start, middle);
CalTask right = new CalTask(arr , middle, end);
// 并行執(zhí)行兩個(gè)“小任務(wù)”
left.fork();
right.fork();
// 把兩個(gè)“小任務(wù)”累加的結(jié)果合并起來
return left.join() + right.join(); // ①
}
}
}
/**
* description: 主函數(shù)
**/
public class Sum
{
public static void main(String[] args)
throws Exception
{
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
// 初始化100個(gè)數(shù)字元素
for (int i = 0 , len = arr.length; i < len ; i++ )
{
int tmp = rand.nextInt(20);
// 對數(shù)組元素賦值,并將數(shù)組元素的值添加到sum總和中宫仗。
total += (arr[i] = tmp);
}
System.out.println(total);
// 創(chuàng)建一個(gè)通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交可分解的CalTask任務(wù)
Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
System.out.println(future.get());
// 關(guān)閉線程池
pool.shutdown();
}
}
??
線程相關(guān)的類
ThreadLocal
類
ThreadLocal,是Thread Local Variable(線程局部變量)的意思够挂,它就是為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本,使每一個(gè)線程都可以獨(dú)立地改變自己的副本藕夫,而不會(huì)和其他線程的副本沖突孽糖。從線程的角度看,就好像每一個(gè)線程都完全擁有該變量一樣毅贮。
它只提供了如下三個(gè)public方法办悟。
T get()
:返回此線程局部變量中當(dāng)前線程副本中的值。void remove()
:刪除此線程局部變量中當(dāng)前線程的值滩褥。void set(T value)
:設(shè)置此線程局部變量中當(dāng)前線程副本中的值病蛉。
代碼示例:
/**
* description: 賬戶類
**/
class Account
{
/* 定義一個(gè)ThreadLocal類型的變量,該變量將是一個(gè)線程局部變量
每個(gè)線程都會(huì)保留該變量的一個(gè)副本 */
private ThreadLocal<String> name = new ThreadLocal<>();
// 定義一個(gè)初始化name成員變量的構(gòu)造器
public Account(String str)
{
this.name.set(str);
// 下面代碼用于訪問當(dāng)前線程的name副本的值
System.out.println("---" + this.name.get());
}
// name的setter和getter方法
public String getName()
{
return name.get();
}
public void setName(String str)
{
this.name.set(str);
}
}
/**
* description: 線程類
**/
class MyTest extends Thread
{
// 定義一個(gè)Account類型的成員變量
private Account account;
public MyTest(Account account, String name)
{
super(name);
this.account = account;
}
public void run()
{
// 循環(huán)10次
for (int i = 0 ; i < 10 ; i++)
{
// 當(dāng)i == 6時(shí)輸出將賬戶名替換成當(dāng)前線程名
if (i == 6)
{
account.setName(getName());
}
// 輸出同一個(gè)賬戶的賬戶名和循環(huán)變量
System.out.println(account.getName() + " 賬戶的i值:" + i);
}
}
}
/**
* description: 主程序
**/
public class ThreadLocalTest
{
public static void main(String[] args)
{
// 啟動(dòng)兩條線程瑰煎,兩條線程共享同一個(gè)Account
Account at = new Account("初始名");
/*
雖然兩條線程共享同一個(gè)賬戶铺然,即只有一個(gè)賬戶名
但由于賬戶名是ThreadLocal類型的,所以每條線程
都完全擁有各自的賬戶名副本丢间,所以從i == 6之后探熔,將看到兩條
線程訪問同一個(gè)賬戶時(shí)看到不同的賬戶名。
*/
new MyTest(at , "線程甲").start();
new MyTest(at , "線程乙").start ();
}
}
??
程序結(jié)果如圖:
分析:
上面Account
類中的三行粗體字代碼分別完成了創(chuàng)建ThreadLocal
對象烘挫、從ThreadLocal
中取出線程局部變量诀艰、修改線程局部變量的操作。
由于程序中的賬戶名是一個(gè)ThreadLocal
變量饮六,所以雖然程序中只有一個(gè)Account
對象其垄,但兩個(gè)子線程將會(huì)產(chǎn)生兩個(gè)賬戶名(主線程也持有一個(gè)賬戶名的副本)。
兩個(gè)線程進(jìn)行循環(huán)時(shí)都會(huì)在i=6
時(shí)將賬戶名改為與線程名相同卤橄,這樣就可以看到兩個(gè)線程擁有兩個(gè)賬戶名的情形绿满,如圖所示。
從上面程序可以看出窟扑,實(shí)際上賬戶名有三個(gè)副本喇颁,主線程一個(gè)漏健,另外啟動(dòng)的兩個(gè)線程各一個(gè),它們的值互不干擾橘霎,每個(gè)線程完全擁有自己的ThreadLocal
變量蔫浆,這就是ThreadLocal
的用途。
ThreadLocal
和其他所有的同步機(jī)制一樣姐叁,都是為了解決多線程中對同一變量的訪問沖突瓦盛。
在普通的同步機(jī)制中,是通過對象加鎖來實(shí)現(xiàn)多個(gè)線程對同一變量的安全訪問的外潜。該變量是多個(gè)線程共享的原环,所以要使用這種同步機(jī)制,需要很細(xì)致地分析在什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫处窥,什么時(shí)候需要鎖定某個(gè)對象嘱吗,什么時(shí)候釋放該對象的鎖等。在這種情況下碧库,系統(tǒng)并沒有將這份資源復(fù)制多份柜与,只是采用了安全機(jī)制來控制對這份資源的訪問而已。
ThreadLocal
從另一個(gè)角度來解決多線程的并發(fā)訪問嵌灰,ThreadLocal
將需要并發(fā)訪問的資源復(fù)制多份,每個(gè)線程擁有一份資源颅悉,每個(gè)線程都擁有自己的資源副本沽瞭,從而也就沒有必要對該變量進(jìn)行同步了。
ThreadLocal
提供了線程安全的共享對象剩瓶,在編寫多線程代碼時(shí)驹溃,可以把不安全的整個(gè)變量封裝進(jìn)ThreadLocal
,或者把該對象與線程相關(guān)的狀態(tài)使用ThreadLocal
保存。
ThreadLocal
并不能替代同步機(jī)制延曙,兩者面向的問題領(lǐng)域不同豌鹤。同步機(jī)制是為了同步多個(gè)線程對相同資源的并發(fā)訪問,是多個(gè)線程之間進(jìn)行通信的有效方式枝缔;
而ThreadLocal
是為了隔離多個(gè)線程的數(shù)據(jù)共享布疙,從根本上避免多個(gè)線程之間對共享資源(變量)的競爭,也就不需要對多個(gè)線程進(jìn)行同步了愿卸。
通常建議:
如果多個(gè)線程之間需要共享資源灵临,以達(dá)到線程之間的通信功能,就使用同步機(jī)制趴荸;如果僅僅需要隔離多個(gè)線程之間的共享沖突儒溉,則可以使用ThreadLocal
。
??
包裝線程不安全的集合
像ArrayList
发钝、LinkedList
顿涣、HashSet
波闹、TreeSet
、HashMap
涛碑、TreeMap
等都是線程不安全的舔痪,也就是說,當(dāng)多個(gè)并發(fā)線程向這些集合中存锌唾、取元素時(shí)锄码,就可能會(huì)破壞這些集合的數(shù)據(jù)完整性。
如果程序中有多個(gè)線程可能訪問以上這些集合晌涕,就可以使用Collections
提供的類方法把這些集合包裝成線程安全的集合滋捶。Collections
提供了如下幾個(gè)靜態(tài)方法。
<T>Collection<T>synchronizedCollection(Collection<T>c)
:返回指定collection對應(yīng)的線程安全的collection余黎。static<T>List<T>synchronizedList(List<T>list)
:返回指定List對象對應(yīng)的線程安全的List對象重窟。static<K,V>Map<K,V> synchronizedMap(Map<K,V>m)
:返回指定Map對象對應(yīng)的線程安全的Map對象。static<T>Set<T>synchronizedSet(Set<T>s)
:返回指定Set對象對應(yīng)的線程安全的Set對象惧财。static<K,V>SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V>m)
:返回指定SortedMap對象對應(yīng)的線程安全的SortedMap對象巡扇。static<T>SortedSet-T>synchronizedSortedSet(SortedSet<T>s)
:返回指定SortedSet對象對應(yīng)的線程安全的SortedSet對象。
例如需要在多線程中使用線程安全的HashMap對象垮衷,則可以采用如下代碼:
//使用Collections的synchronizedMap方法將一個(gè)普通的HashMap包裝成線程安全的類
HashMap m=Collections.synchronizedMap(new HashMap());
??
tips:
如果需要把某個(gè)集合包裝成線程安全的集合厅翔,則應(yīng)該在創(chuàng)建之后立即包裝,如上程序所示搀突,當(dāng)HashMap
對象創(chuàng)建后立即被包裝成線程安全的HashMap
對象刀闷。
線程安全的集合類
線程安全的集合類可分為如下兩類:
以
Concurrent
開頭的集合類,如ConcurrentHashMap
仰迁、ConcurrentSkipListMap
甸昏、ConcurrentSkip ListSet
、
ConcurrentLinkedQueue
和ConcurrentLinkedDeque
以
CopyOnWrite
開頭的集合類徐许,如CopyOnWriteArrayList
施蜜、CopyOnWriteArraySet
其中以Concurrent
開頭的集合類代表了支持并發(fā)訪問的集合,它們可以支持多個(gè)線程并發(fā)寫入訪問雌隅,這些寫入線程的所有操作都是線程安全的翻默,但讀取操作不必鎖定。
以Concurrent
開頭的集合類采用了更復(fù)雜的算法來保證永遠(yuǎn)不會(huì)鎖住整個(gè)集合澄步,因此在并發(fā)寫入時(shí)有較好的性能冰蘑。
在默認(rèn)情況下,ConcurrentHashMap
支持16個(gè)線程并發(fā)寫入村缸,當(dāng)有超過16個(gè)線程并發(fā)向該Map
中寫入數(shù)據(jù)時(shí)祠肥,可能有一些線程需要等待。實(shí)際上,程序通過設(shè)置concurrencyLevel
構(gòu)造參數(shù)(默認(rèn)值為16)來支持更多的并發(fā)寫入線程仇箱。
與前面介紹的HashMap
和普通集合不同的是县恕,因?yàn)?code>ConcurrentLinkedQueue和ConcurrentHashMap
支持多線程并發(fā)訪問,所以當(dāng)使用迭代器來遍歷集合元素時(shí)剂桥,該迭代器可能不能反映出創(chuàng)建迭代器之后所做的修改忠烛,但程序不會(huì)拋出任何異常。
Java8擴(kuò)展了ConcurrentHashMap
的功能权逗,Java8為該類新增了30多個(gè)新方法美尸,這些方法可借助于Stream
和Lambda
表達(dá)式支持執(zhí)行聚集操作。ConcurrentHashMap
新增的方法大致可分為如下三類:
forEach
系列(forEach,forEachKey,forEach Value,forEachEntry)
search
系列(search,searchKeys,search Values,searchEntries)
reduce
系列(reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduceValues)
除此之外斟薇,ConcurrentHashMap
還新增了mappingCount()
师坎、newKeySet()
等方法,增強(qiáng)后的ConcurrentHashMap
更適合作為緩存實(shí)現(xiàn)類使用堪滨。
??
CopyOnWriteAtraySet
由于CopyOnWriteAtraySet
的底層封裝了CopyOnWriteArmayList
胯陋,因此它的實(shí)現(xiàn)機(jī)制完全類似于CopyOnWriteArrayList
集合。
對于CopyOnWriteArrayList
集合袱箱,遏乔,它采用復(fù)制底層數(shù)組的方式來實(shí)現(xiàn)寫操作。
當(dāng)線程對CopyOnWriteArrayList
集合執(zhí)行讀取操作時(shí)发笔,線程將會(huì)直接讀取集合本身盟萨,無須加鎖與阻塞。
當(dāng)線程對CopyOnWriteArrayList
集合執(zhí)行寫入操作時(shí)(包括調(diào)用add()筐咧、
remove()鸯旁、
set()`等方法)該集合會(huì)在底層復(fù)制一份新的數(shù)組,接下來對新的數(shù)組執(zhí)行寫入操作量蕊。
由于對 CopyOnWriteArmayList
集合的寫入操作都是對數(shù)組的副本執(zhí)行操作,因此它是線程安全的艇挨。
需要指出的是残炮,由于CopyOnWriteArrayList
執(zhí)行寫入操作時(shí)需要頻繁地復(fù)制數(shù)組,性能比較差缩滨。
但由于讀操作與寫操作不是操作同一個(gè)數(shù)組势就,而且讀操作也不需要加鎖,因此讀操作就很快脉漏、很安全苞冯。由此可見,CopyOnWriteArayList
適合用在讀取操作遠(yuǎn)遠(yuǎn)大于寫入操作的場景中侧巨,例如緩存等舅锄。