Android/java 多執行緒(一)-Thread的使用以及原始碼分析

bluetooth發表於2021-09-09

執行緒的概念以及狀態

在學習執行緒之前,我們需要普及一個概念,每一個程式執行都會有一個父程式,我們的執行緒就是在此父程式中執行,拿Android來說,預設情況下,啟動一個程式,所有的元件程式都執行在同一個程式中,並且會建立一個執行執行緒在該程式中,俗稱"主執行緒",當我們在該執行緒中做了耗時的操作造成了程式卡頓,我們就叫“執行緒阻塞”了,此時就應該另起一個執行緒來執行耗時操作。

要了解執行緒中方法的使用,就得先了解執行緒的執行狀態,執行緒從建立到執行完畢,一共有6個狀態:

  • NEW(執行緒建立未啟動)

  • RUNNABLE(正在執行中的執行緒)

  • BLOCKED(被阻塞並且在等待監視器鎖釋放)

  • WAITING(等待被喚醒)

  • TIMED_WAITING(等待或睡眠一定時間被喚醒)

  • TERMINATED(執行緒終止,消亡)

這些狀態對應Thread原始碼中的State列舉,透過getState()方法可以獲取該執行緒的執行狀態

其中BLOCKED表示等待監視器鎖的過程,那麼什麼是監視器鎖呢,監視器鎖是為了解決執行緒不安全而誕生的方法,當多個程式同時操作一個資料結構並修改時,這時資料結構是不確定的,我們稱之為“執行緒不安全”,於是我們使用synchronized(同步鎖)lock鎖等機制來解決這種執行緒不安全,套上此鎖,當前有且只有一個執行緒能修改此資料結構,其他的執行緒則需要等待,這就保證了資料結構的一致性,而其他執行緒會進入等待的狀態,也就是我們的BLOCKED狀態。
synchronized使用示例:

    class MyThread extends Thread {        @Override
        public void run() {            synchronized (this) {                //這裡寫修改資料來源的程式碼
            }
        }
    }

lock使用示例:

    class MyThread extends Thread {        private final ReentrantLock lock = new ReentrantLock ();        
        @Override
        public void run() {
            lock.lock();                //這裡寫修改資料來源的程式碼
            lock.unlock();
        }
    }

當然這兩者的應用不僅僅是線上程中,在必要的方法呼叫上也可使用

執行緒的原始碼以及方法使用

Thread原始碼:

public class Thread implements Runnable {
 ...........    /* Some of these are accessed directly by the VM; do not rename them. */
    private volatile long nativePeer;    volatile ThreadGroup group;    volatile boolean daemon;    volatile String name;    volatile int priority;    volatile long stackSize;
    Runnable target;    private static int count = 0;    private long threadInitNumber ;    /**
     * Normal thread local values.
     */
    ThreadLocal.Values localValues;    /**
     * Inheritable thread local values.
     */
    ThreadLocal.Values inheritableValues;    /** Callbacks to run on interruption. */
    private final List<Runnable> interruptActions = new ArrayList<Runnable>();    /**
     * Holds the class loader for this Thread, in case there is one.
     */
    private ClassLoader contextClassLoader;    /**
     * Holds the handler for uncaught exceptions in this Thread,
     * in case there is one.
     */
    private UncaughtExceptionHandler uncaughtHandler;    /**
     * Holds the default handler for uncaught exceptions, in case there is one.
     */
    private static UncaughtExceptionHandler defaultUncaughtHandler;

   .....
}

Thread實現了Runnable介面,並宣告瞭一些執行緒常用的一些變數:

  • group
    執行緒組,執行緒組包含其他的執行緒組,形成了一個樹結構,除了初始的執行緒組外,其他的執行緒組都會有個父程式,其中執行緒能訪問當前執行緒組的資訊,但不能訪問父執行緒組的資訊

  • daemon
    是否是守護執行緒,啥是守護執行緒呢,守護執行緒是依賴於建立它的執行緒的一種執行緒,與普通執行緒的區別是當建立它的執行緒關閉了那它也會關閉,而普通執行緒不會,像我們的垃圾收集器執行緒就是一個守護執行緒

  • threadInitNumber 當前執行緒的識別符號,它是按照執行緒的建立順序來疊加的

  • name 執行緒的名稱,未指定的話就以"Thread-" + threadInitNumber的邏輯命名

  • priority
    執行緒優先順序,有三個狀態MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY,分別對應低,中,高

  • stackSize 堆疊大小

  • target 當前的目標執行緒

  • contextClassLoader 類載入器,用於儲存該執行緒的資訊,斷點續傳等功能可以用到

  • uncaughtHandler
    執行緒未捕獲異常呼叫類,如果沒有對其進行設定,將會預設使用defaultUncaughtHandler來處理異常,由於我們的主執行緒也是一個Thread,所以我們可以透過實現UncaughtExceptionHandler介面並呼叫Thread.setDefaultUncaughtExceptionHandler()方法將我們自定義的異常捕獲類設定給Thread,這樣我們就可以捕獲全域性的異常

瞭解了一些常用變數後,我們再來看一下其中的一些常用方法:

start()方法

用於啟動一個執行緒,只有呼叫此方法,系統才會新開啟一個執行緒並分配給其必要的資源。我們看一下Thread的構造方法,發現最終都是呼叫了init方法,裡面也只是對基本變數進行初始化,並沒有分配到任何的資源:

    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        Thread parent = currentThread();        if (g == null) {
            g = parent.getThreadGroup();
        }

        g.addUnstarted();        this.group = g;        this.target = target;        this.priority = parent.getPriority();        this.daemon = parent.isDaemon();
        setName(name);

        init2(parent);        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        tid = nextThreadID();
    }

所以,新建一個執行緒不會消耗資源,只有當star後才會去分配必要的資源,此時執行緒狀態由NEW變為了RUNNABLE

run()方法

此方法用於處理執行緒中執行的邏輯,如果繼承Thread類則必須重寫該方法

sleep()方法
sleep(long millis)     //引數為毫秒sleep(long millis,int nanoseconds)    //第一引數為毫秒,第二個引數為納秒

讓執行緒休眠一段時間,相當於讓執行緒進入阻塞狀態。注意即使執行緒休眠了其鎖機制依舊生效,其他的執行緒依舊不能訪問其被鎖的資料結構,直到其釋放物件鎖,此時執行緒狀態由RUNNABLE進入TIMED_WAITING

yield()方法

讓當前執行緒交出相應的許可權,為其他執行緒讓步,這是防止CPU過度使用的一種有效手段。效果與sleep差不多,但不能指定具體的時間,並且並不是讓執行緒進入到阻塞狀態,而是進入就緒狀態.此時執行緒狀態由RUNNABLE進入WAITING

join()方法
join()
join(long millis)     //引數為毫秒join(long millis,int nanoseconds)    //第一引數為毫秒,第二個引數為納秒

此方法是讓執行緒優先來執行,其餘的執行緒會暫停,直到此執行緒執行完畢才會繼續執行。你可以將執行緒看成一些正常行駛的車輛(線上程佇列中),當給其中一輛車使用了join(彎道超車),那其餘的車會給其讓行(執行緒進入暫停狀態),優先讓它行駛(join的執行緒執行),直到它到達終點(執行完成),才會繼續行駛(繼續執行)
join原始碼中是呼叫的wait()方法,而wait()方法能使執行緒進入阻塞狀態並釋放物件鎖,所以join方法也是能釋放物件鎖的

interrupt()方法

中斷執行緒,我們知道,執行緒執行完run方法裡的邏輯就會進入終止狀態。呼叫此方法會使一個阻塞的執行緒丟擲InterruptedException異常,從而終止執行緒,可以呼叫isInterrupted()判斷是否終止了執行緒,但非阻塞執行緒如何中斷呢,請看:

class MyThread extends Thread{        private volatile boolean isStop = false;        @Override
        public void run() {            int i = 0;            while(!isStop){
                i++;
            }
        }         
        public void setStop(boolean stop){            this.isStop = stop;
        }
    }

我們可以設定一個變數來控制是否執行完畢,從而結束執行緒,此時執行緒狀態進入TERMINATED狀態



作者:我是黃教主啊
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3402/viewspace-2821569/,如需轉載,請註明出處,否則將追究法律責任。

相關文章