夯實Java基礎系列17:一文搞懂Java多執行緒使用方式、實現原理以及常見面試題

a724888 發表於 2019-10-08

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

Java中的執行緒

Java之父對執行緒的定義是:

執行緒是一個獨立執行的呼叫序列,同一個程式的執行緒在同一時刻共享一些系統資源(比如檔案控制程式碼等)也能訪問同一個程式所建立的物件資源(記憶體資源)。java.lang.Thread物件負責統計和控制這種行為。

每個程式都至少擁有一個執行緒-即作為Java虛擬機器(JVM)啟動引數執行在主類main方法的執行緒。在Java虛擬機器初始化過程中也可能啟動其他的後臺執行緒。這種執行緒的數目和種類因JVM的實現而異。然而所有使用者級執行緒都是顯式被構造並在主執行緒或者是其他使用者執行緒中被啟動。

  本文主要講了java中多執行緒的使用方法、執行緒同步、執行緒資料傳遞、執行緒狀態及相應的一些執行緒函式用法、概述等。在這之前,首先讓我們來了解下在作業系統中程式和執行緒的區別:
  程式:每個程式都有獨立的程式碼和資料空間(程式上下文),程式間的切換會有較大的開銷,一個程式包含1--n個執行緒。(程式是資源分配的最小單位)
  執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。(執行緒是cpu排程的最小單位)
  執行緒和程式一樣分為五個階段:建立、就緒、執行、阻塞、終止。
  多程式是指作業系統能同時執行多個任務(程式)。
  多執行緒是指在同一程式中有多個順序流在執行。
在java中要想實現多執行緒,有兩種手段,一種是繼續Thread類,另外一種是實現Runable介面.(其實準確來講,應該有三種,還有一種是實現Callable介面,並與Future、執行緒池結合使用

Java執行緒狀態機

Java 給多執行緒程式設計提供了內建的支援。 一條執行緒指的是程式中一個單一順序的控制流,一個程式中可以併發多個執行緒,每條執行緒並行執行不同的任務。

多執行緒是多工的一種特別的形式,但多執行緒使用了更小的資源開銷。

這裡定義和執行緒相關的另一個術語 - 程式:一個程式包括由作業系統分配的記憶體空間,包含一個或多個執行緒。一個執行緒不能獨立的存在,它必須是程式的一部分。一個程式一直執行,直到所有的非守護執行緒都結束執行後才能結束。

多執行緒能滿足程式設計師編寫高效率的程式來達到充分利用 CPU 的目的。


一個執行緒的生命週期

執行緒是一個動態執行的過程,它也有一個從產生到死亡的過程。

下圖顯示了一個執行緒完整的生命週期。

夯實Java基礎系列17:一文搞懂Java多執行緒使用方式、實現原理以及常見面試題

  • 新建狀態:

    使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒。

  • 就緒狀態:

    當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。

  • 執行狀態:

    如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。

  • 阻塞狀態:

    如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:

    • 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。

    • 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。

    • 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。

  • 死亡狀態:

    一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

    Java多執行緒實戰

    多執行緒的實現

    public class 多執行緒例項 {

    //繼承thread
    @Test
    public void test1() {
        class A extends Thread {
            
    @Override
            public void run() {
                System.out.println("A run");
            }
        }
        A a = new A();
        a.start();
    }
    //實現Runnable
    @Test
    public void test2() {
        class B implements Runnable {
            
    @Override
            public void run() {
                System.out.println("B run");
            }
        }
        B b = new B();
        //Runable實現類需要由Thread類包裝後才能執行
        new Thread(b).start();
    }
    //有返回值的執行緒
    @Test
    public void test3() {
        Callable callable = new Callable() {
            int sum = 0;
            
    @Override
            public Object call() throws Exception {
                for (int i = 0;i < 5;i ++) {
                    sum += i;
                }
                return sum;
            }
        };
        //這裡要用FutureTask,否則不能加入Thread構造方法
        FutureTask futureTask = new FutureTask(callable);
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    //執行緒池實現
    @Test
    public void test4() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //execute直接執行執行緒
        executorService.execute(new Thread());
        executorService.execute(new Runnable() {
            
    @Override
            public void run() {
                System.out.println("runnable");
            }
        });
        //submit提交有返回結果的任務,執行完後返回結果。
        Future future = executorService.submit(new Callable<String>() {
            
    @Override
            public String call() throws Exception {
                return "a";
            }
        });
        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        ArrayList<String> list = new ArrayList<>();
        //有返回值的執行緒組將返回值存進集合
        for (int i = 0;i < 5;i ++ ) {
            int finalI = i;
            Future future1 = executorService.submit(new Callable<String>() {
                
    @Override
                public String call() throws Exception {
                    return "res" + finalI;
                }
            });
            try {
                list.add((String) future1.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        for (String s : list) {
            System.out.println(s);
        }
    }
    

    }

執行緒狀態轉換

public class 執行緒的狀態轉換 {
//一開始執行緒是init狀態,結束時是terminated狀態
class t implements Runnable {
    private String name;
    public t(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println(name + "run");
    }
}
//測試join,父執行緒在子執行緒執行時進入waiting狀態
@Test
public void test1() throws InterruptedException {
    Thread dad = new Thread(new Runnable() {
        Thread son = new Thread(new t("son"));
        @Override
        public void run() {
            System.out.println("dad init");
            son.start();
            try {
                //保證子執行緒執行完再執行父執行緒
                son.join();
                System.out.println("dad run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    //呼叫start,執行緒進入runnable狀態,等待系統排程
    dad.start();
    //在父執行緒中對子執行緒例項使用join,保證子執行緒在父執行緒之前執行完
}
//測試sleep
@Test
public void test2(){
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("t1 run");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    //主執行緒休眠。進入time waiting狀態
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.start();
}
//執行緒2進入blocked狀態。
public static void main(String[] args) {
    test4();
    Thread.yield();//進入runnable狀態
}
//測試blocked狀態
public static void test4() {
    class A {
        //執行緒1獲得例項鎖以後執行緒2無法獲得例項鎖,所以進入blocked狀態
        synchronized void run() {
            while (true) {
                System.out.println("run");
            }
        }
    }
    A a = new A();
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("t1 get lock");
            a.run();
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("t2 get lock");
            a.run();
        }
    }).start();
}
//volatile保證執行緒可見性
volatile static int flag = 1;
//object作為鎖物件,用於執行緒使用wait和notify方法
volatile static Object o = new Object();
//測試wait和notify
//wait後進入waiting狀態,被notify進入blocked(阻塞等待鎖釋放)或者runnable狀態(獲取到鎖)
public void test5() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            //wait和notify只能在同步程式碼塊內使用
            synchronized (o) {
                while (true) {
                    if (flag == 0) {
                        try {
                            Thread.sleep(2000);
                            System.out.println("thread1 wait");
                            //釋放鎖,執行緒掛起進入object的等待佇列,後續程式碼執行
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("thread1 run");
                    System.out.println("notify t2");
                    flag = 0;
                    //通知等待佇列的一個執行緒獲取鎖
                    o.notify();
                }
            }
        }
    }).start();
    //解釋同上
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                synchronized (o) {
                    if (flag == 1) {
                        try {
                            Thread.sleep(2000);
                            System.out.println("thread2 wait");
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("thread2 run");
                    System.out.println("notify t1");
                    flag = 1;
                    o.notify();
                }
            }
        }
    }).start();
}
//輸出結果是
//    thread1 run
//    notify t2
//    thread1 wait
//    thread2 run
//    notify t1
//    thread2 wait
//    thread1 run
//    notify t2
//不斷迴圈
}

Java Thread常用方法

Thread#yield():

執行此方法會向系統執行緒排程器(Schelduler)發出一個暗示,告訴其當前JAVA執行緒打算放棄對CPU的使用,但該暗示,有可能被排程器忽略。使用該方法,可以防止執行緒對CPU的過度使用,提高系統效能。

Thread#sleep(time)或Thread.sleep(time, nanos):

使當前執行緒進入休眠階段,狀態變為:TIME_WAITING

Thread.interrupt():

中斷當前執行緒的執行,允許當前執行緒對自身進行中斷,否則將會校驗呼叫方執行緒是否有對該執行緒的許可權。

如果當前執行緒因被呼叫Object#wait(),Object#wait(long, int), 或者執行緒本身的join(), join(long),sleep()處於阻塞狀態中,此時呼叫interrupt方法會使丟擲InterruptedException,而且執行緒的阻塞狀態將會被清除。

Thread#interrupted(),返回true或者false:

檢視當前執行緒是否處於中斷狀態,這個方法比較特殊之處在於,如果呼叫成功,會將當前執行緒的interrupt status清除。所以如果連續2次呼叫該方法,第二次將返回false。

Thread.isInterrupted(),返回true或者false:

與上面方法相同的地方在於,該方法返回當前執行緒的中斷狀態。不同的地方在於,它不會清除當前執行緒的interrupt status狀態。

Thread#join(),Thread#join(time):

A執行緒呼叫B執行緒的join()方法,將會使A等待B執行,直到B執行緒終止。如果傳入time引數,將會使A等待B執行time的時間,如果time時間到達,將會切換進A執行緒,繼續執行A執行緒。

構造方法和守護執行緒

構造方法
Thread類中不同的構造方法接受如下引數的不同組合:
一個Runnable物件,這種情況下,Thread.start方法將會呼叫對應Runnable物件的run方法。如果沒有提供Runnable物件,那麼就會立即得到一個Thread.run的預設實現。
一個作為執行緒標識名的String字串,該標識在跟蹤和除錯過程中會非常有用,除此別無它用。
執行緒組(ThreadGroup),用來放置新建立的執行緒,如果提供的ThreadGroup不允許被訪問,那麼就會丟擲一個SecurityException 。
Thread物件擁有一個守護(daemon)標識屬性,這個屬性無法在構造方法中被賦值,但是可以線上程啟動之前設定該屬性(通過setDaemon方法)。
當程式中所有的非守護執行緒都已經終止,呼叫setDaemon方法可能會導致虛擬機器粗暴的終止執行緒並退出。
isDaemon方法能夠返回該屬性的值。守護狀態的作用非常有限,即使是後臺執行緒在程式退出的時候也經常需要做一些清理工作。
(daemon的發音為”day-mon”,這是系統程式設計傳統的遺留,系統守護程式是一個持續執行的程式,比如印表機佇列管理,它總是在系統中執行。)

啟動執行緒的方式和isAlive方法

啟動執行緒
呼叫start方法會觸發Thread例項以一個新的執行緒啟動其run方法。新執行緒不會持有呼叫執行緒的任何同步鎖。

當一個執行緒正常地執行結束或者丟擲某種未檢測的異常(比如,執行時異常(RuntimeException),錯誤(ERROR) 或者其子類)執行緒就會終止。

當執行緒終止之後,是不能被重新啟動的。在同一個Thread上呼叫多次start方法會丟擲InvalidThreadStateException異常。

如果執行緒已經啟動但是還沒有終止,那麼呼叫isAlive方法就會返回true.即使執行緒由於某些原因處於阻塞(Blocked)狀態該方法依然返回true。

如果執行緒已經被取消(cancelled),那麼呼叫其isAlive在什麼時候返回false就因各Java虛擬機器的實現而異了。沒有方法可以得知一個處於非活動狀態的執行緒是否已經被啟動過了。

Java多執行緒優先順序

Java的執行緒實現基本上都是核心級執行緒的實現,所以Java執行緒的具體執行還取決於作業系統的特性。

Java虛擬機器為了實現跨平臺(不同的硬體平臺和各種作業系統)的特性,Java語言線上程排程與排程公平性上未作出任何的承諾,甚至都不會嚴格保證執行緒會被執行。但是Java執行緒卻支援優先順序的方法,這些方法會影響執行緒的排程:

每個執行緒都有一個優先順序,分佈在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之間(分別為1和10)
預設情況下,新建立的執行緒都擁有和建立它的執行緒相同的優先順序。main方法所關聯的初始化執行緒擁有一個預設的優先順序,這個優先順序是Thread.NORM_PRIORITY (5).

執行緒的當前優先順序可以通過getPriority方法獲得。
執行緒的優先順序可以通過setPriority方法來動態的修改,一個執行緒的最高優先順序由其所在的執行緒組限定。

Java多執行緒面試題

這篇文章主要是對多執行緒的問題進行總結的,因此羅列了40個多執行緒的問題。

這些多執行緒的問題,有些來源於各大網站、有些來源於自己的思考。可能有些問題網上有、可能有些問題對應的答案也有、也可能有些各位網友也都看過,但是本文寫作的重心就是所有的問題都會按照自己的理解回答一遍,不會去看網上的答案,因此可能有些問題講的不對,能指正的希望大家不吝指教。

1、多執行緒有什麼用?

一個可能在很多人看來很扯淡的一個問題:我會用多執行緒就好了,還管它有什麼用?在我看來,這個回答更扯淡。所謂”知其然知其所以然”,”會用”只是”知其然”,”為什麼用”才是”知其所以然”,只有達到”知其然知其所以然”的程度才可以說是把一個知識點運用自如。OK,下面說說我對這個問題的看法:

1)發揮多核CPU的優勢

隨著工業的進步,現在的筆記本、桌上型電腦乃至商用的應用伺服器至少也都是雙核的,4核、8核甚至16核的也都不少見,如果是單執行緒的程式,那麼在雙核CPU上就浪費了50%,在4核CPU上就浪費了75%。 單核CPU上所謂的”多執行緒”那是假的多執行緒,同一時間處理器只會處理一段邏輯,只不過執行緒之間切換得比較快,看著像多個執行緒”同時”執行罷了。多核CPU上的多執行緒才是真正的多執行緒,它能讓你的多段邏輯同時工作,多執行緒,可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的。

2)防止阻塞

從程式執行效率的角度來看,單核CPU不但不會發揮出多執行緒的優勢,反而會因為在單核CPU上執行多執行緒導致執行緒上下文的切換,而降低程式整體的效率。但是單核CPU我們還是要應用多執行緒,就是為了防止阻塞。試想,如果單核CPU使用單執行緒,那麼只要這個執行緒阻塞了,比方說遠端讀取某個資料吧,對端遲遲未返回又沒有設定超時時間,那麼你的整個程式在資料返回回來之前就停止執行了。多執行緒可以防止這個問題,多條執行緒同時執行,哪怕一條執行緒的程式碼執行讀取資料阻塞,也不會影響其它任務的執行。

3)便於建模

這是另外一個沒有這麼明顯的優點了。假設有一個大的任務A,單執行緒程式設計,那麼就要考慮很多,建立整個程式模型比較麻煩。但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程式模型,並通過多執行緒分別執行這幾個任務,那就簡單很多了。

2、建立執行緒的方式

比較常見的一個問題了,一般就是兩種:

1)繼承Thread類

2)實現Runnable介面

至於哪個好,不用說肯定是後者好,因為實現介面的方式比繼承類的方式更靈活,也能減少程式之間的耦合度, 面向介面程式設計也是設計模式6大原則的核心。

3、start()方法和run()方法的區別

只有呼叫了start()方法,才會表現出多執行緒的特性,不同執行緒的run()方法裡面的程式碼交替執行。如果只是呼叫run()方法,那麼程式碼還是同步執行的,必須等待一個執行緒的run()方法裡面的程式碼全部執行完畢之後,另外一個執行緒才可以執行其run()方法裡面的程式碼。

4、Runnable介面和Callable介面的區別

有點深的問題了,也看出一個Java程式設計師學習知識的廣度。

Runnable介面中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的程式碼而已;Callable介面中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取非同步執行的結果。

這其實是很有用的一個特性,因為 多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已。而Callable+Future/FutureTask卻可以獲取多執行緒執行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務,真的是非常有用。

5、CyclicBarrier和CountDownLatch的區別

兩個看上去有點像的類,都在java.util.concurrent下,都可以用來表示程式碼執行到某個點上,二者的區別在於:

1)CyclicBarrier的某個執行緒執行到某個點上之後,該執行緒即停止執行,直到所有的執行緒都到達了這個點,所有執行緒才重新執行;CountDownLatch則不是,某執行緒執行到某個點上之後,只是給某個數值-1而已,該執行緒繼續執行。

2)CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務。

3) CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了。

6、volatile關鍵字的作用

一個非常重要的問題,是每個學習、應用多執行緒的Java程式設計師都必須掌握的。理解volatile關鍵字的作用的前提是要理解Java記憶體模型,這裡就不講Java記憶體模型了,可以參見第31點,volatile關鍵字的作用主要有兩個:

1)多執行緒主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變數,保證了其在多執行緒之間的可見性,即每次讀取到volatile變數,一定是最新的資料。

2)程式碼底層執行不像我們看到的高階語言——Java程式這麼簡單,它的執行是 Java程式碼—>位元組碼—>根據位元組碼執行對應的C/C++程式碼—>C/C++程式碼被編譯成組合語言—>和硬體電路互動,現實中,為了獲取更好的效能JVM可能會對指令進行重排序,多執行緒下可能會出現一些意想不到的問題。使用volatile則會對禁止語義重排序,當然這也一定程度上降低了程式碼執行效率。

從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger,更多詳情請點選 這裡進行學習。

7、什麼是執行緒安全

又是一個理論的問題,各式各樣的答案有很多,我給出一個個人認為解釋地最好的: 如果你的程式碼在多執行緒下執行和在單執行緒下執行永遠都能獲得一樣的結果,那麼你的程式碼就是執行緒安全的

這個問題有值得一提的地方,就是執行緒安全也是有幾個級別的:

1)不可變

像String、Integer、Long這些,都是final型別的類,任何一個執行緒都改變不了它們的值,要改變除非新建立一個,因此這些不可變物件不需要任何同步手段就可以直接在多執行緒環境下使用

2)絕對執行緒安全

不管執行時環境如何,呼叫者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標註自己是執行緒安全的類,實際上絕大多數都不是執行緒安全的,不過絕對執行緒安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet

3)相對執行緒安全

相對執行緒安全也就是我們通常意義上所說的執行緒安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限於此,如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是 fail-fast機制

4)執行緒非安全

這個就沒什麼好說的了,ArrayList、LinkedList、HashMap等都是執行緒非安全的類,點選 這裡瞭解為什麼不安全。

8、Java中如何獲取到執行緒dump檔案

死迴圈、死鎖、阻塞、頁面開啟慢等問題,打執行緒dump是最好的解決問題的途徑。所謂執行緒dump也就是執行緒堆疊,獲取到執行緒堆疊有兩步:

1)獲取到執行緒的pid,可以通過使用jps命令,在Linux環境下還可以使用ps -ef | grep java

2)列印執行緒堆疊,可以通過使用jstack pid命令,在Linux環境下還可以使用kill -3 pid

另外提一點,Thread類提供了一個getStackTrace()方法也可以用於獲取執行緒堆疊。這是一個例項方法,因此此方法是和具體執行緒例項繫結的,每次獲取獲取到的是具體某個執行緒當前執行的堆疊。

9、一個執行緒如果出現了執行時異常會怎麼樣

如果這個異常沒有被捕獲的話,這個執行緒就停止執行了。另外重要的一點是: 如果這個執行緒持有某個某個物件的監視器,那麼這個物件監視器會被立即釋放

10、如何在兩個執行緒之間共享資料

通過線上程之間共享物件就可以了,然後通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞佇列BlockingQueue就是為執行緒之間共享資料而設計的

11、sleep方法和wait方法有什麼區別

這個問題常問,sleep方法和wait方法都可以用來放棄CPU一定的時間,不同點在於如果執行緒持有某個物件的監視器,sleep方法不會放棄這個物件的監視器,wait方法會放棄這個物件的監視器

12、生產者消費者模型的作用是什麼

這個問題很理論,但是很重要:

1) 通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的執行效率,這是生產者消費者模型最重要的作用

2)解耦,這是生產者消費者模型附帶的作用,解耦意味著生產者和消費者之間的聯絡少,聯絡越少越可以獨自發展而不需要收到相互的制約

13、ThreadLocal有什麼用

簡單說ThreadLocal就是一種以 空間換時間的做法,在每個Thread裡面維護了一個以開地址法實現的ThreadLocal.ThreadLocalMap,把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了

14、為什麼wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫

這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖

15、wait()方法和notify()/notifyAll()方法在放棄物件監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄物件監視器的時候的區別在於: wait()方法立即釋放物件監視器,notify()/notifyAll()方法則會等待執行緒剩餘程式碼執行完畢才會放棄物件監視器

16、為什麼要使用執行緒池

避免頻繁地建立和銷燬執行緒,達到執行緒物件的重用。另外,使用執行緒池還可以根據專案靈活地控制併發的數目。點選 這裡學習執行緒池詳解。

17、怎麼喚醒一個阻塞的執行緒

如果執行緒是因為呼叫了wait()、sleep()或者join()方法而導致的阻塞,可以中斷執行緒,並且通過丟擲InterruptedException來喚醒它;如果執行緒遇到了IO阻塞,無能為力,因為IO是作業系統實現的,Java程式碼並沒有辦法直接接觸到作業系統。

18、不可變物件對多執行緒有什麼幫助

前面有提到過的一個問題,不可變物件保證了物件的記憶體可見性,對不可變物件的讀取不需要進行額外的同步手段,提升了程式碼執行效率。

19、什麼是多執行緒的上下文切換

多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。

20、執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的

這是一個非常刁鑽和狡猾的問題。請記住:執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法裡面的程式碼才是被執行緒自身所呼叫的。

如果說上面的說法讓你感到困惑,那麼我舉個例子,假設Thread2中new了Thread1,main函式中new了Thread2,那麼:

1)Thread2的構造方法、靜態塊是main執行緒呼叫的,Thread2的run()方法是Thread2自己呼叫的

2)Thread1的構造方法、靜態塊是Thread2呼叫的,Thread1的run()方法是Thread1自己呼叫的

21、高併發、任務執行時間短的業務怎樣使用執行緒池?併發不高、任務執行時間長的業務怎樣使用執行緒池?併發高、業務執行時間長的業務怎樣使用執行緒池?

這是我在併發程式設計網上看到的一個問題,把這個問題放在最後一個,希望每個人都能看到並且思考一下,因為這個問題非常好、非常實際、非常專業。關於這個問題,個人看法是:

1)高併發、任務執行時間短的業務,執行緒池執行緒數可以設定為CPU核數+1,減少執行緒上下文的切換

2)併發不高、任務執行時間長的業務要區分開看:

a)假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作並不佔用CPU,所以不要讓所有的CPU閒下來,可以加大執行緒池中的執行緒數目,讓CPU處理更多的業務

b)假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)一樣吧,執行緒池中的執行緒數設定得少一些,減少執行緒上下文的切換

c)併發高、業務執行時間長,解決這種型別任務的關鍵不在於執行緒池而在於整體架構的設計,看看這些業務裡面某些資料是否能做快取是第一步,增加伺服器是第二步,至於執行緒池的設定,設定參考其他有關執行緒池的文章。最後,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中介軟體對任務進行拆分和解耦。

參考文章

https://blog.csdn.net/zl1zl2zl3/article/details/81868173

https://www.runoob.com/java/java-multithreading.html

https://blog.csdn.net/qq_38038480/article/details/80584715

https://blog.csdn.net/tongxuexie/article/details/80145663

https://www.cnblogs.com/snow-flower/p/6114765.html

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,偶爾講點演算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

夯實Java基礎系列17:一文搞懂Java多執行緒使用方式、實現原理以及常見面試題

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


相關文章

每週開源點評:雲原生 Java、開源安全以及更多行業趨勢 Java

每週開源點評:雲原生 Java、開源安全以及更多行業趨勢

開源社群和行業趨勢的每週總覽。作為我在具有開源開發模型的企業軟體公司擔任高階產品營銷經理的角色的一部分,我為產品營銷人員、經理和其他影響者定期釋出有關開源社群,市場和行業趨勢的定期更新。 以下是該更新
夯實Java基礎系列11:深入理解Java中的回撥機制 Java

夯實Java基礎系列11:深入理解Java中的回撥機制

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章首
面試官,不要再問我三次握手和四次揮手 面試

面試官,不要再問我三次握手和四次揮手

三次握手和四次揮手是各個公司常見的考點,也具有一定的水平區分度,也被一些面試官作為熱身題。很多小夥伴說這個問題剛開始回答的挺好,但是後面越回答越冒冷汗,最後就歇菜了。見過比較典型的面試場景是這樣的:面
夯實Java基礎系列16:一文讀懂Java IO流和常見面試題 Java|面試

夯實Java基礎系列16:一文讀懂Java IO流和常見面試題

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章首
夯實Java基礎系列18:深入理解Java內部類及其實現原理 Java

夯實Java基礎系列18:深入理解Java內部類及其實現原理

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章首
夯實Java基礎系列19:一文搞懂Java集合類框架,以及常見面試題 Java|面試|框架

夯實Java基礎系列19:一文搞懂Java集合類框架,以及常見面試題

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章首
Java面試常用到的技巧有哪些 Java|面試

Java面試常用到的技巧有哪些

快過年了年後就是一個應聘的高峰期,許多Java程式員要面臨著面試的各種問題,而也有許多學院將要去進行面試,在面試的時候會緊張會有種種的困難,但是你要在進行Java面試(m.cnitedu.cn)的時候
面試官出的MySQL索引問題,這篇文章全給你解決! 面試|MySQL

面試官出的MySQL索引問題,這篇文章全給你解決!

原文連結:blog.ouyangsihai.cn &gt;&gt; MySQL的B+樹索引的概念、使用、優化及使用場景 0 前言這篇文章不會講解索引的基礎知識,主要是關於MySQL資料庫的B+樹索引的
夯實Java基礎系列22:一文讀懂Java序列化和反序列化 Java

夯實Java基礎系列22:一文讀懂Java序列化和反序列化

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章首