多線程編程
進(jìn)程
一般可以在同一時(shí)間內(nèi)執(zhí)行多個(gè)程序的操作系統(tǒng)都 有進(jìn)程的概念蜀铲。一個(gè)進(jìn)程就是一個(gè)執(zhí)行中的程序, 而每一個(gè)進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間、一組 系統(tǒng)資源钮孵。在進(jìn)程的概念中,每一個(gè)進(jìn)程的內(nèi)部數(shù) 據(jù)和狀態(tài)都是完全獨(dú)立的眼滤。
在Windows操作系統(tǒng)中一個(gè)進(jìn)程就是一個(gè)exe或者dll 程序巴席,它們相互獨(dú)立,互相也可以通信诅需,在 Android操作系統(tǒng)中進(jìn)程間的通信應(yīng)用也是很多 的漾唉。
線程
線程與進(jìn)程相似荧库,是一段完成某個(gè)特定功能的代 碼,是程序中單個(gè)順序控制的流程赵刑,但與進(jìn)程不同 的是分衫,同類的多個(gè)線程是共享一塊內(nèi)存空間和一組 系統(tǒng)資源。所以系統(tǒng)在各個(gè)線程之間切換時(shí)般此,開銷 要比進(jìn)程小的多蚪战,正因如此,線程被稱為輕量級(jí)進(jìn)程铐懊。一個(gè)進(jìn)程中可以包含多個(gè)線程邀桑。
主線程
Java程序至少會(huì)有一個(gè)線程,這就是主線程科乎,程序 啟動(dòng)后是由JVM創(chuàng)建主線程壁畸,程序結(jié)束時(shí)由JVM停 止主線程。主線程它負(fù)責(zé)管理子線程茅茂,即子線程的 啟動(dòng)捏萍、掛起、停止等等操作空闲。下圖所示是進(jìn)程令杈、 主線程和子線程的關(guān)系,其中主線程負(fù)責(zé)管理子線 程进副,即子線程的啟動(dòng)这揣、掛起、停止等操作
[圖]?進(jìn)程影斑、主線程和子線程關(guān)系
獲取主線程示例代碼如下:
// 獲取主線程
ThreadmainThread=Thread.currentThread();
System.out.println("主線程名:"+mainThread.getName());
創(chuàng)建子線程
Java中創(chuàng)建一個(gè)子線程涉及到:java.lang.Thread類 和java.lang.Runnable接口给赞。Thread是線程類,創(chuàng)建一個(gè)Thread對(duì)象就會(huì)產(chǎn)生一個(gè)新的線程矫户。而線程執(zhí)行的程序代碼是在實(shí)現(xiàn)Runnable接口對(duì)象的run()方法中編寫的片迅,實(shí)現(xiàn)Runnable接口對(duì)象是線程執(zhí)行對(duì) 象。線程執(zhí)行對(duì)象實(shí)現(xiàn)Runnable接口的run()方法皆辽,run() 方法是線程執(zhí)行的入口柑蛇,該線程要執(zhí)行程序代碼都 在此編寫的,run()方法稱為線程體驱闷。
提示 主線程中執(zhí)行入口是main(String[] args) 方法耻台,這里可以控制程序的流程,管理其他的 子線程等空另。子線程執(zhí)行入口是線程執(zhí)行對(duì)象 (實(shí)現(xiàn)Runnable接口對(duì)象)的run()方法盆耽,在這個(gè)方法可以編寫子線程相關(guān)處理代碼。
實(shí)現(xiàn)Runnable接口
創(chuàng)建線程Thread對(duì)象時(shí),可以將線程執(zhí)行對(duì)象傳遞 給它摄杂,這需要是使用Thread類如下兩個(gè)構(gòu)造方法:
Thread(Runnable target, String name):target是線程執(zhí)行對(duì)象坝咐,實(shí)現(xiàn)Runnable接口。name為線程 指定一個(gè)名字析恢。
Thread(Runnable target):target是線程執(zhí)行對(duì) 象墨坚,實(shí)現(xiàn)Runnable接口。線程名字是由JVM分配的映挂。
下面看一個(gè)具體示例泽篮,實(shí)現(xiàn)Runnable接口的線程執(zhí) 行對(duì)象Runner代碼如下:
publicclassRunnerimplementsRunnable{
?@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次數(shù)和線程名稱
System.out.printf("第%d次執(zhí)行 - %s\n",i,Thread.currentThread().getName());
?longsleepTime=(long) (1000*Math.random());
try{
//線程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ? ? ?? }
? ? ?? }
//線程執(zhí)行結(jié)束
System.out.println("執(zhí)行完成! "+Thread.currentThread().getName());
?? }
}
代碼Thread.sleep(sleepTime)是休 眠當(dāng)前線程, sleep是靜態(tài)方法它有兩個(gè)版本:
static void sleep(long millis):在指定的毫秒數(shù)內(nèi) 讓當(dāng)前正在執(zhí)行的線程休眠。
static void sleep(long millis, int nanos) 在指定的毫 秒數(shù)加指定的納秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程 休眠袖肥。
測(cè)試程序代碼如下:
publicclassRunnerTest{
publicstaticvoidmain(String[]args) {
//創(chuàng)建線程t1,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt1=newThread(newRunner());
//開始線程t1
t1.start();
?
//創(chuàng)建線程t2,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt2=newThread(newRunner(),"這里指定線程名稱");
//開始線程t2
t2.start();
?
?? }
}
提示 仔細(xì)分析一下運(yùn)行結(jié)果咪辱,會(huì)發(fā)現(xiàn)兩個(gè)線 程是交錯(cuò)運(yùn)行的,感覺就像是兩個(gè)線程在同時(shí) 運(yùn)行椎组。但是實(shí)際上一臺(tái)PC通常就只有一顆 CPU,在某個(gè)時(shí)刻只能是一個(gè)線程在運(yùn)行历恐,而 Java語言在設(shè)計(jì)時(shí)就充分考慮到線程的并發(fā)調(diào) 度執(zhí)行寸癌。對(duì)于程序員來說,在編程時(shí)要注意給 每個(gè)線程執(zhí)行的時(shí)間和機(jī)會(huì)弱贼,主要是通過讓線 程休眠的辦法(調(diào)用sleep()方法)來讓當(dāng)前線 程暫停執(zhí)行蒸苇,然后由其他線程來爭(zhēng)奪執(zhí)行的機(jī)
會(huì)。如果上面的程序中沒有用到sleep()方法吮旅, 則就是第一個(gè)線程先執(zhí)行完畢溪烤,然后第二個(gè)線 程再執(zhí)行完畢。所以用活sleep()方法是多線程 編程的關(guān)鍵庇勃。
繼承Thread線程類
Thread類也實(shí)現(xiàn)了Runnable接口檬嘀,所以 Thread類也可以作為線程執(zhí)行對(duì)象,這需要繼承 Thread類责嚷,覆蓋run()方法鸳兽。
采用繼承Thread類重新實(shí)現(xiàn)Runner示例,自定義 線程類MyThread代碼如下:
publicclassMyThreadextendsThread{
publicMyThread() {
super();
?? }
?
publicMyThread(Stringname) {
super(name);
?? }
?
//程序執(zhí)行代碼
?
?
@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次數(shù)和線程名稱
System.out.printf("第%d次執(zhí)行 - %s\n",i,Thread.currentThread().getName());
?
longsleepTime=(long) (1000*Math.random());
try{
//線程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ? ? ?? }
? ? ?? }
//線程執(zhí)行結(jié)束
System.out.println("執(zhí)行完成! "+getName());
?? }
}
通 過super調(diào)用父類Thread構(gòu)造方法罕拂,這兩個(gè)Thread類 構(gòu)造方法:
Thread(String name):name為線程指定一個(gè)名 字揍异。代碼第④行調(diào)用都就是此構(gòu)造方法。
Thread():線程名字是JVM分配的爆班。代碼第②行 調(diào)用都就是此構(gòu)造方法衷掷。
測(cè)試程序代碼如下:
//通過繼承Thread線程類來創(chuàng)建線程
//創(chuàng)建線程t1,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt3=newMyThread();
//開始線程t1
t3.start();
?
//創(chuàng)建線程t2,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt4=newMyThread("這里指定線程名稱");
//開始線程t4
t4.start();
提示 由于Java只支持單重繼承,繼承Thread 類的方式不能再繼承其他父類柿菩。當(dāng)開發(fā)一些圖 形界面的應(yīng)用時(shí)戚嗅,需要一個(gè)類既是一個(gè)窗口 (繼承JFrame)又是一個(gè)線程體,那么只能采 用實(shí)現(xiàn)Runnable接口方式。
使用匿名內(nèi)部類和Lambda表達(dá)式實(shí)現(xiàn)線程體
如果線程體使用的地方不是很多渡处,可以不用單獨(dú)定 義一個(gè)類镜悉。可以使用匿名內(nèi)部類或Lambda表達(dá)式直 接實(shí)現(xiàn)Runnable接口医瘫。Runnable中只有一個(gè)方法是 函數(shù)式接口侣肄,可以使用Lambda表達(dá)式。
代碼如下:
//使用匿名內(nèi)部類和Lambda表達(dá)式實(shí)現(xiàn)線 程體
//創(chuàng)建線程t5醇份,參數(shù)是實(shí)現(xiàn)Runnable接口的匿名內(nèi)部類
Threadt5=newThread(newRunnable() {
@Override
publicvoidrun() {
for(inti=0;i<10;i++) {
//打印次數(shù)和線程名稱
System.out.printf("第%d次執(zhí)行 - %s\n",i,Thread.currentThread().getName());
?
longsleepTime=(long) (1000*Math.random());
try{
//線程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ? ? ? ? ? ? ?? }
? ? ? ? ? ? ?? }
//線程執(zhí)行結(jié)束
System.out.println("執(zhí)行完成! "+Thread.currentThread().getName());
?
? ? ? ? ?? }
? ? ?? });
// 開始線程t5
t5.start();
?
//創(chuàng)建線程t6稼锅,參數(shù)是,參數(shù)是實(shí)現(xiàn)Runnable接口的Lambda表達(dá)式
Threadt6=newThread(()->{
for(inti=0;i<10;i++) {
//打印次數(shù)和線程名稱
System.out.printf("第%d次執(zhí)行 - %s\n",i,Thread.currentThread().getName());
?
longsleepTime=(long) (1000*Math.random());
try{
//線程休眠
Thread.sleep(sleepTime);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ? ? ? ? ? ? ?? }
? ? ? ? ? ? ?? }
//線程執(zhí)行結(jié)束
System.out.println("執(zhí)行完成! "+Thread.currentThread().getName());
},"Lambda表達(dá)式");
// 開始線程t6
t6.start();
上述代碼第①行采用匿名內(nèi)部類實(shí)現(xiàn)Runnable接 口僚纷,覆蓋run()方法矩距。這里使用的是Thread(Runnable target)構(gòu)造方法。代碼第②行采用Lambda表達(dá)式實(shí)
現(xiàn)Runnable接口怖竭,覆蓋run()方法锥债。這里使用的是 Thread(Runnable target, String name)構(gòu)造方法, Lambda表達(dá)式是它的第一個(gè)參數(shù)痊臭。匿名內(nèi)部類和 Lambda表達(dá)式代碼雖然很多哮肚,但是它只是一個(gè)參 數(shù),實(shí)現(xiàn)了Runnable接口線程執(zhí)行對(duì)象广匙,
提示 匿名內(nèi)部類和Lambda表達(dá)式不需要定 義一個(gè)線程類文件允趟,使用起來很方便。特別是 Lambda表達(dá)式使代碼變得非常簡潔鸦致。但是客 觀上匿名內(nèi)部類和Lambda表達(dá)式會(huì)使代碼可 讀性變差潮剪,對(duì)于初學(xué)者不容易理解。
線程的狀態(tài)
在線程的生命周期中分唾,線程會(huì)有幾種狀態(tài)抗碰,如圖 23-5所示,線程有5種狀態(tài)鳍寂。下面分別介紹一下改含。
新建狀態(tài)
新建狀態(tài)(New)是通過new等方式創(chuàng)建線程對(duì) 象,它僅僅是一個(gè)空的線程對(duì)象
就緒狀態(tài)
當(dāng)主線程調(diào)用新建線程的start()方法后迄汛,它就進(jìn) 入就緒狀態(tài)(Runnable)捍壤。此時(shí)的線程尚未真正 開始執(zhí)行run()方法,它必須等待CPU的調(diào)度鞍爱。
運(yùn)行狀態(tài)
CPU的調(diào)度就緒狀態(tài)的線程鹃觉,線程進(jìn)入運(yùn)行狀 態(tài)(Running),處于運(yùn)行狀態(tài)的線程獨(dú)占 CPU睹逃,執(zhí)行run()方法盗扇。
阻塞狀態(tài)因?yàn)槟撤N原因運(yùn)行狀態(tài)的線程會(huì)進(jìn)入不可運(yùn)行 狀態(tài)祷肯,即阻塞狀態(tài)(Blocked),處于阻塞狀態(tài)的線程JVM系統(tǒng)不能執(zhí)行該線程疗隶,即使CPU空 閑佑笋,也不能執(zhí)行該線程。如下幾個(gè)原因會(huì)導(dǎo)致 線程進(jìn)入阻塞狀態(tài):
當(dāng)前線程調(diào)用sleep()方法斑鼻,進(jìn)入休眠狀態(tài)蒋纬。
被其他線程調(diào)用了join()方法,等待其他線程 結(jié)束坚弱。
發(fā)出I/O請(qǐng)求蜀备,等待I/O操作完成期間。
當(dāng)前線程調(diào)用wait()方法荒叶。
處于阻塞狀態(tài)可以重新回到就緒狀態(tài)碾阁,如:休 眠結(jié)束、其他線程加入些楣、I/O操作完成和調(diào)用 notify或notifyAll喚醒wait線程脂凶。
死亡狀態(tài)
線程退出run()方法后,就會(huì)進(jìn)入死亡狀態(tài) (Dead)愁茁,線程進(jìn)入死亡狀態(tài)有可以是正常實(shí) 現(xiàn)完成run()方法進(jìn)入艰猬,也可能是由于發(fā)生異常 而進(jìn)入的。
線程管理
線程優(yōu)先級(jí)
線程的調(diào)度程序根據(jù)線程決定每次線程應(yīng)當(dāng)何時(shí)運(yùn) 行埋市,Java提供了10種優(yōu)先級(jí),分別用1~10整數(shù)表 示命贴,最高優(yōu)先級(jí)是10用常量MAX_PRIORITY表 示道宅;最低優(yōu)先級(jí)是1用常量MIN_PRIORITY;默認(rèn) 優(yōu)先級(jí)是5用常量NORM_PRIORITY表示胸蛛。
Thread類提供了setPriority(int newPriority)方法可以 設(shè)置線程優(yōu)先級(jí)污茵,通過getPriority()方法獲得線程優(yōu) 先級(jí)。
設(shè)置線程優(yōu)先級(jí)示例代碼如下:
//通過實(shí)現(xiàn)Runnable接口來創(chuàng)建線程
//創(chuàng)建線程t1,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt1=newThread(newRunner());
//設(shè)置線程優(yōu)先級(jí)10
t1.setPriority(Thread.MAX_PRIORITY);
//開始線程t1
t1.start();
?
//創(chuàng)建線程t2,參數(shù)時(shí)一個(gè)線程執(zhí)行對(duì)象Runner
Threadt2=newThread(newRunner(),"這里指定線程名稱");
//設(shè)置線程優(yōu)先級(jí)1
t1.setPriority(Thread.MIN_PRIORITY);
//開始線程t2
t2.start();
提示 多次運(yùn)行上面的示例會(huì)發(fā)現(xiàn)葬项,t1線程經(jīng) 常先運(yùn)行泞当,但是偶爾t2線程也會(huì)先運(yùn)行。這些 現(xiàn)象說明了:影響線程獲得CPU時(shí)間的因素民珍, 除了受到的線程優(yōu)先級(jí)外襟士,還與操作系統(tǒng)有 關(guān)。
?
等待線程結(jié)束
在介紹現(xiàn)在狀態(tài)時(shí)提到過join()方法嚷量,當(dāng)前線程調(diào)用 t1線程的join()方法陋桂,則阻塞當(dāng)前線程,等待t1線程 結(jié)束蝶溶,如果t1線程結(jié)束或等待超時(shí)嗜历,則當(dāng)前線程回到就緒狀態(tài)。
Thread類提供了多個(gè)版本的join(),它們定義如下:
void join():等待該線程結(jié)束梨州。
void join(long millis):等待該線程結(jié)束的時(shí)間最 長為millis毫秒痕囱。如果超時(shí)為0意味著要一直等下 去。
void join(long millis, int nanos):等待該線程結(jié)束 的時(shí)間最長為millis毫秒加nanos納秒暴匠。
使用join()方法示例代碼如下:
publicclassJoin{
staticintvalue=0;
?
publicstaticvoidmain(String[]args)throwsInterruptedException{
System.out.println("主線程開始");
?
//創(chuàng)建線程t,參數(shù)是一個(gè)線程執(zhí)行對(duì)象Runner
Threadt1=newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println("子開始");
for(inti=0;i<3;i++) {
System.out.println("子執(zhí)行");
value++;
? ? ? ? ? ? ?? }
System.out.println("子結(jié)束");
? ? ? ? ?? }
},"ThreadA");
//開始t線程
t1.start();
//主線程被阻塞,等待t1線程結(jié)束
t1.join();
System.out.println("value = "+value);
System.out.println("主線程結(jié)束");
?? }
}
?
結(jié)果:
提示 使用join()方法的場(chǎng)景是鞍恢,一個(gè)線程依賴 于另外一個(gè)線程的運(yùn)行結(jié)果,所以調(diào)用另一個(gè) 線程的join()方法等它運(yùn)行完成巷查。
線程讓步
線程類Thread還提供一個(gè)靜態(tài)方法yield()有序,調(diào)用 yield()方法能夠使當(dāng)前線程給其他線程讓步。它類 似于sleep()方法岛请,能夠使運(yùn)行狀態(tài)的線程放棄CPU 使用權(quán)旭寿,暫停片刻,然后重新回到就緒狀態(tài)崇败。與 sleep()方法不同的是盅称,sleep()方法是線程進(jìn)行休眠,能夠給其他線程運(yùn)行的機(jī)會(huì)后室,無論線程優(yōu)先級(jí) 高低都有機(jī)會(huì)運(yùn)行缩膝。而yield()方法只給相同優(yōu)先級(jí) 或更高優(yōu)先級(jí)線程機(jī)會(huì)。
示例代碼如下:
Thread.yield();
提示 yield()方法只能給相同優(yōu)先級(jí)或更高優(yōu) 先級(jí)的線程讓步岸霹,yield()方法在實(shí)際開發(fā)中很
少使用疾层,大多都使用sleep()方法,sleep()方法 可以控制時(shí)間贡避,而yield()方法不能痛黎。
?
?
線程停止
線程體中的run()方法結(jié)束,線程進(jìn)入死亡狀態(tài)刮吧,線 程就停止了湖饱。但是有些業(yè)務(wù)比較復(fù)雜,例如想開發(fā) 一個(gè)下載程序杀捻,每隔一段執(zhí)行一次下載任務(wù)井厌,下載 任務(wù)一般會(huì)在由子線程執(zhí)行的,休眠一段時(shí)間再執(zhí) 行致讥。這個(gè)下載子線程中會(huì)有一個(gè)死循環(huán)仅仆,但是為了 能夠停止子線程,設(shè)置一個(gè)結(jié)束變量拄踪。
示例下面如下:
publicclassStopThread{
privatestaticStringcommand="";
?
publicstaticvoidmain(String[]args) {
//創(chuàng)建線程t1蝇恶,參數(shù)是一個(gè)線程執(zhí)行對(duì)象Runner
Threadt1=newThread(newRunnable() {
@Override
publicvoidrun() {
// 一直循環(huán),直到滿足條件在停止線程
while(!"exit".equalsIgnoreCase(command)) {
// 線程開始工作
System.out.println("下載中...");
try{
//線程休眠
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ? ? ? ? ? ? ?? }
? ? ? ? ? ? ?? }
//線程結(jié)束
System.out.println("執(zhí)行完成");
? ? ? ? ?? }
? ? ?? });
//開始線程t1
t1.start();
//輸入
//BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
try(BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in))){
command=br.readLine();
}catch(IOExceptione) {
e.printStackTrace();
? ? ?? }
?? }
}
運(yùn)行結(jié)果:
提示 控制線程的停止有人會(huì)想到使用Thread 提供的stop()方法惶桐,這個(gè)方法已經(jīng)不推薦使用 了撮弧,這個(gè)方法有時(shí)會(huì)引發(fā)嚴(yán)重的系統(tǒng)故障潘懊,類似還是有suspend()和resume()掛起方法。Java現(xiàn)在推薦的做法就是采用本例的結(jié)束變量方式贿衍。
線程安全
多一個(gè)線程同時(shí)運(yùn)行授舟,有時(shí)線程之間需要共享數(shù) 據(jù),一個(gè)線程需要其他線程的數(shù)據(jù)贸辈,否則就不能保 證程序運(yùn)行結(jié)果的正確性释树。
例如有一個(gè)航空公司的機(jī)票銷售,每一天機(jī)票數(shù)量 是有限的擎淤,很多售票點(diǎn)同時(shí)銷售這些機(jī)票奢啥。下面是 一個(gè)模擬銷售機(jī)票系統(tǒng),示例代碼如下
publicclassTicketDB{
?
//機(jī)票數(shù)量
privateintticketCount=5;
?
//獲取當(dāng)前機(jī)票數(shù)量
publicintgetTicketCount(){
returnticketCount;
?? }
?
//銷售機(jī)票
publicvoidsellTicket(){
?
//等待用戶付款
//線程休眠
try{
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ?? }
System.out.printf("第%d號(hào)票已經(jīng)售出\n",ticketCount);
ticketCount--;
?? }
?
調(diào)用代碼如下:
publicclassTicketTest{
publicstaticvoidmain(String[]args) {
Runnablerun=newTicket();
// 創(chuàng)建線程t1
Threadt1=newThread(run);
t1.start();
Threadt2=newThread(run);
t2.start();
?
?? }
?
staticclassTicketimplementsRunnable{
TicketDBdb=newTicketDB();
@Override
publicvoidrun() {
while(true){
intcurrentTC=db.getTicketCount();
//查詢是否有票
if(currentTC>0) {
db.sellTicket();
}else{
//無票退出
break;
? ? ? ? ? ? ?? }
? ? ? ? ?? }
?
? ? ?? }
?? }
}
一次運(yùn)行結(jié)果如下:
雖然可以能每次運(yùn)行的結(jié)果都不一樣嘴拢,但是從結(jié)果 看還是能發(fā)現(xiàn)一些問題:同一張票重復(fù)銷售桩盲、出現(xiàn) 第0號(hào)票和5張票賣了6次。這些問題的根本原因是 多個(gè)線程間共享的數(shù)據(jù)導(dǎo)致數(shù)據(jù)的不一致性席吴。
提示 多個(gè)線程間共享的數(shù)據(jù)稱為共享資源或 臨界資源赌结,由于是CPU負(fù)責(zé)線程的調(diào)度,程序
員無法精確控制多線程的交替順序孝冒。這種情況 下柬姚,多線程對(duì)臨界資源的訪問有時(shí)會(huì)導(dǎo)致數(shù)據(jù) 的不一致性。
?
多線程同步
為了防止多線程對(duì)臨界資源的訪問有時(shí)會(huì)導(dǎo)致數(shù)據(jù) 的不一致性庄涡,Java提供了“互斥”機(jī)制量承,可以為這些 資源對(duì)象加上一把“互斥鎖”,在任一時(shí)刻只能由一 個(gè)線程訪問穴店,即使該線程出現(xiàn)阻塞宴合,該對(duì)象的被鎖 定狀態(tài)也不會(huì)解除,其他線程仍不能訪問該對(duì)象迹鹅, 這就多線程同步。線程同步保證線程安全的重要手 段贞言,但是線程同步客觀上會(huì)導(dǎo)致性能下降斜棚。可以通過兩種方式實(shí)現(xiàn)線程同步该窗,兩種方式都涉及 到使用synchronized關(guān)鍵字弟蚀,一種是synchronized方 法,使用synchronized關(guān)鍵字修飾方法酗失,對(duì)方法進(jìn) 行同步义钉;另一種是synchronized語句,使用 synchronized關(guān)鍵字放在對(duì)象前面限制一段代碼的 執(zhí)行规肴。
synchronized方法
synchronized關(guān)鍵字修飾方法實(shí)現(xiàn)線程同步捶闸,方 法所在的對(duì)象被鎖定夜畴,修改售票系統(tǒng)示 例, TicketDB.java文件代碼如下:
publicclassTicketDB{
?
//機(jī)票數(shù)量
privateintticketCount=5;
?
//獲取當(dāng)前機(jī)票數(shù)量
publicsynchronizedintgetTicketCount(){
returnticketCount;
?? }
//銷售機(jī)票
publicsynchronizedvoidsellTicket(){
?
//等待用戶付款
//線程休眠
try{
Thread.sleep(1000);
}catch(InterruptedExceptione) {
e.printStackTrace();
? ? ?? }
System.out.printf("第%d號(hào)票已經(jīng)售出\n",ticketCount);
ticketCount--;
?? }
?
?
}
?
上述代碼第①行和第②行的方法前都使用了 synchronized關(guān)鍵字删壮,表明這兩個(gè)方法是同步 的贪绘,被鎖定的,每一個(gè)時(shí)刻只能由一個(gè)線程訪 問央碟。并不是每一個(gè)方法都有必要加鎖的税灌,要仔 細(xì)研究加上的必要性,上述代碼第①行加鎖可
? 以防止出現(xiàn)第0號(hào)票情況和5張票賣出6次的情況亿虽;代碼第②行加鎖是防止出現(xiàn)銷售兩種一樣 的票
?
synchronized語句
synchronized語句方式主要用于第三方類菱涤,不方 便修改它的代碼情況。同樣是售票系統(tǒng)示例洛勉,可以不用修改TicketDB.java類粘秆,只修改調(diào)用代碼TicketTest.java實(shí)現(xiàn)同步。
代碼如下:
publicclassTicketTest{
?
publicstaticvoidmain(String[]args) {
?
// ? ? ?? Runnable run = new Ticket();
// ? ? ?? new Thread(run).start();
// ? ? ?? new Thread(run).start();
?
// 創(chuàng)建線程t1
Runnablerun=newTicket();
Threadt1=newThread(run);
t1.start();
Threadt2=newThread(run);
t2.start();
?? }
?
staticclassTicketimplementsRunnable{
TicketDBdb=newTicketDB();
@Override
publicvoidrun() {
while(true){
//同步代碼語句
synchronized(db) {
intcurrentTC=db.getTicketCount();
//查詢是否有票
if(currentTC>0) {
db.sellTicket();
}else{
//無票退出
break;
? ? ? ? ? ? ? ? ?? }
? ? ? ? ? ? ?? }
? ? ? ? ?? }
?
? ? ?? }
?? }
}
?
將需要同步的代碼用大括號(hào)括起來坯认。 synchronized后有小括號(hào)翻擒,將需要同步的對(duì)象括 起來。
線程間通信
上面的示例只是簡單地為特定對(duì)象或方法加 鎖牛哺,但有時(shí)情況會(huì)更加復(fù)雜陋气,如果兩個(gè)線程之間有 依賴關(guān)系,線程之間必須進(jìn)行通信引润,互相協(xié)調(diào)才能 完成工作巩趁。例如有一個(gè)經(jīng)典的堆棧問題,一個(gè)線程生成了一些 數(shù)據(jù)淳附,將數(shù)據(jù)壓棧议慰;另一個(gè)線程消費(fèi)了這些數(shù)據(jù), 將數(shù)據(jù)出棧奴曙。這兩個(gè)線程互相依賴别凹,當(dāng)堆棧為空 時(shí),消費(fèi)線程無法取出數(shù)據(jù)時(shí)洽糟,應(yīng)該通知生成線程 添加數(shù)據(jù)炉菲;當(dāng)堆棧已滿時(shí),生產(chǎn)線程無法添加數(shù)據(jù) 時(shí)坤溃,應(yīng)該通知消費(fèi)線程取出數(shù)據(jù)拍霜。為了實(shí)現(xiàn)線程間通信,需要使用Object類中聲明的5 個(gè)方法:
void wait():使當(dāng)前線程釋放對(duì)象鎖薪介,然后當(dāng)前 線程處于對(duì)象等待隊(duì)列中阻塞狀態(tài)祠饺,如下圖所 示,等待其他線程喚醒汁政。
void wait(long timeout):同wait()方法道偷,等待 timeout毫秒時(shí)間缀旁。
void wait(long timeout, int nanos):同wait()方法,等待timeout毫秒加nanos納秒時(shí)間试疙。
void notify():當(dāng)前線程喚醒此對(duì)象等待隊(duì)列中 的一個(gè)線程诵棵,如下圖所示該線程將進(jìn)入就緒狀 態(tài)。
void notifyAll():當(dāng)前線程喚醒此對(duì)象等待隊(duì)列 中的所有線程祝旷,如下圖所示這些線程將進(jìn)入就 緒狀態(tài)