多線程、線程池、內置鎖 面試題 (一網打淨 ,持續更新)


推薦: 地表最強 開發環境 系列

工欲善其事 必先利其器
地表最強 開發環境: vagrant+java+springcloud+redis+zookeeper鏡像下載(&製做詳解)
地表最強 熱部署:java SpringBoot SpringCloud 熱部署 熱加載 熱調試
地表最強 發請求工具(再見吧, PostMan ):IDEA HTTP Client(史上最全)
地表最強 PPT 小工具: 屌炸天,像寫代碼同樣寫PPT
無編程不創客,無編程不創客,一大波編程高手正在瘋狂創客圈交流、學習中! 找組織,GO

推薦: springCloud 微服務 系列

推薦閱讀
nacos 實戰(史上最全)
sentinel (史上最全+入門教程)
springcloud + webflux 高併發實戰
Webflux(史上最全)
SpringCloud gateway (史上最全)
無編程不創客,無編程不創客,一大波編程高手正在瘋狂創客圈交流、學習中! 找組織,GO

基礎知識

爲何要使用多線程(併發編程的優勢)

  • 充分利用多核CPU的計算能力:經過併發編程的形式能夠將多核CPU的計算能力發揮到極致,性能獲得提高
  • 方便進行業務拆分,提高系統併發能力和性能:在特殊的業務場景下,先天的就適合於併發編程。如今的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制能夠大大提升系統總體的併發能力以及性能。面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分 。

多線程有什麼缺點(併發編程的缺點)

併發編程的目的就是爲了能提升程序的執行效率,提升程序運行速度,可是併發編程並不老是能提升程序運行速度的,並且併發編程可能會遇到不少問題,好比:內存泄漏、上下文切換、線程安全、死鎖等問題。程序員

併發編程三要素是什麼?在 Java 程序中怎麼保證多線程的運行安全?

併發編程三要素(線程的安全性問題體如今):web

原子性:原子,即一個不可再被分割的顆粒。原子性指的是一個或多個操做要麼所有執行成功要麼所有執行失敗。面試

可見性:一個線程對共享變量的修改,另外一個線程可以馬上看到。(synchronized,volatile)redis

有序性:程序執行的順序按照代碼的前後順序執行。(處理器可能會對指令進行重排序)算法

出現線程安全問題的緣由:spring

  • 線程切換帶來的原子性問題
  • 緩存致使的可見性問題
  • 編譯優化帶來的有序性問題

解決辦法:數據庫

  • JDK Atomic開頭的原子類、synchronized、LOCK,能夠解決原子性問題
  • synchronized、volatile、LOCK,能夠解決可見性問題
  • Happens-Before 規則能夠解決有序性問題

並行和併發有什麼區別?

  • 併發:多個任務在同一個 CPU 核上,按細分的時間片輪流(交替)執行,從邏輯上來看那些任務是同時執行。
  • 並行:單位時間內,多個處理器或多核處理器同時處理多個任務,是真正意義上的「同時進行」。
  • 串行:有n個任務,由一個線程按順序執行。因爲任務、方法都在一個線程執行因此不存在線程不安全狀況,也就不存在臨界區的問題。

作一個形象的比喻:

併發 = 兩個隊列和一臺咖啡機。

並行 = 兩個隊列和兩臺咖啡機。

串行 = 一個隊列和一臺咖啡機。

線程和進程區別

什麼是多線程,多線程的優劣?

多線程:多線程是指程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務。

多線程的好處:

能夠提升 CPU 的利用率。在多線程程序中,一個線程必須等待的時候,CPU 能夠運行其它的線程而不是等待,這樣就大大提升了程序的效率。也就是說容許單個程序建立多個並行執行的線程來完成各自的任務。

多線程的劣勢:

  • 線程也是程序,因此線程須要佔用內存,線程越多佔用內存也越多;
  • 多線程須要協調和管理,因此須要 CPU 時間跟蹤線程;
  • 線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。

什麼是線程和進程?

進程

一個在內存中運行的應用程序。每一個進程都有本身獨立的一塊內存空間,一個進程能夠有多個線程,好比在Windows系統中,一個運行的xx.exe就是一個進程。

線程

進程中的一個執行任務(控制單元),負責當前進程中程序的執行。一個進程至少有一個線程,一個進程能夠運行多個線程,多個線程可共享數據。

進程與線程的區別

線程具備許多傳統進程所具備的特徵,故又稱爲輕型進程(Light—Weight Process)或進程元;而把傳統的進程稱爲重型進程(Heavy—Weight Process),它至關於只有一個線程的任務。在引入了線程的操做系統中,一般一個進程都有若干個線程,至少包含一個線程。

根本區別:進程是操做系統資源分配的基本單位,而線程是處理器任務調度和執行的基本單位

資源開銷:每一個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程能夠看作輕量級的進程,同一類線程共享代碼和數據空間,每一個線程都有本身獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。

包含關係:若是一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,因此線程也被稱爲輕權進程或者輕量級進程。

內存分配:同一進程的線程共享本進程的地址空間和資源,而進程之間的地址空間和資源是相互獨立的

影響關係:一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰整個進程都死掉。因此多進程要比多線程健壯。

執行過程:每一個獨立的進程有程序運行的入口、順序執行序列和程序出口。可是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制,二者都可併發執行

什麼是上下文切換?

多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。

歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。

Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。

守護線程和用戶線程有什麼區別呢?

守護線程和用戶線程

  • 用戶 (User) 線程:運行在前臺,執行具體的任務,如程序的主線程、鏈接網絡的子線程等都是用戶線程
  • 守護 (Daemon) 線程:運行在後臺,爲其餘前臺線程服務。也能夠說守護線程是 JVM 中非守護線程的 「傭人」。一旦全部用戶線程都結束運行,守護線程會隨 JVM 一塊兒結束工做

main 函數所在的線程就是一個用戶線程啊,main 函數啓動的同時在 JVM 內部同時還啓動了好多守護線程,好比垃圾回收線程。

比較明顯的區別之一是用戶線程結束,JVM 退出,無論這個時候有沒有守護線程運行。而守護線程不會影響 JVM 的退出。

注意事項:

  1. setDaemon(true)必須在start()方法前執行,不然會拋出 IllegalThreadStateException 異常
  2. 在守護線程中產生的新線程也是守護線程
  3. 不是全部的任務均可以分配給守護線程來執行,好比讀寫操做或者計算邏輯
  4. 守護 (Daemon) 線程中不能依靠 finally 塊的內容來確保執行關閉或清理資源的邏輯。由於咱們上面也說過了一旦全部用戶線程都結束運行,守護線程會隨 JVM 一塊兒結束工做,因此守護 (Daemon) 線程中的 finally 語句塊可能沒法被執行。

如何在 Windows 和 Linux 上查找哪一個線程cpu利用率最高?

windows上面用任務管理器看,linux下能夠用 top 這個工具看。

  1. 找出cpu耗用厲害的進程pid, 終端執行top命令,而後按下shift+p 查找出cpu利用最厲害的pid號
  2. 根據上面第一步拿到的pid號,top -H -p pid 。而後按下shift+p,查找出cpu利用率最厲害的線程號,好比top -H -p 1328
  3. 將獲取到的線程號轉換成16進制,去百度轉換一下就行
  4. 使用jstack工具將進程信息打印輸出,jstack pid號 > /tmp/t.dat,好比jstack 31365 > /tmp/t.dat
  5. 編輯/tmp/t.dat文件,查找線程號對應的信息

建立線程的四種方式

建立線程有哪幾種方式?

建立線程有四種方式:

  • 繼承 Thread 類;
  • 實現 Runnable 接口;
  • 實現 Callable 接口;
  • 使用 Executors 工具類建立線程池

1繼承 Thread 類

步驟

  1. 定義一個Thread類的子類,重寫run方法,將相關邏輯實現,run()方法就是線程要執行的業務邏輯方法
  2. 建立自定義的線程子類對象
  3. 調用子類實例的star()方法來啓動線程
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在執行...");
    }

}
12345678
public class TheadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread(); 	
        myThread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法執行結束");
    }

}

12345678910

運行結果

main main()方法執行結束
Thread-0 run()方法正在執行...
12

2實現 Runnable 接口

步驟

  1. 定義Runnable接口實現類MyRunnable,並重寫run()方法
  2. 建立MyRunnable實例myRunnable,以myRunnable做爲target建立Thead對象,該Thread對象纔是真正的線程對象
  3. 調用線程對象的start()方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法執行中...");
    }

}
12345678
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法執行完成");
    }

}
12345678910

執行結果

main main()方法執行完成
Thread-0 run()方法執行中...
12

3實現 Callable 接口

步驟

  1. 建立實現Callable接口的類myCallable
  2. 以myCallable爲參數建立FutureTask對象
  3. 將FutureTask做爲參數建立Thread對象
  4. 調用線程對象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法執行中...");
        return 1;
    }

}
123456789
public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回結果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法執行完成");
    }

}
12345678910111213141516171819

執行結果

Thread-0 call()方法執行中...
返回結果 1
main main()方法執行完成
123

4使用 Executors 工具類建立線程池

Executors提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。

主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,後續詳細介紹這四種線程池

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法執行中...");
    }

}
12345678
public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("線程任務開始執行");
        executorService.shutdown();
    }

}
1234567891011121314

執行結果

線程任務開始執行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
123456

說一下 runnable 和 callable 有什麼區別?

相同點

  • 都是接口
  • 均可以編寫多線程程序
  • 都採用Thread.start()啓動線程

主要區別

  • Runnable 接口 run 方法無返回值;Callable 接口 call 方法有返回值,是個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果
  • Runnable 接口 run 方法只能拋出運行時異常,且沒法捕獲處理;Callable 接口 call 方法容許拋出異常,能夠獲取異常信息

:Callalbe接口支持返回執行結果,須要調用FutureTask.get()獲得,此方法會阻塞主進程的繼續往下執行,若是不調用不會阻塞。

線程的 run()和 start()有什麼區別?

每一個線程都是經過某個特定Thread對象所對應的方法run()來完成其操做的,run()方法稱爲線程體。經過調用Thread類的start()方法來啓動一個線程。

start() 方法用於啓動線程,run() 方法用於執行線程的運行時代碼。run() 能夠重複調用,而 start() 只能調用一次。

start()方法來啓動一個線程,真正實現了多線程運行。調用start()方法無需等待run方法體代碼執行完畢,能夠直接繼續執行其餘的代碼; 此時線程是處於就緒狀態,並無運行。 而後經過此Thread類調用方法run()來完成其運行狀態, run()方法運行結束, 此線程終止。而後CPU再調度其它線程。

run()方法是在本線程裏的,只是線程裏的一個函數,而不是多線程的。 若是直接調用run(),其實就至關因而調用了一個普通函數而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的代碼,因此執行路徑仍是隻有一條,根本就沒有線程的特徵,因此在多線程執行時要使用start()方法而不是run()方法。

爲何咱們調用 start() 方法時會執行 run() 方法,爲何咱們不能直接調用 run() 方法?

這是另外一個很是經典的 java 多線程面試問題,並且在面試中會常常被問到。很簡單,可是不少人都會答不上來!

new 一個 Thread,線程進入了新建狀態。調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。

而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。

總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。

什麼是 Callable 和 Future?

Callable 接口相似於 Runnable,從名字就能夠看出來了,可是 Runnable 不會返回結果,而且沒法拋出返回結果的異常,而 Callable 功能更強大一些,被線程執行後,能夠返回值,這個返回值能夠被 Future 拿到,也就是說,Future 能夠拿到異步執行任務的返回值。

Future 接口表示異步任務,是一個可能尚未完成的異步任務的結果。因此說 Callable用於產生結果,Future 用於獲取結果。

什麼是 FutureTask

FutureTask 表示一個異步運算的任務。FutureTask 裏面能夠傳入一個 Callable 的具體實現類,能夠對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操做。只有當運算完成的時候結果才能取回,若是運算還沒有完成 get 方法將會阻塞。一個 FutureTask 對象能夠對調用了 Callable 和 Runnable 的對象進行包裝,因爲 FutureTask 也是Runnable 接口的實現類,因此 FutureTask 也能夠放入線程池中。

線程的狀態和基本操做

說說線程的生命週期及五種基本狀態?

線程的基本狀態

  1. 新建(new):新建立了一個線程對象。

  2. 可運行(runnable):線程對象建立後,當調用線程對象的 start()方法,該線程處於就緒狀態,等待被線程調度選中,獲取cpu的使用權。

  3. 運行(running):可運行狀態(runnable)的線程得到了cpu時間片(timeslice),執行程序代碼。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

  4. 阻塞(block):處於運行狀態中的線程因爲某種緣由,暫時放棄對 CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被 CPU 調用以進入到運行狀態。

    阻塞的狀況分三種:
    (一). 等待阻塞:運行狀態中的線程執行 wait()方法,JVM會把該線程放入等待隊列(waitting queue)中,使本線程進入到等待阻塞狀態;
    (二). 同步阻塞:線程在獲取 synchronized 同步鎖失敗(由於鎖被其它線程所佔用),,則JVM會把該線程放入鎖池(lock pool)中,線程會進入同步阻塞狀態;
    (三). 其餘阻塞: 經過調用線程的 sleep()或 join()或發出了 I/O 請求時,線程會進入到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者 I/O 處理完畢時,線程從新轉入就緒狀態。

  5. 死亡(dead):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。

Java 中用到的線程調度算法是什麼?

計算機一般只有一個 CPU,在任意時刻只能執行一條機器指令,每一個線程只有得到CPU 的使用權才能執行指令。所謂多線程的併發運行,實際上是指從宏觀上看,各個線程輪流得到 CPU 的使用權,分別執行各自的任務。在運行池中,會有多個處於就緒狀態的線程在等待 CPU,JAVA 虛擬機的一項任務就是負責線程的調度,線程調度是指按照特定機制爲多個線程分配 CPU 的使用權。

有兩種調度模型:分時調度模型和搶佔式調度模型。

分時調度模型是指讓全部的線程輪流得到 cpu 的使用權,而且平均分配每一個線程佔用的 CPU 的時間片這個也比較好理解。

Java虛擬機採用搶佔式調度模型,是指優先讓可運行池中優先級高的線程佔用CPU,若是可運行池中的線程優先級相同,那麼就隨機選擇一個線程,使其佔用CPU。處於運行狀態的線程會一直運行,直至它不得不放棄 CPU。

線程的調度策略

線程調度器選擇優先級最高的線程運行,可是,若是發生如下狀況,就會終止線程的運行:

(1)線程體中調用了 yield 方法讓出了對 cpu 的佔用權利

(2)線程體中調用了 sleep 方法使線程進入睡眠狀態

(3)線程因爲 IO 操做受到阻塞

(4)另一個更高優先級線程出現

(5)在支持時間片的系統中,該線程的時間片用完

什麼是線程調度器(Thread Scheduler)和時間分片(Time Slicing )?

線程調度器是一個操做系統服務,它負責爲 Runnable 狀態的線程分配 CPU 時間。一旦咱們建立一個線程並啓動它,它的執行便依賴於線程調度器的實現。

時間分片是指將可用的 CPU 時間分配給可用的 Runnable 線程的過程。分配 CPU 時間能夠基於線程優先級或者線程等待的時間。

線程調度並不受到 Java 虛擬機控制,因此由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)。

請說出與線程同步以及線程調度相關的方法。

(1) wait():使一個線程處於等待(阻塞)狀態,而且釋放所持有的對象的鎖;

(2)sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理 InterruptedException 異常;

(3)notify():喚醒一個處於等待狀態的線程,固然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由 JVM 肯定喚醒哪一個線程,並且與優先級無關;

(4)notityAll():喚醒全部處於等待狀態的線程,該方法並非將對象的鎖給全部線程,而是讓它們競爭,只有得到鎖的線程才能進入就緒狀態;

sleep() 和 wait() 有什麼區別?

二者均可以暫停線程的執行

  • 類的不一樣:sleep() 是 Thread線程類的靜態方法,wait() 是 Object類的方法。
  • 是否釋放鎖:sleep() 不釋放鎖;wait() 釋放鎖。
  • 用途不一樣:Wait 一般被用於線程間交互/通訊,sleep 一般被用於暫停執行。
  • 用法不一樣:wait() 方法被調用後,線程不會自動甦醒,須要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成後,線程會自動甦醒。或者可使用wait(long timeout)超時後線程會自動甦醒。

你是如何調用 wait() 方法的?使用 if 塊仍是循環?爲何?

處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出。

wait() 方法應該在循環調用,由於當線程獲取到 CPU 開始執行的時候,其餘條件可能尚未知足,因此在處理前,循環檢測條件是否知足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:

synchronized (monitor) {
    //  判斷條件謂詞是否獲得知足
    while(!locked) {
        //  等待喚醒
        monitor.wait();
    }
    //  處理其餘的業務邏輯
}
12345678

爲何線程通訊的方法 wait(), notify()和 notifyAll()被定義在 Object 類裏?

Java中,任何對象均可以做爲鎖,而且 wait(),notify()等方法用於等待對象的鎖或者喚醒線程,在 Java 的線程中並無可供任何對象使用的鎖,因此任意對象調用方法必定定義在Object類中。

wait(), notify()和 notifyAll()這些方法在同步代碼塊中調用

有的人會說,既然是線程放棄對象鎖,那也能夠把wait()定義在Thread類裏面啊,新定義的線程繼承於Thread類,也不須要從新定義wait()方法的實現。然而,這樣作有一個很是大的問題,一個線程徹底能夠持有不少鎖,你一個線程放棄鎖的時候,到底要放棄哪一個鎖?固然了,這種設計並非不能實現,只是管理起來更加複雜。

綜上所述,wait()、notify()和notifyAll()方法要定義在Object類中。

爲何 wait(), notify()和 notifyAll()必須在同步方法或者同步塊中被調用?

當一個線程須要調用對象的 wait()方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其餘線程調用這個對象上的 notify()方法。一樣的,當一個線程須要調用對象的 notify()方法時,它會釋放這個對象的鎖,以便其餘在等待的線程就能夠獲得這個對象鎖。因爲全部的這些方法都須要線程持有對象的鎖,這樣就只能經過同步來實現,因此他們只能在同步方法或者同步塊中被調用。

Thread 類中的 yield 方法有什麼做用?

使當前線程從執行狀態(運行狀態)變爲可執行態(就緒狀態)。

當前線程到了就緒狀態,那麼接下來哪一個線程會從就緒狀態變成執行狀態呢?多是當前線程,也多是其餘線程,看系統的分配了。

爲何 Thread 類的 sleep()和 yield ()方法是靜態的?

Thread 類的 sleep()和 yield()方法將在當前正在執行的線程上運行。因此在其餘處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲何這些方法是靜態的。它們能夠在當前正在執行的線程中工做,並避免程序員錯誤的認爲能夠在其餘非運行線程調用這些方法。

線程的 sleep()方法和 yield()方法有什麼區別?

(1) sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;

(2) 線程執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;

(3)sleep()方法聲明拋出 InterruptedException,而 yield()方法沒有聲明任何異常;

(4)sleep()方法比 yield()方法(跟操做系統 CPU 調度相關)具備更好的可移植性,一般不建議使用yield()方法來控制併發線程的執行。

如何中止一個正在運行的線程?

在java中有如下3種方法能夠終止正在運行的線程:

  1. 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。
  2. 使用stop方法強行終止,可是不推薦這個方法,由於stop和suspend及resume同樣都是過時做廢的方法。
  3. 使用interrupt方法中斷線程。

Java 中 interrupted 和 isInterrupted 方法的區別?

interrupt:用於中斷線程。調用該方法的線程的狀態爲將被置爲」中斷」狀態。

注意:線程中斷僅僅是置線程的中斷狀態位,不會中止線程。須要用戶本身去監視線程的狀態爲並作處理。支持線程中斷的方法(也就是線程中斷後會拋出interruptedException 的方法)就是在監視線程的中斷狀態,一旦線程的中斷狀態被置爲「中斷狀態」,就會拋出中斷異常。

interrupted:是靜態方法,查看當前中斷信號是true仍是false而且清除中斷信號。若是一個線程被中斷了,第一次調用 interrupted 則返回 true,第二次和後面的就返回 false 了。

isInterrupted:查看當前中斷信號是true仍是false

什麼是阻塞式方法?

阻塞式方法是指程序會一直等待該方法完成期間不作其餘事情,ServerSocket 的accept()方法就是一直等待客戶端鏈接。這裏的阻塞是指調用結果返回以前,當前線程會被掛起,直到獲得結果以後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。

Java 中你怎樣喚醒一個阻塞的線程?

首先 ,wait()、notify() 方法是針對對象的,調用任意對象的 wait()方法都將致使線程阻塞,阻塞的同時也將釋放該對象的鎖,相應地,調用任意對象的 notify()方法則將隨機解除該對象阻塞的線程,但它須要從新獲取該對象的鎖,直到獲取成功才能往下執行;

其次,wait、notify 方法必須在 synchronized 塊或方法中被調用,而且要保證同步塊或方法的鎖對象與調用 wait、notify 方法的對象是同一個,如此一來在調用 wait 以前當前線程就已經成功獲取某對象的鎖,執行 wait 阻塞後當前線程就將以前獲取的對象鎖釋放。

notify() 和 notifyAll() 有什麼區別?

若是線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。

notifyAll() 會喚醒全部的線程,notify() 只會喚醒一個線程。

notifyAll() 調用後,會將所有線程由等待池移到鎖池,而後參與鎖的競爭,競爭成功則繼續執行,若是不成功則留在鎖池等待鎖被釋放後再次參與競爭。而 notify()只會喚醒一個線程,具體喚醒哪個線程由虛擬機控制。

線程間通訊

線程之間的如何通信和協做?

能夠經過中斷 和 共享變量的方式實現線程間的通信和協做

好比說最經典的生產者-消費者模型:當隊列滿時,生產者須要等待隊列有空間才能繼續往裏面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即隊列)的佔用權。由於生產者若是不釋放對臨界資源的佔用權,那麼消費者就沒法消費隊列中的商品,就不會讓隊列有空間,那麼生產者就會一直無限等待下去。所以,通常狀況下,當隊列滿時,會讓生產者交出對臨界資源的佔用權,並進入掛起狀態。而後等待消費者消費了商品,而後消費者通知生產者隊列有空間了。一樣地,當隊列空時,消費者也必須等待,等待生產者通知它隊列中有商品了。這種互相通訊的過程就是線程間的協做。

Java中線程通訊協做的最多見的兩種方式:

一.syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()

二.ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()

線程間直接的數據交換:

三.經過管道進行線程間通訊:1)字節流;2)字符流

如何在兩個線程間共享數據?

在兩個線程間共享變量便可實現共享。

通常來講,共享變量要求變量自己是線程安全的,而後在線程內使用的時候,若是有對共享變量的複合操做,那麼也得保證複合操做的線程安全性。

同步方法和同步塊,哪一個是更好的選擇?

同步塊是更好的選擇,由於它不會鎖住整個對象(固然你也可讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這一般會致使他們中止執行並須要等待得到這個對象上的鎖。

同步塊更要符合開放調用的原則,只在須要鎖住的代碼塊鎖住相應的對象,這樣從側面來講也能夠避免死鎖。

請知道一條原則:同步的範圍越小越好。

什麼是線程同步和線程互斥,有哪幾種實現方式?

當一個線程對共享的數據進行操做時,應使之成爲一個」原子操做「,即在沒有完成相關操做以前,不容許其餘線程打斷它,不然,就會破壞數據的完整性,必然會獲得錯誤的處理結果,這就是線程的同步。

在多線程應用中,考慮不一樣線程之間的數據同步和防止死鎖。當兩個或多個線程之間同時等待對方釋放資源的時候就會造成線程之間的死鎖。爲了防止死鎖的發生,須要經過同步來實現線程安全。

線程互斥是指對於共享的進程系統資源,在各單個線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任什麼時候刻最多隻容許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。線程互斥能夠當作是一種特殊的線程同步。

線程間的同步方法大致可分爲兩類:用戶模式和內核模式。顧名思義,內核模式就是指利用系統內核對象的單一性來進行同步,使用時須要切換內核態與用戶態,而用戶模式就是不須要切換到內核態,只在用戶態完成操做。

用戶模式下的方法有:原子操做(例如一個單一的全局變量),臨界區。內核模式下的方法有:事件,信號量,互斥量。

實現線程同步的方法

  • 同步代碼方法:sychronized 關鍵字修飾的方法
  • 同步代碼塊:sychronized 關鍵字修飾的代碼塊
  • 使用特殊變量域volatile實現線程同步:volatile關鍵字爲域變量的訪問提供了一種免鎖機制
  • 使用重入鎖實現線程同步:reentrantlock類是可衝入、互斥、實現了lock接口的鎖他與sychronized方法具備相同的基本行爲和語義

在監視器(Monitor)內部,是如何作線程同步的?程序應該作哪一種級別的同步?

在 java 虛擬機中,每一個對象( Object 和 class )經過某種邏輯關聯監視器,每一個監視器和一個對象引用相關聯,爲了實現監視器的互斥功能,每一個對象都關聯着一把鎖。

一旦方法或者代碼塊被 synchronized 修飾,那麼這個部分就放入了監視器的監視區域,確保一次只能有一個線程執行該部分的代碼,線程在獲取鎖以前不容許執行該部分的代碼

另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案

線程安全

什麼叫線程安全?servlet 是線程安全嗎?

線程安全是編程中的術語,指某個方法在多線程環境中被調用時,可以正確地處理多個線程之間的共享變量,使程序功能正確完成。

Servlet 不是線程安全的,servlet 是單實例多線程的,當多個線程同時訪問同一個方法,是不能保證共享變量的線程安全性的。

Struts2 的 action 是多實例多線程的,是線程安全的,每一個請求過來都會 new 一個新的 action 分配給這個請求,請求完成後銷燬。

SpringMVC 的 Controller 是線程安全的嗎?不是的,和 Servlet 相似的處理流程。

Struts2 好處是不用考慮線程安全問題;Servlet 和 SpringMVC 須要考慮線程安全問題,可是性能能夠提高不用處理太多的 gc,可使用 ThreadLocal 來處理多線程的問題。

在 Java 程序中怎麼保證多線程的運行安全?

  • 方法一:使用安全類,好比 java.util.concurrent 下的類,使用原子類AtomicInteger
  • 方法二:使用自動鎖 synchronized。
  • 方法三:使用手動鎖 Lock。

手動鎖 Java 示例代碼以下:

Lock lock = new ReentrantLock();
lock. lock();
try {
    System. out. println("得到鎖");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System. out. println("釋放鎖");
    lock. unlock();
}
12345678910

你對線程優先級的理解是什麼?

每個線程都是有優先級的,通常來講,高優先級的線程在運行時會具備優先權,但這依賴於線程調度的實現,這個實現是和操做系統相關的(OS dependent)。咱們能夠定義線程的優先級,可是這並不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個 int 變量(從 1-10),1 表明最低優先級,10 表明最高優先級。

Java 的線程優先級調度會委託給操做系統去處理,因此與具體的操做系統優先級有關,如非特別須要,通常無需設置線程優先級。

線程類的構造方法、靜態塊是被哪一個線程調用的

這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 new這個線程類所在的線程所調用的,而 run 方法裏面的代碼纔是被線程自身所調用的。

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

(1)Thread2 的構造方法、靜態塊是 main 線程調用的,Thread2 的 run()方法是Thread2 本身調用的

(2)Thread1 的構造方法、靜態塊是 Thread2 調用的,Thread1 的 run()方法是Thread1 本身調用的

Java 中怎麼獲取一份線程 dump 文件?你如何在 Java 中獲取線程堆棧?

Dump文件是進程的內存鏡像。能夠把程序的執行狀態經過調試器保存到dump文件中。

在 Linux 下,你能夠經過命令 kill -3 PID (Java 進程的進程 ID)來獲取 Java應用的 dump 文件。

在 Windows 下,你能夠按下 Ctrl + Break 來獲取。這樣 JVM 就會將線程的 dump 文件打印到標準輸出或錯誤文件中,它可能打印在控制檯或者日誌文件中,具體位置依賴應用的配置。

一個線程運行時發生異常會怎樣?

若是異常沒有被捕獲該線程將會中止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常形成線程忽然中斷狀況的一個內嵌接口。當一個未捕獲異常將形成線程中斷的時候,JVM 會使用 Thread.getUncaughtExceptionHandler()來查詢線程的 UncaughtExceptionHandler 並將線程和異常做爲參數傳遞給 handler 的 uncaughtException()方法進行處理。

Java 線程數過多會形成什麼異常?

  • 線程的生命週期開銷很是高

  • 消耗過多的 CPU

    資源若是可運行的線程數量多於可用處理器的數量,那麼有線程將會被閒置。大量空閒的線程會佔用許多內存,給垃圾回收器帶來壓力,並且大量的線程在競爭 CPU資源時還將產生其餘性能的開銷。

  • 下降穩定性JVM

    在可建立線程的數量上存在一個限制,這個限制值將隨着平臺的不一樣而不一樣,而且承受着多個因素制約,包括 JVM 的啓動參數、Thread 構造函數中請求棧的大小,以及底層操做系統對線程的限制等。若是破壞了這些限制,那麼可能拋出OutOfMemoryError 異常。

synchronized 內置鎖

synchronized 的做用?

在 Java 中,synchronized 關鍵字是用來控制線程同步的,就是在多線程的環境下,控制 synchronized 代碼段不被多個線程同時執行。synchronized 能夠修飾類、方法、變量。

另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的,Java 的線程是映射到操做系統的原生線程之上的。若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對synchronized 較大優化,因此如今的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。

說說本身是怎麼使用 synchronized 關鍵字,在項目中用到了嗎

synchronized關鍵字最主要的三種使用方式:

  • 修飾實例方法: 做用於當前對象實例加鎖,進入同步代碼前要得到當前對象實例的鎖
  • 修飾靜態方法: 也就是給當前類加鎖,會做用於類的全部對象實例,由於靜態成員不屬於任何一個實例對象,是類成員( static 代表這是該類的一個靜態資源,無論new了多少個對象,只有一份)。因此若是一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B須要調用這個實例對象所屬類的靜態 synchronized 方法,是容許的,不會發生互斥現象,由於訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖
  • 修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。儘可能不要使用 synchronized(String a) 由於JVM中,字符串常量池具備緩存功能!

下面我以一個常見的面試題爲例講解一下 synchronized 關鍵字的具體使用。

面試中面試官常常會說:「單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單例模式的原理唄!」

雙重校驗鎖實現對象單例(線程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼
        if (uniqueInstance == null) {
            //類對象加鎖
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
1234567891011121314151617181920

另外,須要注意 uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要。

uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton(); 這段代碼實際上是分爲三步執行:

  1. 爲 uniqueInstance 分配內存空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的內存地址

可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。

說一下 synchronized 底層實現原理?

synchronized是Java中的一個關鍵字,在使用的過程當中並無看到顯示的加鎖和解鎖過程。所以有必要經過javap命令,查看相應的字節碼文件。

synchronized 同步語句塊的狀況

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代碼塊");
        }
    }
}
1234567

經過JDK 反彙編指令 javap -c -v SynchronizedDemo

synchronized關鍵字原理

能夠看出在執行同步代碼塊以前以後都有一個monitor字樣,其中前面的是monitorenter,後面的是離開monitorexit,不難想象一個線程也執行同步代碼塊,首先要獲取鎖,而獲取鎖的過程就是monitorenter ,在執行完代碼塊以後,要釋放鎖,釋放鎖就是執行monitorexit指令。

爲何會有兩個monitorexit呢?

這個主要是防止在同步代碼塊中線程因異常退出,而鎖沒有獲得釋放,這必然會形成死鎖(等待的線程永遠獲取不到鎖)。所以最後一個monitorexit是保證在異常狀況下,鎖也能夠獲得釋放,避免死鎖。
僅有ACC_SYNCHRONIZED這麼一個標誌,該標記代表線程進入該方法時,須要monitorenter,退出該方法時須要monitorexit。

synchronized可重入的原理

重入鎖是指一個線程獲取到該鎖以後,該線程能夠繼續得到該鎖。底層原理維護一個計數器,當線程獲取該鎖時,計數器加一,再次得到該鎖時繼續加一,釋放鎖時,計數器減一,當計數器值爲0時,代表該鎖未被任何線程所持有,其它線程能夠競爭獲取鎖。

什麼是自旋

不少 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界作忙循環,這就是自旋。若是作了屢次循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。

多線程中 synchronized 鎖升級的原理是什麼?

synchronized 鎖升級原理:在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,若是一致則能夠直接使用此對象,若是不一致,則升級偏向鎖爲輕量級鎖,經過自旋循環必定次數來獲取鎖,執行必定次數以後,若是尚未正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖,此過程就構成了 synchronized 鎖的升級。

鎖的升級的目的:鎖升級是爲了減低了鎖帶來的性能消耗。在 Java 6 以後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。

線程 B 怎麼知道線程 A 修改了變量

(1)volatile 修飾變量

(2)synchronized 修飾修改變量的方法

(3)wait/notify

(4)while 輪詢

當一個線程進入一個對象的 synchronized 方法 A 以後,其它線程是否可進入此對象的 synchronized 方法 B?

不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。由於非靜態方法上的 synchronized 修飾符要求執行方法時要得到對象的鎖,若是已經進入A 方法說明對象鎖已經被取走,那麼試圖進入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。

synchronized、volatile、CAS 比較

(1)synchronized 是悲觀鎖,屬於搶佔式,會引發其餘線程阻塞。

(2)volatile 提供多線程共享變量可見性和禁止指令重排序優化。

(3)CAS 是基於衝突檢測的樂觀鎖(非阻塞)

synchronized 和 Lock 有什麼區別?

  • 首先synchronized是Java內置關鍵字,在JVM層面,Lock是個Java類;
  • synchronized 能夠給類、方法、代碼塊加鎖;而 lock 只能給代碼塊加鎖。
  • synchronized 不須要手動獲取鎖和釋放鎖,使用簡單,發生異常會自動釋放鎖,不會形成死鎖;而 lock 須要本身加鎖和釋放鎖,若是使用不當沒有 unLock()去釋放鎖就會形成死鎖。
  • 經過 Lock 能夠知道有沒有成功獲取鎖,而 synchronized 卻沒法辦到。

synchronized 和 ReentrantLock 區別是什麼?

synchronized 是和 if、else、for、while 同樣的關鍵字,ReentrantLock 是類,這是兩者的本質區別。既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量

synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大,可是在 Java 6 中對 synchronized 進行了很是多的改進。

相同點:二者都是可重入鎖

二者都是可重入鎖。「可重入鎖」概念是:本身能夠再次獲取本身的內部鎖。好比一個線程得到了某個對象的鎖,此時這個對象鎖尚未釋放,當其再次想要獲取這個對象的鎖的時候仍是能夠獲取的,若是不可鎖重入的話,就會形成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。

主要區別以下:

  • ReentrantLock 使用起來比較靈活,可是必須有釋放鎖的配合動做;
  • ReentrantLock 必須手動獲取與釋放鎖,而 synchronized 不須要手動釋放和開啓鎖;
  • ReentrantLock 只適用於代碼塊鎖,而 synchronized 能夠修飾類、方法、變量等。
  • 兩者的鎖機制其實也是不同的。ReentrantLock 底層調用的是 Unsafe 的park 方法加鎖,synchronized 操做的應該是對象頭中 mark word

Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:

  • 普通同步方法,鎖是當前實例對象

  • 靜態同步方法,鎖是當前類的class對象

  • 同步方法塊,鎖是括號裏面的對象

多線程鎖的升級(膨脹)原理是什麼?

在Java中,鎖共有4種狀態,級別從低到高依次爲:無狀態鎖,偏向鎖,輕量級鎖和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級。

死鎖

什麼是線程死鎖

百度百科:死鎖是指兩個或兩個以上的進程(線程)在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱爲死鎖進程(線程)。

多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止。

以下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。

線程死鎖

下面經過一個例子來講明線程死鎖,代碼模擬了上圖的死鎖的狀況 (代碼來源於《併發編程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}
123456789101112131415161718192021222324252627282930313233343536

輸出結果

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1
1234

線程 A 經過 synchronized (resource1) 得到 resource1 的監視器鎖,而後經過Thread.sleep(1000);讓線程 A 休眠 1s 爲的是讓線程 B 獲得CPU執行權,而後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

造成死鎖的四個必要條件是什麼

產生死鎖的必要條件:

一、互斥條件:所謂互斥就是進程在某一時間內獨佔資源。

二、請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。

三、不剝奪條件:進程已得到資源,在末使用完以前,不能強行剝奪。

四、循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。

這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之 一不知足,就不會發生死鎖。

理解了死鎖的緣由,尤爲是產生死鎖的四個必要條件,就能夠最大可能地避免、預防和 解除死鎖。

防止死鎖能夠採用如下的方法:

  • 儘可能使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),設置超時時間,超時能夠退出防止死鎖。
  • 儘可能使用 Java. util. concurrent 併發類代替本身手寫鎖。
  • 儘可能下降鎖的使用粒度,儘可能不要幾個功能用同一把鎖。
  • 儘可能減小同步的代碼塊。

如何避免線程死鎖

咱們只要破壞產生死鎖的四個條件中的其中一個就能夠了。

破壞互斥條件

這個條件咱們沒有辦法破壞,由於咱們用鎖原本就是想讓他們互斥的(臨界資源須要互斥訪問)。

破壞請求與保持條件

一次性申請全部的資源。

破壞不剝奪條件

佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源。

破壞循環等待條件

靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

咱們對線程 2 的代碼修改爲下面這樣就不會產生死鎖了。

new Thread(() -> {
    synchronized (resource1) {
        System.out.println(Thread.currentThread() + "get resource1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread() + "waiting get resource2");
        synchronized (resource2) {
            System.out.println(Thread.currentThread() + "get resource2");
        }
    }
}, "線程 2").start();
1234567891011121314

輸出結果

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2
123456

咱們分析一下上面的代碼爲何避免了死鎖的發生?

線程 1 首先得到到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。而後線程 1 再去獲取 resource2 的監視器鎖,能夠獲取到。而後線程 1 釋放了對 resource一、resource2 的監視器鎖的佔用,線程 2 獲取到就能夠執行了。這樣就破壞了破壞循環等待條件,所以避免了死鎖。

死鎖與活鎖的區別,死鎖與飢餓的區別?

死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。

活鎖:任務或者執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。

活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,這就是所謂的「活」, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。

飢餓:一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態。

Java 中致使飢餓的緣由:

一、高優先級線程吞噬全部的低優先級線程的 CPU 時間。

二、線程被永久堵塞在一個等待進入同步塊的狀態,由於其餘線程老是能在它以前持續地對該同步塊進行訪問。

三、線程在等待一個自己也處於永久等待完成的對象(好比調用這個對象的 wait 方法),由於其餘線程老是被持續地得到喚醒。

線程池

Executors類建立四種常見線程池

什麼是線程池?有哪幾種建立方式?

池化技術相比你們已經家常便飯了,線程池、數據庫鏈接池、Http 鏈接池等等都是對這個思想的應用。池化技術的思想主要是爲了減小每次獲取資源的消耗,提升對資源的利用率。

在面向對象編程中,建立和銷燬對象是很費時間的,由於建立一個對象要獲取內存資源或者其它更多資源。在 Java 中更是如此,虛擬機將試圖跟蹤每個對象,以便可以在對象銷燬後進行垃圾回收。因此提升服務程序效率的一個手段就是儘量減小建立和銷燬對象的次數,特別是一些很耗資源的對象建立和銷燬,這就是」池化資源」技術產生的緣由。

線程池顧名思義就是事先建立若干個可執行的線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。Java 5+中的 Executor 接口定義一個執行線程的工具。它的子類型即線程池接口是 ExecutorService。要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,所以在工具類 Executors 面提供了一些靜態工廠方法,生成一些經常使用的線程池,以下所示:

(1)newSingleThreadExecutor:建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。

(2)newFixedThreadPool:建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。若是但願在服務器上使用線程池,建議使用 newFixedThreadPool方法來建立線程池,這樣能得到更好的性能。

(3) newCachedThreadPool:建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60 秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說 JVM)可以建立的最大線程大小。

(4)newScheduledThreadPool:建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

線程池有什麼優勢?

  • 下降資源消耗:重用存在的線程,減小對象建立銷燬的開銷。
  • 提升響應速度。可有效的控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。
  • 提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。
  • 附加功能:提供定時執行、按期執行、單線程、併發數控制等功能。

綜上所述使用線程池框架 Executor 能更好的管理線程、提供系統資源使用率。

線程池都有哪些狀態?

  • RUNNING:這是最正常的狀態,接受新的任務,處理等待隊列中的任務。
  • SHUTDOWN:不接受新的任務提交,可是會繼續處理等待隊列中的任務。
  • STOP:不接受新的任務提交,再也不處理等待隊列中的任務,中斷正在執行任務的線程。
  • TIDYING:全部的任務都銷燬了,workCount 爲 0,線程池的狀態在轉換爲 TIDYING 狀態時,會執行鉤子方法 terminated()。
  • TERMINATED:terminated()方法結束後,線程池的狀態就會變成這個。

什麼是 Executor 框架?爲何使用 Executor 框架?

Executor 框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。

每次執行任務建立線程 new Thread()比較消耗性能,建立一個線程是比較耗時、耗資源的,並且無限制的建立線程會引發應用程序內存溢出。

因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。利用Executors 框架能夠很是方便的建立一個線程池。

在 Java 中 Executor 和 Executors 的區別?

  • Executors 工具類的不一樣方法按照咱們的需求建立了不一樣的線程池,來知足業務的需求。
  • Executor 接口對象能執行咱們的線程任務。
  • ExecutorService 接口繼承了 Executor 接口並進行了擴展,提供了更多的方法咱們能得到任務執行的狀態而且能夠獲取任務的返回值。
  • 使用 ThreadPoolExecutor 能夠建立自定義線程池。
  • Future 表示異步計算的結果,他提供了檢查計算是否完成的方法,以等待計算的完成,並可使用 get()方法獲取計算的結果。

線程池中 submit() 和 execute() 方法有什麼區別?

接收參數:execute()只能執行 Runnable 類型的任務。submit()能夠執行 Runnable 和 Callable 類型的任務。

返回值:submit()方法能夠返回持有計算結果的 Future 對象,而execute()沒有

異常處理:submit()方便Exception處理

什麼是線程組,爲何在 Java 中不推薦使用?

ThreadGroup 類,能夠把線程歸屬到某一個線程組中,線程組中能夠有線程對象,也能夠有線程組,組中還能夠有線程,這樣的組織結構有點相似於樹的形式。

線程組和線程池是兩個不一樣的概念,他們的做用徹底不一樣,前者是爲了方便線程的管理,後者是爲了管理線程的生命週期,複用線程,減小建立銷燬線程的開銷。

爲何不推薦使用線程組?由於使用有不少的安全隱患吧,沒有具體追究,若是須要使用,推薦使用線程池。

線程池之ThreadPoolExecutor詳解

Executors和ThreaPoolExecutor建立線程池的區別

《阿里巴巴Java開發手冊》中強制線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險

Executors 各個方法的弊端:

  • newFixedThreadPool 和 newSingleThreadExecutor:
    主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要問題是線程數最大數是 Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至 OOM。

ThreaPoolExecutor建立線程池方式只有一種,就是走它的構造函數,參數本身指定

你知道怎麼建立線程池嗎?

建立線程池的方式有多種,這裏你只須要答 ThreadPoolExecutor 便可。

ThreadPoolExecutor() 是最原始的線程池建立,也是阿里巴巴 Java 開發手冊中明確規範的建立線程池的方式。

ThreadPoolExecutor構造函數重要參數分析

ThreadPoolExecutor 3 個最重要的參數:

  • corePoolSize :核心線程數,線程數定義了最小能夠同時運行的線程數量。
  • maximumPoolSize :線程池中容許存在的工做線程的最大數量
  • workQueue:當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,若是達到的話,任務就會被存放在隊列中。

ThreadPoolExecutor其餘常見參數:

  1. keepAliveTime:線程池中的線程數量大於 corePoolSize 的時候,若是這時沒有新的任務提交,核心線程外的線程不會當即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime纔會被回收銷燬;
  2. unitkeepAliveTime 參數的時間單位。
  3. threadFactory:爲線程池提供建立新線程的線程工廠
  4. handler :線程池任務隊列超過 maxinumPoolSize 以後的拒絕策略

ThreadPoolExecutor飽和策略

ThreadPoolExecutor 飽和策略定義:

若是當前同時運行的線程數量達到最大線程數量而且隊列也已經被放滿了任時,ThreadPoolTaskExecutor 定義一些策略:

  • ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務的處理。
  • ThreadPoolExecutor.CallerRunsPolicy:調用執行本身的線程運行任務。您不會任務請求。可是這種策略會下降對於新任務提交速度,影響程序的總體性能。另外,這個策略喜歡增長隊列容量。若是您的應用程序能夠承受此延遲而且你不能任務丟棄任何一個任務請求的話,你能夠選擇這個策略。
  • ThreadPoolExecutor.DiscardPolicy:不處理新任務,直接丟棄掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略將丟棄最先的未處理的任務請求。

舉個例子: Spring 經過 ThreadPoolTaskExecutor 或者咱們直接經過 ThreadPoolExecutor 的構造函數建立線程池的時候,當咱們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy。在默認狀況下,ThreadPoolExecutor 將拋出 RejectedExecutionException 來拒絕新來的任務 ,這表明你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略爲咱們提供可伸縮隊列。(這個直接查看 ThreadPoolExecutor 的構造函數源碼就能夠看出,比較簡單的緣由,這裏就不貼代碼了)

一個簡單的線程池Demo:Runnable+ThreadPoolExecutor

線程池實現原理

圖解線程池實現原理

爲了讓你們更清楚上面的面試題中的一些概念,我寫了一個簡單的線程池 Demo。

首先建立一個 Runnable 接口的實現類(固然也能夠是 Callable 接口,咱們上面也說了二者的區別。)

import java.util.Date;

/**
 * 這是一個簡單的Runnable類,須要大約5秒鐘來執行其任務。
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}
123456789101112131415161718192021222324252627282930313233

編寫測試程序,咱們這裏以阿里巴巴推薦的使用 ThreadPoolExecutor 構造函數自定義參數的方式來建立線程池。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推薦的建立線程池的方式
        //經過ThreadPoolExecutor構造函數自定義參數建立
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //建立WorkerThread對象(WorkerThread類實現了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //執行Runnable
            executor.execute(worker);
        }
        //終止線程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}
1234567891011121314151617181920212223242526272829303132333435

能夠看到咱們上面的代碼指定了:

  1. corePoolSize: 核心線程數爲 5。
  2. maximumPoolSize :最大線程數 10
  3. keepAliveTime : 等待時間爲 1L。
  4. unit: 等待時間的單位爲 TimeUnit.SECONDS。
  5. workQueue:任務隊列爲 ArrayBlockingQueue,而且容量爲 100;
  6. handler:飽和策略爲 CallerRunsPolicy

Output:

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
1234567891011121314151617181920

若是你提交任務時,線程池隊列已滿,這時會發生什麼

這裏區分一下:

(1)若是使用的是無界隊列 LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於 LinkedBlockingQueue 能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務

(2)若是使用的是有界隊列好比 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue 繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy

ThreadLocal 面試題

ThreadLocal 是什麼?有哪些使用場景?

ThreadLocal 是一個本地線程副本變量工具類,在每一個線程中都建立了一個 ThreadLocalMap 對象,簡單說 ThreadLocal 就是一種以空間換時間的作法,每一個線程能夠訪問本身內部 ThreadLocalMap 對象內的 value。經過這種方式,避免資源在多線程間共享。

原理:線程局部變量是侷限於線程內部的變量,屬於線程自身全部,不在多個線程間共享。Java提供ThreadLocal類來支持線程局部變量,是一種實現線程安全的方式。可是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別當心,在這種狀況下,工做線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工做完成後沒有釋放,Java 應用就存在內存泄露的風險。

經典的使用場景是爲每一個線程分配一個 JDBC 鏈接 Connection。這樣就能夠保證每一個線程的都在各自的 Connection 上進行數據庫的操做,不會出現 A 線程關了 B線程正在使用的 Connection; 還有 Session 管理 等問題。

ThreadLocal 使用例子:

public class TestThreadLocal {
    
    //線程本地存儲變量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//啓動三個線程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
    
    /**
     * 線程本地存儲變量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i <5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
    
}
123456789101112131415161718192021222324252627282930313233343536

打印結果:啓動了 3 個線程,每一個線程最後都打印到 「ThreadLocal num=5」,而不是 num 一直在累加直到值等於 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5
123456789101112131415

什麼是線程局部變量?

線程局部變量是侷限於線程內部的變量,屬於線程自身全部,不在多個線程間共享。Java 提供 ThreadLocal 類來支持線程局部變量,是一種實現線程安全的方式。可是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別當心,在這種狀況下,工做線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工做完成後沒有釋放,Java 應用就存在內存泄露的風險。

ThreadLocal內存泄漏分析與解決方案

ThreadLocal形成內存泄漏的緣由?

ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,而 value 是強引用。因此,若是 ThreadLocal 沒有被外部強引用的狀況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如咱們不作任何措施的話,value 永遠沒法被GC 回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種狀況,在調用 set()get()remove() 方法的時候,會清理掉 key 爲 null 的記錄。使用完 ThreadLocal方法後 最好手動調用remove()方法

ThreadLocal內存泄漏解決方案?

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
  • 在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。

瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備


回到◀瘋狂創客圈

瘋狂創客圈 - Java高併發研習社羣,爲你們開啓大廠之門

相關文章
相關標籤/搜索