線程的狀態(tài)有哪些?
- new
- 新建狀態(tài):線程創(chuàng)建之后
- running
- 可運(yùn)行:可能正在運(yùn)行,也可能正在等待CPU時(shí)間片。
- blocked
- 阻塞:等待獲取一個(gè)排它鎖,如果其線程釋放了鎖就會(huì)結(jié)束此狀態(tài)
- waiting
- 無(wú)限期等待:等待其它線程顯式地喚醒,否則不會(huì)被分配CPU時(shí)間片
- time waiting
- 限期等待:無(wú)需等待其它線程顯式地喚醒,在一定時(shí)間之后會(huì)被系統(tǒng)終止
- terminated
-
終止( /term nei tid/):可以是線程結(jié)束任務(wù)之后自己結(jié)束,或者產(chǎn)生了異常而結(jié)束
image.png
-
線程創(chuàng)建之后它將處于new(新建)狀態(tài),調(diào)用start()方法后開始運(yùn)行,線程這時(shí)候處于ready(可運(yùn)行)狀態(tài)。可運(yùn)行狀態(tài)的線程獲得了cpu時(shí)間片后就處于running(運(yùn)行)狀態(tài)(操作系統(tǒng)隱藏JⅥM中的ready和running狀態(tài),它只能看到 RUNNABLE狀態(tài))。
當(dāng)線程執(zhí)行wai()方法之后,線程進(jìn)入waiting(等待)狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而 time waiting(超時(shí)等待)狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過slep( long millis)方法或wait( long millis)方法可以將Java線程置于 timed_waiting狀態(tài)。當(dāng)超時(shí)時(shí)間到達(dá)后Java線程將會(huì)返回runnable狀態(tài)。當(dāng)線程調(diào)用同步方法時(shí),在沒有獲取到鎖的情況下,線程將會(huì)進(jìn)入到blocked(阻塞)狀態(tài)。線程在執(zhí)行 Runnable的run()方法之后將會(huì)進(jìn)入到 terminated(終止)狀態(tài)。
線程的基本操作
新建線程
public static void main(String[] args) {
Thread t1=new Thread(new CreateThread3());
t1.start();
}
@Override
public void run() {
System.out.println("Oh, I am Runnable");
}
}
notice:
線程調(diào)用run()和start()的區(qū)別
Thread t1=new Thread();
t1.run();
這段代碼雖然也可以通過編譯,但是卻不是創(chuàng)建一個(gè)新線程,而是作為一個(gè)普通方法在當(dāng)前線程中串行執(zhí)行,所以不要用這種方法開啟線程。
終止線程
一般情況下,線程執(zhí)行完后就會(huì)結(jié)束,無(wú)需手工關(guān)閉,但是hread提供了一個(gè)stop()方法。如果你使用stop()方法,就可以立即將一個(gè)線程終止,非常方便。
但是stop()的使用要慎重,這過于暴力,強(qiáng)行把執(zhí)行到一半的線程終止,可能會(huì)引起一些數(shù)據(jù)不一致的問題。
使用stop引起的數(shù)據(jù)不一致問題
假設(shè)為用戶表賦值,他有兩個(gè)屬性。
記錄1:ID=1,NAME=小明
記錄2:ID=2,NAME=小王
如果用同一個(gè)對(duì)象去保存這個(gè)記錄,使用stop會(huì)引起數(shù)據(jù)不一致問題,比如id=2,而Name=小明。
這是因?yàn)門hread.stop()方法在結(jié)束線程時(shí),會(huì)直接終止線程,并且會(huì)立即釋放這個(gè)線程所持有的鎖。而這些鎖恰恰是用來(lái)維持對(duì)象一致性的。如果此時(shí),寫線程寫入數(shù)據(jù)正寫到一半,并強(qiáng)行終止,那么對(duì)象就會(huì)被寫壞,同時(shí),由于鎖已經(jīng)被釋放,另外一個(gè)等待該鎖的讀線程就順理成章的讀到了這個(gè)不一致的對(duì)象。

演示代碼
01 public class StopThreadUnsafe {
02 public static User u=new User();
03 public static class User{
04 private int id;
05 private String name;
06 public User(){
07 id=0;
08 name="0";
09 }
10 //省略setter和getter方法
11 @Override
12 public String toString() {
13 return "User [id=" + id + ", name=" + name + "]";
14 }
15 }
16 public static class ChangeObjectThread extends Thread{
17 @Override
18 public void run(){
19 while(true){
20 synchronized(u){
21 int v=(int)(System.currentTimeMillis()/1000);
22 u.setId(v);
23 //Oh, do sth. else
24 try {
25 Thread.sleep(100);
26 } catch (InterruptedException e) {
27 e.printStackTrace();
28 }
29 u.setName(String.valueOf(v));
30 }
31 Thread.yield();
32 }
33 }
34 }
35
36 public static class ReadObjectThread extends Thread{
37 @Override
38 public void run(){
39 while(true){
40 synchronized(u){
41 if(u.getId() != Integer.parseInt(u.getName())){
42 System.out.println(u.toString());
43 }
44 }
45 Thread.yield();
46 }
47 }
48 }
49
50 public static void main(String[] args) throws InterruptedException {
51 new ReadObjectThread().start();
52 while(true){
53 Thread t=new ChangeObjectThread();
54 t.start();
55 Thread.sleep(150);
55 Thread.sleep(150);
56 t.stop();
57 }
58 }
59 }
執(zhí)行以上代碼,可以很容易得到類似如下輸出,ID和NAME產(chǎn)生了不一致。
User [id=1425135593, name=1425135592]
User [id=1425135594, name=1425135593]
如果在線上環(huán)境跑出以上結(jié)果,那么加班加點(diǎn)估計(jì)是免不了了,因?yàn)檫@類問題一旦出現(xiàn),就很難排查,因?yàn)樗鼈兩踔翛]有任何錯(cuò)誤信息,也沒有線程堆棧。這種情況一旦混雜在動(dòng)則十幾萬(wàn)行的程序代碼中時(shí),發(fā)現(xiàn)它們就全憑經(jīng)驗(yàn)、時(shí)間還有一點(diǎn)點(diǎn)運(yùn)氣了。因此,除非你很清楚你在做什么,否則不要隨便使用stop()方法來(lái)停止一個(gè)線程。
這種問題如何規(guī)避?
方案一
可以看到上文中的User是一個(gè)全局變量,存儲(chǔ)在堆中,線程共用,而且這個(gè)也是線程的共享變量,如果做不到原子操作,必然會(huì)會(huì)有線程安全問題,最好的方法,線程之間不用這種存儲(chǔ)在heap中的變量,將這個(gè)對(duì)象放到方法中創(chuàng)建,即對(duì)象存儲(chǔ)到stack中。
方案二
在線程中設(shè)置一個(gè)標(biāo)記變量,作為while循環(huán)的條件,就算標(biāo)記變量改為終止線程,也不會(huì)立刻結(jié)束,會(huì)等待當(dāng)前循環(huán)的邏輯處理完后,再終止線程。
01 public static class ChangeObjectThread extends Thread {
02 volatile boolean stopme = false;
03
04 public void stopMe(){
05 stopme = true;
06 }
07 @Override
08 public void run() {
09 while (true) {
10 if (stopme){
11 System.out.println("exit by stop me");
12 break;
13 }
14 synchronized (u) {
15 int v = (int) (System.currentTimeMillis() / 1000);
16 u.setId(v);
17 //Oh, do sth. else
18 try {
19 Thread.sleep(100);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 u.setName(String.valueOf(v));
24 }
25 Thread.yield();
26 }
27 }
28 }
線程中斷
這個(gè)線程中斷和上文提到的線程終止不是同一個(gè)意思,線程中斷強(qiáng)調(diào)的是給線程發(fā)送一個(gè)通知,而不是讓線程立刻終止,至于目標(biāo)線程接到通知后如何處理,則完全由目標(biāo)線程自行決定。
jdk提供的線程中斷的三個(gè)方法
public void Thread.interrupt() // 中斷線程
public boolean Thread.isInterrupted() // 判斷是否被中斷
public static boolean Thread.interrupted() // 判斷是否被中斷,并清除當(dāng)前中斷狀態(tài)
Thread.interrupt()方法是一個(gè)實(shí)例方法。它通知目標(biāo)線程中斷,也就是設(shè)置中斷標(biāo)志位。(可以看出這和上面提到的解決數(shù)據(jù)不一致的方案二的思路如出一轍)中斷標(biāo)志位表示當(dāng)前線程已經(jīng)被中斷了。
Thread.isInterrupted()方法也是實(shí)例方法,它判斷當(dāng)前線程是否有被中斷(通過檢查中斷標(biāo)志位)。
靜態(tài)方法Thread.interrupted()也是用來(lái)判斷當(dāng)前線程的中斷狀態(tài),但同時(shí)會(huì)清除當(dāng)前線程的中斷標(biāo)志位狀態(tài)。
調(diào)用中斷會(huì)終止線程么?
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
在這里,雖然對(duì)t1進(jìn)行了中斷,但是在t1中并沒有中斷處理的邏輯,因此,即使t1線程被置上了中斷狀態(tài),但是這個(gè)中斷不會(huì)發(fā)生任何作用。
如果希望t1在中斷后退出,就必須為它增加相應(yīng)的中斷處理代碼:
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//這才是通過標(biāo)記位判斷是否終止線程的正確方式
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
break;
}
Thread.yield();
}
}
只有在線程的業(yè)務(wù)邏輯中添加
if(Thread.currentThread().isInterrupted())
才是正確中斷線程的方式,否則單獨(dú)使用interrupt()起不到終止線程的目的。
isInterrupted()和Thread.sleep()組合使用的正確方式
如果在循環(huán)體中,出現(xiàn)了類似于wait()或者sleep()這樣的操作,則只能通過中斷來(lái)識(shí)別了。
Thread.sleep()方法會(huì)讓當(dāng)前線程休眠若干時(shí)間,它會(huì)拋出一個(gè)InterruptedException中斷異常。InterruptedException不是運(yùn)行時(shí)異常,也就是說程序必須捕獲并且處理它,當(dāng)線程在sleep()休眠時(shí),如果被中斷,這個(gè)異常就會(huì)產(chǎn)生。
01 public static void main(String[] args) throws InterruptedException {
02 Thread t1=new Thread(){
03 @Override
04 public void run(){
05 while(true){
06 if(Thread.currentThread().isInterrupted()){
07 System.out.println("Interruted!");
08 break;
09 }
10 try {
11 Thread.sleep(2000);
12 } catch (InterruptedException e) {
13 System.out.println("Interruted When Sleep");
14 //設(shè)置中斷狀態(tài)
15 Thread.currentThread().interrupt();
16 }
17 Thread.yield();
18 }
19 }
20 };
21 t1.start();
22 Thread.sleep(2000);
23 t1.interrupt();
24 }
注意上述代碼中第10~15行加粗部分,如果在第11行代碼處,線程被中斷,則程序會(huì)拋出異常,并進(jìn)入第13行處理。在catch子句部分,由于已經(jīng)捕獲了中斷,我們可以立即退出線程。但在這里,我們并沒有這么做,因?yàn)橐苍S在這段代碼中,我們還必須進(jìn)行后續(xù)的處理,保證數(shù)據(jù)的一致性和完整性,因此,執(zhí)行了Thread.interrupt()方法再次中斷自己,置上中斷標(biāo)記位。只有這么做,在第6行的中斷檢查中,才能發(fā)現(xiàn)當(dāng)前線程已經(jīng)被中斷了。
注意:Thread.sleep()方法由于中斷而拋出異常,此時(shí),它會(huì)清除中斷標(biāo)記,如果不加處理,那么在下一次循環(huán)開始時(shí),就無(wú)法捕獲這個(gè)中斷,故在異常處理中,再次設(shè)置中斷標(biāo)記位。
等待(wait)和通知(notify)
為了支持多線程之間的協(xié)作,JDK提供了兩個(gè)非常重要的接口線程等待wait()方法和通知notify()方法。這兩個(gè)方法并不是在Thread類中的,而是輸出Object類。這也意味著任何對(duì)象都可以調(diào)用這兩個(gè)方法。
public final void wait() throws InterruptedException
public final native void notify()
當(dāng)在一個(gè)對(duì)象實(shí)例上調(diào)用wait()方法后,當(dāng)前線程就會(huì)在這個(gè)對(duì)象上等待,轉(zhuǎn)為等待狀態(tài),一直等到其他線程調(diào)用了obj.notify()方法為止。這時(shí),obj對(duì)象就儼然成為多個(gè)線程之間的有效通信手段。Object.wait()方法并不是可以隨便調(diào)用的,它必須包含在對(duì)應(yīng)的synchronzied語(yǔ)句中,無(wú)論是wait()或者notify()都需要首先獲得目標(biāo)對(duì)象的一個(gè)監(jiān)視器,即要是想使用waitI()必須先獲取鎖。而wait()方法在執(zhí)行后,會(huì)釋放這個(gè)監(jiān)視器。這樣做的目的是使得其他等待在object對(duì)象上的線程不至于因?yàn)樵摼€程的休眠而全部無(wú)法正常執(zhí)行。
需要注意的是,object.notify()喚醒線程,它就會(huì)從這個(gè)等待隊(duì)列中,隨機(jī)選擇一個(gè)線程,并將其喚醒,這個(gè)選擇是不公平的,并不是先等待的線程會(huì)優(yōu)先被選擇,這個(gè)選擇完全是隨機(jī)的。所以有些時(shí)候?yàn)榱吮苊馑姥h(huán),直接使用notifyAll(),然后先獲取鎖的線程得以執(zhí)行業(yè)務(wù)邏輯。
等待和通知的例子
01 public class SimpleWN {
02 final static Object object = new Object();
03 public static class T1 extends Thread{
04 public void run()
05 {
06 synchronized (object) {
07 System.out.println(System.currentTimeMillis()+":T1 start! ");
08 try {
09 System.out.println(System.currentTimeMillis()+":T1 wait for object ");
10 object.wait();
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 System.out.println(System.currentTimeMillis()+":T1 end!");
15 }
16 }
17 }
18 public static class T2 extends Thread{
19 public void run()
20 {
21 synchronized (object) {
22 System.out.println(System.currentTimeMillis()+":T2 thread");
23 object.notify();
24 System.out.println(System.currentTimeMillis()+":T2 end!");
25 try {
26 Thread.sleep(2000);
27 } catch (InterruptedException e) {
28 }
29 }
30 }
31 }
32 public static void main(String[] args) {
33 Thread t1 = new T1() ;
34 Thread t2 = new T2() ;
35 t1.start();
36 t2.start();
37 }
38 }
掛起(suspend)和繼續(xù)執(zhí)行(resume)線程
線程掛起(suspend)和繼續(xù)執(zhí)行(resume)。這兩個(gè)操作是一對(duì)相反的操作,被掛起的線程,必須要等到resume()操作后,才能繼續(xù)指定,但是jdk早已將他們標(biāo)注為廢棄方法,并不推薦使用,suspend()在導(dǎo)致線程暫停的同時(shí),并不會(huì)去釋放任何鎖資源。此時(shí),其他任何線程想要訪問被它暫用的鎖時(shí),都會(huì)被牽連。
等待線程結(jié)束(join)和謙讓(yield)
join
很多時(shí)候,一個(gè)線程的輸入可能非常依賴于另外一個(gè)或者多個(gè)線程的輸出,此時(shí),這個(gè)線程就需要等待依賴線程執(zhí)行完畢,才能繼續(xù)執(zhí)行。JDK提供了join()操作來(lái)實(shí)現(xiàn)這個(gè)功能
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
第一個(gè)join()方法表示無(wú)限等待,它會(huì)一直阻塞當(dāng)前線程,直到目標(biāo)線程執(zhí)行完畢。第二個(gè)方法給出了一個(gè)最大等待時(shí)間,如果超過給定時(shí)間目標(biāo)線程還在執(zhí)行,當(dāng)前線程也會(huì)因?yàn)椤暗炔患傲恕?,而繼續(xù)往下執(zhí)行。
這里提供一個(gè)簡(jiǎn)單點(diǎn)的join()實(shí)例,供大家參考:
public class JoinMain {
public volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<10000000;i++);
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at=new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
主函數(shù)中,如果不使用join()等待AddThread,那么得到的i很可能是0或者一個(gè)非常小的數(shù)字。因?yàn)锳ddThread還沒開始執(zhí)行,i的值就已經(jīng)被輸出了。但在使用join()方法后,表示主線程愿意等待AddThread執(zhí)行完畢,跟著AddThread一起往前走,故在join()返回時(shí),AddThread已經(jīng)執(zhí)行完成,故i總是10000000。
有關(guān)join(),我還想再補(bǔ)充一點(diǎn),join()的本質(zhì)是讓調(diào)用線程wait()在當(dāng)前線程對(duì)象實(shí)例上。下面是JDK中join()實(shí)現(xiàn)的核心代碼片段:
while (isAlive()) {
wait(0);
}
可以看到,它讓調(diào)用線程在當(dāng)前線程對(duì)象上進(jìn)行等待。當(dāng)線程執(zhí)行完成后,被等待的線程會(huì)在退出前調(diào)用notifyAll()通知所有的等待線程繼續(xù)執(zhí)行。因此,值得注意的一點(diǎn)是:不要在應(yīng)用程序中,在Thread對(duì)象實(shí)例上使用類似wait()或者notify()等方法,因?yàn)檫@很有可能會(huì)影響系統(tǒng)API的工作,或者被系統(tǒng)API所影響。
yield
public static native void yield();
這是一個(gè)靜態(tài)方法,一旦執(zhí)行,它會(huì)使當(dāng)前線程讓出CPU。但要注意,讓出CPU并不表示當(dāng)前線程不執(zhí)行了。當(dāng)前線程在讓出CPU后,還會(huì)進(jìn)行CPU資源的爭(zhēng)奪,但是是否能夠再次被分配到,就不一定了。因此,對(duì)Thread.yield()的調(diào)用就好像是在說:我已經(jīng)完成一些最重要的工作了,我應(yīng)該是可以休息一下了,可以給其他線程一些工作機(jī)會(huì)啦!
如果你覺得一個(gè)線程不那么重要,或者優(yōu)先級(jí)非常低,而且又害怕它會(huì)占用太多的CPU資源,那么可以在適當(dāng)?shù)臅r(shí)候調(diào)用Thread.yield(),給予其他重要線程更多的工作機(jī)會(huì)。
