關於多線程,你必須知道的那些玩意兒

進程與線程

概念

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

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

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

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

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

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

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

生命週期

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

進程 線程

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

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

而對於線程,他在JavaThread類中對應了6種狀態,能夠自行進行查看。併發

多線程編程入門

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

廢話很少說,咱們趕忙入門一下。

// 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,這就是協程所帶來的好處。

相關文章
相關標籤/搜索