【Android面試】關於多線程,你必須知道的那些玩意兒

進程與線程

概念

進程和線程作爲必知必會的知識,想來讀者們也都是耳熟能詳了,但真的是這樣嘛?今天我們就來重新捋一捋,看看有沒有什麼知識點欠缺的。

先來一張我隨手截的活動監視器的圖,分清一下什麼叫做進程,什麼叫做線程。

想來很多面試官會問,你對進程和線程的理解是什麼,他們有什麼樣的區別呢?其實不用死記硬背,記住上面的圖就OK了。

正好裏面有個奇形怪狀的App,我們就拿愛優騰中的愛舉例。

先來插個題外話,今天突然看到愛奇藝給我的推送,推出了新的會員機制 —— 星鑽VIP會員,超前點播、支持 五臺 設備在線、。。我預計之後可能還會推出新的VIP等級會員,那我先給他安排一下名字,你看星鑽是不是星耀+鑽石,那下一個等級我們就叫做耀王VIP會員(榮耀王者)。哇!!太讚了把,愛奇藝運營商過來打錢。🙄🙄🙄🙄,作爲愛奇藝的老黃金VIP用戶了,女朋友用一下,分享給室友用一下,我自己要麼沒得看到了,要麼只能夜深人靜的時候,🤔🤔🤔🤔,點到爲止好吧,輪到你發揮無限的想象力了。。

收!!回到我們的正題,我們不是講到了進程和線程嘛,那進程是什麼,顯而易見嘛這不是,上面已經寫了一個 進程名稱 了,那顯然就是愛奇藝這整一隻龐然大物嘛。 那線程呢?

你是否看到愛奇藝中的數據加載上並不是一次性的,這些任務的進行就是依靠我們的線程來進行執行的,你可以把這樣的一個個數據加載過程認爲是一條條線程。

生命週期

不管是進程還是線程,生和死是他們必然要去經歷的過程。

進程 線程

你能看到進程中少了兩個狀態,也就是他的出生和他的死亡,不過這是同樣是爲了方便我們去進行記憶。 進程因創建而產生,因調度而執行,因得不到資源而阻塞,因得不到資源而阻塞,因撤銷而消亡。 圖中代表的4個值:

  1. 得到CPU的時間片 / 調度。
  2. 時間片用完,等待下一個時間片。
  3. 等待 I/O 操作 / 等待事件發生。
  4. I/O操作結束 / 事件完成。

而對於線程,他在JavaThread類中對應了6種狀態,可以自行進行查看。

多線程編程入門

多線程編程就好像我們這樣生活,週末我呆在家裏邊燒開水,邊讓洗衣機洗衣服,邊炒菜,一秒鐘幹三件事,你是不是也有點心動呢?

廢話不多說,我們趕緊入門一下。

// 1
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("this is a Runnable");
    }
}
// 2
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("this is thread");
    }
}

// 具體使用
public class Main {
    public static void main(String[] args) {
        // 第一種
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
        // 第二種
        MyThread thread2 = new MyThread();
        thread2.start();
    }
}

一般來說推薦第一種寫法,也就是重寫Runnable了。不過這樣的玩意兒存在他全是好事嘛???顯然作爲高手的你們肯定知道他有問題存在了。我們以一段代碼爲例。

public class Main {
    public int i = 0;
    public void increase(){
        I++;
    }

    public static void main(String[] args) {
        final Main main = new Main();
        for(int i=0; i< 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j=0; j<1000; j++){
                        main.increase();
                    }
                }
            }).start();
        }
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(main.i);
    }
}

這樣的一段程序,你覺得最後跑出來的數據是什麼?他會是10000嘛?

以答案作爲標準,顯然不是,他甚至說可能下次跑出來也不是我給你的這個數值,但是這是爲什麼呢?這就牽扯到我們的線程同步問題了。

線程同步

一般情況下,我們可以通過三種方式來實現。

  • Synchronized
  • Lock
  • Volatile

在操作系統中,有這麼一個概念,叫做臨界區。其實就是同一時間只能允許存在一個任務訪問的代碼區間。代碼模版如下:

Lock lock = new ReentrantLock();
public void lockModel(){
    lock.lock();
    // 用於書寫共同代碼,比如說賣同一輛動車的車票等等。
    lock.unlock();
}

// 上述模版近似等價於下面的函數
public synchronized void lockModel(){}

其實這就是大家常說的鎖機制,通過加解鎖的方法,來保證數據的正確性。

但是鎖的開銷還是我們需要考慮的範疇,在不太必要時,我們更頻繁的會使用是volatile關鍵詞來修飾變量,來保證數據的準確性。

對上述的共享變量內存而言,如果線程A和B之間要通信,則必須先更新主內存中的共享變量,然後由另外一個線程去主內存中去讀取。但是普通變量一般是不可見的。而volatile關鍵詞就將這件事情變成了可能。

打個比方,共享變量如果使用了volatile關鍵詞,這個時候線程B改變了共享變量副本,線程A就能夠感知到,然後經歷上述的通信步驟。

這個時候就保障了可見性。

但是另外兩種特性,也就是有序性和原子性中,原子性是無法保障的。拿我們最開始的Main的類做例子,就只改變一個變量。

public volatile int i = 0;

他最後的數值終究不是10000,這是爲什麼呢?其實對代碼進行反編譯,你能夠注意到這樣的一個問題。

iconst_0  //把數值0 push到操作數棧
istore_1 // 把操作數棧寫回到本地變量第2個位置
iinc 1,1  // 把本地變量表第2個位置加1     
iload_1 // 把本地變量第2個位置的值push到操作數棧
istore_1 // 把操作數據棧寫回本地變量第2個位置

一個++i的操作被反編譯後出現的結果如上,給人的感覺是啥,你還會覺得它是原子操作嗎?

Synchronized

這個章節的最後來簡單介紹一下synchronized這個老大哥,他從過去的版本被優化後性能高幅度提高。

在他的內部結構依舊和我們Lock類似,但是存在了這樣的三種鎖。

偏向鎖   --------->   輕量鎖(棧幀)   --------->   重量鎖(Monitor)
       (存在線程爭奪)         (自旋一定次數還是拿不到鎖)

三種加鎖對象:

  1. 實例方法
  2. 靜態方法
  3. 代碼塊
public class SyncDemo {
    // 對同一個實例加鎖
    private synchronized void fun(){}
    // 對同一個類加鎖
    private synchronized static void fun_static(){}
    // 視情況而定
    // 1\. this:實例加鎖
    // 2\. SyncDemo.class:類加鎖
    private void fun_inner(){
        synchronized(this){

        }
        synchronized(SyncDemo.class){

        }
    }
}

線程池

讓我們先來正題感受一下線程池的工作流程

五大參數

  1. 任務隊列(workQueue)
  2. 核心線程數(coolPoolSize): 即使處於空閒狀態,也會被保留下來的線程
  3. 最大線程數(maximumPoolSize): 核心線程數 + 非核心線程數。控制可以創建的線程的數量。
  4. 飽和策略(RejectedExecutionHandler)
  5. 存活時間(keepAliveTime): 設定非核心線程空閒下來後將被銷燬的時間

任務隊列

  • 基於數組的有界阻塞隊列(ArrayBlockingQueue): 放入的任務有限,到達上限時會觸發拒絕策略。
  • 基於鏈表的無界阻塞隊列(LinkedBlockingQuene): 可以放入無限多的任務。
  • 不緩存的隊列(SynchronousQuene): 一次只能進行一個任務的生產和消費。
  • 帶優先級的阻塞隊列(PriorityBlockingQueue): 可以設置任務的優先級。
  • 帶時延的任務隊列(DelayedWorkQueue)

飽和策略

  • CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        // 如果線程池還沒關閉,就在調用者線程中直接執行Runnable
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
  • AbortPolicy
public static class AbortPolicy implements RejectedExecutionHandler {
        // 拒絕任務,並且拋出RejectedExecutionException異常
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  • DiscardPolicy
 public static class DiscardPolicy implements RejectedExecutionHandler {
        // 拒絕任務,但是啥也不幹
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
  • DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        // 如果線程池還沒有關閉,就把隊列中最早的任務拋棄,把當前的線程插入
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

五種線程池

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

固定線程池 , 最大線程數和核心線程數的數量相同,也就意味着只有核心線程了,多出的任務,將會被放置到LinkedBlockingQueue中。

CachedThreadPool

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

沒有核心線程,最大線程數爲無窮,適用於頻繁IO的操作,因爲他們的任務量小,但是任務基數非常龐大,使用核心線程處理的話,數量創建方面就很成問題。

ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        // 最後對應的還是 ThreadPoolExecutor                                   
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
}

SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

核心線程數和最大線程數相同,且都爲1,也就意味着任務是按序工作的。

WorkStealingPool

public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(), // 可用的處理器數
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

這是JDK1.8以後才加入的線程池,引入了搶佔式,雖然這個概念挺早就有了。本質上就是如果當前有兩個核在工作,一個核的任務已經處理完成,而另一個還有大量工作積壓,那我們的這個空閒核就會趕緊衝過去幫忙。

優勢

  • 線程的複用

每次使用線程我們是不是需要去創建一個Thread,然後start(),然後就等結果,最後的銷燬就等着垃圾回收機制來了。 但是問題是如果有1000個任務呢,你要創建1000個Thread嗎?如果創建了,那回收又要花多久的時間?

  • 控制線程的併發數

存在覈心線程和非核心線程,還有任務隊列,那麼就可以保證資源的使用和爭奪是處於一個可控的狀態的。

  • 線程的管理

協程

Q1:什麼是協程? 一種比線程更加輕量級的存在,和進程還有線程不同的地方時他的掌權者不再是操作系統,而是程序了。但是你要注意,協程不像線程,線程最後會被CPU進行操作,但是協程是一種粒度更小的函數,我們可以對其進行控制,他的開始和暫停操作我們可以認爲是C中的goto

我們通過引入Kotlin的第三方庫來完成一些使用上的講解。

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"

引入完成後我們以launch()爲例來講解。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
)

你可以看到3個參數CoroutineContextCoroutineStartblock

  1. CoroutineContext:
    • Dispatchers.Default - 默認
    • Dispatchers.IO - 適用於IO操作的線程
    • Dispatchers.Main - 主線程
    • Dispatchers.Unconfined - 沒指定,就是在當前線程
  2. CoroutineStart:
    • DEAFAULT - 默認模式
    • ATOMIC - 這種模式下協程執行之前不能被取消
    • UNDISPATCHED - 立即在當前線程執行協程體,遇到第一個suspend函數調用
    • LAZY - 懶加載模式,需要的時候開啓
  3. block: 寫一些你要用的方法。
// 當然還有async、runBlocking等用法
GlobalScope.launch(Dispatchers.Default,
            CoroutineStart.ATOMIC,
            { Log.e("Main", "run") }
        )

Q2:他的優勢是什麼? 其實我們從Q1中已經進行過了回答,協程的掌權者是程序,那我們就不會再有經過用戶態到內核態的切換,節省了很多的系統開銷。同時我們說過他用的是類似於goto跳轉方式,就類似於將我們的堆棧空間拆分,這就是我所說的更小粒度的函數,假如我們有3個協程ABC在運行,放在主函數中時假如是這樣的壓棧順序,ABC。那從C想要返回A時勢必要經過B,而協程我們可以直接去運行A,這就是協程所帶來的好處。

最後這裏是關於我自己的Android 學習,面試文檔,視頻收集大整理,有興趣的夥伴們可以看看~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章