《實(shí)戰(zhàn)高并發(fā)程序設(shè)計(jì)》讀書筆記-線程基本狀態(tài)和基本操作

線程的狀態(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ì)象。


image.png

演示代碼

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ì)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容