Java併發編程(總結最全面的面試題!!!)

Java面試總結匯總,整理了包括Java重點知識,以及經常使用開源框架,歡迎你們閱讀。文章可能有錯誤的地方,由於我的知識有限,歡迎各位大佬指出!文章持續更新中......java

ID 標題 地址
1 設計模式面試題(總結最全面的面試題) juejin.cn/post/684490…
2 Java基礎知識面試題(總結最全面的面試題) juejin.cn/post/684490…
3 Java集合面試題(總結最全面的面試題) juejin.cn/post/684490…
4 JavaIO、BIO、NIO、AIO、Netty面試題(總結最全面的面試題) juejin.cn/post/684490…
5 Java併發編程面試題(總結最全面的面試題) juejin.cn/post/684490…
6 Java異常面試題(總結最全面的面試題) juejin.cn/post/684490…
7 Java虛擬機(JVM)面試題(總結最全面的面試題) juejin.cn/post/684490…
8 Spring面試題(總結最全面的面試題) juejin.cn/post/684490…
9 Spring MVC面試題(總結最全面的面試題) juejin.cn/post/684490…
10 Spring Boot面試題(總結最全面的面試題) juejin.cn/post/684490…
11 Spring Cloud面試題(總結最全面的面試題) juejin.cn/post/684490…
12 Redis面試題(總結最全面的面試題) juejin.cn/post/684490…
13 MyBatis面試題(總結最全面的面試題) juejin.cn/post/684490…
14 MySQL面試題(總結最全面的面試題) juejin.cn/post/684490…
15 TCP、UDP、Socket、HTTP面試題(總結最全面的面試題) juejin.cn/post/684490…
16 Nginx面試題(總結最全面的面試題) juejin.cn/post/684490…
17 ElasticSearch面試題
18 kafka面試題
19 RabbitMQ面試題(總結最全面的面試題) juejin.cn/post/684490…
20 Dubbo面試題(總結最全面的面試題) juejin.cn/post/684490…
21 ZooKeeper面試題(總結最全面的面試題) juejin.cn/post/684490…
22 Netty面試題(總結最全面的面試題)
23 Tomcat面試題(總結最全面的面試題) juejin.cn/post/684490…
24 Linux面試題(總結最全面的面試題) juejin.cn/post/684490…
25 互聯網相關面試題(總結最全面的面試題)
26 互聯網安全面試題(總結最全面的面試題)

基礎知識

爲何要使用併發編程

  • 提高多核CPU的利用率:通常來講一臺主機上的會有多個CPU核心,咱們能夠建立多個線程,理論上講操做系統能夠將多個線程分配給不一樣的CPU去執行,每一個CPU執行一個線程,這樣就提升了CPU的使用效率,若是使用單線程就只能有一個CPU核心被使用。linux

  • 好比當咱們在網上購物時,爲了提高響應速度,須要拆分,減庫存,生成訂單等等這些操做,就能夠進行拆分利用多線程的技術完成。面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分 。程序員

  • 簡單來講就是:面試

    • 充分利用多核CPU的計算能力;
    • 方便進行業務拆分,提高應用性能

多線程應用場景

  • 例如: 迅雷多線程下載、數據庫鏈接池、分批發送短信等。

併發編程有什麼缺點

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

併發編程三個必要因素是什麼?

  • 原子性:原子,即一個不可再被分割的顆粒。原子性指的是一個或多個操做要麼所有執行成功要麼所有執行失敗。
  • 可見性:一個線程對共享變量的修改,另外一個線程可以馬上看到。(synchronized,volatile)
  • 有序性:程序執行的順序按照代碼的前後順序執行。(處理器可能會對指令進行重排序)

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

  • 出現線程安全問題的緣由通常都是三個緣由:算法

    • 線程切換帶來的原子性問題 解決辦法:使用多線程之間同步synchronized或使用鎖(lock)。數據庫

    • 緩存致使的可見性問題 解決辦法:synchronized、volatile、LOCK,能夠解決可見性問題編程

    • 編譯優化帶來的有序性問題 解決辦法:Happens-Before 規則能夠解決有序性問題windows

並行和併發有什麼區別?

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

作一個形象的比喻:設計模式

  • 併發 = 倆我的用一臺電腦。數組

  • 並行 = 倆我的分配了倆臺電腦。

  • 串行 = 倆我的排隊使用一臺電腦。

什麼是多線程

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

多線程的好處

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

多線程的劣勢:

  • 線程也是程序,因此線程須要佔用內存,線程越多佔用內存也越多;

  • 多線程須要協調和管理,因此須要 CPU 時間跟蹤線程;

  • 線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。

線程和進程區別

  • 什麼是線程和進程?

    • 進程

      一個在內存中運行的應用程序。 每一個正在系統上運行的程序都是一個進程

    • 線程

      進程中的一個執行任務(控制單元), 它負責在程序裏獨立執行。

一個進程至少有一個線程,一個進程能夠運行多個線程,多個線程可共享數據。

  • 進程與線程的區別

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

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

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

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

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

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

什麼是上下文切換?

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

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

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

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

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

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

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

  • windows上面用任務管理器看,linux下能夠用 top 這個工具看。
    • 找出cpu耗用厲害的進程pid, 終端執行top命令,而後按下shift+p (shift+m是找出消耗內存最高)查找出cpu利用最厲害的pid號
    • 根據上面第一步拿到的pid號,top -H -p pid 。而後按下shift+p,查找出cpu利用率最厲害的線程號,好比top -H -p 1328
    • 將獲取到的線程號轉換成16進制,去百度轉換一下就行
    • 使用jstack工具將進程信息打印輸出,jstack pid號 > /tmp/t.dat,好比jstack 31365 > /tmp/t.dat
    • 編輯/tmp/t.dat文件,查找線程號對應的信息

或者直接使用JDK自帶的工具查看「jconsole」 、「visualVm」,這都是JDK自帶的,能夠直接在JDK的bin目錄下找到直接使用

什麼是線程死鎖

  • 死鎖是指兩個或兩個以上的進程(線程)在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱爲死鎖進程(線程)。
  • 多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止。
  • 以下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。

在這裏插入圖片描述

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

  • 互斥條件:在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,就只能等待,直至佔有資源的進程用畢釋放。
  • 佔有且等待條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。
  • 不可搶佔條件:別人已經佔有了某項資源,你不能由於本身也須要該資源,就去把別人的資源搶過來。
  • 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。(好比一個進程集合,A在等B,B在等C,C在等A)

如何避免線程死鎖

  1. 避免一個線程同時得到多個鎖
  2. 避免一個線程在鎖內同時佔用多個資源,儘可能保證每一個鎖只佔用一個資源
  3. 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制

建立線程的四種方式

  • 繼承 Thread 類;

    public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在執行...");
    }
    複製代碼
  • 實現 Runnable 接口;

    public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法執行中...");
    }
    複製代碼
  • 實現 Callable 接口;

    public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法執行中...");
        return 1;
    }
    複製代碼
  • 使用匿名內部類方式

    public class CreateRunnable {
        public static void main(String[] args) {
            //建立多線程建立開始
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("i:" + i);
                    }
                }
            });
            thread.start();
        }
    }
    複製代碼

說一下 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 也能夠放入線程池中。

線程的狀態

在這裏插入圖片描述

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

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

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

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

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

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

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

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

    • 分時調度模型是指讓全部的線程輪流得到 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();
    }
    // 處理其餘的業務邏輯
}
複製代碼

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

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

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

爲何 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種方法能夠終止正在運行的線程:
    • 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。
    • 使用stop方法強行終止,可是不推薦這個方法,由於stop和suspend及resume同樣都是過時做廢的方法。
    • 使用interrupt方法中斷線程。

Java 中 interrupted 和 isInterrupted 方法的區別?

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

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

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

  • isInterrupted:是能夠返回當前中斷信號是true仍是false,與interrupt最大的差異

什麼是阻塞式方法?

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

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

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

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

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

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

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

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

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

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

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

Java 如何實現多線程之間的通信和協做?

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

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

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

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

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

  • 線程間直接的數據交換:

    • 三.經過管道進行線程間通訊:字節流、字符流

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

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

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

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

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

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

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

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

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

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

  • 實現線程同步的方法

    • 同步代碼方法:sychronized 關鍵字修飾的方法

    • 同步代碼塊:sychronized 關鍵字修飾的代碼塊

    • 使用特殊變量域volatile實現線程同步:volatile關鍵字爲域變量的訪問提供了一種免鎖機制

    • 使用重入鎖實現線程同步:reentrantlock類是可衝入、互斥、實現了lock接口的鎖他與sychronized方法具備相同的基本行爲和語義

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

  • 在 java 虛擬機中,監視器和鎖在Java虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每個監視器都和一個對象引用相關聯。線程在獲取鎖以前不容許執行同步代碼。

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

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

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

  • 有倆種可能:

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

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

什麼叫線程安全?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();
    }
    複製代碼

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

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

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

  • 固然,若是你真的想設置優先級能夠經過setPriority()方法設置,可是設置了不必定會該變,這個是不許確的

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

  • 這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 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 異常。

多線程的經常使用方法

方法 名 描述
sleep() 強迫一個線程睡眠N毫秒
isAlive() 判斷一個線程是否存活。
join() 等待線程終止。
activeCount() 程序中活躍的線程數。
enumerate() 枚舉程序中的線程。
currentThread() 獲得當前線程。
isDaemon() 一個線程是否爲守護線程。
setDaemon() 設置一個線程爲守護線程。
setName() 爲線程設置一個名稱。
wait() 強迫一個線程等待。
notify() 通知一個線程繼續運行。
setPriority() 設置一個線程的優先級。

併發理論

Java中垃圾回收有什麼目的?何時進行垃圾回收?

  • 垃圾回收是在內存中存在沒有引用的對象或超過做用域的對象時進行的。

  • 垃圾回收的目的是識別而且丟棄應用再也不使用的對象來釋放和重用資源。

線程之間如何通訊及線程之間如何同步

  • 在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊及線程之間如何同步。通訊是指線程之間以如何來交換信息。通常線程之間的通訊機制有兩種:共享內存和消息傳遞。

  • Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。

Java內存模型

  • 共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。

在這裏插入圖片描述

  • 從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:
    1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
    2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。

下面經過示意圖來講明線程之間的通訊

在這裏插入圖片描述

  • 總結:什麼是Java內存模型:java內存模型簡稱jmm,定義了一個線程對另外一個線程可見。共享變量存放在主內存中,每一個線程都有本身的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,因此就會發生線程安全問題。

若是對象的引用被置爲null,垃圾收集器是否會當即釋放對象佔用的內存?

  • 不會,在下一個垃圾回調週期中,這個對象將是被可回收的。

  • 也就是說並不會當即被垃圾收集器馬上回收,而是在下一次垃圾回收時纔會釋放其佔用的內存。

finalize()方法何時被調用?析構函數(finalization)的目的是什麼?

  • 1.垃圾回收器(garbage colector)決定回收某對象時,就會運行該對象的finalize()方法; finalize是Object類的一個方法,該方法在Object類中的聲明protected void finalize() throws Throwable { } 在垃圾回收器執行時會調用被回收對象的finalize()方法,能夠覆蓋此方法來實現對其資源的回收。注意:一旦垃圾回收器準備釋放對象佔用的內存,將首先調用該對象的finalize()方法,而且下一次垃圾回收動做發生時,才真正回收對象佔用的內存空間

    1. GC原本就是內存回收了,應用還須要在finalization作什麼呢? 答案是大部分時候,什麼都不用作(也就是不須要重載)。只有在某些很特殊的狀況下,好比你調用了一些native的方法(通常是C寫的),能夠要在finaliztion裏去調用C的釋放函數。
      • Finalizetion主要用來釋放被對象佔用的資源(不是指內存,而是指其餘資源,好比文件(File Handle)、端口(ports)、數據庫鏈接(DB Connection)等)。然而,它不能真正有效地工做。

什麼是重排序

  • 程序執行的順序按照代碼的前後順序執行。
  • 通常來講處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,進行從新排序(重排序),它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
int a = 5;    //語句1
int r = 3;    //語句2
a = a + 2;    //語句3
r = a*a;      //語句4
複製代碼
  • 則由於重排序,他還可能執行順序爲(這裏標註的是語句的執行順序) 2-1-3-4,1-3-2-4 但毫不可能 2-1-4-3,由於這打破了依賴關係。
  • 顯然重排序對單線程運行是不會有任何問題,可是多線程就不必定了,因此咱們在多線程編程時就得考慮這個問題了。

重排序實際執行的指令步驟

在這裏插入圖片描述

  1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
  • 這些重排序對於單線程沒問題,可是多線程均可能會致使多線程程序出現內存可見性問題。

重排序遵照的規則

  • as-if-serial:
    1. 無論怎麼排序,結果不能改變
    2. 不存在數據依賴的能夠被編譯器和處理器重排序
    3. 一個操做依賴兩個操做,這兩個操做若是不存在依賴能夠重排序
    4. 單線程根據此規則不會有問題,可是重排序後多線程會有問題

as-if-serial規則和happens-before規則的區別

  • as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。

  • as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。

  • as-if-serial語義和happens-before這麼作的目的,都是爲了在不改變程序執行結果的前提下,儘量地提升程序執行的並行度。

併發關鍵字 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中,字符串常量池具備緩存功能!

單例模式瞭解嗎?給我解釋一下雙重檢驗鎖方式實現單例模式!」

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

說明:

  • 雙鎖機制的出現是爲了解決前面同步問題和性能問題,看下面的代碼,簡單分析下確實是解決了多線程並行進來不會出現重複new對象,並且也實現了懶加載

    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;
      }
    複製代碼

    }

另外,須要注意 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的語義底層是經過一個monitor(監視器鎖)的對象來完成,

  • 每一個對象有一個監視器鎖(monitor)。每一個Synchronized修飾過的代碼當它的monitor被佔用時就會處於鎖定狀態而且嘗試獲取monitor的全部權 ,過程:

    一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。

    二、若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.

    三、若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。

synchronized是能夠經過 反彙編指令 javap命令,查看相應的字節碼文件。

synchronized可重入的原理

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

什麼是自旋

  • 不少 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界作忙循環,這就是自旋。若是作了屢次循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。
  • 忙循環:就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麼作的目的是爲了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可能會在另外一個內核運行,這樣會重建緩存。爲了不重建緩存和減小等待重建的時間就能夠使用它了。

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

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

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

  • 偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,減小加鎖/解鎖的一些CAS操做(好比等待隊列的一些CAS操做),這種狀況下,就會給線程加一個偏向鎖。 若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。

  • 輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,輕量級鎖就會升級爲重量級鎖;

  • 重量級鎖是synchronized ,是 Java 虛擬機中最爲基礎的鎖實現。在這種狀態下,Java 虛擬機會阻塞加鎖失敗的線程,而且在目標鎖被釋放的時候,喚醒這些線程。

線程 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對象
    • 同步方法塊,鎖是括號裏面的對象

volatile 關鍵字的做用

  • 對於可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其餘線程是可見的。當一個共享變量被 volatile 修飾時,它會保證修改的值會當即被更新到主內存中,當有其餘線程須要讀取時,它會去內存中讀取新值。

  • 從實踐角度而言,volatile 的一個重要做用就是和 CAS 結合,保證了原子性,詳細的能夠參見 java.util.concurrent.atomic 包下的類,好比 AtomicInteger。

  • volatile 經常使用於多線程環境下的單次操做(單次讀或者單次寫)。

Java 中能建立 volatile 數組嗎?

  • 能,Java 中能夠建立 volatile 類型數組,不過只是一個指向數組的引用,而不是整個數組。意思是,若是改變引用指向的數組,將會受到 volatile 的保護,可是若是多個線程同時改變數組的元素,volatile 標示符就不能起到以前的保護做用了。

volatile 變量和 atomic 變量有什麼不一樣?

  • volatile 變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用 volatile 修飾 count 變量,那麼 count++ 操做就不是原子性的。

  • 而 AtomicInteger 類提供的 atomic 方法可讓這種操做具備原子性如getAndIncrement()方法會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。

volatile 能使得一個非原子操做變成原子操做嗎?

  • 關鍵字volatile的主要做用是使變量在多個線程間可見,但沒法保證原子性,對於多個線程訪問同一個實例變量須要加鎖進行同步。

  • 雖然volatile只能保證可見性不能保證原子性,但用volatile修飾long和double能夠保證其操做原子性。

因此從Oracle Java Spec裏面能夠看到:

  • 對於64位的long和double,若是沒有被volatile修飾,那麼對其操做能夠不是原子的。在操做的時候,能夠分紅兩步,每次對32位操做。
  • 若是使用volatile修飾long和double,那麼其讀寫都是原子操做
  • 對於64位的引用地址的讀寫,都是原子操做
  • 在實現JVM時,能夠自由選擇是否把讀寫long和double做爲原子操做
  • 推薦JVM實現爲原子操做

synchronized 和 volatile 的區別是什麼?

  • synchronized 表示只有一個線程能夠獲取做用對象的鎖,執行代碼,阻塞其餘線程。

  • volatile 表示變量在 CPU 的寄存器中是不肯定的,必須從主存中讀取。保證多線程環境下變量的可見性;禁止指令重排序。

區別

  • volatile 是變量修飾符;synchronized 能夠修飾類、方法、變量。

  • volatile 僅能實現變量的修改可見性,不能保證原子性;而 synchronized 則能夠保證變量的修改可見性和原子性。

  • volatile 不會形成線程的阻塞;synchronized 可能會形成線程的阻塞。

  • volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。

  • volatile關鍵字是線程同步的輕量級實現,因此volatile性能確定比synchronized關鍵字要好。可是volatile關鍵字只能用於變量而synchronized關鍵字能夠修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後執行效率有了顯著提高,實際開發中使用 synchronized 關鍵字的場景仍是更多一些。

final不可變對象,它對寫併發應用有什麼幫助?

  • 不可變對象(Immutable Objects)即對象一旦被建立它的狀態(對象的數據,也即對象屬性值)就不能改變,反之即爲可變對象(Mutable Objects)。

  • 不可變對象的類即爲不可變類(Immutable Class)。Java 平臺類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。

  • 只有知足以下狀態,一個對象纔是不可變的;

    • 它的狀態不能在建立後再被修改;

    • 全部域都是 final 類型;而且,它被正確建立(建立期間沒有發生 this 引用的逸出)。

不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。

Lock 接口和synchronized 對比同步它有什麼優點?

  • Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。

  • 它的優點有:

    • (1)能夠使鎖更公平

    • (2)能夠使線程在等待鎖的時候響應中斷

    • (3)可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間

    • (4)能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖

  • 總體上來講 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件隊列的(newCondition 方法)鎖操做。另外 Lock 的實現類基本都支持非公平鎖(默認)和公平鎖,synchronized 只支持非公平鎖,固然,在大部分狀況下,非公平鎖是高效的選擇。

樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?

  • 悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比 Java 裏面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。

  • 樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,能夠使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫提供的相似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

什麼是 CAS

  • CAS 是 compare and swap 的縮寫,即咱們所說的比較交換。

  • cas 是一種基於鎖的操做,並且是樂觀鎖。在 java 中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,好比經過給記錄加 version 來獲取數據,性能較悲觀鎖有很大的提升。

  • CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和 A 的值是同樣的,那麼就將內存裏面的值更新成 B。CAS是經過無限循環來獲取數據的,若果在第一輪循環中,a 線程獲取地址裏面的值被b 線程修改了,那麼 a 線程須要自旋,到下次循環纔有可能機會執行。

java.util.concurrent.atomic 包下的類大可能是使用 CAS 操做來實現的(AtomicInteger,AtomicBoolean,AtomicLong)

CAS 的會產生什麼問題?

  • 一、ABA 問題:

    好比說一個線程 one 從內存位置 V 中取出 A,這時候另外一個線程 two 也從內存中取出 A,而且 two 進行了一些操做變成了 B,而後 two 又將 V 位置的數據變成 A,這時候線程 one 進行 CAS 操做發現內存中仍然是 A,而後 one 操做成功。儘管線程 one 的 CAS 操做成功,但可能存在潛藏的問題。從 Java1.5 開始 JDK 的 atomic包裏提供了一個類 AtomicStampedReference 來解決 ABA 問題。

  • 二、循環時間長開銷大:

    對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS 自旋的機率會比較大,從而浪費更多的 CPU 資源,效率低於 synchronized。

  • 三、只能保證一個共享變量的原子操做:

    當對一個共享變量執行操做時,咱們能夠使用循環 CAS 的方式來保證原子操做,可是對多個共享變量操做時,循環 CAS 就沒法保證操做的原子性,這個時候就能夠用鎖。

什麼是原子類

  • java.util.concurrent.atomic包:是原子類的小工具包,支持在單個變量上解除鎖的線程安全編程 原子變量類至關於一種泛化的 volatile 變量,可以支持原子的和有條件的讀-改-寫操做。

  • 好比:AtomicInteger 表示一個int類型的值,並提供了 get 和 set 方法,這些 Volatile 類型的int變量在讀取和寫入上有着相同的內存語義。它還提供了一個原子的 compareAndSet 方法(若是該方法成功執行,那麼將實現與讀取/寫入一個 volatile 變量相同的內存效果),以及原子的添加、遞增和遞減等方法。AtomicInteger 表面上很是像一個擴展的 Counter 類,但在發生競爭的狀況下能提供更高的可伸縮性,由於它直接利用了硬件對併發的支持。

簡單來講就是原子類來實現CAS無鎖模式的算法

原子類的經常使用類

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicReference

說一下 Atomic的原理?

  • Atomic包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操做時,具備排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程能夠向自旋鎖同樣,繼續嘗試,一直等到執行成功。

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

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

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

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

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

    Java 中致使飢餓的緣由:

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

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

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

線程池

什麼是線程池?

  • Java中的線程池是運用場景最多的併發框架,幾乎全部須要異步或併發執行任務的程序均可以使用線程池。在開發過程當中,合理地使用線程池可以帶來許多好處。
    • 下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
    • 提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。
    • 提升線程的可管理性。線程是稀缺資源,若是無限制地建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一分配、調優和監控。可是,要作到合理利用

線程池做用?

  • 線程池是爲忽然大量爆發的線程設計的,經過有限的幾個固定線程爲大量的操做服務,減小了建立和銷燬線程所需的時間,從而提升效率。

  • 若是一個線程所須要執行的時間很是長的話,就不必用線程池了(不是不能做長時間操做,而是不宜。原本下降線程建立和銷燬,結果你那麼久我還很差控制還不如直接建立線程),何況咱們還不能控制線程池中線程的開始、掛起、和停止。

線程池有什麼優勢?

  • 下降資源消耗:重用存在的線程,減小對象建立銷燬的開銷。

  • 提升響應速度。可有效的控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。

  • 提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。

  • 附加功能:提供定時執行、按期執行、單線程、併發數控制等功能。

什麼是ThreadPoolExecutor?

  • ThreadPoolExecutor就是線程池

    ThreadPoolExecutor其實也是JAVA的一個類,咱們通常經過Executors工廠類的方法,經過傳入不一樣的參數,就能夠構造出適用於不一樣應用場景下的ThreadPoolExecutor(線程池)

構造參數圖:

在這裏插入圖片描述
構造參數參數介紹:

corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(好比秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略
複製代碼

什麼是Executors?

  • Executors框架實現的就是線程池的功能。

    Executors工廠類中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其實也只是ThreadPoolExecutor的構造函數參數不一樣而已。經過傳入不一樣的參數,就能夠構造出適用於不一樣應用場景下的線程池,

Executor工廠類如何建立線程池圖:

在這裏插入圖片描述

線程池四種建立方式?

  • Java經過Executors(jdk1.5併發包)提供四種線程池,分別爲:
    1. newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
    2. newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
    3. newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
    4. newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。

在 Java 中 Executor 和 Executors 的區別?

  • Executors 工具類的不一樣方法按照咱們的需求建立了不一樣的線程池,來知足業務的需求。

  • Executor 接口對象能執行咱們的線程任務。

  • ExecutorService 接口繼承了 Executor 接口並進行了擴展,提供了更多的方法咱們能得到任務執行的狀態而且能夠獲取任務的返回值。

  • 使用 ThreadPoolExecutor 能夠建立自定義線程池。

四種構建線程池的區別及特色?

1. newCachedThreadPool
  • 特色:newCachedThreadPool建立一個可緩存線程池,若是當前線程池的長度超過了處理的須要時,它能夠靈活的回收空閒的線程,當須要增長時, 它能夠靈活的添加新的線程,而不會對池的長度做任何限制

  • 缺點:他雖然能夠無線的新建線程,可是容易形成堆外內存溢出,由於它的最大值是在初始化的時候設置爲 Integer.MAX_VALUE,通常來講機器都沒那麼大內存給它不斷使用。固然知道可能出問題的點,就能夠去重寫一個方法限制一下這個最大值

  • 總結:線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。

  • 代碼示例:

    package com.lijie;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TestNewCachedThreadPool {
        public static void main(String[] args) {
            // 建立無限大小線程池,由jvm自動回收
            ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                final int temp = i;
                newCachedThreadPool.execute(new Runnable() {
                    public void run() {
                        try {
                            Thread.sleep(100);
                        } catch (Exception e) {
                        }
                        System.out.println(Thread.currentThread().getName() + ",i==" + temp);
                    }
                });
            }
        }
    }
    
    複製代碼
2.newFixedThreadPool
  • 特色:建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。定長線程池的大小最好根據系統資源進行設置。

  • 缺點:線程數量是固定的,可是阻塞隊列是無界隊列。若是有不少請求積壓,阻塞隊列愈來愈長,容易致使OOM(超出內存空間)

  • 總結:請求的擠壓必定要和分配的線程池大小匹配,定線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()

Runtime.getRuntime().availableProcessors()方法是查看電腦CPU核心數量)

  • 代碼示例:

    package com.lijie;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TestNewFixedThreadPool {
        public static void main(String[] args) {
            ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                final int temp = i;
                newFixedThreadPool.execute(new Runnable() {
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + ",i==" + temp);
                    }
                });
            }
        }
    }
    
    複製代碼
3.newScheduledThreadPool
  • 特色:建立一個固定長度的線程池,並且支持定時的以及週期性的任務執行,相似於Timer(Timer是Java的一個定時器類)

  • 缺點:因爲全部任務都是由同一個線程來調度,所以全部任務都是串行執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到以後的任務(好比:一個任務出錯,之後的任務都沒法繼續)。

  • 代碼示例:

    package com.lijie;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class TestNewScheduledThreadPool {
        public static void main(String[] args) {
            //定義線程池大小爲3
            ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
            for (int i = 0; i < 10; i++) {
                final int temp = i;
                newScheduledThreadPool.schedule(new Runnable() {
                    public void run() {
                        System.out.println("i:" + temp);
                    }
                }, 3, TimeUnit.SECONDS);//這裏表示延遲3秒執行。
            }
        }
    }
    
    複製代碼
4.newSingleThreadExecutor
  • 特色:建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它,他必須保證前一項任務執行完畢才能執行後一項。保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。

  • 缺點:缺點的話,很明顯,他是單線程的,高併發業務下有點無力

  • 總結:保證全部任務按照指定順序執行的,若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它

  • 代碼示例:

    package com.lijie;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TestNewSingleThreadExecutor {
        public static void main(String[] args) {
            ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                final int index = i;
                newSingleThreadExecutor.execute(new Runnable() {
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + " index:" + index);
                        try {
                            Thread.sleep(200);
                        } catch (Exception e) {
                        }
                    }
                });
            }
        }
    }
    
    複製代碼

線程池都有哪些狀態?

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

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

  • 相同點:
    • 相同點就是均可以開啓線程執行池中的任務。
  • 不一樣點:
    • 接收參數:execute()只能執行 Runnable 類型的任務。submit()能夠執行 Runnable 和 Callable 類型的任務。
    • 返回值:submit()方法能夠返回持有計算結果的 Future 對象,而execute()沒有
    • 異常處理:submit()方便Exception處理

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

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

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

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

ThreadPoolExecutor飽和策略有哪些?

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

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

如何自定義線程線程池?

  • 先看ThreadPoolExecutor(線程池)這個類的構造參數

    在這裏插入圖片描述
    構造參數參數介紹:

    corePoolSize 核心線程數量
    maximumPoolSize 最大線程數量
    keepAliveTime 線程保持時間,N個時間單位
    unit 時間單位(好比秒,分)
    workQueue 阻塞隊列
    threadFactory 線程工廠
    handler 線程池拒絕策略
    
    複製代碼
  • 代碼示例:

    package com.lijie;
    
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class Test001 {
        public static void main(String[] args) {
            //建立線程池
            ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
            for (int i = 1; i <= 6; i++) {
                TaskThred t1 = new TaskThred("任務" + i);
                //executor.execute(t1);是執行線程方法
                executor.execute(t1);
            }
            //executor.shutdown()再也不接受新的任務,而且等待以前提交的任務都執行完再關閉,阻塞隊列中的任務不會再執行。
            executor.shutdown();
        }
    }
    
    class TaskThred implements Runnable {
        private String taskName;
    
        public TaskThred(String taskName) {
            this.taskName = taskName;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName() + taskName);
        }
    }
    
    複製代碼

線程池的執行原理?

在這裏插入圖片描述

  • 提交一個任務到線程池中,線程池的處理流程以下:

    1. 判斷線程池裏的核心線程是否都在執行任務,若是不是(核心線程空閒或者還有核心線程沒有被建立)則建立一個新的工做線程來執行任務。若是核心線程都在執行任務,則進入下個流程。

    2. 線程池判斷工做隊列是否已滿,若是工做隊列沒有滿,則將新提交的任務存儲在這個工做隊列裏。若是工做隊列滿了,則進入下個流程。

    3. 判斷線程池裏的線程是否都處於工做狀態,若是沒有,則建立一個新的工做線程來執行任務。若是已經滿了,則交給飽和策略來處理這個任務。

如何合理分配線程池大小?

  • 要合理的分配線程池的大小要根據實際狀況來定,簡單的來講的話就是根據CPU密集和IO密集來分配
什麼是CPU密集
  • CPU密集的意思是該任務須要大量的運算,而沒有阻塞,CPU一直全速運行。

  • CPU密集任務只有在真正的多核CPU上纔可能獲得加速(經過多線程),而在單核CPU上,不管你開幾個模擬的多線程,該任務都不可能獲得加速,由於CPU總的運算能力就那樣。

什麼是IO密集
  • IO密集型,即該任務須要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會致使浪費大量的CPU運算能力浪費在等待。因此在IO密集型任務中使用多線程能夠大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
分配CPU和IO密集:
  1. CPU密集型時,任務能夠少配置線程數,大概和機器的cpu核數至關,這樣能夠使得每一個線程都在執行任務

  2. IO密集型時,大部分線程都阻塞,故須要多配置線程數,2*cpu核數

精確來講的話的話:
  • 從如下幾個角度分析任務的特性:

    • 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。

    • 任務的優先級:高、中、低。

    • 任務的執行時間:長、中、短。

    • 任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接等。

能夠得出一個結論:

  • 線程等待時間比CPU執行時間比例越高,須要越多線程。
  • 線程CPU執行時間比等待時間比例越高,須要越少線程。

併發容器

你常用什麼併發容器,爲何?

  • 答:Vector、ConcurrentHashMap、HasTable

  • 通常軟件開發中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等

  • 可是在多線程開發中就不能亂用容器,若是使用了未加鎖(非同步)的的集合,你的數據就會很是的混亂。由此在多線程開發中須要使用的容器必須是加鎖(同步)的容器。

什麼是Vector

  • Vector與ArrayList同樣,也是經過數組實現的,不一樣的是它支持線程的同步,即某一時刻只有一個線程可以寫Vector,避免多線程同時寫而引發的不一致性,但實現同步須要很高的花費,訪問它比訪問ArrayList慢不少

    ArrayList是最經常使用的List實現類,內部是經過數組實現的,它容許對元素進行快速隨機訪問。當從ArrayList的中間位置插入或者刪除元素時,須要對數組進行復制、移動、代價比較高。所以,它適合隨機查找和遍歷,不適合插入和刪除。ArrayList的缺點是每一個元素之間不能有間隔。

ArrayList和Vector有什麼不一樣之處?

  • Vector方法帶上了synchronized關鍵字,是線程同步的
  1. ArrayList添加方法源碼
    在這裏插入圖片描述
  2. Vector添加源碼(加鎖了synchronized關鍵字)
    在這裏插入圖片描述

爲何HashTable是線程安全的?

  • 由於HasTable的內部方法都被synchronized修飾了,因此是線程安全的。其餘的都和HashMap同樣
  1. HashMap添加方法的源碼
    在這裏插入圖片描述
  2. HashTable添加方法的源碼
    在這裏插入圖片描述

用過ConcurrentHashMap,講一下他和HashTable的不一樣之處?

  • ConcurrentHashMap是Java5中支持高併發、高吞吐量的線程安全HashMap實現。它由Segment數組結構和HashEntry數組結構組成。Segment數組在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵-值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構;一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素;每一個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先得到它對應的Segment鎖。

  • 看不懂???很正常,我也看不懂

  • 總結:

    1. HashTable就是實現了HashMap加上了synchronized,而ConcurrentHashMap底層採用分段的數組+鏈表實現,線程安全
    2. ConcurrentHashMap經過把整個Map分爲N個Segment,能夠提供相同的線程安全,可是效率提高N倍,默認提高16倍。
    3. 而且讀操做不加鎖,因爲HashEntry的value變量是 volatile的,也能保證讀取到最新的值。
    4. Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨佔,ConcurrentHashMap容許多個修改操做併發進行,其關鍵在於使用了鎖分離技術
    5. 擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不須要擴容,有效避免無效擴容

Collections.synchronized * 是什麼?

注意:* 號表明後面是還有內容的

  • 此方法是幹什麼的呢,他完徹底全的能夠把List、Map、Set接口底下的集合變成線程安全的集合

  • Collections.synchronized * :原理是什麼,我猜的話是代理模式:Java代理模式理解

在這裏插入圖片描述

Java 中 ConcurrentHashMap 的併發度是什麼?

  • ConcurrentHashMap 把實際 map 劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap 類構造函數的一個可選參數,默認值爲 16,這樣在多線程狀況下就能避免爭用。

  • 在 JDK8 後,它摒棄了 Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用 CAS 算法。同時加入了更多的輔助變量來提升併發度,具體內容仍是查看源碼吧。

什麼是併發容器的實現?

  • 何爲同步容器:能夠簡單地理解爲經過 synchronized 來實現同步的容器,若是有多個線程調用同步容器的方法,它們將會串行執行。好比 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。能夠經過查看 Vector,Hashtable 等這些同步容器的實現代碼,能夠看到這些容器實現線程安全的方式就是將它們的狀態封裝起來,並在須要同步的方法上加上關鍵字 synchronized。

  • 併發容器使用了與同步容器徹底不一樣的加鎖策略來提供更高的併發性和伸縮性,例如在 ConcurrentHashMap 中採用了一種粒度更細的加鎖機制,能夠稱爲分段鎖,在這種鎖機制下,容許任意數量的讀線程併發地訪問 map,而且執行讀操做的線程和寫操做的線程也能夠併發的訪問 map,同時容許必定數量的寫操做線程併發地修改 map,因此它能夠在併發環境下實現更高的吞吐量。

Java 中的同步集合與併發集合有什麼區別?

  • 同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在 Java1.5 以前程序員們只有同步集合來用且在多線程併發的時候會致使爭用,阻礙了系統的擴展性。Java5 介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現代技術提升了可擴展性。

SynchronizedMap 和 ConcurrentHashMap 有什麼區別?

  • SynchronizedMap 一次鎖住整張表來保證線程安全,因此每次只能有一個線程來訪爲 map。

  • ConcurrentHashMap 使用分段鎖來保證在多線程下的性能。

  • ConcurrentHashMap 中則是一次鎖住一個桶。ConcurrentHashMap 默認將hash 表分爲 16 個桶,諸如 get,put,remove 等經常使用操做只鎖當前須要用到的桶。

  • 這樣,原來只能一個線程進入,如今卻能同時有 16 個寫線程執行,併發性能的提高是顯而易見的。

  • 另外 ConcurrentHashMap 使用了一種不一樣的迭代方式。在這種迭代方式中,當iterator 被建立後集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之的是在改變時 new 新的數據從而不影響原有的數據,iterator 完成後再將頭指針替換爲新的數據 ,這樣 iterator線程能夠使用原來老的數據,而寫線程也能夠併發的完成改變。

CopyOnWriteArrayList 是什麼?

  • CopyOnWriteArrayList 是一個併發容器。有不少人稱它是線程安全的,我認爲這句話不嚴謹,缺乏一個前提條件,那就是非複合場景下操做它是線程安全的。

  • CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會拋出 ConcurrentModificationException。在CopyOnWriteArrayList 中,寫入將致使建立整個底層數組的副本,而源數組將保留在原地,使得複製的數組在被修改時,讀取操做能夠安全地執行。

CopyOnWriteArrayList 的使用場景?

  • 合適讀多寫少的場景。

CopyOnWriteArrayList 的缺點?

  • 因爲寫操做的時候,須要拷貝數組,會消耗內存,若是原數組的內容比較多的狀況下,可能致使 young gc 或者 full gc。
  • 不能用於實時讀的場景,像拷貝數組、新增元素都須要時間,因此調用一個 set 操做後,讀取到數據可能仍是舊的,雖然CopyOnWriteArrayList 能作到最終一致性,可是仍是無法知足實時性要求。
  • 因爲實際使用中可能無法保證 CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次 add/set 都要從新複製數組,這個代價實在過高昂了。在高性能的互聯網應用中,這種操做分分鐘引發故障。

CopyOnWriteArrayList 的設計思想?

  • 讀寫分離,讀和寫分開
  • 最終一致性
  • 使用另外開闢空間的思路,來解決併發衝突

併發隊列

什麼是併發隊列:

  • 消息隊列不少人知道:消息隊列是分佈式系統中重要的組件,是系統與系統直接的通訊

  • 併發隊列是什麼:併發隊列多個線程以有次序共享數據的重要組件

併發隊列和併發集合的區別:

那就有可能要說了,咱們併發集合不是也能夠實現多線程之間的數據共享嗎,其實也是有區別的:

  • 隊列遵循「先進先出」的規則,能夠想象成排隊檢票,隊列通常用來解決大數據量採集處理和顯示的。

  • 併發集合就是在多個線程中共享數據的

怎麼判斷併發隊列是阻塞隊列仍是非阻塞隊列

  • 在併發隊列上JDK提供了Queue接口,一個是以Queue接口下的BlockingQueue接口爲表明的阻塞隊列,另外一個是高性能(無堵塞)隊列。

阻塞隊列和非阻塞隊列區別

  • 當隊列阻塞隊列爲空的時,從隊列中獲取元素的操做將會被阻塞。

  • 或者當阻塞隊列是滿時,往隊列裏添加元素的操做會被阻塞。

  • 或者試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其餘的線程往空的隊列插入新的元素。

  • 試圖往已滿的阻塞隊列中添加新元素的線程一樣也會被阻塞,直到其餘的線程使隊列從新變得空閒起來

經常使用併發列隊的介紹:

  1. 非堵塞隊列:

    1. ArrayDeque, (數組雙端隊列)

      ArrayDeque (非堵塞隊列)是JDK容器中的一個雙端隊列實現,內部使用數組進行元素存儲,不容許存儲null值,能夠高效的進行元素查找和尾部插入取出,是用做隊列、雙端隊列、棧的絕佳選擇,性能比LinkedList還要好。

    2. PriorityQueue, (優先級隊列)

      PriorityQueue (非堵塞隊列) 一個基於優先級的無界優先級隊列。優先級隊列的元素按照其天然順序進行排序,或者根據構造隊列時提供的 Comparator 進行排序,具體取決於所使用的構造方法。該隊列不容許使用 null 元素也不容許插入不可比較的對象

    3. ConcurrentLinkedQueue, (基於鏈表的併發隊列)

      ConcurrentLinkedQueue (非堵塞隊列): 是一個適用於高併發場景下的隊列,經過無鎖的方式,實現了高併發狀態下的高性能。ConcurrentLinkedQueue的性能要好於BlockingQueue接口,它是一個基於連接節點的無界線程安全隊列。該隊列的元素遵循先進先出的原則。該隊列不容許null元素。

  2. 堵塞隊列:

    1. DelayQueue, (基於時間優先級的隊列,延期阻塞隊列)

      DelayQueue是一個沒有邊界BlockingQueue實現,加入其中的元素必需實現Delayed接口。當生產者線程調用put之類的方法加入元素時,會觸發Delayed接口中的compareTo方法進行排序,也就是說隊列中元素的順序是按到期時間排序的,而非它們進入隊列的順序。排在隊列頭部的元素是最先到期的,越日後到期時間赿晚。

    2. ArrayBlockingQueue, (基於數組的併發阻塞隊列)

      ArrayBlockingQueue是一個有邊界的阻塞隊列,它的內部實現是一個數組。有邊界的意思是它的容量是有限的,咱們必須在其初始化的時候指定它的容量大小,容量大小一旦指定就不可改變。ArrayBlockingQueue是以先進先出的方式存儲數據

    3. LinkedBlockingQueue, (基於鏈表的FIFO阻塞隊列)

      LinkedBlockingQueue阻塞隊列大小的配置是可選的,若是咱們初始化時指定一個大小,它就是有邊界的,若是不指定,它就是無邊界的。說是無邊界,實際上是採用了默認大小爲Integer.MAX_VALUE的容量 。它的內部實現是一個鏈表。

    4. LinkedBlockingDeque, (基於鏈表的FIFO雙端阻塞隊列)

      LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列,便可以從隊列的兩端插入和移除元素。雙向隊列由於多了一個操做隊列的入口,在多線程同時入隊時,也就減小了一半的競爭。

      相比於其餘阻塞隊列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first結尾的方法,表示插入、獲取獲移除雙端隊列的第一個元素。以last結尾的方法,表示插入、獲取獲移除雙端隊列的最後一個元素。

      LinkedBlockingDeque是可選容量的,在初始化時能夠設置容量防止其過分膨脹,若是不設置,默認容量大小爲Integer.MAX_VALUE。

    5. PriorityBlockingQueue, (帶優先級的無界阻塞隊列)

      priorityBlockingQueue是一個無界隊列,它沒有限制,在內存容許的狀況下能夠無限添加元素;它又是具備優先級的隊列,是經過構造函數傳入的對象來判斷,傳入的對象必須實現comparable接口。

    6. SynchronousQueue (併發同步阻塞隊列)

      SynchronousQueue是一個內部只能包含一個元素的隊列。插入元素到隊列的線程被阻塞,直到另外一個線程從隊列中獲取了隊列中存儲的元素。一樣,若是線程嘗試獲取元素而且當前不存在任何元素,則該線程將被阻塞,直到線程將元素插入隊列。

      將這個類稱爲隊列有點誇大其詞。這更像是一個點。

併發隊列的經常使用方法

無論是那種列隊,是那個類,當是他們使用的方法都是差很少的

方法名 描述
add() 在不超出隊列長度的狀況下插入元素,能夠當即執行,成功返回true,若是隊列滿了就拋出異常。
offer() 在不超出隊列長度的狀況下插入元素的時候則能夠當即在隊列的尾部插入指定元素,成功時返回true,若是此隊列已滿,則返回false。
put() 插入元素的時候,若是隊列滿了就進行等待,直到隊列可用。
take() 從隊列中獲取值,若是隊列中沒有值,線程會一直阻塞,直到隊列中有值,而且該方法取得了該值。
poll(long timeout, TimeUnit unit) 在給定的時間裏,從隊列中獲取值,若是沒有取到會拋出異常。
remainingCapacity() 獲取隊列中剩餘的空間。
remove(Object o) 從隊列中移除指定的值。
contains(Object o) 判斷隊列中是否擁有該值。
drainTo(Collection c) 將隊列中值,所有移除,併發設置到給定的集合中。

併發工具類

經常使用的併發工具類有哪些?

  • CountDownLatch

    CountDownLatch 類位於java.util.concurrent包下,利用它能夠實現相似計數器的功能。好比有一個任務A,它要等待其餘3個任務執行完畢以後才能執行,此時就能夠利用CountDownLatch來實現這種功能了。

  • CyclicBarrier (迴環柵欄) CyclicBarrier它的做用就是會讓全部線程都等待完成後纔會繼續下一步行動。

    CyclicBarrier初始化時規定一個數目,而後計算調用了CyclicBarrier.await()進入等待的線程數。當線程數達到了這個數目時,全部進入等待狀態的線程被喚醒並繼續。

    CyclicBarrier初始時還可帶一個Runnable的參數, 此Runnable任務在CyclicBarrier的數目達到後,全部其它線程被喚醒前被執行。

  • Semaphore (信號量) Semaphore 是 synchronized 的增強版,做用是控制線程的併發數量(容許自定義多少線程同時訪問)。就這一點而言,單純的synchronized 關鍵字是實現不了的。

    Semaphore是一種基於計數的信號量。它能夠設定一個閾值,基於此,多個線程競爭獲取許可信號,作本身的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。Semaphore能夠用來構建一些對象池,資源池之類的,好比數據庫鏈接池,咱們也能夠建立計數爲1的Semaphore,將其做爲一種相似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。它的用法以下:

相關文章
相關標籤/搜索