日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

面经——多线程

發(fā)布時間:2024/2/28 编程问答 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面经——多线程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

面經(jīng)——多線程


  • 創(chuàng)建線程和終止線程方式
  • Runnable和callable區(qū)別
  • synchronize問題詳解
  • 樂觀鎖和悲觀鎖
  • 線程安全和非線程安全區(qū)別
  • JMM 內(nèi)存模型
  • volatile解析
  • 公平鎖和非公平鎖區(qū)別?為什么公平鎖效率低?
  • 鎖優(yōu)化(自旋鎖、自適應(yīng)自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖、重量級鎖解釋)
  • AQS原理及應(yīng)用
  • CAS
  • 線程同步方法
  • ThreadLocal原理
  • ReenTrantLock原理
  • 線程狀態(tài),start,run,wait,notify,yiled,sleep,join等方法的作用以及區(qū)別
  • 關(guān)于 Atomic 原子類
  • 線程池相關(guān)
  • 手寫簡單的線程池,體現(xiàn)線程復(fù)用
  • 手寫消費者生產(chǎn)者模式
  • 手寫阻塞隊列
  • 手寫多線程交替打印ABC
  • 注:題目從牛客 Java部門面經(jīng)整理而來。
    2020秋招面經(jīng)大匯總!(崗位劃分)


    1. 創(chuàng)建線程和終止線程方式

    創(chuàng)建線程有四種方式:

  • 繼承Thread 重寫 run 方法。
  • 實現(xiàn) Runnable 接口。
  • 實現(xiàn) Callable 接口。
  • 使用Executor框架來創(chuàng)建線程池
  • 中斷線程方式:

  • new Thread().isInterrupted() 方法用于獲取當(dāng)前線程的中斷狀態(tài)
  • new Thread().interrupted() 方法用于設(shè)置當(dāng)前線程的中斷狀態(tài),即中斷當(dāng)前線程
  • Thread.interrupted()用于獲取當(dāng)前線程的中斷狀態(tài),同時還會重置中斷狀態(tài)

  • 2. Runnable和callable區(qū)別

  • Runnable 沒有返回值,Callable 可以拿到有返回值,Callable 可以看作是 Runnable 的補充。
  • Callable接口的call()方法允許拋出異常;Runnable的run()方法異常只能在內(nèi)部消化,不能往上繼續(xù)拋

  • 3. synchronize問題詳解

    synchronized 問題新開了一遍筆記:synchronized面試五連擊


    4. 樂觀鎖和悲觀鎖

    1. 樂觀鎖和悲觀鎖區(qū)別

    • 悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現(xiàn)。

    • 樂觀鎖:總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號機(jī)制和CAS算法實現(xiàn)。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫提供的類似于write_condition機(jī)制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現(xiàn)方式CAS實現(xiàn)的。

    2. 兩種鎖的使用場景
    從上面對兩種鎖的介紹,我們知道兩種鎖各有優(yōu)缺點,不可認(rèn)為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發(fā)生的時候,這樣可以省去了鎖的開銷,加大了系統(tǒng)的整個吞吐量。但如果是多寫的情況,一般會經(jīng)常產(chǎn)生沖突,這就會導(dǎo)致上層應(yīng)用會不斷的進(jìn)行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。


    5. 線程安全和非線程安全區(qū)別

    線程安全就是多線程訪問時,采用了加鎖機(jī)制,當(dāng)一個線程訪問該類的某個數(shù)據(jù)時,進(jìn)行保護(hù),其他線程不能進(jìn)行訪問直到該線程讀取完,其他線程才可使用。不會出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)污染。

    線程不安全就是不提供數(shù)據(jù)訪問保護(hù),有可能出現(xiàn)多個線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù)

    比如ArrayList是非線程安全的,Vector是線程安全的;HashMap是非線程安全的,HashTable是線程安全的;StringBuilder是非線程安全的,StringBuffer是線程安全的。


    6. JMM 內(nèi)存模型

    Java 內(nèi)存模型試圖屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓 Java 程序在各種平臺下都能達(dá)到一致的內(nèi)存
    訪問效果。

    主內(nèi)存與工作內(nèi)存

    處理器上的寄存器的讀寫的速度比內(nèi)存快幾個數(shù)量級,為了解決這種速度矛盾,在它們之間加入了高速緩存。
    加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內(nèi)存區(qū)域,那么多個緩存的數(shù)據(jù)可能會不一致,需要一些協(xié)議來解決這個問題。

    解決緩存一致性方案有兩種:

  • 通過在總線加LOCK#鎖的方式;
  • 通過緩存一致性協(xié)議。
  • 但是方案1存在一個問題,它是采用一種獨占的方式來實現(xiàn)的,即總線加LOCK#鎖的話,只能有一個CPU能夠運行,其他CPU都得阻塞,效率較為低下。

    方案二 緩存一致性協(xié)議(MESI協(xié)議)它確保每個緩存中使用的共享變量的副本是一致的。所以JMM就解決這個問題。

    JMM(Java內(nèi)存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。

    JMM關(guān)于同步的規(guī)定:

    • 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
    • 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
    • 加鎖解鎖是同一把鎖

    由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量 的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成 后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個線程中的工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,因此不同的線程間無法去訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成

    內(nèi)存間交互操作

    Java 內(nèi)存模型定義了 8 個操作來完成主內(nèi)存和工作內(nèi)存的交互操作。

    • read:把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中
    • load:在 read 之后執(zhí)行,把 read 得到的值放入工作內(nèi)存的變量副本中
    • use:把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎
    • assign:把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
    • store:把工作內(nèi)存的一個變量的值傳送到主內(nèi)存中
    • write:在 store 之后執(zhí)行,把 store 得到的值放入主內(nèi)存的變量中
    • lock:作用于主內(nèi)存的變量
    • unlock
    內(nèi)存模型三大特性

    1. 原子性
    Java 內(nèi)存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執(zhí)行 assign 賦值操作,這個操作就是原子性的。但是 Java 內(nèi)存模型允許虛擬機(jī)將沒有被 volatile 修飾的64 位數(shù)據(jù)(long,double)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,即 load、store、read 和 write 操作可以不具備原子性。

    有一個錯誤認(rèn)識就是,int 等原子性的類型在多線程環(huán)境中不會出現(xiàn)線程安全問題。前面的線程不安全示例代碼中,
    cnt 屬于 int 類型變量,1000 個線程對它進(jìn)行自增操作之后,得到的值為 997 而不是 1000。

    為了方便討論,將內(nèi)存間的交互操作簡化為 3 個:load、assign、store。

    下圖演示了兩個線程同時對 cnt 進(jìn)行操作,load、assign、store 這一系列操作整體上看不具備原子性,那么在 T1修改 cnt 并且還沒有將修改后的值寫入主內(nèi)存,T2 依然可以讀入舊值。可以看出,這兩個線程雖然執(zhí)行了兩次自增運算,但是主內(nèi)存中 cnt 的值最后為 1 而不是 2。因此對 int 類型讀寫操作滿足原子性只是說明 load、assign、store 這些單個操作具備原子性。

    AtomicInteger 能保證多個線程修改的原子性。

    除了使用原子類之外,也可以使用 synchronized 互斥鎖來保證操作的原子性。它對應(yīng)的內(nèi)存間交互操作為:lock 和
    unlock,在虛擬機(jī)實現(xiàn)上對應(yīng)的字節(jié)碼指令為 monitorenter 和 monitorexit。

    2. 可見性
    可見性指當(dāng)一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。Java 內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來實現(xiàn)可見性的。

    主要有三種實現(xiàn)可見性的方式:

  • volatile
  • synchronized,對一個變量執(zhí)行 unlock 操作之前,必須把變量值同步回主內(nèi)存。
  • final,被 final 關(guān)鍵字修飾的字段在構(gòu)造器中一旦初始化完成,并且沒有發(fā)生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那么其它線程就能看見 final 字段的值。
  • 對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,因為 volatile 并不能保證操作的原子性

    3. 有序性
    有序性是指:在本線程內(nèi)觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因為發(fā)生了指令重排序。在 Java 內(nèi)存模型中,允許編譯器和處理器對指令進(jìn)行重排序,重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。

    volatile 關(guān)鍵字通過添加內(nèi)存屏障的方式來禁止指令重排,即重排序時不能把后面的指令放到內(nèi)存屏障之前。
    也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼。

    擴(kuò)展:內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄,是一個CPU指令,它的作用有兩個:

  • 保證特定操作的執(zhí)行順序,
  • 保證某些變量的內(nèi)存可見性(利用該特性實現(xiàn)volatile的內(nèi)存可見性)。
  • 由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能
    和這條MemoryBarrier指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。內(nèi)存屏障另外一個作
    用是強制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。


    7. volatile解析

    volatile是 java虛擬機(jī) 提供的輕量級的同步機(jī)制(可以理解成乞丐版的synchronized)

    特性有:

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排
  • 1. 保證可見性
    理解volatile特性之一保證可見性之前要先理解什么是JMM內(nèi)存模型的可見性(參考上面),JMM 內(nèi)存模型就是 volatile保證可見性特性的原理。

    2. 不保證原子性
    i++;這條代碼可以分為3個步驟:

  • 從主內(nèi)存取值;
  • 執(zhí)行+1;
  • 值重新寫回主內(nèi)存
  • 如果使用volatile修飾,它只能保證第一步是從主內(nèi)存取得最新值和指令不被重新排序.

    例如:從主內(nèi)存取到最新的值a=1,線程A執(zhí)行完+1操作(a=2),如果這個時候線程A讓出時間片,其他線程修改a的值為5,線程A繼續(xù)執(zhí)行,把a=2寫如到主內(nèi)存,這個時候就線程不安全了。主要原因就是把值寫回到主內(nèi)存時,并沒有判斷主內(nèi)存的最新值和之前取到的值一樣就寫回主內(nèi)存了。所以,volatile不保證原子性。

    那么如何解決volatile不保證原子性的問題?
    我們可以用java.util.concurrent.atomic包下的 AtomicInteger解決這個問題。

    3. 禁止指令重排
    volatile實現(xiàn)禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象。volatile禁止指令重排功能 依賴內(nèi)存屏障(內(nèi)容見上)實現(xiàn)。

    4. 單例模式下 volatile 的作用
    在多線程環(huán)境下,底層為了優(yōu)化有指令重排,加入volatile可以禁止指令重排。

    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;}

    8. 公平鎖和非公平鎖區(qū)別?為什么公平鎖效率低?

    1. 公平鎖和非公平鎖區(qū)別?

  • 公平鎖:在并發(fā)壞境中.每個線程在獲取鎖時會先查看此鎖維護(hù)的等待隊列,如果為空,或者當(dāng)前線程是等待隊列的第一個,就占有鎖。否則就會加入到等待隊列中,以后會按照FIFO的規(guī)則從隊列中取到鎖。

  • 非公平鎖:上來就直接嘗試占有鎖,如果嘗試失敗,就執(zhí)行公平鎖邏輯。

  • 2. 為什么公平鎖效率低?
    公平鎖要維護(hù)一個隊列,后來的線程要加鎖,即使鎖空閑,也要先檢查有沒有其他線程在等待,如果有自己要掛起,加到隊列后面,然后喚醒隊列最前面的線程。這種情況下相比較非公平鎖多了一次掛起和喚醒,多了線程切換的開銷,這就是非公平鎖效率高于公平鎖的原因,因為非公平鎖減少了線程掛起的幾率,后來的線程有一定幾率逃離被掛起的開銷。


    9. 鎖優(yōu)化(自旋鎖、自適應(yīng)自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖、重量級鎖解釋)

    這里的鎖優(yōu)化主要是指 JVM 對 synchronized 的優(yōu)化。

    1. 自旋鎖和自適應(yīng)自旋鎖
    互斥同步對性能的最大的影響是阻塞的實現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成。在許多應(yīng)用中,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間。自旋鎖的思想是讓一個線程在請求一個共享數(shù)據(jù)的鎖時執(zhí)行忙循環(huán)(自旋)一段時間,如果在這段時間內(nèi)能獲得鎖,就可以避免進(jìn)入阻塞狀態(tài)。

    自旋鎖雖然能避免進(jìn)入阻塞狀態(tài)從而減少開銷,但是它需要進(jìn)行忙循環(huán)操作占用 CPU 時間,它只適用于共享數(shù)據(jù)的鎖定狀態(tài)很短的場景。自旋等待的時間必須有一定的限度,超過了限定的次數(shù)仍然沒有成功獲取鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式掛起線程了。自旋次數(shù)的默認(rèn)值是10,用戶可以通過-XX:PreBlockSpin來更改。

    在 JDK 1.6 中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的次數(shù)不再固定了,而是由前一次在同一個鎖上的自旋次數(shù)及鎖的擁有者的狀態(tài)來決定。如果在同一個鎖對象上,自旋線程之前剛剛獲得過鎖,且現(xiàn)在持有鎖的線程正在運行中,那么虛擬機(jī)會認(rèn)為這次自旋也很有可能會成功,進(jìn)而允許該線程等待持續(xù)相對更長的時間,比如100個循環(huán)。反之,如果某個鎖自旋很少獲得過成功,那么之后再獲取鎖的時候?qū)⒖赡苁÷缘糇孕^程,以避免浪費處理器資源。

    2. 鎖消除
    鎖消除是指對于被檢測出不可能存在競爭的共享數(shù)據(jù)的鎖進(jìn)行消除。

    鎖消除主要是通過逃逸分析來支持,如果堆上的共享數(shù)據(jù)不可能逃逸出去被其它線程訪問到,那么就可以把它們當(dāng)成私有數(shù)據(jù)對待,也就可以將它們的鎖進(jìn)行消除。

    對于一些看起來沒有加鎖的代碼,其實隱式的加了很多鎖。例如下面的字符串拼接代碼就隱式加了鎖:

    public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3; }

    String 是一個不可變的類,編譯器會對 String 的拼接自動優(yōu)化。在 JDK 1.5 之前,會轉(zhuǎn)化為StringBuffer 對象的連續(xù) append() 操作:

    public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString(); }

    每個 append() 方法中都有一個同步塊。虛擬機(jī)觀察變量 sb,很快就會發(fā)現(xiàn)它的動態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是說,sb 的所有引用永遠(yuǎn)不會逃逸到 concatString() 方法之外,其他線程無法訪問到它,因此可以進(jìn)行消除。

    3. 鎖粗化
    如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,頻繁的加鎖操作就會導(dǎo)致性能損耗。

    上一節(jié)的示例代碼中連續(xù)的 append() 方法就屬于這類情況。如果虛擬機(jī)探測到由這樣的一串零碎的操作都對同一個對象加鎖,將會把加鎖的范圍擴(kuò)展(粗化)到整個操作序列的外部。對于上一節(jié)的示例代碼就是擴(kuò)展到第一個 append() 操作之前直至最后一個 append() 操作之后,這樣只需要加鎖一次就可以了。

    4. 偏向鎖
    引入偏向鎖的目的和引入輕量級鎖的目的很像,它們都是為了沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。但是不同是:輕量級鎖在無競爭的情況下使用 CAS 操作去代替使用互斥量,而偏向鎖在無競爭的情況下會把整個同步都消除掉。

    偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,這種情況下,就會給線程加一個偏向鎖。

    如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖。

    5. 輕量級鎖
    輕量級鎖的目標(biāo)是,減少無實際競爭情況下,使用重量級鎖產(chǎn)生的性能消耗,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。

    如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥操作的開銷。但如果存在鎖競爭,除了互斥量開銷外,還會額外發(fā)生CAS操作,因此在有鎖競爭的情況下,輕量級鎖比傳統(tǒng)的重量級鎖更慢,如果鎖競爭激烈,那么輕量級將很快膨脹為重量級鎖。

    6. 重量級鎖
    內(nèi)置鎖在Java中被抽象為監(jiān)視器鎖(monitor)。在JDK 1.6之前,監(jiān)視器鎖可以認(rèn)為直接對應(yīng)底層操作系統(tǒng)中的互斥量(mutex)。這種同步方式的成本非常高,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。因此稱這種鎖為“重量級鎖”。


    10. AQS原理及應(yīng)用

    1. AQS 介紹
    AQS的全稱為(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。

    AQS是一個用來構(gòu)建鎖和同步器的框架,使用AQS能簡單且高效地構(gòu)造出應(yīng)用廣泛的大量的同步器,比如我們提到的 ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    2. AQS 原理概覽
    AQS核心思想是,如果被請求的共享資源空閑,則將當(dāng)前請求資源的線程設(shè)置為有效的工作線程,并且將共享資源設(shè)置為鎖定狀態(tài)。如果被請求的共享資源被占用,那么就需要一套線程阻塞等待以及被喚醒時鎖分配的機(jī)制,這個機(jī)制AQS是用CLH隊列鎖實現(xiàn)的,即將暫時獲取不到鎖的線程加入到隊列中。

    CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結(jié)點之間的關(guān)聯(lián)關(guān)系)。AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結(jié)點(Node)來實現(xiàn)鎖的分配。

    看個AQS(AbstractQueuedSynchronizer)原理圖:

    AQS使用一個int成員變量來表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成獲取資源線程的排隊工作。AQS使用CAS對該
    同步狀態(tài)進(jìn)行原子操作實現(xiàn)對其值的修改。

    private volatile int state;//共享變量,使用volatile修飾保證線程可見性

    狀態(tài)信息通過procted類型的getState,setState,compareAndSetState進(jìn)行操作。

    //返回同步狀態(tài)的當(dāng)前值 protected final int getState() {return state; } // 設(shè)置同步狀態(tài)的值 protected final void setState(int newState) {state = newState; } //原子地(CAS操作)將同步狀態(tài)值設(shè)置為給定值update如果當(dāng)前同步狀態(tài)的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

    3. AQS 對資源的共享方式
    AQS定義兩種資源共享方式

  • Exclusive(獨占):只有一個線程能執(zhí)行,如ReentrantLock。又可分為公平鎖和非公平鎖:
    公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    非公平鎖:當(dāng)線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執(zhí)行,如Semaphore/CountDownLatch。Semaphore、
  • ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某
    一資源進(jìn)行讀。

    不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源 state 的獲取與釋放方
    式即可,至于具體線程等待隊列的維護(hù)(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了。

    4. AQS底層使用了模板方法模式
    同步器的設(shè)計是基于模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經(jīng)典的一個應(yīng)
    用):

  • 使用者繼承AbstractQueuedSynchronizer并重寫指定的方法。(這些重寫方法很簡單,無非是對于共享資源state的獲取和釋放)
  • 將AQS組合在自定義同步組件的實現(xiàn)中,并調(diào)用其模板方法,而這些模板方法會調(diào)用使用者重寫的方法。
  • 這和我們以往通過實現(xiàn)接口的方式有很大區(qū)別,這是模板方法模式很經(jīng)典的一個運用。

    AQS使用了模板方法模式,自定義同步器時需要重寫下面幾個AQS提供的模板方法:

    isHeldExclusively()//該線程是否正在獨占資源。只有用到condition才需要去實現(xiàn)它。 tryAcquire(int)//獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。 tryRelease(int)//獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。 tryAcquireShared(int)//共享方式。嘗試獲取資源。負(fù)數(shù)表示失敗;0表示成功,但沒有剩余可用資源;正數(shù)表示成功,且有剩余資源。 tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

    默認(rèn)情況下,每個方法都拋出 UnsupportedOperationException 。 這些方法的實現(xiàn)必須是內(nèi)部線程安全的,并且
    通常應(yīng)該簡短而不是阻塞。AQS類中的其他方法都是final ,所以無法被其他類使用,只有這幾個方法可以被其他類
    使用。

    以ReentrantLock為例,state初始化為0,表示未鎖定狀態(tài)。A線程lock()時,會調(diào)用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機(jī)會獲取該鎖。當(dāng)然,釋放鎖之前,A線程自己是可以重復(fù)獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態(tài)的。

    再以CountDownLatch以例,任務(wù)分為N個子線程去執(zhí)行,state也初始化為N(注意N要與線程個數(shù)一致)。這N個子線程是并行執(zhí)行的,每個子線程執(zhí)行完后countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執(zhí)行完后(即state=0),會unpark()主調(diào)用線程,然后主調(diào)用線程就會從await()函數(shù)返回,繼續(xù)后余動作。

    一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實現(xiàn) tryAcquire-tryRelease 、tryAcquireShared-tryReleaseShared 中的一種即可。但AQS也支持自定義同步器同時實現(xiàn)獨占和共享兩種方式,如 ReentrantReadWriteLock 。

    5. AQS 組件總結(jié)
    Semaphore(信號量)-允許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。

    CountDownLatch (倒計時器): CountDownLatch是一個同步工具類,用來協(xié)調(diào)多個線程之間的同步。這個工具通常用來控制線程等待,讓一些線程阻塞直到另一些線程完成一系列操作后才被喚醒。

    CyclicBarrier(循環(huán)柵欄): CyclicBarrier的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)。作用是讓一組線程到達(dá)一個屏障(也可以叫
    同步點)時被阻塞,直到最后一個線程到達(dá)屏障時,屏障才會開門,所有被屏障攔截的線程才會繼續(xù)干活。CyclicBarrier默認(rèn)的構(gòu)造方法是 CyclicBarrier(int parties),其參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用await方法告訴 CyclicBarrier 我已經(jīng)到達(dá)了屏障,然后當(dāng)前線程被阻塞。


    11. CAS

    1. CAS是什么
    CAS的全稱為Compare-And-Swap,它是一條CPU并發(fā)原語。

    它的功能是判斷內(nèi)存某個位置的值是否為期望值,如果是則更改為新的值,這個過程是原子的。

    CAS并發(fā)原語體現(xiàn)在JAVA語言中就是sun.misc.Unsafe類中的CAS方法,JVM會幫我們實現(xiàn)CAS匯編指令。這是一種完全依賴于硬件的功能,通過它實現(xiàn)了原子操作。再次強調(diào),由于CAS是一種系統(tǒng)原語,原語屬于操作系統(tǒng)用語范疇范,是由若干條指令組成的,用于完成某個功能的一個過程,并且原語的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷,也就說CAS是一條CPU的原了指令,不會造成所謂的數(shù)據(jù)不一致問題。

    2. CAS底層原理Unsafe深入解析
    Unsafe是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地(native)方法來訪問,Unsafe相當(dāng)于一個后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。Unsafe類存在于sun.misc包中,其內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存,因為Java中CAS操作的執(zhí)行依賴于Unsafe類的方法。

    注意Unsafe類中的所有方法都是native修飾的,也就是Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù)。

    原子整型在i++中操作多線程環(huán)境下不需要加synchronized,也能保證線程安全,是因為它用的是Unsafe類,源代碼如下:

    getAndAddInt()方法底層調(diào)用的是unsafe,傳三個參數(shù),當(dāng)前對象,內(nèi)存地址偏移量,增量1。底層調(diào)用的是CAS思想,如果比較成功+1,失敗再重新獲得比較一次,直至成功為止。


    var1 AtomicInteger 對象本身
    var2 該對象值的引用地址
    var4 需要變動的數(shù)量
    var5 是用 var1,var2 找出的主內(nèi)存中真實的值,用該對象當(dāng)前的值與 var5 比較:如果相同,更新var5+var4并返回 var5。如果不同,繼續(xù)取值然后再比較,直到更新完成。

    3. CAS缺點

  • 循環(huán)時間開銷很大
    通過看源碼,我們發(fā)現(xiàn)有個do while,如果CAS失敗,會一直進(jìn)行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。
  • 只能保證一個共享變量的原子操作
    當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作。但是,對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可用鎖來保證原子性。ABA問題概述
  • CAS會導(dǎo)致"ABA問題”
    CAS算法實現(xiàn)一個重要前提需要取出內(nèi)存中某時刻的數(shù)據(jù)并在當(dāng)下時刻比較并替換,那么在這個時間差類會導(dǎo)致數(shù)據(jù)的變化。
    比如說一個線程1 從內(nèi)存位置V中取出A,這時候另一個線程2 也從內(nèi)存中取出A,并且線程2 了一些操作將值變成了B,然后線程2 又將V位置的數(shù)據(jù)變成A,這時候線程1 進(jìn)行CAS作發(fā)現(xiàn)內(nèi)存中仍然是A,然后線程One操作成功。
    盡管線程One的CAS慢作成功,但是不代表這個過程就是沒有問題的。
    解決:解決ABA問題只靠CAS不能解決,還需要用到原子引用技術(shù)。即AtomicReference

  • 12. 線程同步方法

    1. 同步方法
    synchronized 關(guān)鍵字修飾方法。由于 java 的每個對象都有一個內(nèi)置鎖,當(dāng)用此關(guān)鍵字修飾方法時,內(nèi)置鎖會保護(hù)整個方法。在調(diào)用該方法前,需要獲得內(nèi)置鎖,否則就處于阻塞狀態(tài)。synchronized關(guān)鍵字也可以修飾靜態(tài)方法,此時如果調(diào)用該靜態(tài)方法,將會鎖住整個類.

    代碼如下:

    public synchronized void save(){}

    2. 同步代碼塊
    synchronized關(guān)鍵字修飾的語句塊。被該關(guān)鍵字修飾的語句塊會自動加上內(nèi)置鎖,從而實現(xiàn)同步。同步是一種高開銷的操作,因此應(yīng)該盡量減少同步內(nèi)容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關(guān)鍵代碼即可。

    代碼如下:

    synchronized(object){}

    3. 使用特殊域變量(volatile)實現(xiàn)線程同步

  • volatile關(guān)鍵字為域變量的訪問提供一種免鎖機(jī)制
  • 使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能被其他現(xiàn)象更新
  • 因此每次使用該域就要重新計算,而不是使用寄存器中的值
  • volatile不會提供任何原子操作,它也不能用來修飾final類型的變量
  • 4. 使用重入鎖實現(xiàn)線程同步
    在 javaSE5.0 新增了一個 java.concurrent 包來支持同步。ReentrantLock類可以重入、互斥、實現(xiàn)了Lock接口的鎖。

    5. 使用局部變量實現(xiàn)線程同步
    如果使用ThreadLocal管理變量,則每一個使用變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產(chǎn)生影響。

    ThreadLocal與同步機(jī)制:

  • ThreadLocal與同步機(jī)制都是為了解決多線程中相同變量的訪問沖突問題
  • 前者采用以“空間換時間”的方法,后者采用以“時間換空間”的方式
  • 6.使用阻塞隊列實現(xiàn)線程同步

    7.使用原子變量實現(xiàn)線程同步
    需要使用線程同步的根本原因在于對普通變量的操作不是原子的。

    原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作,即這幾種行為要么同時完成,要么都不完成。

    在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡化線程同步,其中AtomicInteger 表可以用原子方式更新int的值。


    13. ThreadLocal

    早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路。

    當(dāng)使用ThreadLocal維護(hù)變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。

    ThreadLocal是如何做到為每一個線程維護(hù)變量的副本的呢?在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應(yīng)線程的變量副本。

    ThreadLocal 和同步機(jī)制的比較
    ThreadLocal和線程同步機(jī)制都是為了解決多線程中相同變量的訪問沖突問題。

    在同步機(jī)制中,通過對象的鎖機(jī)制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機(jī)制要求程序慎密地分析什么時候?qū)ψ兞窟M(jìn)行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設(shè)計和編寫難度相對較大。

    而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數(shù)據(jù)的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進(jìn)ThreadLocal。

    概括起來說,對于多線程資源共享的問題,同步機(jī)制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。


    14. ReenTrantLock原理

    1. ReentrantLock基本概念

  • 主要利用CAS+AQS隊列來實現(xiàn)。
  • 是可重入鎖。可重入鎖是指同一個線程可以多次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。
  • 是可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應(yīng)中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。
  • 支持公平鎖與非公平鎖。synchronized是非公平鎖,而ReentrantLock的默認(rèn)實現(xiàn)是非公平鎖,但是也可以設(shè)置為公平鎖。
  • 與 synchronized 區(qū)別見上面 synchronized 筆記部分。


    15. 線程狀態(tài),start,run,wait,notify,yiled,sleep,join等方法的作用以及區(qū)別

    1. 線程狀態(tài)

    • NEW:線程剛創(chuàng)建
    • RUNNABLE:在 JVM 中正在運行的線程
    • BLOCKED:線程處于阻塞狀態(tài),等待監(jiān)視鎖
    • WAITING:等待狀態(tài)
    • TIMED_WAITING:等待指定的時間后重新被喚醒的狀態(tài)
    • TERMINATED:執(zhí)行完成

    2. start,run,wait,notify,yiled,sleep,join作用及區(qū)別

    1. run()和start()
    把需要處理的代碼放到run()方法中,start()方法啟動線程將自動調(diào)用run()方法,這個由java的內(nèi)存機(jī)制規(guī)定的。并且run()方法必需是public訪問權(quán)限,返回值類型為void。

    2. wait()
    wait()方法使當(dāng)前線程暫停執(zhí)行并釋放對象鎖標(biāo)示,讓其他線程可以進(jìn)入synchronized數(shù)據(jù)塊,當(dāng)前線程被放入對象等待池中。

    3. notify() 和 notifyAll()

  • notifyAll()會喚醒所有的線程,notify()之后會喚醒一個線程。
  • notifyAll()調(diào)用后,會將所有線程由等待池移到鎖池,然后參與鎖的競爭,競爭成功則繼續(xù)執(zhí)行,如果不成功則留在鎖池等待鎖被釋放后再次參與競爭。而notify()只會喚醒一個線程,具體喚醒哪一個線程由虛擬機(jī)控制。
  • 4. yiled()
    使用 yield() 的目的是讓具有相同優(yōu)先級或者更高優(yōu)先級的線程之間能夠適當(dāng)?shù)妮啌Q執(zhí)行。當(dāng)一個線程使用了yield( )方法之后,它就會把自己CPU執(zhí)行的時間讓掉,讓自己或者其它的線程運行。

    使當(dāng)前線程從執(zhí)行狀態(tài)(運行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。從而讓其它具有相同優(yōu)先級的等待線程獲取執(zhí)行權(quán)。但是,并不能保證在當(dāng)前線程調(diào)用yield()之后,其它具有相同優(yōu)先級的線程就一定能獲得執(zhí)行權(quán)。也有可能是當(dāng)前線程又進(jìn)入到“運行狀態(tài)”繼續(xù)運行。

    5. sleep()

  • Thread類,必須帶一個時間參數(shù)。
  • 使調(diào)用該方法的線程進(jìn)入停滯狀態(tài),所以執(zhí)行 sleep() 的線程在指定的時間內(nèi)肯定不會被執(zhí)行。
  • sleep(long)是不會釋放鎖標(biāo)志的,也就是說如果有synchronized同步塊,其他線程仍然不能訪問共享數(shù)據(jù)。
  • sleep(long)可使優(yōu)先級低的線程得到執(zhí)行的機(jī)會,當(dāng)然也可以讓同優(yōu)先級的線程有執(zhí)行的機(jī)會。
  • 該方法要捕捉異常
  • 用途:例如有兩個線程同時執(zhí)行(沒有synchronized)一個線程優(yōu)先級為MAX_PRIORITY,另一個為MIN_PRIORITY。如果沒有Sleep()方法,只有高優(yōu)先級的線程執(zhí)行完畢后,低優(yōu)先級的線程才能夠執(zhí)行;但是高優(yōu)先級的線程sleep(500)后,低優(yōu)先級就有機(jī)會執(zhí)行了
  • 總之,sleep()可以使低優(yōu)先級的線程得到執(zhí)行的機(jī)會,當(dāng)然也可以讓同優(yōu)先級、高優(yōu)先級的線程有執(zhí)行的機(jī)會。
  • 6. join()
    join方法的主要作用就是同步,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。在A線程中調(diào)用了B線程的 join() 方法時,表示只有當(dāng)B線程執(zhí)行完畢時,A線程才能繼續(xù)執(zhí)行。

    7. wait()和notify(),notifyAll()是Object類的方法,sleep()和yield()是Thread類的方法。

    8. 為什么wait和notify方法要在同步塊中調(diào)用
    wait()和notify()因為是線程之間的通信,它們存在競態(tài),會對對象的“鎖標(biāo)志”進(jìn)行操作,所以它們必需在Synchronized函數(shù)或者 synchronized block 中進(jìn)行調(diào)用。如果在non-synchronized 函數(shù)或 non-synchronized block 中進(jìn)行調(diào)用,雖然能編譯通過,但在運行時會發(fā)生IllegalMonitorStateException的異常。

    9. wait和sleep區(qū)別

  • sleep()方法是Thread的靜態(tài)方法,而wait是Object實例方法
  • wait()方法必須要在同步方法或者同步塊中調(diào)用,也就是必須已經(jīng)獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放占有的對象鎖,使得該線程進(jìn)入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU并不會釋放掉對象鎖;
  • sleep()方法在休眠時間達(dá)到后如果再次獲得CPU時間片就會繼續(xù)執(zhí)行,而wait()方法必須等待Object.notift/Object.notifyAll通知后,才會離開等待池,并且再次獲得CPU時間片才會繼續(xù)執(zhí)行。
  • sleep方法有可能會拋出異常,所以需要進(jìn)行異常處理;wait方法不需要處理。

  • 16. 關(guān)于 Atomic 原子類

    1. 介紹一下Atomic 原子類
    Atomic 翻譯成中文是原子的意思。 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不
    會被其他線程干擾。所以,所謂原子類說簡單點就是具有原子/原子操作特征的類。

    并發(fā)包 java.util.concurrent 的原子類都存放在 java.util.concurrent.atomic 下,如下圖所示。

    2. AtomicInteger 類的原理
    AtomicInteger 線程安全原理簡單分析
    AtomicInteger 類的部分源碼:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較并替換”的作用) private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); } } private volatile int value;

    AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免synchronized 的高開銷,執(zhí)行效率大為提升。

    CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內(nèi)存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內(nèi)存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。


    17. 線程池相關(guān)

    1. 為什么要用線程池?

    線程池提供了一種限制和管理資源(包括執(zhí)行一個任務(wù))的方式。 每個線程池還維護(hù)一些基本統(tǒng)計信息,例如已完成任務(wù)的數(shù)量。

    使用線程池的好處:

  • 降低資源消耗。 通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
  • 提高響應(yīng)速度。 當(dāng)任務(wù)到達(dá)時,任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
  • 提高線程的可管理性。 線程是稀缺資源,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
  • 2. 執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢?
  • execute() 方法用于提交不需要返回值的任務(wù),所以無法判斷任務(wù)是否被線程池執(zhí)行成功與否;
  • submit()方法用于提交需要返回值的任務(wù)。線程池會返回一個future類型的對象,通過這個future對象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值,get()方法會阻塞當(dāng)前線程直到任務(wù)完成,而使用get(long timeout,TimeUnit unit) 方法則會阻塞當(dāng)前線程一段時間后立即返回,這時候有可能任務(wù)沒有執(zhí)行完。
  • 3. 如何創(chuàng)建線程池

    《阿里巴巴Java開發(fā)手冊》中強制線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風(fēng)險。

    Executors 返回線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導(dǎo)致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會創(chuàng)建大量線程,從而導(dǎo)致OOM。
  • 方式一:通過構(gòu)造方法實現(xiàn)

    方式二:通過Executor 框架的工具類Executors來實現(xiàn),我們可以創(chuàng)建三種類型的ThreadPoolExecutor:

    • FixedThreadPool : 該方法返回一個固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當(dāng)有一個新的任務(wù)提交時,線程池中若有空閑線程,則立即執(zhí)行。若沒有,則新的任務(wù)會被暫存在一個任務(wù)隊列中,待有線程空閑時,便處理在任務(wù)隊列中的任務(wù)。
    • SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多余一個任務(wù)被提交到該線程池,任務(wù)會被保存在一個任務(wù)隊列中,待線程空閑,按先入先出的順序執(zhí)行隊列中的任務(wù)。
    • CachedThreadPool: 該方法返回一個可根據(jù)實際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復(fù)用,則會優(yōu)先使用可復(fù)用的線程。若所有線程均在工作,又有新的任務(wù)提交,則會創(chuàng)建新的線程處理任務(wù)。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線程池進(jìn)行復(fù)用。

    對應(yīng)Executors工具類中的方法如圖所示:

    4. 線程池構(gòu)造函數(shù)7大參數(shù)

  • corePoolSize:線程池中的常駐核心線程數(shù)
    在創(chuàng)建了線程池后,當(dāng)有請求任務(wù)來之后,就會安排池中的線程去執(zhí)行請求任務(wù),近似理解為今日當(dāng)值線程。當(dāng)線程池中的線程數(shù)目達(dá)到corePoolSize后,就會把到達(dá)的任務(wù)放到緩存隊列當(dāng)中。
  • maximumPoolSize:線程池能夠容納同時執(zhí)行的最大線程數(shù),此值必須大于等于1
  • keepAliveTime:多余的空閑線程的存活時間
    當(dāng)空閑時間達(dá)到keepAIiveTime值時,多余空閑線程會被銷毀直到只剩下corePoolSize個線程為止
  • unit:keepAIiveTime的單位
  • workQueue:任務(wù)隊列,被提交但尚未被執(zhí)行的任務(wù)。
  • threadFactory: 表示生成線程池中工作線程的線程工廠,用于創(chuàng)建線程一般用默認(rèn)的即可。
  • handIer:拒絕策略,表示當(dāng)隊列滿了并且工作線程大于等于線程池的最大線程數(shù) (maximumPoolSize) 處理方式
  • 5. 線程池處理任務(wù)過程
  • 在創(chuàng)建了線程池后,等待提交過來的任務(wù)請求。

  • 當(dāng)調(diào)用execute()方法添加一個請求任務(wù)時,線程池會做如下判斷:
    2.1 如果正在運行的線程數(shù)量小于corePoolSi,那么馬上創(chuàng)建線程運行這個任務(wù):
    2.2 如果正在運行的線程數(shù)量大于或等于corePoolSize,那么將這個任務(wù)放入隊列;
    2.3 如果這時候隊列滿了且正在運行的線程數(shù)量還小maximumPoolSize,那么還是要創(chuàng)建非核心線程立刻運行這個任務(wù):
    2.4 如果隊列滿了且正在運行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會啟動飽和拒絕策略來執(zhí)行。

  • 當(dāng)一個線程完成在務(wù)時,它會從隊列中取下一個任務(wù)來執(zhí)行。

  • 當(dāng)一個線程無事可做超過一定的時間(keepAliveTime)時,線程池會判斷:
    如果當(dāng)前運行的線程數(shù)大于corePoolSize,那么這個線程就被停掉。所以線程池的所有任務(wù)完成后它最終會收縮到corePoolSize的大小。

  • 6. 線程池的4種拒絕策略理論概述

    拒絕策略概述:等待隊列已經(jīng)滿了,再也塞不下新任務(wù)了,同時,線程池中的max線程也達(dá)到了,無法繼續(xù)為新任務(wù)服務(wù)。這時候我們就需要拒絕策略機(jī)制合理處理這個問題。

    4種JDK內(nèi)置拒絕策略

    • AbortPolicy(默認(rèn)):直接拋出RejectedExecutionException異常阻止系統(tǒng)正常運行。
    • CallerRunsPolicy:"調(diào)用者運行"一種調(diào)節(jié)機(jī)制,該策略既不會拋棄任務(wù),也不會拋出異常,而是將某些任務(wù)回退到調(diào)用者,從而降低新任務(wù)的流量。
    • DiscardOldestPolicy:拋棄隊列中等待最久的任務(wù),然后把當(dāng)前任務(wù)加入隊列中嘗試再次提交當(dāng)前任務(wù)。
    • DiscardPolicy:直接丟棄任務(wù),不予任何處理也不拋出異常。如果允許任務(wù)丟失,這是最好的一種方案。

    以上內(nèi)置拒絕策略均實現(xiàn)了RejectedExecutionHandler接口

    7. 線程池配置合理線程數(shù)

    1. 要合理配置線程數(shù)首先要知道公司服務(wù)器或阿里云是幾核的
    代碼查看服務(wù)器核數(shù):

    System.out.println(Runtime.getRuntime().availableProcessors());

    比如我的CPU核數(shù)4核,執(zhí)行結(jié)果:

    2. 看是CPU密集型還是 IO密集型任務(wù)線程

    1. CPU密集型

  • CPU密集的意思是該任務(wù)需要大量的運算,而沒有阻塞,CPU一直全速運行。
  • CPU密集任務(wù)只有在真正的多核CPU上才可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程該任務(wù)都不可能得到加速,因為CPU總的運算能力就那些。
  • CPU密集型任務(wù)配置盡可能少的線程數(shù)量:
    公式:CPU核數(shù)+1個線程的線程池
  • 2. IO密集型
    方法一:
    由于IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如 CPU核數(shù)*2

    方法二:

  • IO密集型,即該任務(wù)需要大量的IO,即大量的阻塞。
  • 在單線程上運IO密集型的任務(wù)會導(dǎo)致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務(wù)中使用多線程可以大大的加速程序運行,即使在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
  • IO密集型時,大部分線程都阻塞,故需要多配置線程數(shù):
    參考公式:CPU核數(shù)/(1-阻系數(shù)) 比如8核CPU:8/(1-0.9)=80個線程數(shù),阻塞系數(shù)在0.8~0.9之間

  • 18. 手寫簡單的線程池,體現(xiàn)線程復(fù)用

    19. 手寫消費者生產(chǎn)者模式

    20. 手寫阻塞隊列

    21. 手寫多線程交替打印ABC

    總結(jié)

    以上是生活随笔為你收集整理的面经——多线程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。