一逼庞、什么是線程
線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位蛇更,它被包含在進(jìn)程之中,是進(jìn)程中的實際運(yùn)作單位。程序員可以通過它進(jìn)行多處理器編程派任,你可以使用多線程對運(yùn)算密集型任務(wù)提速砸逊。
一組并發(fā)線程運(yùn)行在一個進(jìn)程的上下文中,每個線程都有它自己獨立的線程上下文掌逛,例如:棧师逸、程序計數(shù)器、線程ID豆混、條件碼篓像、寄存器集合等,每個線程和其它的線程一起共享除此之外的進(jìn)程上下文的剩余部分皿伺。這里有一點要特別注意员辩,就是寄存器是從不共享的,而虛擬存儲器總是共享的鸵鸥。
線程和進(jìn)程的區(qū)別:
- 線程是進(jìn)程的一部分奠滑,所以線程有的時候被稱為是輕權(quán)進(jìn)程或者輕量級進(jìn)程。
- 一個沒有線程的進(jìn)程是可以被看作單線程的脂男,如果一個進(jìn)程內(nèi)擁有多個進(jìn)程养叛,進(jìn)程的執(zhí)行過程不是一條線(線程)的,而是多條線(線程)共同完成的宰翅。
- 系統(tǒng)在運(yùn)行的時候會為每個進(jìn)程分配不同的內(nèi)存區(qū)域弃甥,但是不會為線程分配內(nèi)存(線程所使用的資源是它所屬的進(jìn)程的資源),線程組只能共享資源汁讼。那就是說淆攻,除了CPU之外(線程在運(yùn)行的時候要占用CPU資源),計算機(jī)內(nèi)部的軟硬件資源的分配與線程無關(guān)嘿架,線程只能共享它所屬進(jìn)程的資源瓶珊。
- 與進(jìn)程的控制表PCB相似,線程也有自己的控制表TCB耸彪,但是TCB中所保存的線程狀態(tài)比PCB表中少多了伞芹。
- 進(jìn)程是系統(tǒng)所有資源分配時候的一個基本單位,擁有一個完整的虛擬空間地址蝉娜,并不依賴線程而獨立存在唱较。
二、實現(xiàn)線程
JAVA語言對線程的支持主要體現(xiàn)在Thread類和Runnable接口上面召川,因此JAVA實現(xiàn)多線程有兩種方法:①實現(xiàn)Runnable接口 ②繼承Thread類南缓。
2.1 線程創(chuàng)建及啟動
①線程創(chuàng)建
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)
②線程啟動
void start()
注意別把start()方法和run()方法搞混了。start()方法被用來啟動新創(chuàng)建的線程荧呐,而且start()內(nèi)部調(diào)用了run()方法汉形,這和直接調(diào)用run()方法的效果不一樣纸镊。
1) start()
用start方法來啟動線程,真正實現(xiàn)了多線程運(yùn)行概疆,這時無需等待run方法體代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行下面的代碼逗威。通過調(diào)用Thread類的start()方法來啟動一個線程,這時此線程處于就緒(可運(yùn)行)狀態(tài)岔冀,并沒有運(yùn)行庵楷,一旦得到cpu時間片,就開始執(zhí)行run()方法楣颠,這里方法run()稱為線程體,它包含了要執(zhí)行的這個線程的內(nèi)容咐蚯,run()方法運(yùn)行結(jié)束童漩,此線程隨即終止。
2) run()
run()方法只是類的一個普通方法而已春锋,如果直接調(diào)用run()方法矫膨,程序中依然只有主線程這一個線程,其程序執(zhí)行路徑還是只有一條期奔,還是要順序執(zhí)行侧馅,還是要等待run方法體執(zhí)行完畢后才可繼續(xù)執(zhí)行下面的代碼,這樣就沒有達(dá)到寫線程的目的呐萌。
總結(jié):調(diào)用start方法方可啟動線程馁痴,而run方法只是thread的一個普通方法調(diào)用,還是在主線程里執(zhí)行肺孤。
2.2 獲取線程的引用
static Thread currentThread():返回當(dāng)前運(yùn)行的線程引用
2.3 停止線程的方法
??如果我們想停止線程罗晕,不應(yīng)該用stop(),因為stop()使得線程戛然而止赠堵,完成了什么工作小渊,哪些工作還沒有做,都不知道茫叭,且清理工作也沒有做酬屉,所以stop()不是正確的停止線程方法,所以也被棄用了揍愁。
??之后Java API的設(shè)計者就沒有提供一個兼容且線程安全的方法來停止一個線程呐萨。當(dāng)run()或者call()方法執(zhí)行完的時候線程會自動結(jié)束,如果要手動結(jié)束一個線程吗垮,你可以用volatile布爾變量在線程執(zhí)行中設(shè)置狀態(tài)標(biāo)識來退出run()方法的循環(huán)或者是取消任務(wù)來中斷線程垛吗。
//volatile是可見性的關(guān)鍵,保證了線程正確讀取變量的值
volatile boolean keepRunning = true;
public void run(){
while(keepRunning){
//讓線程"發(fā)動5連擊"
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName() +"正在運(yùn)行");
//讓出處理器時間烁登,使得所有線程能同時再去競爭CPU資源從而獲取運(yùn)行的機(jī)會
Thread.yield();
}
}
//完成最后一次業(yè)務(wù)后跳出while循環(huán)后怯屉,之后進(jìn)行一些清理工作
}
線程終止后蔚舀,其生命周期結(jié)束了,即進(jìn)入死亡態(tài)锨络,終止后的線程不能再被調(diào)度執(zhí)行赌躺。那么線程退出run()方法,是否代表該線程結(jié)束羡儿?還是釋放完內(nèi)存之后礼患?答案就是:執(zhí)行完run之后所有語句=該線程結(jié)束≠臨時內(nèi)存被回收。簡單來說掠归,就是線程結(jié)束就是run方法執(zhí)行結(jié)束之后缅叠,線程進(jìn)入死亡態(tài),他占用的那部分就有可能被回收虏冻,但不一定是立即被回收肤粱。
2.4 測試線程狀態(tài)的方法
??可以通過Thread 中的isAlive() 方法來獲取線程是否處于活動狀態(tài)。線程由start() 方法啟動后厨相,直到其被終止之間的任何時刻领曼,都處于'Alive'狀態(tài)。
2.5 線程的暫停和恢復(fù)
??有幾種方法可以暫停一個線程的執(zhí)行蛮穿,在適當(dāng)?shù)臅r候再恢復(fù)其執(zhí)行庶骄。
1.sleep() 方法
當(dāng)前線程睡眠(停止執(zhí)行)若干毫秒,線程由運(yùn)行中狀態(tài)進(jìn)入不可運(yùn)行狀態(tài)践磅,停止執(zhí)行時間到后線程進(jìn)入可運(yùn)行狀態(tài)单刁。
static void sleep(long millis)
static void sleep(long millis,int nanos)
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
線程休眠要點
①線程休眠總是暫停當(dāng)前線程
②在被喚醒并開始執(zhí)行前,線程休眠的實際時間取決于系統(tǒng)計時器和調(diào)度器府适。對比較清閑的系統(tǒng)來說,實際休眠的時間十分接近于指定的休眠時間,但對于繁忙的系統(tǒng),兩者之間的差距就較大窜骄。
③線程休眠并不會釋放當(dāng)前線程已經(jīng)獲取的任何鎖
④線程休眠并不會丟失當(dāng)前線程已經(jīng)獲取的任何監(jiān)視器虐骑。
⑤其他線程可以中斷當(dāng)前進(jìn)程的休眠,但會拋出InterruptedException異常颠黎。
2.join()
join()的作用是:“等待該線程終止”文判。這里需要理解的就是該線程是指的主線程等待子線程的終止竞惋,也就是在子線程調(diào)用了join()方法后面的代碼浑厚,只有等到子線程結(jié)束了才能執(zhí)行。
void join()
void join(long millis)
void join(long millis,int nanos)
//加入join是為了讓Stage線程最后停止钠导,如果不加有可能Stage線程結(jié)束,actor線程還未停止
//好比導(dǎo)演喊停,演員還在演
public class Stage extends Thread(){
public void run(){
ActorRunnable actorTask = new ActorRunnable();
Thread actor = new Thread(actorTask1,"演員1")窗宇;
//當(dāng)前是Stage和actor兩個線程同時競爭CPU資源
actor.start();
try{
actor.join();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
3)yield()
yield()使得當(dāng)前運(yùn)行線程釋放處理器資源蝇完,但并不意味著退出和暫停,只是傻咖,告訴線程調(diào)度如果有人需要朋魔,可以先拿去,我過會再執(zhí)行卿操,沒人需要警检,我繼續(xù)執(zhí)行。但是它的這種主動退出沒有任何保障害淤,就是在當(dāng)前進(jìn)入可運(yùn)行狀態(tài)時扇雕,還是有可能被JVM選中再回到運(yùn)行狀態(tài)的。
static void yield()
Thread.yield( ):使當(dāng)前線程從執(zhí)行狀態(tài)(運(yùn)行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))窥摄。
2.6 使用線程代碼示例
public class ActorRunnable implements Runnable{
volatile boolean keepRunning = true;
public void run(){
while(keepRunning){
//讓線程"發(fā)動5連擊"
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName() +"正在運(yùn)行");
//讓出處理器時間镶奉,使得所有線程能同時再去競爭CPU資源從而獲取運(yùn)行的機(jī)會
Thread.yield();
}
}
}
public class Stage extends Thread(){
public void run(){
//演員1的任務(wù)
ActorRunnable actorTask1 = new ActorRunnable();
//演員2的任務(wù)
ActorRunnable actorTask2 = new ActorRunnable();
//Thread中構(gòu)造方法里面的runnable對象用于提供線程業(yè)務(wù)接口run方法
Thread actor1 = new Thread(actorTask1,"演員1");
Thread actor2 = new Thread(actorTask2,"演員2")崭放;
//actor1哨苛、actor2兩個線程由本線程啟動
actor1.start();
actor2.start();
//讓舞臺線程暫時休眠。因為在本實例中是actor1币砂、actor2和stage三個線程共同競爭CPU資源的
//由于當(dāng)stage競爭到了cpu時就會終止另外兩個線程建峭,所以在stage線程中調(diào)用了sleep方法來給actor1和actor2方法提供最少50ms的運(yùn)行時間。
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
//只有當(dāng)Thread.yield()的時候决摧,選中的是stage線程迹缀,才能執(zhí)行到下面停止actor的方法。
//但是actor1停止了并不意味著actor2也會停止蜜徽,因為執(zhí)行完actor1.keepRunning = false后祝懂,可能stage線程的cpu資源被搶走了
actor1.keepRunning = false;
actor2.keepRunning = false;
}
}
public void main(String[] args){
new stage().start();
}
三、Thread VS Runnable
我們剛接觸的時候可能會迷糊繼承Thread類和實現(xiàn)Runnable接口實現(xiàn)多線程拘鞋,其實在接觸后我們會發(fā)現(xiàn)這完全是兩個不同的實現(xiàn)多線程砚蓬。
- Runnable方式可以避免Thread方式由于java單繼承特性帶來的缺陷
- Runnable的代碼可以被多個線程(Thread實例)共享,適合于多個線程處理同一資源的情況盆色;而Thread方式是多個線程分別完成自己的任務(wù)灰蛙。
第二個是什么意思呢祟剔?我們用代碼來說明。假設(shè)到年關(guān)了摩梧,現(xiàn)在還剩下五張火車票物延,那現(xiàn)在有三個窗口去賣這五張火車票,我們用三個線程去模擬這三個窗口去同時賣這五張火車票仅父。
用Thread實現(xiàn)賣票系統(tǒng)
class MyThread extends Thread{
private int ticketsCont = 5; //5張火車票
private String name ; //窗口叛薯,即線程名字
public MyThread(String name){
this.name = name;
}
public void run(){
while(ticketCont>0){
ticketCont--;
System.out.println(name+"賣掉了一張票,剩余票數(shù)為:"+ticketCont);
}
}
}
public class TicketThread{
public void main(String[] args){
//創(chuàng)建三個線程笙纤,模擬三個窗口賣票
MyThread mt1 = new MyThread("窗口1");
MyThread mt2 = new MyThread("窗口2");
MyThread mt3 = new MyThread("窗口3");
//啟動三個線程耗溜,也即是開始賣票
mt1.start();
mt2.start();
mt3.start();
}
}
運(yùn)行結(jié)果:總共有5張票,但是卻賣了15張票
用Runnable實現(xiàn)賣票系統(tǒng)
class MyThread implements Runnable{
private int ticketsCont = 5; //5張火車票
public void run(){
while(ticketCont>0){
ticketCont--;
System.out.println(Thread.currentThread().getName()+"賣掉了一張票省容,剩余票數(shù)為:"+ticketCont);
}
}
}
public class TicketThread{
public void main(String[] args){
MyThread mt = new MyThread();
//創(chuàng)建三個線程抖拴,模擬三個窗口賣票
Thread th1 = new Thread(mt,"窗口2");
Thread th2 = new Thread(mt,"窗口2");
Thread th3 = new Thread(mt,"窗口3");
//啟動三個線程,也即是開始賣票
th1.start();
th2.start();
th3.start();
}
}
運(yùn)行結(jié)果:正常賣出五張票
??在Runnable模擬的方法中腥椒,因為MyThread實現(xiàn)Runnable接口的阿宅。MyThread mt = new MyThread();之后我們把mt傳給三個線程對象,也就是說三個線程傳遞的對象是同一個Runnable對象笼蛛,所以三個線程對象用的都是同一個Runnable對象里面的代碼洒放,自然而然資源也是共享的,所以它們?nèi)齻€加在一起總共賣了五張火車票伐弹。
??但是為什么打印出來剩余票數(shù)是4、1榨为、3惨好、0、2呢随闺?因為線程的執(zhí)行時間是隨機(jī)的日川。首先線程1啟動,獲取到CPU資源矩乐,然后轉(zhuǎn)到run方法去執(zhí)行龄句。執(zhí)行完之后又把CPU資源讓出來,票數(shù)剩余4張散罕。這個時候主線程又繼續(xù)執(zhí)行分歇,然后線程1沒有獲取了CPU資源,就在等待欧漱,線程2獲取到了CPU資源就去執(zhí)行run方法职抡,剩余票數(shù)變成3張......
四、守護(hù)線程
JAVA線程有兩類:
①用戶線程
- 運(yùn)行在前臺误甚,執(zhí)行具體的任務(wù)
- 程序的主線程缚甩、連接網(wǎng)絡(luò)的子線程等都是用戶線程
②守護(hù)線程
- 運(yùn)行在后臺谱净,為其他前臺線程服務(wù)
- 一旦所有用戶線程都運(yùn)行結(jié)束,守護(hù)線程會隨JVM一起結(jié)束工作
- 最常見的守護(hù)線程就是垃圾回收線程擅威、數(shù)據(jù)庫連接池中的監(jiān)測線程壕探、JVM虛擬機(jī)啟動后的監(jiān)測線程。
如何設(shè)置守護(hù)線程郊丛?可以通過調(diào)用Thread類的setDaemon(true)方法來設(shè)置當(dāng)前的線程為守護(hù)線程李请。要注意的是,setDaemon(true)必須在start()方法之前調(diào)用宾袜,否則會拋出IllegalThreadStateException異常
public class DeamonThredemo{
public static void main(String[] args){
//這里的主線程就是main
System.out.println("進(jìn)入主線程"+Thread.currentThread().getName());
//deamonThread實現(xiàn)了Runnable接口
DeamonThread deamonThread = new DeamonThread();
Thread thread = new Thread(deamonThread);
//將thread設(shè)置為守護(hù)線程
thread.setDaemon(true)
//啟動守護(hù)線程
thread.start();
//主線程工作捻艳。在主線程工作的同時守護(hù)線程會一直工作,直到主線程退出
System.out.println("程序退出了主線程"+Thread.currentThread().getName());
}
}
但不是所有的任務(wù)都可以分配給守護(hù)線程來運(yùn)行庆猫,比如讀寫操作或者計算邏輯為什么呢认轨?
??一旦所有的用戶線程都退出運(yùn)行了,守護(hù)線程也覺得自己的存在沒有必要了月培,因為它沒有守護(hù)的對象了嘁字,這時守護(hù)線程也會結(jié)束掉工作。如果我在守護(hù)線程里面做了一些讀寫操作杉畜,而當(dāng)這個讀寫操作做到一半的時候纪蜒,所有的用戶線程都退出來了,這個時候守護(hù)線程也沒有存在的必要了此叠,他也會結(jié)束掉自己纯续。但是這個時候讀寫操作還沒進(jìn)行完,守護(hù)線程就退出來了灭袁,那程序就崩潰了猬错。