Java多線程基礎(chǔ)(一)——java線程狀態(tài)與操作

1線程基本操作

1.1 創(chuàng)建線程
  • 1.繼承java.lang.Thread
public class MyThread extends Thread {
    //復(fù)寫run方法.
    public void run() {
       //...
    }
}
public class MultiThread {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        //調(diào)用start 來 啟動子線程
        t.start();   
        //主線程繼續(xù)同時(shí)向下執(zhí)行
        //...
    }
}
  • 2.實(shí)現(xiàn)java.lang.Runnable接口
//實(shí)現(xiàn)runable
public class MyThread implements Runnable {
    public void run() {
        //...
    }
}
public class MultiThread {
    public static void main(String[] args) {
        //Thread的構(gòu)造參數(shù)為 Runable接口
        Thread t = new Thread(new MyThread());
        t.start();    //啟動子線程
        //主線程繼續(xù)同時(shí)向下執(zhí)行
        //..
    }
}
1.2 線程的結(jié)束

主線程執(zhí)行結(jié)束,子線程未結(jié)束,進(jìn)程不退出,為何? 如果把子線程設(shè)置為 deamon Thread,則主線程退出,進(jìn)程就退出了.

所有非deamon線程都退出時(shí),進(jìn)程才退出.

1.3 暫停線程

java.lang.Thread.sleep(xxx)方法(注意是類方法)。使當(dāng)前正在執(zhí)行的線程暫停指定的時(shí)間,如果線程持有鎖,sleep方法并不會釋放鎖

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.print(i + " ");
            try {
                Thread.sleep(1000);    //當(dāng)前main線程暫停1000ms
            } catch (InterruptedException e) {
            }
        }
    }
}

上述代碼中,當(dāng)main線程調(diào)用Thread.sleep(1000)后,線程會被暫停,如果被interrupt,則會拋出InterruptedException異常。

1.4 線程中斷

java.lang.Thread#interrupt()方法
Thread實(shí)例方法。當(dāng)被interrupt的線程處于waitting狀態(tài)如wait、sleep、join時(shí),會拋出InterruptedException異常,異常拋出后線程內(nèi)的 中斷狀態(tài)被重置為false,所以捕獲異常后,要及時(shí)退出線程.。
事實(shí)上,interrupt方法只是改變目標(biāo)線程的中斷狀態(tài)(interrupt status),而那些會拋出InterruptedException異常的方法,如wait、sleep、join等,都是在方法內(nèi)部不斷地檢查中斷狀態(tài)的值

java.lang.Thread#isInterrupted()方法
Thread實(shí)例方法:用來檢查指定線程的中斷狀態(tài)。true為中斷狀態(tài); false為: 非中斷。

java.lang.Thread#interrupted方法
Thread類方法:返回調(diào)用此方法的線程的中斷狀態(tài),并清除中斷狀態(tài)(置為false) ;

1.5 線程同步

synchronized 是管程,細(xì)節(jié)查看管程資料
synchronized 可使用 對象實(shí)例鎖 和 類鎖.

package com.rock.multithread.base;

public class SynchronizedDemo {

    /**
     * 實(shí)例方法鎖 即 this 實(shí)例鎖
     * @return
     */
    public synchronized String 實(shí)例方法鎖(){
        return "";
    }

    /**
     * 同 this 實(shí)例鎖
     * @return
     */
    public String this鎖(){
        synchronized (this){
            return "";
        }
    }

    /**
     * 靜態(tài)方法鎖 即類鎖
     * @return
     */
    public static synchronized String 靜態(tài)方法鎖(){
        return "";
    }

    /**
     * 類鎖,同上
     * @return
     */
    public String 類方法鎖(){
        synchronized (SynchronizedDemo.class){
            return  "";
        }
    }

}

1.6 線程協(xié)調(diào)

1Object#wait
放棄鎖,等通知

  • 先進(jìn)入同步代碼塊,即拿到鎖
  • xxx.wait(); 把當(dāng)前線程放入鎖對象的wait set 中,釋放鎖;
  • 等待別的線程調(diào)用 通知方法 給信號后,剛執(zhí)行wait的線程 從wait set 中移除,放入鎖池隊(duì)列中,被系統(tǒng)調(diào)度喚醒后重新持有鎖,執(zhí)行wait后的代碼.
  • 通知方法 包括notify,notifyAll,interrupt
    ...

2.Object#notify / Object#notifyall
通知等待的線程

  • 先進(jìn)入同步代碼塊,即拿到鎖
  • notify / notifyall 給等待線程發(fā)信號,兩者的區(qū)別是,notify方法 只從 wait set中移動一個(gè)線程到鎖池中,notifyAll是 wait set中的全部線程都移到鎖池中.
  • 繼續(xù)執(zhí)行notify/notifyAll 后的方法,直到退出同步代碼塊,才釋放鎖.這里是關(guān)鍵,發(fā)送通知后,線程不會立即終止執(zhí)行,所以后續(xù)的代碼可能還會修改競態(tài)條件.

4.wait的使用為何要加while
1.案例1

public class WaitNotifyDemo {

    private volatile  boolean isEmpty= false;

    /**
     * 假喚醒實(shí)例1 ,提供消費(fèi)條件,喚醒消費(fèi)線程,又把消費(fèi)條件給取消了,這樣消費(fèi)線程醒來,缺不滿足條件.
     */
    public synchronized void fakeProvider1(){
        System.out.println("冒牌provider 把isEmpty 設(shè)置為true,通知后,還把信號再修改為false");
        isEmpty = true;
        notify();//發(fā)送通知后,線程不會退出
        isEmpty = false; //說不清的原因 信號又被重置了.這種情況下,consumer線程醒來后,其實(shí)isEmpty條件是不滿足的,所以應(yīng)該應(yīng)用用while循環(huán)來判斷條件
    }
    public synchronized void consumer1() throws InterruptedException {
        //使用while循環(huán)來防止假喚醒.所謂的假喚醒本質(zhì)是,喚醒后不滿足繼續(xù)執(zhí)行的條件,所以繼續(xù)判斷下條件是否滿足,不滿足就繼續(xù)wait.
        while (isEmpty){
            //條件不滿足,等待
            this.wait();
        }
        System.out.println("成功消費(fèi)一次,isEmpty 設(shè)置為true");
        isEmpty = true;
    }
}

2.案例2

public class WaitNotifyDemo2 {

    private volatile  boolean isEmpty= false;
    /**
     * 一個(gè)provider
     * 多個(gè)線程執(zhí)行消費(fèi)
     * 正常的設(shè)置消費(fèi)條件.發(fā)通知
     */
    public synchronized void provider(){
        System.out.println("provider 把isEmpty 設(shè)置為true,通知所有等待線程");
        isEmpty = true;
        notifyAll();
    }

    /**
     * 多個(gè)消費(fèi)者都喚醒后,其中1個(gè)消費(fèi)者拿到鎖,執(zhí)行消費(fèi)后,把狀態(tài)重置了.另外一個(gè)消費(fèi)者的執(zhí)行條件就不滿足了,要繼續(xù)等.
     * @throws InterruptedException
     */
    public synchronized void consumer() throws InterruptedException {
        //使用while循環(huán)來防止假喚醒.所謂的假喚醒本質(zhì)是,喚醒后不滿足繼續(xù)執(zhí)行的條件,所以繼續(xù)判斷下條件是否滿足,不滿足就繼續(xù)wait.
        while (isEmpty){
            //條件不滿足,等待
            this.wait();
        }
        System.out.println("成功消費(fèi)一次,isEmpty 設(shè)置為true");
        isEmpty = true;
    }
}
1.6 線程讓步

java.lang.Thread.yield()方法:線程讓步 不會釋放鎖
線程執(zhí)行了yield()方法后,就會進(jìn)入Runnable(就緒狀態(tài)),【不同于sleep()和join()方法,因?yàn)檫@兩個(gè)方法是使線程進(jìn)入阻塞狀態(tài)】。除此之外,yield()方法還與線程優(yōu)先級有關(guān),當(dāng)某個(gè)線程調(diào)用yield()方法時(shí),就會從運(yùn)行狀態(tài)轉(zhuǎn)換到就緒狀態(tài)后,CPU從就緒狀態(tài)線程隊(duì)列中只會選擇與該線程優(yōu)先級相同或者更高優(yōu)先級的線程去執(zhí)行。

1.7 等待線程的結(jié)束

java.lang.Thread.join方法
實(shí)例方法.當(dāng)前線程調(diào)用目標(biāo)線程實(shí)例的join方法后,阻塞當(dāng)前線程直到目標(biāo)線程中run方法運(yùn)行結(jié)束.

1.8 通用線程狀態(tài)
image.png

這“五態(tài)模型”的詳細(xì)情況如下所示。

  1. 初始狀態(tài),指的是線程已經(jīng)被創(chuàng)建,但是還不允許分配CPU執(zhí)行。這個(gè)狀態(tài)屬于編程語言特有的,不過這里所謂的被創(chuàng)建,僅僅是在編程語言層面被創(chuàng)建,而在操作系統(tǒng)層面,真正的線程還沒有創(chuàng)建。
  2. 可運(yùn)行狀態(tài),指的是線程可以分配CPU執(zhí)行。在這種狀態(tài)下,真正的操作系統(tǒng)線程已經(jīng)被成功創(chuàng)建了,所以可以分配CPU執(zhí)行。
  3. 當(dāng)有空閑的CPU時(shí),操作系統(tǒng)會將其分配給一個(gè)處于可運(yùn)行狀態(tài)的線程,被分配到CPU的線程的狀態(tài)就轉(zhuǎn)換成了運(yùn)行狀態(tài)
  4. 運(yùn)行狀態(tài)的線程如果調(diào)用一個(gè)阻塞的API(例如以阻塞方式讀文件)或者等待某個(gè)事件(例如條件變量),那么線程的狀態(tài)就會轉(zhuǎn)換到休眠狀態(tài),同時(shí)釋放CPU使用權(quán),休眠狀態(tài)的線程永遠(yuǎn)沒有機(jī)會獲得CPU使用權(quán)。當(dāng)?shù)却氖录霈F(xiàn)了,線程就會從休眠狀態(tài)轉(zhuǎn)換到可運(yùn)行狀態(tài)。
  5. 線程執(zhí)行完或者出現(xiàn)異常就會進(jìn)入終止?fàn)顟B(tài),終止?fàn)顟B(tài)的線程不會切換到其他任何狀態(tài),進(jìn)入終止?fàn)顟B(tài)也就意味著線程的生命周期結(jié)束了。
1.9 Java線程狀態(tài)

Java語言里則把可運(yùn)行狀態(tài)和運(yùn)行狀態(tài)合并了,這兩個(gè)狀態(tài)在操作系統(tǒng)調(diào)度層面有用,而JVM層面不關(guān)心這兩個(gè)狀態(tài),因?yàn)镴VM把線程調(diào)度交給操作系統(tǒng)處理了。

Java語言里把休眠狀態(tài) 細(xì)化為 BLOCKED(阻塞狀態(tài)),WAITING(無時(shí)限等待),TIMED_WAITING(有時(shí)限等待)
所以java線程狀態(tài)為:


image.png
image.png
  1. NEW vs RUNNABLE
    new出來對象后,調(diào)用其start方法.

  2. RUNNABLE vs BLOCKED

  • 運(yùn)行中(runnable)的線程等待synchronized的隱式鎖 ,進(jìn)入blocked狀態(tài),等到獲得了synchronized的隱式鎖后,進(jìn)入runnable狀態(tài)
  • 線程中執(zhí)行阻塞式API的調(diào)用后 還是處于runnable狀態(tài)

熟悉操作系統(tǒng)線程的生命周期的話,可能會有個(gè)疑問:線程調(diào)用阻塞式API時(shí),是否會轉(zhuǎn)換到BLOCKED狀態(tài)呢?在操作系統(tǒng)層面,線程是會轉(zhuǎn)換到休眠狀態(tài)的,但是在JVM層面,Java線程的狀態(tài)不會發(fā)生變化,也就是說Java線程的狀態(tài)會依然保持RUNNABLE狀態(tài)。JVM層面并不關(guān)心操作系統(tǒng)調(diào)度相關(guān)的狀態(tài),因?yàn)樵贘VM看來,等待CPU使用權(quán)(操作系統(tǒng)層面此時(shí)處于可執(zhí)行狀態(tài))與等待I/O(操作系統(tǒng)層面此時(shí)處于休眠狀態(tài))沒有區(qū)別,都是在等待某個(gè)資源,所以都?xì)w入了RUNNABLE狀態(tài)。
平時(shí)所謂的Java在調(diào)用阻塞式API時(shí),線程會阻塞,指的是操作系統(tǒng)線程的狀態(tài),并不是Java線程的狀態(tài)

  1. RUNNABLE vs WAITING
  • 獲得synchronized隱式鎖的線程,調(diào)用無參數(shù)的Object.wait()方法。

  • 調(diào)用無參數(shù)的Thread.join()方法。其中的join()是一種線程同步方法,thread-A中執(zhí)行thread-B.join();thread-A等待thread-B執(zhí)行完,其狀態(tài)會從RUNNABLE轉(zhuǎn)換到WAITING。當(dāng)線程thread-B執(zhí)行完,thread-A又會從WAITING狀態(tài)轉(zhuǎn)換到RUNNABLE。

  • 調(diào)用LockSupport.park()方法。Java并發(fā)包中的鎖,都是基于LockSupport實(shí)現(xiàn)的。調(diào)用LockSupport.park()方法,當(dāng)前線程會阻塞,線程的狀態(tài)會從RUNNABLE轉(zhuǎn)換到WAITING。調(diào)用LockSupport.unpark(Thread thread)可喚醒目標(biāo)線程,目標(biāo)線程的狀態(tài)又會從WAITING狀態(tài)轉(zhuǎn)換到RUNNABLE。

  1. RUNNABLE vs TIMED_WAITING
  • 調(diào)用帶超時(shí)參數(shù)的Thread.sleep(long millis)方法;
  • 獲得synchronized隱式鎖的線程,調(diào)用帶超時(shí)參數(shù)的Object.wait(long timeout)方法;
  • 調(diào)用帶超時(shí)參數(shù)的Thread.join(long millis)方法;
  • 調(diào)用帶超時(shí)參數(shù)的LockSupport.parkNanos(Object blocker, long deadline)方法;
  • 調(diào)用帶超時(shí)參數(shù)的LockSupport.parkUntil(long deadline)方法。

TIMED_WAITING和WAITING狀態(tài)的區(qū)別,僅僅是操作方法多了超時(shí)參數(shù)

  1. RUNNABLE vs TERMINATED
  • 線程執(zhí)行完 run() 方法后,會自動轉(zhuǎn)換到TERMINATED狀態(tài)
  • 如果執(zhí)行run()方法的時(shí)候異常拋出,也會導(dǎo)致線程終止
  • Thread#stop 已經(jīng)標(biāo)記為@Deprecated,不建議使用了。正確的姿勢其實(shí)是調(diào)用interrupt()方法
  • stop()方法會真的殺死線程,不給線程喘息的機(jī)會,如果線程持有synchronized隱式鎖,也不會釋放,那其他線程就再也沒機(jī)會獲得synchronized隱式鎖,這實(shí)在是太危險(xiǎn)了。所以該方法就不建議使用了,類似的方法還有suspend() 和 resume()方法,這兩個(gè)方法同樣也都不建議使用了,所以這里也就不多介紹了
  • interrupt()方法僅僅是通知線程,線程有機(jī)會執(zhí)行一些后續(xù)操作,同時(shí)也可以無視這個(gè)通知。被interrupt的線程,是怎么收到通知的呢?一種是異常,另一種是主動檢測。
  • 當(dāng)線程A處于WAITING、TIMED_WAITING狀態(tài)時(shí),如果其他線程調(diào)用線程A的interrupt()方法,會使線程A返回到RUNNABLE狀態(tài),同時(shí)線程A的代碼會觸發(fā)InterruptedException異常。上面我們提到轉(zhuǎn)換到WAITING、TIMED_WAITING狀態(tài)的觸發(fā)條件,都是調(diào)用了類似wait()、join()、sleep()這樣的方法,我們看這些方法的簽名,發(fā)現(xiàn)都會throws InterruptedException這個(gè)異常。這個(gè)異常的觸發(fā)條件就是:其他線程調(diào)用了該線程的interrupt()方法。
  • 當(dāng)線程A處于RUNNABLE狀態(tài)時(shí),并且阻塞在java.nio.channels.InterruptibleChannel上時(shí),如果其他線程調(diào)用線程A的interrupt()方法,線程A會觸發(fā)java.nio.channels.ClosedByInterruptException這個(gè)異常;而阻塞在java.nio.channels.Selector上時(shí),如果其他線程調(diào)用線程A的interrupt()方法,線程A的java.nio.channels.Selector會立即返回。
  • 上面這兩種情況屬于被中斷的線程通過異常的方式獲得了通知。還有一種是主動檢測,如果線程處于RUNNABLE狀態(tài),并且沒有阻塞在某個(gè)I/O操作上,例如中斷計(jì)算圓周率的線程A,這時(shí)就得依賴線程A主動檢測中斷狀態(tài)了。如果其他線程調(diào)用線程A的interrupt()方法,那么線程A可以通過isInterrupted()方法,檢測是不是自己被中斷了。
Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
  // 省略業(yè)務(wù)代碼無數(shù)
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    //如果sleep方法拋出了此異常,表示被中斷了,此處應(yīng)該退出線程
    //若catch中不退出線程,因則應(yīng)該應(yīng)該重置一下中斷標(biāo)示,因?yàn)閽伋霎惓:螅袛鄻?biāo)示會自動清除掉!
    Thread.currentThread().interrupt();
    e.printStackTrace();
  }
}
2.0 entry set vs wait set

參考管程之后這里就很清晰了.


image.png

鎖對象有個(gè) Entry Set 和 Wait Set 和當(dāng)前持鎖線程(The Owner)
WaitSet :處于wait狀態(tài)的線程,會被加入到wait set;
EntryList:處于等待鎖block狀態(tài)的線程,會被加入到entry set;
The Owner :表示某一線程成功競爭到對象鎖。


并發(fā)基本概念之 管程
Java多線程基礎(chǔ)(一)——線程與鎖
Java并發(fā)編程實(shí)戰(zhàn)
JVM源碼分析之Object.wait/notify實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。