驚天秘密!從Thread開始,揭露Android線程通訊的詭計和主線程的陰謀

2021-02-20 安卓巴士Android開發者門戶

背景介紹

我們在Android開發過程中,幾乎都離不開線程。但是你對線程的了解有多少呢?它完美運行的背後,究竟隱藏了多少不為人知的秘密呢?線程間互通暗語,傳遞信息究竟是如何做到的呢?Looper、Handler、MessageQueue究竟在這背後進行了怎樣的運作。本期,讓我們一起從Thread開始,逐步探尋這個完美的線程鏈背後的秘密。

注意,大部分分析在代碼中,所以請仔細關注代碼哦!

從Tread的創建流程開始

在這一個環節,我們將一起一步步的分析Thread的創建流程。

話不多說,直接代碼裡看。

線程創建的起始點init()

// 創建Thread的公有構造函數,都調用的都是這個私有的init()方法。我們看看到底幹什麼了。

/**

     *

     * @param 線程組

     * @param 就是我們平時接觸最多的Runnable同學

     * @param 指定線程的名稱

     * @param 指定線程堆棧的大小

     */

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {

        Thread parent = currentThread();            //先獲取當前運行中的線程。這一個Native函數,暫時不用理會它怎麼做到的。黑盒思想,哈哈!

        if (g == null) {

            g = parent.getThreadGroup();            //如果沒有指定ThreadGroup,將獲取父線程的TreadGroup

        }

        g.addUnstarted();                           //將ThreadGroup中的就緒線程計數器增加一。注意,此時線程還並沒有被真正加入到ThreadGroup中。

        this.group = g;                             //將Thread實例的group賦值。從這裡開始線程就擁有ThreadGroup了。

        this.target = target;                       //給Thread實例設置Runnable。以後start()的時候執行的就是它了。

        this.priority = parent.getPriority();       //設置線程的優先權重為父線程的權重

        this.daemon = parent.isDaemon();            //根據父線程是否是守護線程來確定Thread實例是否是守護線程。

        setName(name);                              //設置線程的名稱  

        init2(parent);                              //納尼?又一個初始化,參數還是父線程。不急,稍後在看。

        /* Stash the specified stack size in case the VM cares */

        this.stackSize = stackSize;                 //設置線程的堆棧大小

        tid = nextThreadID();                       //線程的id。這是個靜態變量,調用這個方法會自增,然後作為線程的id。

    }


第二個init2()

至此,我們的Thread就初始化完成了,Thread的幾個重要成員變量都賦值了。

啟動線程,開車啦!

通常,我們這樣了啟動一條線程。

Thread threadDemo = new Thread(() -> {

    });

threadDemo.start();

那麼start()背後究竟隱藏著什麼樣不可告人的秘密呢?是人性的扭曲?還是道德的淪喪?讓我們一起點進start()。探尋start()背後的秘密。

//如我們所見,這個方法是加了鎖的。原因是避免開發者在其它線程調用同一個Thread實例的這個方法,從而儘量避免拋出異常。

//這個方法之所以能夠執行我們傳入的Runnable裡的run()方法,是應為JVM調用了Thread實例的run()方法。

public synchronized void start() {

        //檢查線程狀態是否為0,為0表示是一個新狀態,即還沒被start()過。不為0就拋出異常。

        //就是說,我們一個Thread實例,我們只能調用一次start()方法。

        if (threadStatus != 0)

            throw new IllegalThreadStateException();

        //從這裡開始才真正的線程加入到ThreadGroup組裡。再重複一次,前面只是把nUnstartedThreads這個計數器進行了增量,並沒有添加線程。

        //同時,當線程啟動了之後,nUnstartedThreads計數器會-1。因為就緒狀態的線程少了一條啊!

        group.add(this);

        started = false;

        try {

            nativeCreate(this, stackSize, daemon);  //又是個Native方法。這裡交由JVM處理,會調用Thread實例的run()方法。

            started = true;

        } finally {

            try {

                if (!started) {

                    group.threadStartFailed(this);  //如果沒有被啟動成功,Thread將會被移除ThreadGroup,同時,nUnstartedThreads計數器又增量1了。

                }

            } catch (Throwable ignore) {

               

            }

        }

    }

好吧,最精華的函數是native的,先當黑盒處理吧。只要知道它能夠調用到Thread實例的run()方法就行了。那我們再看看run()方法到底幹了什麼神奇的事呢?

黑實驗

上面的實驗表明了,我們完全可以用Thread來作為Runnable。

幾個常見的線程手段(操作)

Thread.sleep()那不可告人的秘密

我們平時使用Thread.sleep()的頻率也比較高,所以我們在一起研究研究Thread.sleep()被調用的時候發生了什麼。

在開始之前,先介紹一個概念——納秒。1納秒=十億分之一秒。可見用它計時將會非常的精準。但是由於設備限制,這個值有時候並不是那麼準確,但還是比毫秒的控制粒度小很多。

//平時我們調用的Thread.sleep(long)最後調用到這個方法來,後一個陌生一點的參數就是納秒。

//你可以在納秒級控制線程。

public static void sleep(long millis, int nanos)

    throws InterruptedException {

        //下面三個檢測毫秒和納秒的設置是否合法。

        if (millis < 0) {

            throw new IllegalArgumentException("millis < 0: " + millis);

        }

        if (nanos < 0) {

            throw new IllegalArgumentException("nanos < 0: " + nanos);

        }

        if (nanos > 999999) {

            throw new IllegalArgumentException("nanos > 999999: " + nanos);

        }

    

        if (millis == 0 && nanos == 0) {

            if (Thread.interrupted()) {   //當睡眠時間為0時,檢測線程是否中斷,並清除線程的中斷狀態標記。這是個Native的方法。

              throw new InterruptedException();  //如果線程被設置了中斷狀態為true了(調用Thread.interrupt())。那麼他將拋出異常。如果在catch住這個異常之後return線程,那麼線程就停止了。  

              //需要注意,在調用了Thread.sleep()之後,再調用isInterrupted()得到的結果永遠是False。別忘了Thread.interrupted()在檢測的同時還會清除標記位置哦!

            }

            return;

        }

        long start = System.nanoTime();  //類似System.currentTimeMillis()。但是獲取的是納秒,可能不準。

        long duration = (millis * NANOS_PER_MILLI) + nanos;  

        Object lock = currentThread().lock;  //獲得當前線程的鎖。

        synchronized (lock) {   //對當前線程的鎖對象進行同步操作

            while (true) {

                sleep(lock, millis, nanos);  //這裡又是一個Native的方法,並且也會拋出InterruptedException異常。

                //據我估計,調用這個函數睡眠的時長是不確定的。

                long now = System.nanoTime();

                long elapsed = now - start;  //計算線程睡了多久了

                if (elapsed >= duration) {   //如果當前睡眠時長,已經滿足我們的需求,就退出循環,睡眠結束。

                    break;

                }

                duration -= elapsed;   //減去已經睡眠的時間,重新計算需要睡眠的時長。

                start = now;

                millis = duration / NANOS_PER_MILLI;  //重新計算毫秒部分

                nanos = (int) (duration % NANOS_PER_MILLI); //重新計算微秒部分

            }

        }

    }

通過上面的分析可以知道,使線程休眠的核心方法就是一個Native函數sleep(lock, millis, nanos),並且它休眠的時常是不確定的。因此,Thread.sleep()方法使用了一個循環,每次檢查休眠時長是否滿足需求。

同時,需要注意一點,如果線程的interruted狀態在調用sleep()方法時被設置為true,那麼在開始休眠循環前會拋出InterruptedException異常。

Thread.yield()究竟隱藏了什麼?

這個方法是Native的。調用這個方法可以提示cpu,當前線程將放棄目前cpu的使用權,和其它線程重新一起爭奪新的cpu使用權限。當前線程可能再次獲得執行,也可能沒獲得。就醬。

無處不在的wait()究竟是什麼?

大家一定經常見到,不論是哪一個對象的實例,都會在最下面出現幾個名為wait()的方法。等待?它們究竟是怎樣的一種存在,讓我們一起點擊去看看。

哎喲我去,都是Native函數啊。

那就看看文檔它到底是什麼吧。

根據文檔的描述,wait()配合notify()和notifyAll()能夠實現線程間通訊,即同步。在線程中調用wait()必須在同步代碼塊中調用,否則會拋出IllegalMonitorStateException異常。因為wait()函數需要釋放相應對象的鎖。當線程執行到wait()時,對象會把當前線程放入自己的線程池中,並且釋放鎖,然後阻塞在這個地方。直到該對象調用了notify()或者notifyAll()後,該線程才能重新獲得,或者有可能獲得對象的鎖,然後繼續執行後面的語句。

呃。。。好吧,在說明一下notify()和notifyAll()的區別。

扒一扒Looper、Handler、MessageQueue之間的愛恨情仇

我們可能過去都寫過形如這樣的代碼:

很多同學知道,在線程中使用Handler時(除了Android主線程)必須把它放在Looper.prepare()和Looper.loop()之間。否則會拋出RuntimeException異常。但是為什麼要這麼做呢?下面我們一起來扒一扒這其中的內幕。

從Looper.prepare()開始

當Looper.prepare()被調用時,發生了什麼?

經過上面的分析,我們已經知道Looper.prepare()調用之後發生了什麼。

但是問題來了!sThreadLocal是個靜態的ThreadLocal 實例(在Android中ThreadLocal的範型固定為Looper)。就是說,當前進程中的所有線程都共享這一個ThreadLocal。那麼,Looper.prepare()既然是個靜態方法,Looper是如何確定現在應該和哪一個線程建立綁定關係的呢?我們接著往裡扒。

來看看ThreadLocal的get()、set()方法。

創建Handler

Handler可以用來實現線程間的通行。在Android中我們在子線程作完數據處理工作時,就常常需要通過Handler來通知主線程更新UI。平時我們都使用new Handler()來在一個線程中創建Handler實例,但是它是如何知道自己應該處理那個線程的任務呢。下面就一起扒一扒Handler。

public Handler() {

        this(null, false); 

}

    

public Handler(Callback callback, boolean async) {      //可以看到,最終調用了這個方法。

        if (FIND_POTENTIAL_LEAKS) {

            final Class<? extends Handler> klass = getClass();

            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&

                    (klass.getModifiers() & Modifier.STATIC) == 0) {

                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +

                    klass.getCanonicalName());

            }

        }

        mLooper = Looper.myLooper();                    //重點啊!在這裡Handler和當前Thread的Looper綁定了。Looper.myLooper()就是從ThreadLocale中取出當前線程的Looper。

        if (mLooper == null) {

            //如果子線程中new Handler()之前沒有調用Looper.prepare(),那麼當前線程的Looper就還沒創建。就會拋出這個異常。

            throw new RuntimeException(

                "Can't create handler inside thread that has not called Looper.prepare()");

        }

        mQueue = mLooper.mQueue;  //賦值Looper的MessageQueue給Handler。

        mCallback = callback;

        mAsynchronous = async;

    }

Looper.loop()

我們都知道,在Handler創建之後,還需要調用一下Looper.loop(),不然發送消息到Handler沒有用!接下來,扒一扒Looper究竟有什麼樣的魔力,能夠把消息準確的送到Handler中處理。

public static void loop() {

        final Looper me = myLooper();   //這個方法前面已經提到過了,就是獲取到當前線程中的Looper對象。

        if (me == null) { 

            //沒有Looper.prepare()是要報錯的!

            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");

        }

        final MessageQueue queue = me.mQueue;       //獲取到Looper的MessageQueue成員變量,這是在Looper創建的時候new的。

        //這是個Native方法,作用就是檢測一下當前線程是否屬於當前進程。並且會持續跟蹤其真實的身份。

        //在IPC機制中,這個方法用來清除IPCThreadState的pid和uid信息。並且返回一個身份,便於使用restoreCallingIdentity()來恢復。

        Binder.clearCallingIdentity();

        final long ident = Binder.clearCallingIdentity();

        for (;;) {  //重點(敲黑板)!這裡是個死循環,一直等待抽取消息、發送消息。

            Message msg = queue.next(); //  從MessageQueue中抽取一條消息。至於怎麼取的,我們稍後再看。

            if (msg == null) {

                // No message indicates that the message queue is quitting.

                return;

            }

            // This must be in a local variable, in case a UI event sets the logger

            final Printer logging = me.mLogging;

            if (logging != null) {

                logging.println(">>>>> Dispatching to " + msg.target + " " +

                        msg.callback + ": " + msg.what);

            }

            final long traceTag = me.mTraceTag;   //取得MessageQueue的跟蹤標記

            if (traceTag != 0) {

                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));  //開始跟蹤本線程的MessageQueue中的當前消息,是Native的方法。

            }

            try {

                msg.target.dispatchMessage(msg);   //嘗試分派消息到和Message綁定的Handler中

            } finally {

                if (traceTag != 0) {

                    Trace.traceEnd(traceTag);      //這個和Trace.traceBegin()配套使用。

                }

            }

            if (logging != null) {

                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

            }

            

            final long newIdent = Binder.clearCallingIdentity();   //what?又調用這個Native方法了。這裡主要是為了再次驗證,線程所在的進程是否發生改變。

            if (ident != newIdent) {

                Log.wtf(TAG, "Thread identity changed from 0x"

                        + Long.toHexString(ident) + " to 0x"

                        + Long.toHexString(newIdent) + " while dispatching to "

                        + msg.target.getClass().getName() + " "

                        + msg.callback + " what=" + msg.what);

            }

            msg.recycleUnchecked();   //回收釋放消息。

        }

    }

從上面的分析可以知道,當調用了Looper.loop()之後,線程就就會被一個for(;;)死循環阻塞,每次等待MessageQueue的next()方法取出一條Message才開始往下繼續執行。然後通過Message獲取到相應的Handler (就是target成員變量),Handler再通過dispatchMessage()方法,把Message派發到handleMessage()中處理。

這裡需要注意,當線程loop起來是時,線程就一直在循環中。就是說Looper.loop()後面的代碼就不能被執行了。想要執行,需要先退出loop。

Looper myLooper = Looper.myLoop();

myLooper.quit();        //普通退出方式。

myLooper.quitSafely();  //安全的退出方式。

現在又產生一個疑問,MessageQueue的next()方法是如何阻塞住線程的呢?接下來,扒一扒這個幕後黑手MessageQueue。

幕後黑手MessageQueue

MessageQueue是一個用單鏈的數據結構來維護消息列表。

可以看到。MessageQueue在取消息(調用next())時,會進入一個死循環,直到取出一條Message返回。這就是為什麼Looper.loop()會在queue.next()處等待的原因。

那麼,一條Message是如何添加到MessageQueue中呢?要弄明白最後的真相,我們需要調查一下mHandler.post()這個方法。

Handler究竟對Message做了什麼?

Handler的post()系列方法,最終調用的都是下面這個方法:

接下來就看看MessageQueue的enqueueMessage()作了什麼。

至此,我們已經揭露了Looper、Handler、MessageQueue隱藏的秘密。

另一個疑問?

也許你已經注意到在主線程中可以直接使用Handler,而不需要Looper.prepare()和Looper.loop()。為什麼可以做到這樣呢?根據之前的分析可以知道,主線程中必然存在Looper.prepare()和Looper.loop()。既然如此,為什麼主線程沒有被loop()阻塞呢?看一下ActivityThread來弄清楚到底是怎麼回事。

注意ActivityThread並沒有繼承Thread,它的Handler是繼承Handler的私有內部類H.class。在H.class的handleMessage()中,它接受並執行主線程中的各種生命周期狀態消息。UI的16ms的繪製也是通過Handler來實現的。也就是說,主線程中的所有操作都是在Looper.prepareMainLooper()和Looper.loop()之間進行的。進一步說是在主Handler中進行的。

總結

Android中Thread在創建時進行初始化,會使用當前線程作為父線程,並繼承它的一些配置。

Thread初始化時會被添加到指定/父線程的ThreadGroup中進行管理。

Thread正真啟動是一個native函數完成的。

在Android的線程間通信中,需要先創建Looper,就是調用Looper.prepare()。這個過程中會自動依賴當前Thread,並且創建MessageQueue。經過上一步,就可以創建Handler了,默認情況下,Handler會自動依賴當前線程的Looper,從而依賴相應的MessageQueue,也就知道該把消息放在哪個地方了。MessageQueue通過Message.next實現了一個單鍊表結構來緩存Message。消息需要送達Handler處理,還必須調用Looper.loop()啟動線程的消息泵送循環。loop()內部是無限循環,阻塞在MessageQueue的next()方法上,因為next()方法內部也是一個無限循環,直到成功從鍊表中抽取一條消息返回為止。然後,在loop()方法中繼續進行處理,主要就是把消息派送到目標Handler中。接著進入下一次循環,等待下一條消息。由於這個機制,線程就相當於阻塞在loop()這了。

經過上面的揭露,我們已經對線程及其相互之間通訊的秘密有所了解。掌握了這些以後,相信在以後的開發過程中我們可以思路清晰的進行線程的使用,並且能夠吸收Android在設計過程中的精華思想。

感謝  CoorChice 同學投稿,Blog地址:

https://chenbingx.github.io/

本篇文章如有對您開發有幫助的話,歡迎在作者的 Github 給個Star 也可以分享給小夥伴哦; 小編每天都兢兢業業的為整理乾貨,支持小編在下方給鼓勵+1,需要投稿與及有疑問的小夥伴可以在下方留言,小編會第一時間與您聯繫!

基於 Material Design 的 Gank IO 客戶端(內含妹子圖)

玩轉仿探探卡片式滑動效果

程式設計師也要失業了?微軟發明了要取代他們的AI【Bus Weekly】43期

英語不好?是時候打造一款AndroidStudio中記單詞的翻譯插件了

相關焦點

  • 【專業知識】Android主線程的消息系統(Handler\Looper)
    Handler是Android系統中比較重要的一個知識,在Android多線程面試經常會被問到,在實際項目中的確也經常用到。當然也比較複雜,知識比較多,牽扯到的類有Thread、Looper、Message、MessageQueue。
  • Android中的多線程「安全」
    Android沿用了JAVA的線程模型,其中的線程分為主線程和子線程,其中主線程又叫UI線程。在Android系統中,在默認情況下,一個應用程式內的各個組件(如Activity、BroadcastReceiver、Service)都會在同一個進程(Process)裡執行,且由此進程的主線程負責執行。
  • 多線程與多進程 | 多線程
    線程有開始,順序執行和結束三部分。它有一個自己的指令指針,記錄自己運行到什麼地方。線程的運行可能被搶佔(中斷)或暫時的被掛起(睡眠),讓其他線程運行,這叫做讓步。一個進程中的各個線程之間共享同一片數據空間,所以線程之間可以比進程之間更方便地共享數據以及相互通訊。
  • 圖文|Android 使用Thread 和多線程使用互斥鎖
    如果只new了一次線程,多次start,會出現怎麼樣的情況呢?android 線程start的函數原型如下public synchronized void start() {                        if (threadStatus !
  • Android線程間通信之handler
    相信寫過android的童鞋,一定對handler很熟悉。因為使用頻率實在太高了。尤其是在非ui線程,想要刷新ui控制項的時候。因為ui控制項的刷新只能在主線程做,但是我們可能有在非ui線程卻需要更新ui的需求,比如在一個後臺線程下載了圖片需要更新到ui上,這時候就需要主線程handler來發送更新的message。
  • Python多線程—Thread和Threading
    又到周四,科普Time,今天給大家講講Python多線程中的兩個模塊,Thread和Threading。針對兩個模塊下文中會給出一些實例,給大家加深印象。 說到Python的多線程呢,先介紹一下一定繞不過去的坑,全局解釋器鎖GIL。
  • C++並發與多線程__C++如何線程創建線程以及函數join()和detach()用法和區別
    前言:通常一個程序運行起來,也就等於一個進程在運行,這個進程中會有一個主線程自動創建並運行,當程序的main()函數返回之後那麼此主線程也就運行結束
  • UNIX(多線程):07---線程啟動、結束,創建線程多法、join,detach
    線程啟動、結束,創建線程多法、join,detach範例演示線程運行的開始和結束#include
  • C++ 線程局部變量thread_local
    全局變量和函數內定義的靜態變量,是同一進程中各個線程都可以訪問的共享變量,因此它們存在多線程讀寫問題。* t_object_3 = nullptr; 除了以上之外,關於線程局部存儲變量的聲明和使用還需注意以下幾點:  如果變量聲明中使用量關鍵字static或者extern,那麼關鍵字__thread必須緊隨其後。
  • UNIX(多線程):03--- 認識std::thread
    <thread> 頭文件摘要<thread> 頭文件聲明了 std::thread 線程類及 std::swap (交換兩個線程對象)輔助函數。另外命名空間 std::this_thread 也聲明在 <thread> 頭文件中。
  • Java多線程:帶你了解神秘的線程變量 ThreadLocal
    }                System.out.println(name + ":" + threadLocal.get());            }        }    }測試結果線程1:線程1的threadLocal線程2:線程2的threadLocal
  • UNIX(多線程):08---線程傳參詳解,detach()陷阱,成員函數做線程函數
    ;thread mythread(myprint, val, buf); mythread.join();std::cout << "主線程收尾" << std::endl;return 0;}要避免的陷阱(解釋1)
  • 走進C++11(二十四)一統江湖之線程 -- std::thread
    C++11的標準類std::thread對線程進行了封裝,定義了C++11標準中的一些表示線程的類、用於互斥訪問的類與方法等。應用C++11中的std::thread便於多線程程序的移值。std::thread類成員函數:(1) get_id:獲取線程ID,返回一個類型為std::thread::id的對象。
  • 【技術分享】Python3(11) Python 進程和線程
    所以我們今天只聊一些進程、線程的概念,和Python中封裝的一些使用方法。千裡之行,始於足下,我們開始吧。進程和線程進程和線程是多任務作業系統中的概念 ,如Mac OS X,UNIX,Linux,Windows等作業系統,對於作業系統來說,一個任務就是一個進程(Process),如在一臺Android設備(android 採用Linux做內核)上打開一個網易雲客戶端聽歌、打開一個微信客戶端聊天、打開一個今日頭條看新聞等每一個應用就是一個進程
  • ThreadPoolExecutor線程池源碼解析與應用分析
    1 線程池介紹 首先我們先來看一下線程池的完整結構示意圖1.1:圖1.1 線程池完整流程示意圖 通過這個圖我們可以看到線程池一共包含來幾部分:這裡主線程是用來創建和調用線程池的,我們暫不歸納到線程池中。
  • iOS多線程全套:線程生命周期,多線程的四種解決方案,線程安全問題,GCD的使用,NSOperation的使用(上)
    非正常死亡,當滿足某個條件後,在線程內部中止執行/在主線程中止線程對象還有線程的exit和cancel[NSThread exit]:一旦強行終止線程,後續的所有代碼都不會被執行。[thread cancel]取消:並不會直接取消線程,只是給線程對象添加 isCancelled 標記。
  • 小知識:啟動了Activity 的 app 至少有幾個線程?
    該文源於 wanandroid 上的每日一問板塊。
  • Android子線程也能修改UI?
    XXX:為什麼我的控制項可以在子線程裡面更新我(不假思索):你是不是在onCreate裡面開了一個子線程,然後更新了UIXXX:好像是這樣。。我:你試試將子線程沉睡5秒鐘時間,應該就會閃退了XXX:我試試。N分鐘以後.
  • C# 線程基礎
    這需要組織多個線程間的通信和相互同步。這說明PrintNumbers方法同時運行在主線程和另一個線程中。4:14:24:34:44:54:64:74:84:9Thread complete使用Thread.Join(),該方法允許我們等待直到線程thread完成,才執行主線程列印Thread complete代碼。
  • Python3多線程
    線程:所有的線程都運行在同一個進程當中,共享相同的運行環境。線程有開始、順序執行和結束三個部分, 線程是作業系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。由於單線程效率低,程序中往往要引入多線程編程。