java基础提升篇:深入浅出Java多线程
初遇
Java給多線程編程提供了內(nèi)置的支持。一個(gè)多線程程序包含兩個(gè)或多個(gè)能并發(fā)運(yùn)行的部分。程序的每一部分都稱作一個(gè)線程,并且每個(gè)線程定義了一個(gè)獨(dú)立的執(zhí)行路徑。
多線程是多任務(wù)的一種特別的形式,但多線程使用了更小的資源開(kāi)銷。
這里定義和線程相關(guān)的另一個(gè)術(shù)語(yǔ) - 進(jìn)程:一個(gè)進(jìn)程包括由操作系統(tǒng)分配的內(nèi)存空間,包含一個(gè)或多個(gè)線程。一個(gè)線程不能獨(dú)立的存在,它必須是進(jìn)程的一部分。一個(gè)進(jìn)程一直運(yùn)行,直到所有的非守候線程都結(jié)束運(yùn)行后才能結(jié)束。
多線程能滿足程序員編寫(xiě)高效率的程序來(lái)達(dá)到充分利用CPU的目的。
1. 多線程基礎(chǔ)概念介紹
進(jìn)程是程序(任務(wù))的執(zhí)行過(guò)程,它持有資源(共享內(nèi)存,共享文件)和線程。
分析:
① 執(zhí)行過(guò)程 是動(dòng)態(tài)性的,你放在電腦磁盤(pán)上的某個(gè)eclipse或者QQ文件并不是我們的進(jìn)程,只有當(dāng)你雙擊運(yùn)行可執(zhí)行文件,使eclipse或者QQ運(yùn)行之后,這才稱為進(jìn)程。它是一個(gè)執(zhí)行過(guò)程,是一個(gè)動(dòng)態(tài)的概念。
② 它持有資源(共享內(nèi)存,共享文件)和線程:我們說(shuō)進(jìn)程是資源的載體,也是線程的載體。這里的資源可以理解為內(nèi)存。我們知道程序是要從內(nèi)存中讀取數(shù)據(jù)進(jìn)行運(yùn)行的,所以每個(gè)進(jìn)程獲得執(zhí)行的時(shí)候會(huì)被分配一個(gè)內(nèi)存。
③ 線程是什么?
如果我們把進(jìn)程比作一個(gè)班級(jí),那么班級(jí)中的每個(gè)學(xué)生可以將它視作一個(gè)線程。學(xué)生是班級(jí)中的最小單元,構(gòu)成了班級(jí)中的最小單位。一個(gè)班級(jí)有可以多個(gè)學(xué)生,這些學(xué)生都使用共同的桌椅、書(shū)籍以及黑板等等進(jìn)行學(xué)習(xí)和生活。
在這個(gè)意義上我們說(shuō):
線程是系統(tǒng)中最小的執(zhí)行單元;同一進(jìn)程中可以有多個(gè)線程;線程共享進(jìn)程的資源。
④ 線程是如何交互?
就如同一個(gè)班級(jí)中的多個(gè)學(xué)生一樣,我們說(shuō)多個(gè)線程需要通信才能正確的工作,這種通信,我們稱作線程的交互。
⑤ 交互的方式:互斥、同步
類比班級(jí),就是在同一班級(jí)之內(nèi),同學(xué)之間通過(guò)相互的協(xié)作才能完成某些任務(wù),有時(shí)這種協(xié)作是需要競(jìng)爭(zhēng)的,比如學(xué)習(xí),班級(jí)之內(nèi)公共的學(xué)習(xí)資料是有限的,愛(ài)學(xué)習(xí)的同學(xué)需要搶占它,需要競(jìng)爭(zhēng),當(dāng)一個(gè)同學(xué)使用完了之后另一個(gè)同學(xué)才可以使用;如果一個(gè)同學(xué)正在使用,那么其他新來(lái)的同學(xué)只能等待;另一方面需要同步協(xié)作,就好比班級(jí)六一需要排演節(jié)目,同學(xué)需要齊心協(xié)力相互配合才能將節(jié)目演好,這就是進(jìn)程交互。
一個(gè)線程的生命周期
線程經(jīng)過(guò)其生命周期的各個(gè)階段。下圖顯示了一個(gè)線程完整的生命周期。
- 新建狀態(tài):
使用 new 關(guān)鍵字和 Thread 類或其子類建立一個(gè)線程對(duì)象后,該線程對(duì)象就處于新建狀態(tài)。它保持這個(gè)狀態(tài)直到程序 start() 這個(gè)線程。
- 就緒狀態(tài):
當(dāng)線程對(duì)象調(diào)用了start()方法之后,該線程就進(jìn)入就緒狀態(tài)。就緒狀態(tài)的線程處于就緒隊(duì)列中,要等待JVM里線程調(diào)度器的調(diào)度。
-
運(yùn)行狀態(tài):
如果就緒狀態(tài)的線程獲取 CPU 資源,就可以執(zhí)行 run(),此時(shí)線程便處于運(yùn)行狀態(tài)。處于運(yùn)行狀態(tài)的線程最為復(fù)雜,它可以變?yōu)樽枞麪顟B(tài)、就緒狀態(tài)和死亡狀態(tài)。
-
阻塞狀態(tài):
如果一個(gè)線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運(yùn)行狀態(tài)進(jìn)入阻塞狀態(tài)。在睡眠時(shí)間已到或獲得設(shè)備資源后可以重新進(jìn)入就緒狀態(tài)。
- 死亡狀態(tài):
一個(gè)運(yùn)行狀態(tài)的線程完成任務(wù)或者其他終止條件發(fā)生時(shí),該線程就切換到終止?fàn)顟B(tài)。
線程的狀態(tài)轉(zhuǎn)換圖
1、新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。
2、就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,其他線程調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程池中,變得可運(yùn)行,等待獲取CPU的使用權(quán)。
3、運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼。
4、阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分三種:
(一)、等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中。
(二)、同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池中。
(三)、其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。
5、死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
線程的調(diào)度
1、調(diào)整線程優(yōu)先級(jí):
每一個(gè)Java線程都有一個(gè)優(yōu)先級(jí),這樣有助于操作系統(tǒng)確定線程的調(diào)度順序。
Java線程的優(yōu)先級(jí)用整數(shù)表示,取值范圍是1~10,Thread類有以下三個(gè)靜態(tài)常量:
static int MAX_PRIORITY
線程可以具有的最高優(yōu)先級(jí),取值為10。
static int MIN_PRIORITY
線程可以具有的最低優(yōu)先級(jí),取值為1。
static int NORM_PRIORITY
分配給線程的默認(rèn)優(yōu)先級(jí),取值為5。
Thread類的setPriority()和getPriority()方法分別用來(lái)設(shè)置和獲取線程的優(yōu)先級(jí)。
每個(gè)線程都有默認(rèn)的優(yōu)先級(jí)。主線程的默認(rèn)優(yōu)先級(jí)為T(mén)hread.NORM_PRIORITY。
線程的優(yōu)先級(jí)有繼承關(guān)系,比如A線程中創(chuàng)建了B線程,那么B將和A具有相同的優(yōu)先級(jí)。
JVM提供了10個(gè)線程優(yōu)先級(jí),但與常見(jiàn)的操作系統(tǒng)都不能很好的映射。如果希望程序能移植到各個(gè)操作系統(tǒng)中,應(yīng)該僅僅使用Thread類有以下三個(gè)靜態(tài)常量作為優(yōu)先級(jí),這樣能保證同樣的優(yōu)先級(jí)采用了同樣的調(diào)度方式。
具有較高優(yōu)先級(jí)的線程對(duì)程序更重要,并且應(yīng)該在低優(yōu)先級(jí)的線程之前分配處理器資源。但是,線程優(yōu)先級(jí)不能保證線程執(zhí)行的順序,而且非常依賴于平臺(tái)。
2、線程睡眠:Thread.sleep(long millis)方法,使線程轉(zhuǎn)到阻塞狀態(tài)。millis參數(shù)設(shè)定睡眠的時(shí)間,以毫秒為單位。當(dāng)睡眠結(jié)束后,就轉(zhuǎn)為就緒(Runnable)狀態(tài)。sleep()平臺(tái)移植性好。
3、線程等待:Object類中的wait()方法,導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的 notify() 方法或 notifyAll() 喚醒方法。這個(gè)兩個(gè)喚醒方法也是Object類中的方法,行為等價(jià)于調(diào)用 wait(0) 一樣。
4、線程讓步:Thread.yield() 方法,暫停當(dāng)前正在執(zhí)行的線程對(duì)象,把執(zhí)行機(jī)會(huì)讓給相同或者更高優(yōu)先級(jí)的線程。
5、線程加入:join()方法,等待其他線程終止。在當(dāng)前線程中調(diào)用另一個(gè)線程的join()方法,則當(dāng)前線程轉(zhuǎn)入阻塞狀態(tài),直到另一個(gè)進(jìn)程運(yùn)行結(jié)束,當(dāng)前線程再由阻塞轉(zhuǎn)為就緒狀態(tài)。
6、線程喚醒:Object類中的notify()方法,喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此對(duì)象上等待,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的,并在對(duì)實(shí)現(xiàn)做出決定時(shí)發(fā)生。線程通過(guò)調(diào)用其中一個(gè) wait 方法,在對(duì)象的監(jiān)視器上等待。 直到當(dāng)前的線程放棄此對(duì)象上的鎖定,才能繼續(xù)執(zhí)行被喚醒的線程。被喚醒的線程將以常規(guī)方式與在該對(duì)象上主動(dòng)同步的其他所有線程進(jìn)行競(jìng)爭(zhēng);例如,喚醒的線程在作為鎖定此對(duì)象的下一個(gè)線程方面沒(méi)有可靠的特權(quán)或劣勢(shì)。類似的方法還有一個(gè)notifyAll(),喚醒在此對(duì)象監(jiān)視器上等待的所有線程。
注意:Thread中suspend()和resume()兩個(gè)方法在JDK1.5中已經(jīng)廢除,不再介紹。因?yàn)橛兴梨i傾向。
7、常見(jiàn)線程名詞解釋
主線程:JVM調(diào)用程序main()所產(chǎn)生的線程。
當(dāng)前線程:這個(gè)是容易混淆的概念。一般指通過(guò)Thread.currentThread()來(lái)獲取的進(jìn)程。
后臺(tái)線程:指為其他線程提供服務(wù)的線程,也稱為守護(hù)線程。JVM的垃圾回收線程就是一個(gè)后臺(tái)線程。
前臺(tái)線程:是指接受后臺(tái)線程服務(wù)的線程,其實(shí)前臺(tái)后臺(tái)線程是聯(lián)系在一起,就像傀儡和幕后操縱者一樣的關(guān)系。傀儡是前臺(tái)線程、幕后操縱者是后臺(tái)線程。由前臺(tái)線程創(chuàng)建的線程默認(rèn)也是前臺(tái)線程。可以通過(guò)isDaemon()和setDaemon()方法來(lái)判斷和設(shè)置一個(gè)線程是否為后臺(tái)線程。
一些常見(jiàn)問(wèn)題
1、線程的名字,一個(gè)運(yùn)行中的線程總是有名字的,名字有兩個(gè)來(lái)源,一個(gè)是虛擬機(jī)自己給的名字,一個(gè)是你自己的定的名字。在沒(méi)有指定線程名字的情況下,虛擬機(jī)總會(huì)為線程指定名字,并且主線程的名字總是main,非主線程的名字不確定。
2、線程都可以設(shè)置名字,也可以獲取線程的名字,連主線程也不例外。
3、獲取當(dāng)前線程的對(duì)象的方法是:Thread.currentThread();
4、每個(gè)線程都將啟動(dòng),每個(gè)線程都將運(yùn)行直到完成。一系列線程以某種順序啟動(dòng)并不意味著將按該順序執(zhí)行。對(duì)于任何一組啟動(dòng)的線程來(lái)說(shuō),調(diào)度程序不能保證其執(zhí)行次序,持續(xù)時(shí)間也無(wú)法保證。
5、當(dāng)線程目標(biāo)run()方法結(jié)束時(shí)該線程完成。
6、一旦線程啟動(dòng),它就永遠(yuǎn)不能再重新啟動(dòng)。只有一個(gè)新的線程可以被啟動(dòng),并且只能一次。一個(gè)可運(yùn)行的線程或死線程可以被重新啟動(dòng)。
7、線程的調(diào)度是JVM的一部分,在一個(gè)CPU的機(jī)器上上,實(shí)際上一次只能運(yùn)行一個(gè)線程。一次只有一個(gè)線程棧執(zhí)行。JVM線程調(diào)度程序決定實(shí)際運(yùn)行哪個(gè)處于可運(yùn)行狀態(tài)的線程。
眾多可運(yùn)行線程中的某一個(gè)會(huì)被選中做為當(dāng)前線程。可運(yùn)行線程被選擇運(yùn)行的順序是沒(méi)有保障的。
8、盡管通常采用隊(duì)列形式,但這是沒(méi)有保障的。隊(duì)列形式是指當(dāng)一個(gè)線程完成“一輪”時(shí),它移到可運(yùn)行隊(duì)列的尾部等待,直到它最終排隊(duì)到該隊(duì)列的前端為止,它才能被再次選中。事實(shí)上,我們把它稱為可運(yùn)行池而不是一個(gè)可運(yùn)行隊(duì)列,目的是幫助認(rèn)識(shí)線程并不都是以某種有保障的順序排列唱呢個(gè)一個(gè)隊(duì)列的事實(shí)。
9、盡管我們沒(méi)有無(wú)法控制線程調(diào)度程序,但可以通過(guò)別的方式來(lái)影響線程調(diào)度的方式。
2. Java 中線程的常用方法介紹
Java語(yǔ)言對(duì)線程的支持
主要體現(xiàn)在Thread類和Runnable接口上,都繼承于java.lang包。它們都有個(gè)共同的方法:public void run()。
run方法為我們提供了線程實(shí)際工作執(zhí)行的代碼。
下表列出了Thread類的一些重要方法:
| 1 | public void start()使該線程開(kāi)始執(zhí)行;Java 虛擬機(jī)調(diào)用該線程的 run 方法。 |
| 2 | public void run()如果該線程是使用獨(dú)立的 Runnable 運(yùn)行對(duì)象構(gòu)造的,則調(diào)用該 Runnable 對(duì)象的 run 方法;否則,該方法不執(zhí)行任何操作并返回。 |
| 3 | public final void setName(String name)改變線程名稱,使之與參數(shù) name 相同。 |
| 4 | public final void setPriority(int priority)更改線程的優(yōu)先級(jí)。 |
| 5 | public final void setDaemon(boolean on)將該線程標(biāo)記為守護(hù)線程或用戶線程。 |
| 6 | public final void join(long millisec)等待該線程終止的時(shí)間最長(zhǎng)為 millis 毫秒。 |
| 7 | public void interrupt()中斷線程。 |
| 8 | public final boolean isAlive()測(cè)試線程是否處于活動(dòng)狀態(tài)。 |
測(cè)試線程是否處于活動(dòng)狀態(tài)。 上述方法是被Thread對(duì)象調(diào)用的。下面的方法是Thread類的靜態(tài)方法。
| 1 | public static void yield()暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。 |
| 2 | public static void sleep(long millisec)在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行),此操作受到系統(tǒng)計(jì)時(shí)器和調(diào)度程序精度和準(zhǔn)確性的影響。 |
| 3 | public static boolean holdsLock(Object x)當(dāng)且僅當(dāng)當(dāng)前線程在指定的對(duì)象上保持監(jiān)視器鎖時(shí),才返回 true。 |
| 4 | public static Thread currentThread()返回對(duì)當(dāng)前正在執(zhí)行的線程對(duì)象的引用。 |
| 5 | public static void dumpStack()將當(dāng)前線程的堆棧跟蹤打印至標(biāo)準(zhǔn)錯(cuò)誤流。 |
Thread常用的方法
3. 線程初體驗(yàn)(編碼示例)
創(chuàng)建線程的方法有兩種:
1.繼承Thread類本身
2.實(shí)現(xiàn)Runnable接口
線程中的方法比較有特點(diǎn),比如:啟動(dòng)(start),休眠(sleep),停止等,多個(gè)線程是交互執(zhí)行的(cpu在某個(gè)時(shí)刻。只能執(zhí)行一個(gè)線程,當(dāng)一個(gè)線程休眠了或者執(zhí)行完畢了,另一個(gè)線程才能占用cpu來(lái)執(zhí)行)因?yàn)檫@是cpu的結(jié)構(gòu)來(lái)決定的,在某個(gè)時(shí)刻cpu只能執(zhí)行一個(gè)線程,不過(guò)速度相當(dāng)快,對(duì)于人來(lái)將可以認(rèn)為是并行執(zhí)行的。
在一個(gè)java文件中,可以有多個(gè)類(此處說(shuō)的是外部類),但只能有一個(gè)public類。
這兩種創(chuàng)建線程的方法本質(zhì)沒(méi)有任何的不同,一個(gè)是實(shí)現(xiàn)Runnable接口,一個(gè)是繼承Thread類。
使用實(shí)現(xiàn)Runnable接口這種方法:
1.可以避免java的單繼承的特性帶來(lái)的局限性;
2.適合多個(gè)相同程序的代碼去處理同一個(gè)資源情況,把線程同程序的代碼及數(shù)據(jù)有效的分離,較好的體現(xiàn)了面向?qū)ο蟮脑O(shè)計(jì)思想。開(kāi)發(fā)中大多數(shù)情況下都使用實(shí)現(xiàn)Runnable接口這種方法創(chuàng)建線程。
實(shí)現(xiàn)Runnable接口創(chuàng)建的線程最終還是要通過(guò)將自身實(shí)例作為參數(shù)傳遞給Thread然后執(zhí)行
語(yǔ)法: Thread actress=new Thread(Runnable target ,String name);
例如:
Thread actressThread=new Thread(new Actress(),"Ms.runnable"); actressThread.start();代碼示例:
package com.study.thread;public class Actor extends Thread{public void run() {System.out.println(getName() + "是一個(gè)演員!");int count = 0;boolean keepRunning = true;while(keepRunning){System.out.println(getName()+"登臺(tái)演出:"+ (++count));if(count == 100){keepRunning = false;}if(count%10== 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}System.out.println(getName() + "的演出結(jié)束了!");}public static void main(String[] args) {Thread actor = new Actor();//向上轉(zhuǎn)型:子類轉(zhuǎn)型為父類,子類對(duì)象就會(huì)遺失和父類不同的方法。向上轉(zhuǎn)型符合Java提倡的面向抽象編程思想,還可以減輕編程工作量actor.setName("Mr. Thread");actor.start();//調(diào)用Thread的構(gòu)造函數(shù)Thread(Runnable target, String name)Thread actressThread = new Thread(new Actress(), "Ms. Runnable");actressThread.start();}} //注意:在“xx.java”文件中可以有多個(gè)類,但是只能有一個(gè)Public類。這里所說(shuō)的不是內(nèi)部類,都是一個(gè)個(gè)獨(dú)立的外部類 class Actress implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "是一個(gè)演員!");//Runnable沒(méi)有g(shù)etName()方法,需要通過(guò)線程的currentThread()方法獲得線程名稱int count = 0;boolean keepRunning = true;while(keepRunning){System.out.println(Thread.currentThread().getName()+"登臺(tái)演出:"+ (++count));if(count == 100){keepRunning = false;}if(count%10== 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}System.out.println(Thread.currentThread().getName() + "的演出結(jié)束了!");}}/***運(yùn)行結(jié)果Mr. Thread線程和Ms. Runnable線程是交替執(zhí)行的情況*分析:計(jì)算機(jī)CPU處理器在同一時(shí)間同一個(gè)處理器同一個(gè)核只能運(yùn)行一條線程,*當(dāng)一條線程休眠之后,另外一個(gè)線程才獲得處理器時(shí)間*/運(yùn)行結(jié)果:
示例2:
ArmyRunnable 類:
package com.study.threadTest1;/*** 軍隊(duì)線程* 模擬作戰(zhàn)雙方的行為*/ public class ArmyRunnable implements Runnable {/* volatile關(guān)鍵字* volatile保證了線程可以正確的讀取其他線程寫(xiě)入的值* 如果不寫(xiě)成volatile,由于可見(jiàn)性的問(wèn)題,當(dāng)前線程有可能不能讀到這個(gè)值* 關(guān)于可見(jiàn)性的問(wèn)題可以參考JMM(Java內(nèi)存模型),里面講述了:happens-before原則、可見(jiàn)性* 用volatile修飾的變量,線程在每次使用變量的時(shí)候,都會(huì)讀取變量修改后的值*/volatile boolean keepRunning = true;@Overridepublic void run() {while (keepRunning) {//發(fā)動(dòng)5連擊for(int i=0;i<5;i++){System.out.println(Thread.currentThread().getName()+"進(jìn)攻對(duì)方["+i+"]");//讓出了處理器時(shí)間,下次該誰(shuí)進(jìn)攻還不一定呢!Thread.yield();//yield()當(dāng)前運(yùn)行線程釋放處理器資源} }System.out.println(Thread.currentThread().getName()+"結(jié)束了戰(zhàn)斗!");}}KeyPersonThread 類:
package com.study.threadTest1;public class KeyPersonThread extends Thread {public void run(){System.out.println(Thread.currentThread().getName()+"開(kāi)始了戰(zhàn)斗!");for(int i=0;i<10;i++){System.out.println(Thread.currentThread().getName()+"左突右殺,攻擊隋軍...");}System.out.println(Thread.currentThread().getName()+"結(jié)束了戰(zhàn)斗!");}}Stage 類:
package com.study.threadTest1;/*** 隋唐演義大戲舞臺(tái) 6 */ public class Stage extends Thread {public void run(){System.out.println("歡迎觀看隋唐演義");//讓觀眾們安靜片刻,等待大戲上演try {Thread.sleep(5000);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println("大幕徐徐拉開(kāi)");try {Thread.sleep(5000);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println("話說(shuō)隋朝末年,隋軍與農(nóng)民起義軍殺得昏天黑地...");ArmyRunnable armyTaskOfSuiDynasty = new ArmyRunnable();ArmyRunnable armyTaskOfRevolt = new ArmyRunnable();//使用Runnable接口創(chuàng)建線程Thread armyOfSuiDynasty = new Thread(armyTaskOfSuiDynasty,"隋軍");Thread armyOfRevolt = new Thread(armyTaskOfRevolt,"農(nóng)民起義軍");//啟動(dòng)線程,讓軍隊(duì)開(kāi)始作戰(zhàn)armyOfSuiDynasty.start();armyOfRevolt.start();//舞臺(tái)線程休眠,大家專心觀看軍隊(duì)廝殺try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("正當(dāng)雙方激戰(zhàn)正酣,半路殺出了個(gè)程咬金");Thread mrCheng = new KeyPersonThread();mrCheng.setName("程咬金");System.out.println("程咬金的理想就是結(jié)束戰(zhàn)爭(zhēng),使百姓安居樂(lè)業(yè)!");//停止軍隊(duì)作戰(zhàn)//停止線程的方法armyTaskOfSuiDynasty.keepRunning = false;armyTaskOfRevolt.keepRunning = false;try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}/** 歷史大戲留給關(guān)鍵人物*/mrCheng.start();//萬(wàn)眾矚目,所有線程等待程先生完成歷史使命try {mrCheng.join();//join()使其他線程等待當(dāng)前線程終止} catch (InterruptedException e) {e.printStackTrace();}System.out.println("戰(zhàn)爭(zhēng)結(jié)束,人民安居樂(lè)業(yè),程先生實(shí)現(xiàn)了積極的人生夢(mèng)想,為人民作出了貢獻(xiàn)!");System.out.println("謝謝觀看隋唐演義,再見(jiàn)!");}public static void main(String[] args) {new Stage().start();}}運(yùn)行結(jié)果:
4. Java 線程的正確停止
如何正確的停止Java中的線程?
stop方法:該方法使線程戛然而止(突然停止),完成了哪些工作,哪些工作還沒(méi)有做都不清楚,且清理工作也沒(méi)有做。
stop方法不是正確的停止線程方法。線程停止不推薦使用stop方法。
正確的方法—設(shè)置退出標(biāo)志
使用volatile 定義boolean running=true,通過(guò)設(shè)置標(biāo)志變量running,來(lái)結(jié)束線程。
如本文:volatile boolean keepRunning=true;
這樣做的好處是:使得線程有機(jī)會(huì)使得一個(gè)完整的業(yè)務(wù)步驟被完整地執(zhí)行,在執(zhí)行完業(yè)務(wù)步驟后有充分的時(shí)間去做代碼的清理工作,使得線程代碼在實(shí)際中更安全。
廣為流傳的錯(cuò)誤方法—interrupt方法
當(dāng)一個(gè)線程運(yùn)行時(shí),另一個(gè)線程可以調(diào)用對(duì)應(yīng)的 Thread 對(duì)象的 interrupt()方法來(lái)中斷它,該方法只是在目標(biāo)線程中設(shè)置一個(gè)標(biāo)志,表示它已經(jīng)被中斷,并立即返回。這里需要注意的是,如果只是單純的調(diào)用 interrupt()方法,線程并沒(méi)有實(shí)際被中斷,會(huì)繼續(xù)往下執(zhí)行。
代碼示例:
package com.study.threadStop;/*** 錯(cuò)誤終止進(jìn)程的方式——interrupt*/ public class WrongWayStopThread extends Thread {public static void main(String[] args) {WrongWayStopThread thread = new WrongWayStopThread();System.out.println("Start Thread...");thread.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Interrupting thread...");thread.interrupt();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Stopping application...");}public void run() {while(true){System.out.println("Thread is running...");long time = System.currentTimeMillis();while ((System.currentTimeMillis()-time) <1000) {//這部分的作用大致相當(dāng)于Thread.sleep(1000),注意此處為什么沒(méi)有使用休眠的方法//減少屏幕輸出的空循環(huán)(使得每秒鐘只輸出一行信息)}}} }運(yùn)行結(jié)果:
由結(jié)果看到interrupt()方法并沒(méi)有使線程中斷,線程還是會(huì)繼續(xù)往下執(zhí)行。
Java API中介紹:
但是interrupt()方法可以使我們的中斷狀態(tài)發(fā)生改變,可以調(diào)用isInterrupted 方法
將上處run方法代碼改為下面一樣,程序就可以正常結(jié)束了。
但是這種所使用的退出方法實(shí)質(zhì)上還是前面說(shuō)的使用退出旗標(biāo)的方法,不過(guò)這里所使用的退出旗標(biāo)是一個(gè)特殊的標(biāo)志“線程是否被中斷的狀態(tài)”。
這部分代碼相當(dāng)于線程休眠1秒鐘的代碼。但是為什么沒(méi)有使用Thread.sleep(1000)。如果采用這種方法就會(huì)出現(xiàn)
線程沒(méi)有正常結(jié)束,而且還拋出了一個(gè)異常,異常拋出位置在調(diào)用interrupt方法之后。為什么會(huì)有這種結(jié)果?
在API文檔中說(shuō)過(guò):如果線程由于調(diào)用的某些方法(比如sleep,join。。。)而進(jìn)入一種阻塞狀態(tài)時(shí),此時(shí)如果這個(gè)線程再被調(diào)用interrupt方法,它會(huì)產(chǎn)生兩個(gè)結(jié)果:第一,它的中斷狀態(tài)被清除clear,而不是被設(shè)置set。那isInterrupted 就不能返回是否被中斷的正確狀態(tài),那while函數(shù)就不能正確的退出。第二,sleep方法會(huì)收到InterruptedException被中斷。
interrupt()方法只能設(shè)置interrupt標(biāo)志位(且在線程阻塞情況下,標(biāo)志位會(huì)被清除,更無(wú)法設(shè)置中斷標(biāo)志位),無(wú)法停止線程。
5. 線程交互
爭(zhēng)用條件:
1、當(dāng)多個(gè)線程同時(shí)共享訪問(wèn)同一數(shù)據(jù)(內(nèi)存區(qū)域)時(shí),每個(gè)線程都嘗試操作該數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)被破壞(corrupted),這種現(xiàn)象稱為爭(zhēng)用條件
2、原因是,每個(gè)線程在操作數(shù)據(jù)時(shí),會(huì)先將數(shù)據(jù)初值讀【取到自己獲得的內(nèi)存中】,然后在內(nèi)存中進(jìn)行運(yùn)算后,重新賦值到數(shù)據(jù)。
3、爭(zhēng)用條件:線程1在還【未重新將值賦回去時(shí)】,線程1阻塞,線程2開(kāi)始訪問(wèn)該數(shù)據(jù),然后進(jìn)行了修改,之后被阻塞的線程1再獲得資源,而將之前計(jì)算的值覆蓋掉線程2所修改的值,就出現(xiàn)了數(shù)據(jù)丟失情況。
互斥與同步:守恒的能量
1、線程的特點(diǎn),共享同一進(jìn)程的資源,同一時(shí)刻只能有一個(gè)線程占用CPU
2、由于線程有如上的特點(diǎn),所以就會(huì)存在多個(gè)線程爭(zhēng)搶資源的現(xiàn)象,就會(huì)存在爭(zhēng)用條件這種現(xiàn)象
3、為了讓線程能夠正確的運(yùn)行,不破壞共享的數(shù)據(jù),所以,就產(chǎn)生了同步和互斥的兩種線程運(yùn)行的機(jī)制
4、線程的互斥(加鎖實(shí)現(xiàn)):線程的運(yùn)行隔離開(kāi)來(lái),互不影響,使用synchronized關(guān)鍵字實(shí)現(xiàn)互斥行為,此關(guān)鍵字即可以出現(xiàn)在方法體之上也可以出現(xiàn)在方法體內(nèi),以一種塊的形式出現(xiàn),在此代碼塊中有線程的等待和喚醒動(dòng)作,用于支持線程的同步控制
5、線程的同步(線程的等待和喚醒:wait()+notifyAll()):線程的運(yùn)行有相互的通信控制,運(yùn)行完一個(gè)再正確的運(yùn)行另一個(gè)
6、鎖的概念:比如private final Object lockObj=new Object();
7、互斥實(shí)現(xiàn)方式:synchronized關(guān)鍵字
synchronized(lockObj){—執(zhí)行代碼----}加鎖操作
lockObj.wait();線程進(jìn)入等待狀態(tài),以避免線程持續(xù)申請(qǐng)鎖,而不去競(jìng)爭(zhēng)cpu資源
lockObj.notifyAll();喚醒所有l(wèi)ockObj對(duì)象上等待的線程
8、加鎖操作會(huì)開(kāi)銷系統(tǒng)資源,降低效率
同步問(wèn)題提出
線程的同步是為了防止多個(gè)線程訪問(wèn)一個(gè)數(shù)據(jù)對(duì)象時(shí),對(duì)數(shù)據(jù)造成的破壞。
例如:兩個(gè)線程ThreadA、ThreadB都操作同一個(gè)對(duì)象Foo對(duì)象,并修改Foo對(duì)象上的數(shù)據(jù)。
運(yùn)行結(jié)果:
Thread-A : 當(dāng)前foo對(duì)象的x值= 40 Thread-B : 當(dāng)前foo對(duì)象的x值= 40 Thread-B : 當(dāng)前foo對(duì)象的x值= -20 Thread-A : 當(dāng)前foo對(duì)象的x值= -50 Thread-A : 當(dāng)前foo對(duì)象的x值= -80 Thread-B : 當(dāng)前foo對(duì)象的x值= -80 Process finished with exit code 0從結(jié)果發(fā)現(xiàn),這樣的輸出值明顯是不合理的。原因是兩個(gè)線程不加控制的訪問(wèn)Foo對(duì)象并修改其數(shù)據(jù)所致。
如果要保持結(jié)果的合理性,只需要達(dá)到一個(gè)目的,就是將對(duì)Foo的訪問(wèn)加以限制,每次只能有一個(gè)線程在訪問(wèn)。這樣就能保證Foo對(duì)象中數(shù)據(jù)的合理性了。
在具體的Java代碼中需要完成一下兩個(gè)操作:
把競(jìng)爭(zhēng)訪問(wèn)的資源類Foo變量x標(biāo)識(shí)為private;
同步哪些修改變量的代碼,使用synchronized關(guān)鍵字同步方法或代碼。
同步和鎖定
1、鎖的原理
Java中每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖
當(dāng)程序運(yùn)行到非靜態(tài)的synchronized同步方法上時(shí),自動(dòng)獲得與正在執(zhí)行代碼類的當(dāng)前實(shí)例(this實(shí)例)有關(guān)的鎖。獲得一個(gè)對(duì)象的鎖也稱為獲取鎖、鎖定對(duì)象、在對(duì)象上鎖定或在對(duì)象上同步。
當(dāng)程序運(yùn)行到synchronized同步方法或代碼塊時(shí)才該對(duì)象鎖才起作用。
一個(gè)對(duì)象只有一個(gè)鎖。所以,如果一個(gè)線程獲得該鎖,就沒(méi)有其他線程可以獲得鎖,直到第一個(gè)線程釋放(或返回)鎖。這也意味著任何其他線程都不能進(jìn)入該對(duì)象上的synchronized方法或代碼塊,直到該鎖被釋放。
釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
關(guān)于鎖和同步,有一下幾個(gè)要點(diǎn):
1)、只能同步方法,而不能同步變量和類;
2)、每個(gè)對(duì)象只有一個(gè)鎖;當(dāng)提到同步時(shí),應(yīng)該清楚在什么上同步?也就是說(shuō),在哪個(gè)對(duì)象上同步?
3)、不必同步類中所有的方法,類可以同時(shí)擁有同步和非同步方法。
4)、如果兩個(gè)線程要執(zhí)行一個(gè)類中的synchronized方法,并且兩個(gè)線程使用相同的實(shí)例來(lái)調(diào)用方法,那么一次只能有一個(gè)線程能夠執(zhí)行方法,另一個(gè)需要等待,直到鎖被釋放。也就是說(shuō):如果一個(gè)線程在對(duì)象上獲得一個(gè)鎖,就沒(méi)有任何其他線程可以進(jìn)入(該對(duì)象的)類中的任何一個(gè)同步方法。
5)、如果線程擁有同步和非同步方法,則非同步方法可以被多個(gè)線程自由訪問(wèn)而不受鎖的限制。
6)、線程睡眠時(shí),它所持的任何鎖都不會(huì)釋放。
7)、線程可以獲得多個(gè)鎖。比如,在一個(gè)對(duì)象的同步方法里面調(diào)用另外一個(gè)對(duì)象的同步方法,則獲取了兩個(gè)對(duì)象的同步鎖。
8)、同步損害并發(fā)性,應(yīng)該盡可能縮小同步范圍。同步不但可以同步整個(gè)方法,還可以同步方法中一部分代碼塊。
9)、在使用同步代碼塊時(shí)候,應(yīng)該指定在哪個(gè)對(duì)象上同步,也就是說(shuō)要獲取哪個(gè)對(duì)象的鎖。例如:
當(dāng)然,同步方法也可以改寫(xiě)為非同步方法,但功能完全一樣的,例如:
public synchronized int getX() {return x++;}與
public int getX() {synchronized (this) {return x;}}效果是完全一樣的。
靜態(tài)方法同步
要同步靜態(tài)方法,需要一個(gè)用于整個(gè)類對(duì)象的鎖,這個(gè)對(duì)象是就是這個(gè)類(XXX.class)。
例如:
等價(jià)于
public static int setName(String name){synchronized(Xxx.class){Xxx.name = name;} }線程同步小結(jié)
1、線程同步的目的是為了保護(hù)多個(gè)線程訪問(wèn)一個(gè)資源時(shí)對(duì)資源的破壞。
2、線程同步方法是通過(guò)鎖來(lái)實(shí)現(xiàn),每個(gè)對(duì)象都有切僅有一個(gè)鎖,這個(gè)鎖與一個(gè)特定的對(duì)象關(guān)聯(lián),線程一旦獲取了對(duì)象鎖,其他訪問(wèn)該對(duì)象的線程就無(wú)法再訪問(wèn)該對(duì)象的其他同步方法。
3、對(duì)于靜態(tài)同步方法,鎖是針對(duì)這個(gè)類的,鎖對(duì)象是該類的Class對(duì)象。靜態(tài)和非靜態(tài)方法的鎖互不干預(yù)。一個(gè)線程獲得鎖,當(dāng)在一個(gè)同步方法中訪問(wèn)另外對(duì)象上的同步方法時(shí),會(huì)獲取這兩個(gè)對(duì)象鎖。
4、對(duì)于同步,要時(shí)刻清醒在哪個(gè)對(duì)象上同步,這是關(guān)鍵。
5、編寫(xiě)線程安全的類,需要時(shí)刻注意對(duì)多個(gè)線程競(jìng)爭(zhēng)訪問(wèn)資源的邏輯和安全做出正確的判斷,對(duì)“原子”操作做出分析,并保證原子操作期間別的線程無(wú)法訪問(wèn)競(jìng)爭(zhēng)資源。
6、當(dāng)多個(gè)線程等待一個(gè)對(duì)象鎖時(shí),沒(méi)有獲取到鎖的線程將發(fā)生阻塞。
7、死鎖是線程間相互等待鎖鎖造成的,在實(shí)際中發(fā)生的概率非常的小。真讓你寫(xiě)個(gè)死鎖程序,不一定好使,呵呵。但是,一旦程序發(fā)生死鎖,程序?qū)⑺赖簟?/p>
深入剖析互斥與同步
互斥的實(shí)現(xiàn)(加鎖):synchronized(lockObj); 保證的同一時(shí)間,只有一個(gè)線程獲得lockObj.
同步的實(shí)現(xiàn):wait()/notify()/notifyAll()
**注意:**wait()、notify()、notifyAll()方法均屬于Object對(duì)象,而不是Thread對(duì)象。
- void notify()
喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線程。 - void notifyAll()
喚醒在此對(duì)象監(jiān)視器上等待的所有線程。 - void wait()
導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的 notify() 方法或 notifyAll() 方法。
當(dāng)然,wait()還有另外兩個(gè)重載方法:
- void wait(long timeout)
導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的 notify() 方法或 notifyAll() 方法,或者超過(guò)指定的時(shí)間量。 - void wait(long timeout, int nanos)
導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的 notify() 方法或 notifyAll() 方法,或者其他某個(gè)線程中斷當(dāng)前線程,或者已超過(guò)某個(gè)實(shí)際時(shí)間量。
notify()喚醒wait set中的一條線程,而notifyall()喚醒所有線程。
同步是兩個(gè)線程之間的一種交互的操作(一個(gè)線程發(fā)出消息另外一個(gè)線程響應(yīng))
關(guān)于等待/通知,要記住的關(guān)鍵點(diǎn)是:
必須從同步環(huán)境內(nèi)調(diào)用wait()、notify()、notifyAll()方法。線程不能調(diào)用對(duì)象上等待或通知的方法,除非它擁有那個(gè)對(duì)象的鎖。
wait()、notify()、notifyAll()都是Object的實(shí)例方法。與每個(gè)對(duì)象具有鎖一樣,每個(gè)對(duì)象可以有一個(gè)線程列表,他們等待來(lái)自該信號(hào)(通知)。線程通過(guò)執(zhí)行對(duì)象上的wait()方法獲得這個(gè)等待列表。從那時(shí)候起,它不再執(zhí)行任何其他指令,直到調(diào)用對(duì)象的notify()方法為止。如果多個(gè)線程在同一個(gè)對(duì)象上等待,則將只選擇一個(gè)線程(不保證以何種順序)繼續(xù)執(zhí)行。如果沒(méi)有線程等待,則不采取任何特殊操作。
下面看個(gè)例子就明白了:
結(jié)果:
等待對(duì)象b完成計(jì)算。。。
b對(duì)象計(jì)算的總和是:5050
Process finished with exit code 0
千萬(wàn)注意:
當(dāng)在對(duì)象上調(diào)用wait()方法時(shí),執(zhí)行該代碼的線程立即放棄它在對(duì)象上的鎖。然而調(diào)用notify()時(shí),并不意味著這時(shí)線程會(huì)放棄其鎖。如果線程榮然在完成同步代碼,則線程在移出之前不會(huì)放棄鎖。因此,只要調(diào)用notify()并不意味著這時(shí)該鎖變得可用。
多個(gè)線程在等待一個(gè)對(duì)象鎖時(shí)候使用notifyAll():
在多數(shù)情況下,最好通知等待某個(gè)對(duì)象的所有線程。如果這樣做,可以在對(duì)象上使用notifyAll()讓所有在此對(duì)象上等待的線程沖出等待區(qū),返回到可運(yùn)行狀態(tài)。
如何理解同步:Wait Set
Critical Section(臨界資源)Wait Set(等待區(qū)域)
wait set 類似于線程的休息室,訪問(wèn)共享數(shù)據(jù)的代碼稱為critical section。一個(gè)線程獲取鎖,然后進(jìn)入臨界區(qū),發(fā)現(xiàn)某些條件不滿足,然后調(diào)用鎖對(duì)象上的wait方法,然后線程釋放掉鎖資源,進(jìn)入鎖對(duì)象上的wait set。由于線程釋放釋放了理解資源,其他線程可以獲取所資源,然后執(zhí)行,完了以后調(diào)用notify,通知鎖對(duì)象上的等待線程。
Ps:若調(diào)用notify();則隨機(jī)拿出(這隨機(jī)拿出是內(nèi)部的算法,無(wú)需了解)一條在等待的資源進(jìn)行準(zhǔn)備進(jìn)入Critical Section;若調(diào)用notifyAll();則全部取出進(jìn)行準(zhǔn)備進(jìn)入Critical Section。
6. 總結(jié)與展望
擴(kuò)展建議:如何擴(kuò)展Java并發(fā)知識(shí)
1、Java Memory Mode : JMM描述了java線程如何通過(guò)內(nèi)存進(jìn)行交互,了解happens-before , synchronized,voliatile & final
2、Locks % Condition:Java鎖機(jī)制和等待條件的高層實(shí)現(xiàn) java.util,concurrent.locks
3、線程安全性:原子性與可見(jiàn)性, java.util.concurrent.atomic synchronized(鎖的方法塊)&volatile(定義公共資源) DeadLocks(死鎖)–了解什么是死鎖,死鎖產(chǎn)生的條件
4、多線程編程常用的交互模型
· Producer-Consumer模型(生產(chǎn)者-消費(fèi)者模型)
· Read-Write Lock模型(讀寫(xiě)鎖模型)
· Future模型
· Worker Thread模型
考慮在Java并發(fā)實(shí)現(xiàn)當(dāng)中,有哪些類實(shí)現(xiàn)了這些模型,供我們直接調(diào)用
5、Java5中并發(fā)編程工具:java.util.concurrent 包下的
例如:線程池ExcutorService 、Callable&Future 、BlockingQueue
6、推薦書(shū)本:CoreJava 、 JavaConcurrency In Practice
- 出處:http://www.cnblogs.com/Qian123/p/5670304.html
文章有不當(dāng)之處,歡迎指正,你也可以關(guān)注我的微信公眾號(hào):好好學(xué)java,獲取優(yōu)質(zhì)學(xué)習(xí)資源,也可以加入QQ技術(shù)交流群:766946816,咋們來(lái)聊聊java。
總結(jié)
以上是生活随笔為你收集整理的java基础提升篇:深入浅出Java多线程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java基础提升篇:Java 序列化的高
- 下一篇: 并发基础篇(一): Java 并发性和多