Java 多線程

1. 什么是線程和進(jìn)程?

1.1 進(jìn)程

進(jìn)程是程序的一次執(zhí)行過程预吆,是系統(tǒng)運(yùn)行程序的基本單位,因此進(jìn)程是動(dòng)態(tài)的浊吏。系統(tǒng)運(yùn)行一個(gè)程序即是一個(gè)進(jìn)程從創(chuàng)建掀序,運(yùn)行到消亡的過程帆焕。

1.2 線程

線程與進(jìn)程相似,但線程是一個(gè)比進(jìn)程更小的執(zhí)行單位不恭。一個(gè)進(jìn)程在其執(zhí)行的過程中可以產(chǎn)生多個(gè)線程叶雹。與進(jìn)程不同的是同類的多個(gè)線程共享進(jìn)程的堆和方法區(qū)資源,但每個(gè)線程有自己的程序計(jì)數(shù)器换吧、虛擬機(jī)棧和本地方法棧折晦,所以系統(tǒng)在產(chǎn)生一個(gè)線程,或是在各個(gè)線程之間作切換工作時(shí)沾瓦,負(fù)擔(dān)要比進(jìn)程小得多满着,也正因?yàn)槿绱耍€程也被稱為輕量級(jí)進(jìn)程贯莺。


image.png

從上圖可以看出:一個(gè)進(jìn)程中可以有多個(gè)線程风喇,多個(gè)線程共享進(jìn)程的堆和方法區(qū) (JDK1.8 之后的元空間)資源,但是每個(gè)線程有自己的程序計(jì)數(shù)器缕探、虛擬機(jī)棧 和 本地方法棧魂莫。

總結(jié): 線程 是 進(jìn)程 劃分成的更小的運(yùn)行單位。線程和進(jìn)程最大的不同在于基本上各進(jìn)程是獨(dú)立的爹耗,而各線程則不一定耙考,因?yàn)橥贿M(jìn)程中的線程極有可能會(huì)相互影響。線程執(zhí)行開銷小潭兽,但不利于資源的管理和保護(hù)琳骡;而進(jìn)程正相反

1.2.1 程序結(jié)束器為什么是私有的?

程序計(jì)數(shù)器主要有下面兩個(gè)作用:

  1. 字節(jié)碼解釋器通過改變程序計(jì)數(shù)器來依次讀取指令讼溺,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行最易、選擇怒坯、循環(huán)、異常處理藻懒。
  2. 在多線程的情況下剔猿,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置,從而當(dāng)線程被切換回來的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了嬉荆。
    需要注意的是归敬,如果執(zhí)行的是 native 方法,那么程序計(jì)數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時(shí)程序計(jì)數(shù)器記錄的才是下一條指令的地址汪茧。

所以椅亚,程序計(jì)數(shù)器私有主要是為了線程切換后能恢復(fù)到正確的執(zhí)行位置。

1.2.2 堆 和 方法區(qū)

堆和方法區(qū)是所有線程共享的資源舱污,其中堆是進(jìn)程中最大的一塊內(nèi)存呀舔,主要用于存放新創(chuàng)建的對(duì)象 (所有對(duì)象都在這里分配內(nèi)存),方法區(qū)主要用于存放已被加載的類信息扩灯、常量媚赖、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)珠插。

2. 使用多線程可能帶來的問題

并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度惧磺,但是并發(fā)編程并不總是能提高程序運(yùn)行速度的,而且并發(fā)編程可能會(huì)遇到很多問題捻撑,比如:內(nèi)存泄漏磨隘、上下文切換、死鎖 布讹。
1.使用ThreadLocal不當(dāng)可能會(huì)導(dǎo)致內(nèi)存泄露
2.使用ThreadLocal不當(dāng)可能會(huì)導(dǎo)致內(nèi)存泄露

3. 什么是上下文切換琳拭?

多線程編程中一般線程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線程使用描验,為了讓這些線程都能得到有效執(zhí)行白嘁,CPU 采取的策略是為每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線程使用膘流,這個(gè)過程就屬于一次上下文切換絮缅。

概括來說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換回這個(gè)任務(wù)時(shí)呼股,可以再加載這個(gè)任務(wù)的狀態(tài)耕魄。任務(wù)從保存到再加載的過程就是一次上下文切換。

上下文切換通常是計(jì)算密集型的彭谁。也就是說吸奴,它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中缠局,每次切換都需要納秒量級(jí)的時(shí)間则奥。所以,上下文切換對(duì)系統(tǒng)來說意味著消耗大量的 CPU 時(shí)間狭园,事實(shí)上读处,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。

Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn)唱矛,其中有一項(xiàng)就是罚舱,其上下文切換和模式切換的時(shí)間消耗非常少井辜。

4. 死鎖

4.1 死鎖的概念

線程死鎖描述的是這樣一種情況:多個(gè)線程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放管闷。由于線程被無限期地阻塞粥脚,因此程序不可能正常終止。

如下圖所示渐北,線程 A 持有資源 2阿逃,線程 B 持有資源 1,他們同時(shí)都想申請(qǐng)對(duì)方的資源赃蛛,所以這兩個(gè)線程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)恃锉。


image.png

下面通過一個(gè)例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源于《并發(fā)編程之美》):

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

Output

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

線程 A 通過 synchronized (resource1) 獲得 resource1 的監(jiān)視器鎖,然后通過Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執(zhí)行然后獲取到 resource2 的監(jiān)視器鎖呕臂。線程 A 和線程 B 休眠結(jié)束了都開始企圖請(qǐng)求獲取對(duì)方的資源破托,然后這兩個(gè)線程就會(huì)陷入互相等待的狀態(tài),這也就產(chǎn)生了死鎖歧蒋。上面的例子符合產(chǎn)生死鎖的四個(gè)必要條件土砂。

學(xué)過操作系統(tǒng)的朋友都知道產(chǎn)生死鎖必須具備以下四個(gè)條件:

  1. 互斥條件:該資源任意一個(gè)時(shí)刻只由一個(gè)線程占用。
  2. 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí)谜洽,對(duì)已獲得的資源保持不放萝映。
  3. 不可剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強(qiáng)行剝奪,只有自己使用完畢后才釋放資源阐虚。
  4. 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系序臂。

4.2 如何避免線程死鎖

上面說了產(chǎn)生死鎖的四個(gè)必要條件,為了避免死鎖实束,我們只要破壞產(chǎn)生死鎖的四個(gè)條件中的其中一個(gè)就可以了“赂眩現(xiàn)在我們來挨個(gè)分析一下:

  1. 破壞互斥條件 :這個(gè)條件我們沒有辦法破壞,因?yàn)槲覀冇面i本來就是想讓他們互斥的(臨界資源需要互斥訪問)咸灿。
  2. 破壞請(qǐng)求與保持條件 :一次性申請(qǐng)所有的資源构订。
  3. 破壞不可剝奪條件 :占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到避矢,可以主動(dòng)釋放它占有的資源悼瘾。
  4. 破壞循環(huán)等待條件 :靠按序申請(qǐng)資源來預(yù)防。按某一順序申請(qǐng)資源审胸,釋放資源則反序釋放分尸。破壞循環(huán)等待條件。

我們對(duì)線程 2 的代碼修改成下面這樣就不會(huì)產(chǎn)生死鎖了歹嘹。

        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();

Output

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

Process finished with exit code 0

我們分析一下上面的代碼為什么避免了死鎖的發(fā)生?

線程 1 首先獲得到 resource1 的監(jiān)視器鎖,這時(shí)候線程 2 就獲取不到了。然后線程 1 再去獲取 resource2 的監(jiān)視器鎖孔庭,可以獲取到尺上。然后線程 1 釋放了對(duì) resource1材蛛、resource2 的監(jiān)視器鎖的占用,線程 2 獲取到就可以執(zhí)行了怎抛。這樣就破壞了破壞循環(huán)等待條件卑吭,因此避免了死鎖。

5. sleep() 和 wait() 的區(qū)別 和 共同點(diǎn)

  1. 兩者最主要的區(qū)別在于:sleep() 方法沒有釋放鎖马绝,而 wait() 方法釋放了鎖
  2. 兩者都可以暫停線程的執(zhí)行豆赏。
    wait() 通常被用于線程間交互/通信,sleep()通常被用于暫停執(zhí)行富稻。
    wait() 方法被調(diào)用后掷邦,線程不會(huì)自動(dòng)蘇醒,需要?jiǎng)e的線程調(diào)用同一個(gè)對(duì)象上的 notify()或者 notifyAll() 方法椭赋。sleep()方法執(zhí)行完成后抚岗,線程會(huì)自動(dòng)蘇醒∧恼或者可以使用 wait(long timeout) 超時(shí)后線程會(huì)自動(dòng)蘇醒宣蔚。

6. 為什么我們調(diào)用 start() 方法時(shí)會(huì)執(zhí)行 run() 方法,為什么我們不能直接調(diào)用 run() 方法认境?

new 一個(gè) Thread胚委,線程進(jìn)入了新建狀態(tài)。調(diào)用 start()方法叉信,會(huì)啟動(dòng)一個(gè)線程并使線程進(jìn)入了就緒狀態(tài)亩冬,當(dāng)分配到時(shí)間片后就可以開始運(yùn)行了。 start() 會(huì)執(zhí)行線程的相應(yīng)準(zhǔn)備工作茉盏,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容鉴未,這是真正的多線程工作。 但是鸠姨,直接執(zhí)行 run() 方法铜秆,會(huì)把 run() 方法當(dāng)成一個(gè) main 線程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線程中執(zhí)行它讶迁,所以這并不是多線程工作连茧。

總結(jié): 調(diào)用 start() 方法方可啟動(dòng)線程并使線程進(jìn)入就緒狀態(tài),直接執(zhí)行 run() 方法的話不會(huì)以多線程的方式執(zhí)行巍糯。

7. 對(duì) synchronized 關(guān)鍵字的了解

synchronized 關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性啸驯,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線程執(zhí)行。

另外祟峦,在 Java 早期版本中罚斗,synchronized 屬于 重量級(jí)鎖,效率低下宅楞。

為什么呢针姿?

因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的袱吆,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個(gè)線程距淫,都需要操作系統(tǒng)幫忙完成绞绒,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間榕暇,時(shí)間成本相對(duì)較高蓬衡。

慶幸的是在 Java 6 之后 Java 官方對(duì)從 JVM 層面對(duì) synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯(cuò)了彤枢。JDK1.6 對(duì)鎖的實(shí)現(xiàn)引入了大量的優(yōu)化狰晚,如自旋鎖、適應(yīng)性自旋鎖堂污、鎖消除家肯、鎖粗化、偏向鎖盟猖、輕量級(jí)鎖等技術(shù)來減少鎖操作的開銷讨衣。

7.1 “單例模式了解嗎?手寫一下式镐!解釋一下雙重檢驗(yàn)鎖方式實(shí)現(xiàn)單例模式的原理唄反镇!”

雙重校驗(yàn)鎖實(shí)現(xiàn)對(duì)象單例(線程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

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

另外娘汞,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要歹茶。

uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:

  1. 為 uniqueInstance 分配內(nèi)存空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的內(nèi)存地址

但是由于 JVM 具有指令重排的特性你弦,執(zhí)行順序有可能變成 1->3->2惊豺。指令重排在單線程環(huán)境下不會(huì)出現(xiàn)問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例禽作。例如尸昧,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空旷偿,因此返回 uniqueInstance烹俗,但此時(shí) uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排萍程,保證在多線程環(huán)境下也能正常運(yùn)行幢妄。

7.2 構(gòu)造方法可以使用 synchronized 關(guān)鍵字修飾么?

先說結(jié)論:構(gòu)造方法不能使用 synchronized 關(guān)鍵字修飾。

構(gòu)造方法本身就屬于線程安全的茫负,不存在同步的構(gòu)造方法一說蕉鸳。

7.3 synchronized 關(guān)鍵字的底層原理

總結(jié):
synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置忍法,monitorexit 指令則指明同步代碼塊的結(jié)束位置潮尝。

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令无虚,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法衍锚。

不過兩者的本質(zhì)都是對(duì)對(duì)象監(jiān)視器 monitor 的獲取。

8. 講一下 JMM(Java 內(nèi)存模型)

在 JDK1.2 之前嗤堰,Java 的內(nèi)存模型實(shí)現(xiàn)總是從主存(即共享內(nèi)存)讀取變量戴质,是不需要進(jìn)行特別的注意的。而在當(dāng)前的 Java 內(nèi)存模型下踢匣,線程可以把變量保存本地內(nèi)存(比如機(jī)器的寄存器)中告匠,而不是直接在主存中進(jìn)行讀寫。這就可能造成一個(gè)線程在主存中修改了一個(gè)變量的值离唬,而另外一個(gè)線程還繼續(xù)使用它在寄存器中的變量值的拷貝后专,造成數(shù)據(jù)的不一致羽戒。

image.png

要解決這個(gè)問題凉馆,就需要把變量聲明為volatile,這就指示 JVM浦辨,這個(gè)變量是共享且不穩(wěn)定的嫂用,每次使用它都到主存中進(jìn)行讀取型凳。

所以,volatile 關(guān)鍵字 除了防止 JVM 的指令重排 嘱函,還有一個(gè)重要的作用就是保證變量的可見性甘畅。

image.png

8.1 synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別

synchronized 關(guān)鍵字和 volatile 關(guān)鍵字是兩個(gè)互補(bǔ)的存在,而不是對(duì)立的存在往弓!

  1. volatile 關(guān)鍵字是線程同步的輕量級(jí)實(shí)現(xiàn)疏唾,所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile 關(guān)鍵字只能用于變量而 synchronized 關(guān)鍵字可以修飾方法以及代碼塊函似。
  2. volatile 關(guān)鍵字能保證數(shù)據(jù)的可見性槐脏,但不能保證數(shù)據(jù)的原子性。synchronized 關(guān)鍵字兩者都能保證缴淋。
  3. volatile關(guān)鍵字主要用于解決變量在多個(gè)線程之間的可見性准给,而 synchronized 關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性。

9. ThreadLocal 了解嗎重抖?

通常情況下露氮,我們創(chuàng)建的變量是可以被任何一個(gè)線程訪問并修改的。如果想實(shí)現(xiàn)每一個(gè)線程都有自己的專屬本地變量該如何解決呢钟沛? JDK 中提供的ThreadLocal類正是為了解決這樣的問題畔规。 ThreadLocal類主要解決的就是讓每個(gè)線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數(shù)據(jù)的盒子恨统,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)叁扫。

如果你創(chuàng)建了一個(gè)ThreadLocal變量三妈,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的本地副本,這也是ThreadLocal變量名的由來莫绣。他們可以使用 get() 和 set() 方法來獲取默認(rèn)值或?qū)⑵渲蹈臑楫?dāng)前線程所存的副本的值畴蒲,從而避免了線程安全問題。

9.1 ThreadLocal 原理

從 Thread類源代碼入手对室。

public class Thread implements Runnable {
 ......
//與此線程有關(guān)的ThreadLocal值模燥。由ThreadLocal類維護(hù)
ThreadLocal.ThreadLocalMap threadLocals = null;

//與此線程有關(guān)的InheritableThreadLocal值。由InheritableThreadLocal類維護(hù)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

從上面Thread類 源代碼可以看出Thread 類中有一個(gè) threadLocals 和 一個(gè) inheritableThreadLocals 變量掩宜,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實(shí)現(xiàn)的定制化的 HashMap蔫骂。默認(rèn)情況下這兩個(gè)變量都是 null,只有當(dāng)前線程調(diào)用 ThreadLocal 類的 set或get方法時(shí)才創(chuàng)建它們牺汤,實(shí)際上調(diào)用這兩個(gè)方法的時(shí)候辽旋,我們調(diào)用的是ThreadLocalMap類對(duì)應(yīng)的 get()、set()方法檐迟。

ThreadLocal類的set()方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

通過上面這些內(nèi)容补胚,我們足以通過猜測(cè)得出結(jié)論:最終的變量是放在了當(dāng)前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上锅减,ThreadLocal 可以理解為只是ThreadLocalMap的封裝糖儡,傳遞了變量值。 ThrealLocal 類中可以通過Thread.currentThread()獲取到當(dāng)前線程對(duì)象后怔匣,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對(duì)象握联。

每個(gè)Thread中都具備一個(gè)ThreadLocalMap,而ThreadLocalMap可以存儲(chǔ)以ThreadLocal為 key 每瞒,Object 對(duì)象為 value 的鍵值對(duì)金闽。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 ......
}

比如我們?cè)谕粋€(gè)線程中聲明了兩個(gè) ThreadLocal 對(duì)象的話,會(huì)使用 Thread內(nèi)部都是使用僅有那個(gè)ThreadLocalMap 存放數(shù)據(jù)的剿骨,ThreadLocalMap的 key 就是 ThreadLocal對(duì)象代芜,value 就是 ThreadLocal 對(duì)象調(diào)用set方法設(shè)置的值。

ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類浓利。

9.2 ThreadLocal 內(nèi)存泄露問題了解不挤庇?

ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強(qiáng)引用。所以贷掖,如果 ThreadLocal 沒有被外部強(qiáng)引用的情況下嫡秕,在垃圾回收的時(shí)候,key 會(huì)被清理掉苹威,而 value 不會(huì)被清理掉昆咽。這樣一來,ThreadLocalMap 中就會(huì)出現(xiàn) key 為 null 的 Entry。假如我們不做任何措施的話掷酗,value 永遠(yuǎn)無法被 GC 回收调违,這個(gè)時(shí)候就可能會(huì)產(chǎn)生內(nèi)存泄露。ThreadLocalMap 實(shí)現(xiàn)中已經(jīng)考慮了這種情況泻轰,在調(diào)用 set()技肩、get()、remove() 方法的時(shí)候浮声,會(huì)清理掉 key 為 null 的記錄亩鬼。使用完 ThreadLocal方法后 最好手動(dòng)調(diào)用remove()方法

      static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

如果一個(gè)對(duì)象只具有弱引用,那就類似于可有可無的生活用品阿蝶。弱引用與軟引用的區(qū)別在于:只具有弱引用的對(duì)象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內(nèi)存區(qū)域的過程中黄绩,一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象羡洁,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存爽丹。不過筑煮,由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程, 因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象粤蝎。

弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用真仲,如果弱引用所引用的對(duì)象被垃圾回收,Java 虛擬機(jī)就會(huì)把這個(gè)弱引用加入到與之關(guān)聯(lián)的引用隊(duì)列中初澎。

10. 線程池

10.1 為什么要使用線程池秸应?

池化技術(shù)相比大家已經(jīng)屢見不鮮了,線程池碑宴、數(shù)據(jù)庫連接池软啼、Http 連接池等等都是對(duì)這個(gè)思想的應(yīng)用。池化技術(shù)的思想主要是為了減少每次獲取資源的消耗延柠,提高對(duì)資源的利用率祸挪。

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

這里借用《Java 并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:

  1. 降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗增热。
  2. 提高響應(yīng)速度整以。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行钓葫。
  3. 提高線程的可管理性悄蕾。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源帆调,還會(huì)降低系統(tǒng)的穩(wěn)定性奠骄,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控番刊。

10.2 實(shí)現(xiàn) Runnable 接口和 Callable 接口的區(qū)別

Runnable自 Java 1.0 以來一直存在含鳞,但Callable僅在 Java 1.5 中引入,目的就是為了來處理Runnable不支持的用例。**Runnable 接口不會(huì)返回結(jié)果或拋出檢查異常芹务,但是Callable 接口可以蝉绷。所以,如果任務(wù)不需要返回結(jié)果或拋出異常推薦使用 **Runnable 接口枣抱,這樣代碼看起來會(huì)更加簡(jiǎn)潔熔吗。

工具類 Executors 可以實(shí)現(xiàn) Runnable 對(duì)象和 Callable 對(duì)象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)或 Executors.callable(Runnable task佳晶,Object resule))桅狠。

Runnable.java

@FunctionalInterface
public interface Runnable {
   /**
    * 被線程執(zhí)行,沒有返回值也無法拋出異常
    */
    public abstract void run();
}

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * 計(jì)算結(jié)果轿秧,或在無法這樣做時(shí)拋出異常中跌。
     * @return 計(jì)算得出的結(jié)果
     * @throws 如果無法計(jì)算結(jié)果,則拋出異常
     */
    V call() throws Exception;
}

10.3 執(zhí)行 execute()方法和 submit()方法的區(qū)別是什么呢菇篡?

  1. execute()方法用于提交不需要返回值的任務(wù)漩符,所以無法判斷任務(wù)是否被線程池執(zhí)行成功與否;
  2. submit()方法用于提交需要返回值的任務(wù)驱还。線程池會(huì)返回一個(gè) Future 類型的對(duì)象嗜暴,通過這個(gè) Future 對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過 Future 的 get()方法來獲取返回值议蟆,get()方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成灼伤,而使用 get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回咪鲜,這時(shí)候有可能任務(wù)沒有執(zhí)行完狐赡。

10.4 如何創(chuàng)建線程池?

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

Executors 返回線程池對(duì)象的弊端如下:

  1. FixedThreadPool 和 SingleThreadExecutor : 允許請(qǐng)求的隊(duì)列長度為 Integer.MAX_VALUE 享郊,可能堆積大量的請(qǐng)求览祖,從而導(dǎo)致 OOM。
  2. CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE 炊琉,可能會(huì)創(chuàng)建大量線程展蒂,從而導(dǎo)致 OOM又活。

方式一:通過構(gòu)造方法實(shí)現(xiàn)


image.png

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

  1. FixedThreadPool : 該方法返回一個(gè)固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變锰悼。當(dāng)有一個(gè)新的任務(wù)提交時(shí)柳骄,線程池中若有空閑線程,則立即執(zhí)行箕般。若沒有耐薯,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列中,待有線程空閑時(shí)丝里,便處理在任務(wù)隊(duì)列中的任務(wù)曲初。
  2. SingleThreadExecutor: 方法返回一個(gè)只有一個(gè)線程的線程池。若多余一個(gè)任務(wù)被提交到該線程池杯聚,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列中臼婆,待線程空閑,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)幌绍。
  3. CachedThreadPool: 該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線程數(shù)量的線程池目锭。線程池的線程數(shù)量不確定,但若有空閑線程可以復(fù)用纷捞,則會(huì)優(yōu)先使用可復(fù)用的線程。若所有線程均在工作被去,又有新的任務(wù)提交主儡,則會(huì)創(chuàng)建新的線程處理任務(wù)。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后惨缆,將返回線程池進(jìn)行復(fù)用糜值。

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


image.png
image.png

11. ThreadPoolExecutor 類分析

ThreadPoolExecutor 類中提供的四個(gè)構(gòu)造方法。我們來看最長的那個(gè)坯墨,其余三個(gè)都是在這個(gè)構(gòu)造方法的基礎(chǔ)上產(chǎn)生(其他幾個(gè)構(gòu)造方法說白點(diǎn)都是給定某些默認(rèn)參數(shù)的構(gòu)造方法比如默認(rèn)制定拒絕策略是什么)寂汇,這里就不貼代碼講了,比較簡(jiǎn)單捣染。

    /**
     * 用給定的初始參數(shù)創(chuàng)建一個(gè)新的ThreadPoolExecutor骄瓣。
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面這些對(duì)創(chuàng)建 非常重要,在后面使用線程池的過程中你一定會(huì)用到耍攘!

11.1 ThreadPoolExecutor`構(gòu)造函數(shù)重要參數(shù)分析

ThreadPoolExecutor 3 個(gè)最重要的參數(shù):

  1. corePoolSize : 核心線程數(shù)線程數(shù)定義了最小可以同時(shí)運(yùn)行的線程數(shù)量榕栏。
  2. maximumPoolSize : 當(dāng)隊(duì)列中存放的任務(wù)達(dá)到隊(duì)列容量的時(shí)候,當(dāng)前可以同時(shí)運(yùn)行的線程數(shù)量變?yōu)樽畲缶€程數(shù)蕾各。
  3. workQueue: 當(dāng)新任務(wù)來的時(shí)候會(huì)先判斷當(dāng)前運(yùn)行的線程數(shù)量是否達(dá)到核心線程數(shù)扒磁,如果達(dá)到的話,新任務(wù)就會(huì)被存放在隊(duì)列中式曲。

ThreadPoolExecutor其他常見參數(shù):

  1. keepAliveTime:當(dāng)線程池中的線程數(shù)量大于 corePoolSize 的時(shí)候妨托,如果這時(shí)沒有新的任務(wù)提交,核心線程外的線程不會(huì)立即銷毀,而是會(huì)等待兰伤,直到等待的時(shí)間超過了 keepAliveTime才會(huì)被回收銷毀内颗;
  2. unit : keepAliveTime 參數(shù)的時(shí)間單位。
  3. threadFactory :executor 創(chuàng)建新線程的時(shí)候會(huì)用到医清。
  4. handler :飽和策略起暮。關(guān)于飽和策略下面單獨(dú)介紹一下。

11.2 ThreadPoolExecutor 飽和策略

ThreadPoolExecutor 飽和策略定義:
如果當(dāng)前同時(shí)運(yùn)行的線程數(shù)量達(dá)到最大線程數(shù)量并且隊(duì)列也已經(jīng)被放滿了任務(wù)時(shí)会烙,ThreadPoolTaskExecutor 定義一些策略:

  1. ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務(wù)的處理负懦。
  2. ThreadPoolExecutor.CallerRunsPolicy:調(diào)用執(zhí)行自己的線程運(yùn)行任務(wù)。您不會(huì)任務(wù)請(qǐng)求柏腻。但是這種策略會(huì)降低對(duì)于新任務(wù)提交速度纸厉,影響程序的整體性能。另外五嫂,這個(gè)策略喜歡增加隊(duì)列容量颗品。如果您的應(yīng)用程序可以承受此延遲并且你不能任務(wù)丟棄任何一個(gè)任務(wù)請(qǐng)求的話,你可以選擇這個(gè)策略沃缘。
  3. ThreadPoolExecutor.DiscardPolicy: 不處理新任務(wù)躯枢,直接丟棄掉。
  4. ThreadPoolExecutor.DiscardOldestPolicy: 此策略將丟棄最早的未處理的任務(wù)請(qǐng)求槐臀。

11.3 線程池原理分析

image.png

12. 介紹一下 Atomic 原子類

Atomic 翻譯成中文是原子的意思锄蹂。在化學(xué)上,我們知道原子是構(gòu)成一般物質(zhì)的最小單位水慨,在化學(xué)反應(yīng)中是不可分割的得糜。在我們這里 Atomic 是指一個(gè)操作是不可中斷的。即使是在多個(gè)線程一起執(zhí)行的時(shí)候晰洒,一個(gè)操作一旦開始朝抖,就不會(huì)被其他線程干擾。

所以谍珊,所謂原子類說簡(jiǎn)單點(diǎn)就是具有原子/原子操作特征的類治宣。

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

image.png

12.1 JUC 包中的原子類是哪 4 類?

  1. 基本類型
    使用原子的方式更新基本類型
    AtomicInteger:整形原子類
    AtomicLong:長整型原子類
    AtomicBoolean:布爾型原子類

  2. 數(shù)組類型
    使用原子的方式更新數(shù)組里的某個(gè)元素
    AtomicIntegerArray:整形數(shù)組原子類
    AtomicLongArray:長整形數(shù)組原子類
    AtomicReferenceArray:引用類型數(shù)組原子類

  3. 引用類型
    AtomicReference:引用類型原子類
    AtomicStampedReference:原子更新帶有版本號(hào)的引用類型砌滞。該類將整數(shù)值與引用關(guān)聯(lián)起來炼七,可用于解決原子的更新數(shù)據(jù)和數(shù)據(jù)的版本號(hào),可以解決使用 CAS 進(jìn)行原子更新時(shí)可能出現(xiàn)的 ABA 問題布持。
    AtomicMarkableReference :原子更新帶有標(biāo)記位的引用類型

  4. 對(duì)象的屬性修改類型
    AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    AtomicLongFieldUpdater:原子更新長整形字段的更新器
    AtomicReferenceFieldUpdater:原子更新引用類型字段的更新器

13. AQS 了解么豌拙?

AQS 的全稱為(AbstractQueuedSynchronizer),這個(gè)類在java.util.concurrent.locks包下面题暖。

image.png

AQS 是一個(gè)用來構(gòu)建鎖和同步器的框架按傅,使用 AQS 能簡(jiǎn)單且高效地構(gòu)造出應(yīng)用廣泛的大量的同步器捉超,比如我們提到的 ReentrantLock,Semaphore唯绍,其他的諸如 ReentrantReadWriteLock拼岳,SynchronousQueue,F(xiàn)utureTask 等等皆是基于 AQS 的况芒。當(dāng)然惜纸,我們自己也能利用 AQS 非常輕松容易地構(gòu)造出符合我們自己需求的同步器。

13.1 AQS 原理了解么绝骚?

在面試中被問到并發(fā)知識(shí)的時(shí)候耐版,大多都會(huì)被問到“請(qǐng)你說一下自己對(duì)于 AQS 原理的理解”。下面給大家一個(gè)示例供大家參加压汪,面試不是背題粪牲,大家一定要加入自己的思想,即使加入不了自己的思想也要保證自己能夠通俗的講出來而不是背出來止剖。

13.2 AQS 原理概覽

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

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

image.png

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

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

狀態(tài)信息通過 protected 類型的 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);
}

13.3 AQS 對(duì)資源的共享方式

AQS 定義兩種資源共享方式

  1. Exclusive(獨(dú)占):只有一個(gè)線程能執(zhí)行较坛,如 ReentrantLock。又可分為公平鎖和非公平鎖:
    公平鎖:按照線程在隊(duì)列中的排隊(duì)順序扒最,先到者先拿到鎖
    非公平鎖:當(dāng)線程要獲取鎖時(shí)丑勤,無視隊(duì)列順序直接去搶鎖,誰搶到就是誰的
  2. Share(共享):多個(gè)線程可同時(shí)執(zhí)行吧趣,如CountDownLatch法竞、Semaphore耙厚、CountDownLatch、 CyclicBarrier岔霸、ReadWriteLock 我們都會(huì)在后面講到薛躬。

ReentrantReadWriteLock 可以看成是組合式,因?yàn)?ReentrantReadWriteLock 也就是讀寫鎖允許多個(gè)線程同時(shí)對(duì)某一資源進(jìn)行讀呆细。

這和我們以往通過實(shí)現(xiàn)接口的方式有很大區(qū)別型宝,這是模板方法模式很經(jīng)典的一個(gè)運(yùn)用。

AQS 使用了模板方法模式絮爷,自定義同步器時(shí)需要重寫下面幾個(gè) AQS 提供的模板方法:

isHeldExclusively()//該線程是否正在獨(dú)占資源趴酣。只有用到condition才需要去實(shí)現(xiàn)它。
tryAcquire(int)//獨(dú)占方式略水。嘗試獲取資源价卤,成功則返回true,失敗則返回false渊涝。
tryRelease(int)//獨(dú)占方式慎璧。嘗試釋放資源,成功則返回true跨释,失敗則返回false胸私。
tryAcquireShared(int)//共享方式。嘗試獲取資源鳖谈。負(fù)數(shù)表示失斔晏邸;0表示成功缆娃,但沒有剩余可用資源捷绒;正數(shù)表示成功,且有剩余資源贯要。
tryReleaseShared(int)//共享方式暖侨。嘗試釋放資源,成功則返回true崇渗,失敗則返回false字逗。

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

  1. 以 ReentrantLock 為例驶臊,state 初始化為 0套腹,表示未鎖定狀態(tài)绪抛。A 線程 lock()時(shí),會(huì)調(diào)用 tryAcquire()獨(dú)占該鎖并將 state+1电禀。此后幢码,其他線程再 tryAcquire()時(shí)就會(huì)失敗,直到 A 線程 unlock()到 state=0(即釋放鎖)為止尖飞,其它線程才有機(jī)會(huì)獲取該鎖症副。當(dāng)然,釋放鎖之前政基,A 線程自己是可以重復(fù)獲取此鎖的(state 會(huì)累加)贞铣,這就是可重入的概念。但要注意沮明,獲取多少次就要釋放多么次辕坝,這樣才能保證 state 是能回到零態(tài)的。

  2. 再以 CountDownLatch 以例荐健,任務(wù)分為 N 個(gè)子線程去執(zhí)行酱畅,state 也初始化為 N(注意 N 要與線程個(gè)數(shù)一致)。這 N 個(gè)子線程是并行執(zhí)行的江场,每個(gè)子線程執(zhí)行完后countDown() 一次纺酸,state 會(huì) CAS(Compare and Swap)減 1。等到所有子線程都執(zhí)行完后(即 state=0)址否,會(huì) unpark()主調(diào)用線程餐蔬,然后主調(diào)用線程就會(huì)從 await() 函數(shù)返回,繼續(xù)后余動(dòng)作佑附。

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

13.4 AQS 組件總結(jié)

  1. Semaphore(信號(hào)量)-允許多個(gè)線程同時(shí)訪問: synchronized 和 ReentrantLock 都是一次只允許一個(gè)線程訪問某個(gè)資源痪寻,Semaphore(信號(hào)量)可以指定多個(gè)線程同時(shí)訪問某個(gè)資源螺句。
  2. CountDownLatch(倒計(jì)時(shí)器): CountDownLatch 是一個(gè)同步工具類,用來協(xié)調(diào)多個(gè)線程之間的同步橡类。這個(gè)工具通常用來控制線程等待蛇尚,它可以讓某一個(gè)線程等待直到倒計(jì)時(shí)結(jié)束,再開始執(zhí)行顾画。

13.5 用過 CountDownLatch 么取劫?什么場(chǎng)景下用的匆笤?

CountDownLatch 的作用就是 允許 count 個(gè)線程阻塞在一個(gè)地方,直至所有線程的任務(wù)都執(zhí)行完畢谱邪。之前在項(xiàng)目中炮捧,有一個(gè)使用多線程讀取多個(gè)文件處理的場(chǎng)景,我用到了 CountDownLatch 惦银。具體場(chǎng)景是下面這樣的:

我們要讀取處理 6 個(gè)文件咆课,這 6 個(gè)任務(wù)都是沒有執(zhí)行順序依賴的任務(wù),但是我們需要返回給用戶的時(shí)候?qū)⑦@幾個(gè)文件的處理的結(jié)果進(jìn)行統(tǒng)計(jì)整理扯俱。

為此我們定義了一個(gè)線程池和 count 為 6 的CountDownLatch對(duì)象 书蚪。使用線程池處理讀取任務(wù),每一個(gè)線程處理完之后就將 count-1迅栅,調(diào)用CountDownLatch對(duì)象的 await()方法殊校,直到所有文件讀取完之后,才會(huì)接著執(zhí)行后面的邏輯读存。

偽代碼是下面這樣的:

public class CountDownLatchExample1 {
  // 處理文件的數(shù)量
  private static final int threadCount = 6;

  public static void main(String[] args) throws InterruptedException {
    // 創(chuàng)建一個(gè)具有固定線程數(shù)量的線程池對(duì)象(推薦使用構(gòu)造方法創(chuàng)建)
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
    for (int i = 0; i < threadCount; i++) {
      final int threadnum = i;
      threadPool.execute(() -> {
        try {
          //處理文件的業(yè)務(wù)操作
          ......
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          //表示一個(gè)文件已經(jīng)被完成
          countDownLatch.countDown();
        }

      });
    }
    countDownLatch.await();
    threadPool.shutdown();
    System.out.println("finish");
  }

}

有沒有可以改進(jìn)的地方呢为流?

可以使用 CompletableFuture 類來改進(jìn)!Java8 的 CompletableFuture 提供了很多對(duì)多線程友好的方法宪萄,使用它可以很方便地為我們編寫多線程程序艺谆,什么異步、串行拜英、并行或者等待所有線程執(zhí)行完任務(wù)什么的都非常方便静汤。

CompletableFuture<Void> task1 =
  CompletableFuture.supplyAsync(()->{
    //自定義業(yè)務(wù)操作
  });
......
CompletableFuture<Void> task6 =
  CompletableFuture.supplyAsync(()->{
    //自定義業(yè)務(wù)操作
  });
......
 CompletableFuture<Void> headerFuture=CompletableFuture.allOf(task1,.....,task6);

  try {
    headerFuture.join();
  } catch (Exception ex) {
    ......
  }
System.out.println("all done. ");

上面的代碼還可以接續(xù)優(yōu)化,當(dāng)任務(wù)過多的時(shí)候居凶,把每一個(gè) task 都列出來不太現(xiàn)實(shí)虫给,可以考慮通過循環(huán)來添加任務(wù)。

//文件夾位置
List<String> filePaths = Arrays.asList(...)
// 異步處理所有文件
List<CompletableFuture<String>> fileFutures = filePaths.stream()
        .map(filePath -> doSomeThing(filePath))
        .collect(Collectors.toList());
// 將他們合并起來
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
        fileFutures.toArray(new CompletableFuture[fileFutures.size()])
);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侠碧,一起剝皮案震驚了整個(gè)濱河市抹估,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弄兜,老刑警劉巖药蜻,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異替饿,居然都是意外死亡语泽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門视卢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踱卵,“玉大人,你說我怎么就攤上這事据过⊥锷埃” “怎么了妒挎?”我有些...
    開封第一講書人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長西饵。 經(jīng)常有香客問我酝掩,道長,這世上最難降的妖魔是什么眷柔? 我笑而不...
    開封第一講書人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任庸队,我火速辦了婚禮,結(jié)果婚禮上闯割,老公的妹妹穿的比我還像新娘彻消。我一直安慰自己,他們只是感情好宙拉,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開白布宾尚。 她就那樣靜靜地躺著,像睡著了一般谢澈。 火紅的嫁衣襯著肌膚如雪煌贴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評(píng)論 1 291
  • 那天锥忿,我揣著相機(jī)與錄音牛郑,去河邊找鬼。 笑死敬鬓,一個(gè)胖子當(dāng)著我的面吹牛淹朋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钉答,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼础芍,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了数尿?” 一聲冷哼從身側(cè)響起仑性,我...
    開封第一講書人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎右蹦,沒想到半個(gè)月后诊杆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡何陆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年晨汹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甲献。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宰缤,死狀恐怖颂翼,靈堂內(nèi)的尸體忽然破棺而出晃洒,到底是詐尸還是另有隱情慨灭,我是刑警寧澤,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布球及,位于F島的核電站氧骤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏吃引。R本人自食惡果不足惜筹陵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镊尺。 院中可真熱鬧朦佩,春花似錦、人聲如沸庐氮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弄砍。三九已至仙畦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間音婶,已是汗流浹背慨畸。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衣式,地道東北人寸士。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像碴卧,于是被迫代替她去往敵國和親碉京。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容