Java并發(fā)多線程基礎(chǔ)學(xué)習(xí)筆記

[TOC]

0 前言

為什么需要學(xué)習(xí)并發(fā)編程?

  1. 大廠JD硬性要求,也是高級(jí)工程師必經(jīng)之路奕污,幾乎所有的程序都需要并發(fā)和多線程
  2. 面試高頻出現(xiàn)萎羔,書(shū)籍、網(wǎng)絡(luò)博客內(nèi)容水平參差不齊碳默,知識(shí)點(diǎn)凌亂
  3. 眾多框架的原理和基礎(chǔ)贾陷,Spring 線程池、單例的應(yīng)用嘱根;數(shù)據(jù)庫(kù)的樂(lè)觀鎖思想髓废;Log4J2 對(duì)阻塞隊(duì)列的應(yīng)用

本門(mén)課程的優(yōu)點(diǎn)

  1. 系統(tǒng):成體系不容易忘記,思維導(dǎo)圖该抒,為什么->演示代碼->分析原理->得出結(jié)論
  2. 內(nèi)容豐富:線程 8 大核心基礎(chǔ)慌洪,java內(nèi)存模型,死鎖
  3. 分析面試題:答題思路,引申解答
  4. 分析本質(zhì):深入原理分析設(shè)計(jì)理念,interrupt與stop停止線程,wait必須在同步塊中使用贞远,JMM
  5. 學(xué)習(xí)方法:技術(shù)提高途徑爽醋、技術(shù)前沿動(dòng)態(tài)乌昔、業(yè)務(wù)中成長(zhǎng)、自頂向下學(xué)習(xí)
  6. 通俗易懂:近朱者赤-happen-before,森然火災(zāi)-線上事故,夫妻遷讓-死鎖
  7. 逐步迭代:從0開(kāi)始因痛,逐漸優(yōu)化,重視思路岸更,分析錯(cuò)誤代碼到修復(fù)問(wèn)題
  8. 案例演示豐富
  9. 習(xí)題檢驗(yàn):總結(jié)知識(shí)點(diǎn)鸵膏,檢驗(yàn)學(xué)習(xí)效果,知識(shí)卡防止走神
  10. 配套資料:思維導(dǎo)圖怎炊,知識(shí)點(diǎn)文檔谭企,面試題總結(jié)

線程八大核心

20191227022954.png

1. 創(chuàng)建多線程(核心1)

查看Oracle Java官方文檔Thread 類(lèi)注釋,可知創(chuàng)建線程有兩種方法结胀,即實(shí)現(xiàn)Runable接口和繼承Thread類(lèi)赞咙。

/**
73 * There are two ways to create a new thread of execution. One is to declare a class to be a subclass of <code>Thread</code>

99 * The other way to create a thread is to declare a class that implements the <code>Runnable</code> interface.
**/

1. 實(shí)現(xiàn)Runable接口

public class RunnableStyle implements Runnable {
    public static void main(String[] args) {
        // 將將我們創(chuàng)建的 RunnableStyle 作為構(gòu)造函數(shù)參數(shù)傳入Thread
        Thread t = new Thread(new RunnableStyle());
        t.start();
    }

    @Override
    public void run() {
        System.out.println("實(shí)現(xiàn)Runnable接口創(chuàng)建線程");
    }
}

2. 繼承Thread類(lèi)

public class ThreadStyle extends Thread {
    public static void main(String[] args) {
        ThreadStyle t = new ThreadStyle();
        t.start();
    }
    @Override
    public void run() {
        System.out.println("繼承Thread類(lèi)創(chuàng)建線程");
    }
}

兩種創(chuàng)建方式的對(duì)比

方法1 實(shí)現(xiàn) Runable 接口更好

  1. 可擴(kuò)展责循,java 只能單繼承多實(shí)現(xiàn)糟港,繼承 Thread 類(lèi)后就不能繼承其他類(lèi),限制了可擴(kuò)展性院仿;而 Runnable 方式可以實(shí)現(xiàn)多個(gè)接口
  2. 節(jié)約資源秸抚,繼承 Thread 類(lèi)每次要新建一個(gè)任務(wù),每次只能去新建一個(gè)線程歹垫,而新建一個(gè)線程開(kāi)銷(xiāo)是比較大的(具體在第8章)剥汤,需要?jiǎng)?chuàng)建、執(zhí)行和銷(xiāo)毀排惨;而 Runnable 方式可以利用線程池工具吭敢,可以避免創(chuàng)建線程、銷(xiāo)毀線程帶來(lái)的開(kāi)銷(xiāo)暮芭。線程創(chuàng)建需要開(kāi)辟虛擬機(jī)棧鹿驼、本地方法棧、程序計(jì)數(shù)器等線程私有的內(nèi)存空間辕宏,線程銷(xiāo)毀時(shí)需要回收這些資源畜晰,頻繁創(chuàng)建銷(xiāo)毀線程會(huì)浪費(fèi)大量系統(tǒng)資源
  3. 解耦,實(shí)現(xiàn) Runnable 接口解耦瑞筐,一是具體的業(yè)務(wù)邏輯在run()方法中凄鼻,二是控制線程生命周期是 Thread 類(lèi),兩個(gè)目的不一樣,不建議寫(xiě)在一個(gè)類(lèi)中块蚌,應(yīng)該解耦闰非。?

本質(zhì)區(qū)別

方式1 實(shí)現(xiàn) Runable 創(chuàng)建線程匈子,查看下方 Thread.run()代碼和注釋可知河胎,啟動(dòng)線程前,要將我們創(chuàng)建的 RunnableStyle 作為 Thread 構(gòu)造函數(shù)的參數(shù)target虎敦,所以實(shí)際運(yùn)行的是target.run()游岳,即我們線程類(lèi) RunnableStyle 的run()方法

方式2 繼承Thread類(lèi),會(huì)重寫(xiě)Thread.run()方法其徙,啟動(dòng)線程后直接運(yùn)行我們線程類(lèi) ThreadStyle 的run()方法

public class Thread implements Runnable {
    
    private Runnable target;
    
    // 構(gòu)造函數(shù)胚迫,傳入我們寫(xiě)的Runnable
    public Thread(Runnable target) {...}

    /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     */
    @Override
    public void run() {
        if (target != null) {
            // 調(diào)用Runnable的run()方法
            target.run();
        }
    }
}

思考題: 同時(shí)使用 Runnable 和 Thread 兩種創(chuàng)建線程的方式會(huì)怎么樣?

Runnable 方式中我們創(chuàng)建的 Runnable 實(shí)例 target 會(huì)作為構(gòu)造函數(shù)參數(shù)傳入到Thread類(lèi)唾那,然后被Thread.run()調(diào)用執(zhí)行访锻,而繼承 Thread 方式會(huì)覆寫(xiě)Thread.run()方法,就使得 target 不會(huì)被調(diào)用執(zhí)行闹获,查看詳細(xì)代碼

面試題 1: 創(chuàng)建/實(shí)現(xiàn)線程有幾種方式期犬?

  1. 兩種方法,根據(jù) Thread 類(lèi)的注釋(或 Java 官方文檔)避诽,分別是實(shí)現(xiàn) Runnable 接口和繼承 Thread 類(lèi)
  2. 準(zhǔn)確的講龟虎,本質(zhì)都是一種方式,本質(zhì)都是構(gòu)造 Thread 類(lèi)沙庐,調(diào)用Thread.run()方法鲤妥,只不過(guò)一種 Runnable 實(shí)現(xiàn)類(lèi)作為 target 傳入Thread,然后調(diào)用target.run()方法拱雏,另一種是直接重寫(xiě) run() 方法(參考上面本質(zhì)區(qū)別)
  3. 分析優(yōu)缺點(diǎn)棉安,可擴(kuò)展性、節(jié)約資源铸抑、解耦(參考上面兩種創(chuàng)建方式的對(duì)比)
  4. 分析常見(jiàn)的 6 種典型錯(cuò)誤觀點(diǎn)贡耽,線程池、Callable等本質(zhì)都是實(shí)現(xiàn) Runnable 接口

6 種典型錯(cuò)誤觀點(diǎn)分析

  • 線程池創(chuàng)建線程也算一種新建線程的方式 示例代碼

ExecutorService 本質(zhì)都是使用線程工廠創(chuàng)建線程鹊汛,查看源碼可知線程工廠都是 實(shí)現(xiàn) Runnable 接口構(gòu)造 Thread 類(lèi)的方法創(chuàng)建線程

    // @see java.util.concurrent.Executors.DefaultThreadFactory#newThread(java.lang.Runnable)
    
    // 線程池的線程工廠蒲赂,創(chuàng)建線程的方式如下,實(shí)現(xiàn)Runnable接口柒昏,構(gòu)造Thread類(lèi)
    public Thread newThread(Runnable r) {
        // 傳入用戶的Runnable實(shí)例凳宙,設(shè)置線程組,線程名稱等
        Thread t = new Thread(group, r,
                                namePrefix + threadNumber.getAndIncrement(),
                                0);
        // ......
        return t;
    }

創(chuàng)建線程池也可以用戶自定義線程工廠职祷,從下方代碼中也可以看出氏涩,用戶自定義線程工廠線程工廠也是實(shí)現(xiàn) Runnable 接口構(gòu)造 Thread 類(lèi)的方法創(chuàng)建線程

// 用戶自定義線程工廠届囚,見(jiàn)碼出高效p239
public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);

    UserThreadFactory(String whatFeatrueOfGroup) {
        namePrefix = "UserThreadFactory's " + whatFeatrueOfGroup + "-Worker-";
    }

    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();

        // task是用戶實(shí)現(xiàn) Runnable 接口創(chuàng)建的,構(gòu)造Thread類(lèi)創(chuàng)建線程
        Thread thread = new Thread(null, task, name, 0);
        System.out.println(thread.getName());
        return thread;
    }
}
  • 通過(guò) Callable 和 FutureTask 創(chuàng)建線程是尖,也算是一種新建線程的方式 示例代碼

查看示例代碼可知意系,Thread構(gòu)造函數(shù)參數(shù)是 futureTask,Runnable 的實(shí)現(xiàn)類(lèi)饺汹,查看下方FutureTask代碼蛔添,可知FutureTask.run()調(diào)用了Callable.call()方法,并把返回值保存到FutureTask.outcome兜辞,本質(zhì)還是實(shí)現(xiàn) Runnable 接口迎瞧。

public class FutureTask implements RunnableFuture {

    private Object outcome;     // 保存call()返回值
    private Callable callable;

    // 構(gòu)造方法,出入用戶創(chuàng)建的callable
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    @Override 
    public void run() {
        // .....
        // callable是FutureTask構(gòu)造函數(shù)傳入的
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                // 在run()方法中調(diào)用用戶定義的Callable.call()
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);    // 將返回值保存到outcome
        }
        // ......
    }
}
20191227052650.png
  • 無(wú)返回值是實(shí)現(xiàn) Runnable 接口逸吵,有返回值是實(shí)現(xiàn) Callable 接口凶硅,所以 Callable 是新的創(chuàng)建線程的方式 示例代碼

與上一個(gè)問(wèn)題類(lèi)似,本質(zhì)還是要借助 FutureTask(FutureTask 實(shí)現(xiàn)了 Runnable 接口)扫皱,構(gòu)造 Thread 類(lèi)創(chuàng)建線程足绅,啟動(dòng)線程后會(huì)調(diào)用target.run(),即FutureTask.run()韩脑,其中會(huì)調(diào)用Callable.call()方法氢妈,所以不算是一種新的創(chuàng)建線程方式,本質(zhì)還是實(shí)現(xiàn) Runable 接口段多,不過(guò)是創(chuàng)建 FutureTask 實(shí)現(xiàn) Runnable 接口的工作JDK幫我們做了首量。

  • 定時(shí)器

運(yùn)行示例代碼,會(huì)生成AnonymousInnerClassStyle1.class衩匣,反編譯發(fā)現(xiàn)該類(lèi)**實(shí)現(xiàn)了 Runnable 接口**蕾总,AnonymousInnerClassStyle2.class反編譯發(fā)現(xiàn)該類(lèi)繼承了 Thread 類(lèi)

查看示例代碼 粥航,代碼中打印了 lambda 表達(dá)式實(shí)現(xiàn)的接口琅捏,創(chuàng)建線程本質(zhì)還是實(shí)現(xiàn) Runnable 接口,但并不完全等價(jià)于匿名內(nèi)部類(lèi)的方式递雀,邏輯與下方代碼類(lèi)似柄延。

雖然也是匿名內(nèi)部類(lèi),不會(huì)生成內(nèi)部類(lèi)的 .class 文件缀程,而會(huì)動(dòng)態(tài)生成內(nèi)部類(lèi)LambdaStyle$$Lambda$1搜吧,lambda 表達(dá)式中的內(nèi)容會(huì)被編譯成靜態(tài)方法LambdaStyle.lambda$main$0()動(dòng)態(tài)生成的內(nèi)部類(lèi) Runnable 實(shí)例LambdaStyle$$Lambda$1直接調(diào)用靜態(tài)方法LambdaStyle.lambda$main$0()杨凑,詳細(xì)參考《Java8 實(shí)戰(zhàn)》附錄D和掘金小冊(cè)《JVM 字節(jié)碼從入門(mén)到精通》第9節(jié)

public class LambdaStyle {

    private static void lambda$main$0() {
        System.out.println("hello, lambda");
    }
}

final class LambdaStyle$$Lambda$1 implements Runnable {
    @Override
    public void run() {
        LambdaStyle.lambda$main$0();
    }
}

面試題 2:實(shí)現(xiàn) Runnable 接口和繼承 Thread 類(lèi)的哪種方式更好滤奈?

  1. 可擴(kuò)展性,Java 不支持多繼承撩满,繼承 Thread 類(lèi)后就不能繼承其他類(lèi)蜒程,限制了可擴(kuò)展性绅你,而實(shí)現(xiàn) Runnable 方式可以實(shí)現(xiàn)多個(gè)接口
  2. 代碼架構(gòu)角度,實(shí)現(xiàn) Runnable 接口解耦昭躺,一是具體的業(yè)務(wù)邏輯在run()方法中忌锯,二是控制線程生命周期是 Thread 類(lèi),兩個(gè)目的不一樣领炫,不建議寫(xiě)在一個(gè)類(lèi)中偶垮,應(yīng)該解耦。
  3. 節(jié)約資源帝洪,繼承 Thread 類(lèi)甸私,新建任務(wù)只能去 new 一個(gè)對(duì)象,但是資源損耗比較大逝淹,繼承 Thread 類(lèi)每次要新建一個(gè)任務(wù)缓待,每次只能去新建一個(gè)線程,而新建一個(gè)線程開(kāi)銷(xiāo)是比較大的(具體在第8章)族沃,需要?jiǎng)?chuàng)建频祝、執(zhí)行和銷(xiāo)毀;而 Runnable 方式可以利用線程池工具傳入Runnable 實(shí)例 target脆淹,可以避免創(chuàng)建線程常空、銷(xiāo)毀線程帶來(lái)的開(kāi)銷(xiāo)。線程創(chuàng)建需要開(kāi)辟虛擬機(jī)棧盖溺、本地方法棧漓糙、程序計(jì)數(shù)器等線程私有的內(nèi)存空間,線程銷(xiāo)毀時(shí)需要回收這些資源烘嘱,頻繁創(chuàng)建銷(xiāo)毀線程會(huì)浪費(fèi)大量系統(tǒng)資源

彩蛋:學(xué)習(xí)編程知識(shí)的優(yōu)質(zhì)路徑

  • 宏觀

    1. 責(zé)任心昆禽,不要放過(guò)任何 Bug,找到原因并去解決蝇庭,因?yàn)楹芏?Bug 都需要非常深入的知識(shí)才能解決醉鳖,解決問(wèn)題的能力比學(xué)很多的知識(shí)更重要
    2. 主動(dòng),永遠(yuǎn)不要覺(jué)得自己的時(shí)間多余哮内,不斷重構(gòu)盗棵、優(yōu)化、學(xué)習(xí)北发、總結(jié)
    3. 敢于承擔(dān)纹因,對(duì)于沒(méi)碰過(guò)的技術(shù)難題,在一定調(diào)研后琳拨,敢于承擔(dān)瞭恰,讓工作充滿挑戰(zhàn),攻克難關(guān)的過(guò)程進(jìn)步飛速
    4. 關(guān)心產(chǎn)品和業(yè)務(wù)狱庇,不僅要寫(xiě)好代碼惊畏,更要在業(yè)務(wù)層面多思考
  • 微觀

    1. 系統(tǒng)學(xué)習(xí)是牢,碎片化知識(shí)公眾號(hào)文章最容易忘記和一葉障目,要看經(jīng)典書(shū)籍的譯本
    2. 官方文檔驳棱,專家撰寫(xiě)不斷迭代,最權(quán)威的蜡豹,像線程實(shí)現(xiàn)方式百度結(jié)果有非常多的錯(cuò)誤
    3. 分析源碼较雕,隨著看源碼和官方文檔的次數(shù)增多趁怔,就會(huì)更熟悉更快湿硝,而baidu并不能達(dá)到這個(gè)效果
    4. 英文搜索,前面幾個(gè)不能解決問(wèn)題润努,再搜索 Google 和 StackOverflow图柏,用英文更容易找到正確答案如Annoymouses Class
    5. 多實(shí)踐,遇到新知識(shí)任连,多動(dòng)手寫(xiě)Demo蚤吹,并嘗試用到項(xiàng)目里,三個(gè)階段随抠,看裁着、寫(xiě)、生產(chǎn)環(huán)境

彩蛋:如何了解技術(shù)領(lǐng)域的最新動(dòng)態(tài)

  • 高質(zhì)量固定途徑拱她,掘金二驰、阮一峰博客
  • 訂閱技術(shù)論壇,InfoQ
  • 公眾號(hào)

彩蛋:如何在業(yè)務(wù)開(kāi)發(fā)中成長(zhǎng)

  • 偏業(yè)務(wù)方向開(kāi)發(fā)秉沼,了解業(yè)務(wù)核心模型架構(gòu)桶雀,如電商交易、訂單唬复、結(jié)算等核心系統(tǒng)的設(shè)計(jì)
  • 偏技術(shù)方向開(kāi)發(fā)矗积,通用性非常強(qiáng),就業(yè)方向廣敞咧,如中間件棘捣、RPC,APM
  • 兩個(gè) 25% 理論休建,在一個(gè)領(lǐng)域達(dá)到前 25% 比較容易乍恐,但前 5% 很難评疗,如果能在兩個(gè)領(lǐng)域做到前 25%,一旦把兩個(gè)領(lǐng)域能結(jié)合起來(lái)茵烈,就能做到非常優(yōu)秀的 5%百匆,如小灰是編程+寫(xiě)作領(lǐng)域,liuyubo是編程+授課領(lǐng)域呜投,雷軍是編程+管理胧华,兩個(gè)領(lǐng)域 25% 非常不錯(cuò)的職業(yè)規(guī)劃

2. 啟動(dòng)多線程(核心2)

查看示例代碼,啟動(dòng)線程調(diào)用start()方法宙彪,然后會(huì)調(diào)用本地方法start0()矩动,開(kāi)辟新線程,而調(diào)用run()方法相當(dāng)于Main線程調(diào)用方法释漆,無(wú)法啟動(dòng)新線程

Thread 類(lèi)的源代碼分析

    /* Java thread status for tools, initialized to indicate thread 'not yet started'
     * 線程的狀態(tài)標(biāo)志悲没,初始化為0來(lái)標(biāo)志線程尚未start()
     */
    private volatile int threadStatus = 0;
    
    // synchronized保證線程安全,即同一個(gè)線程對(duì)象不能同時(shí)調(diào)用start()方法
    public synchronized void start() {
        /**
         * 0 狀態(tài)值對(duì)應(yīng)線程的 NEW 狀態(tài)男图,如果兩次調(diào)用start()方法會(huì)拋出線程狀態(tài)異常
         * A zero status value corresponds to state "NEW".
         * 
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        // 添加到線程組
        group.add(this);

        boolean started = false;
        try {
            // 調(diào)用native方法start0
            start0();
            started = true;
        } finally {
            // ......
        }
    }

    // native方法示姿,開(kāi)辟新線程,更改線程狀態(tài)threadStatus逊笆,C++代碼
    private native void start0();

start0()方法在 Thread.c 中栈戳,具體邏輯在jvm.cpp,詳細(xì)見(jiàn) Java 線程源碼解析之 start


面試題 3:一個(gè)線程能調(diào)用兩次start()方法嗎难裆?會(huì)發(fā)生什么情況子檀?

  1. 一個(gè)線程只能調(diào)用一次start()方法,否則會(huì)拋出IllegalThreadStateException乃戈,因?yàn)?code>start()方法的衛(wèi)語(yǔ)句會(huì)檢查線程狀態(tài)褂痰,線程狀態(tài)不為 NEW 則拋出異常
  2. start()synchronized 修飾的線程安全方法,不存在線程已啟動(dòng)但線程狀態(tài) threadStatus 還未修改的情況症虑,所以也不用擔(dān)心被調(diào)用兩次
  3. 即使線程執(zhí)行結(jié)束(TERMINATED)也不能再調(diào)用start方法缩歪,只有線程狀態(tài)為 NEW 時(shí)才可以調(diào)用 start。線程池復(fù)用的線程是不退出的谍憔,復(fù)用的是Runnable實(shí)例匪蝙,而不是對(duì)同一個(gè)線程調(diào)用了多次start()方法,見(jiàn)第4章
  4. 為什么這么設(shè)計(jì)习贫?

問(wèn)題逛球? 線程結(jié)束后能不能再調(diào)用start,那什么時(shí)候結(jié)束沈条?

面試題 4:既然start()還是會(huì)調(diào)用run()方法需忿,為什么我們不直接調(diào)用run()方法呢?

  1. 因?yàn)檎{(diào)用start()方法才會(huì)真的啟動(dòng)一個(gè)新線程蜡歹,而調(diào)用run()只是簡(jiǎn)單的調(diào)用方法
  2. start()方法會(huì)調(diào)用本地方法start0()屋厘,然后開(kāi)辟新線程,代碼可以在 OpenJDK 中查看(詳細(xì)參考 第2章 啟動(dòng)多線程)

3. 停止線程(核心3)

一般情況下都是線程執(zhí)行完畢之后停止月而,如果用戶想要主動(dòng)停止線程汗洒,可以使用Thread.interrupt來(lái)通知線程停止,Thread.interrupt并不能真正的中斷線程父款,而是「通知線程應(yīng)該停止了」溢谤,具體到底停止還是繼續(xù)運(yùn)行,應(yīng)該由被通知的線程自己寫(xiě)代碼處理憨攒。

具體來(lái)說(shuō)世杀,當(dāng)對(duì)一個(gè)線程,調(diào)用 interrupt() 時(shí):

  1. 如果線程處于正掣渭活動(dòng)狀態(tài)瞻坝,那么會(huì)將該線程的中斷標(biāo)志位設(shè)置為 true,僅此而已
  2. 如果線程處于被阻塞狀態(tài)(例如處于sleep, wait, join 等狀態(tài))杏瞻,那么線程將立即退出被阻塞狀態(tài)所刀,并拋出一個(gè)InterruptedException異常。僅此而已捞挥。
  3. 被設(shè)置中斷標(biāo)志的線程將繼續(xù)正常運(yùn)行浮创,不受影響,具體停止線程的邏輯需要自己寫(xiě)代碼處理

停止正称龊活動(dòng)狀態(tài)線程

停止正痴杜活動(dòng)線程的代碼邏輯是Main線程使用interrupt()方法發(fā)送中斷請(qǐng)求(相當(dāng)于修改了標(biāo)志位),當(dāng)任務(wù)線程接收到中斷請(qǐng)求讹俊,然后使用Thread.interrupted()檢測(cè)標(biāo)志位雏掠,退出while循環(huán),結(jié)束線程劣像,代碼范式如下:

    Thread thread = new Thread(() -> {
        // 檢測(cè)中斷標(biāo)志位谜嫉,如果收到中斷請(qǐng)求,退出while循環(huán)弱判。所有的業(yè)務(wù)邏輯應(yīng)該都寫(xiě)在while循環(huán)中
        while (!Thread.interrupted()) {
            // do more work.
        }
    });
    thread.start();

    // 一段時(shí)間以后
    thread.interrupt();
public class RightWayStopThreadWithoutSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            // 當(dāng)接收到中斷信號(hào)盆顾,退出循環(huán),任務(wù)結(jié)束
            while (!Thread.interrupted()) {
                System.out.println("線程正在運(yùn)行..");
            }
        };

        Thread t = new Thread(r);
        t.start();
        Thread.sleep(100L);    // 等待線程啟動(dòng)完成

        System.out.println("是否收到中斷信號(hào):" + t.isInterrupted());
        /*
         * 發(fā)送中斷信號(hào),改變中斷標(biāo)志位, 僅此而已
         * 如果線程循環(huán)條件是while (true), t線程會(huì)繼續(xù)執(zhí)行下去
         * 如果線程循環(huán)條件是while (!Thread.interrupted()), t線程會(huì)退出
         */
        t.interrupt();
        System.out.println("是否收到中斷信號(hào):" + t.isInterrupted());
    }
}

停止被阻塞狀態(tài)線程

代碼邏輯任務(wù)線程處于被阻塞狀態(tài)(例如處于·sleep, wait, join等狀態(tài))屋群,Main線程使用interrupt()方法發(fā)送中斷通知改變中斷標(biāo)志位闸婴,當(dāng)任務(wù)線程調(diào)用sleep等方法時(shí),發(fā)現(xiàn)中斷標(biāo)志位被修改芍躏,Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)志位復(fù)位邪乍,然后立即退出被阻塞狀態(tài),并拋出一個(gè)InterruptedException異常

public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            int num = 0;
            try {
                // 每次循環(huán)都會(huì)sleep的,不需要!Thread.currentThread().isInterrupted()判斷條件庇楞,
                // 因?yàn)樵趻伋鯥nterruptedException之前Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)志位復(fù)位榜配,
                // 即使調(diào)用!Thread.currentThread().isInterrupted()返回也是true
                while (num <= 10000) { 
                    if (num % 100 == 0) {
                        // 驗(yàn)證JVM是否將中斷標(biāo)志位復(fù)位,返回true吕晌,說(shuō)明復(fù)位了蛋褥,故加在while循環(huán)條件中無(wú)用
                        System.out.println(!Thread.currentThread().isInterrupted());    // 
                        System.out.println(num + "是100的倍數(shù)");
                    }
                    num++;

                    // 這里會(huì)檢測(cè)中斷標(biāo)志位,如果被修改則拋出異常退出while循環(huán)
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                System.out.println("收到中斷信號(hào)Interrupt, 拋出異常, 結(jié)束線程");
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(r);
        thread.start();
        // 等待線程完全啟動(dòng)
        Thread.sleep(5000);
        // 發(fā)送中斷通知睛驳,修改中斷標(biāo)志位
        thread.interrupt();
    }
}

通過(guò)上面的代碼我們也可以清楚的知道烙心,這也是在調(diào)用Thread.sleep()方法時(shí)需要處理InterruptedException異常的原因

不能停止的線程

運(yùn)行下面的示例代碼,可以發(fā)現(xiàn)與上一小節(jié)的運(yùn)行結(jié)果不同乏沸,會(huì)一直進(jìn)行循環(huán)淫茵,原因是try-catch沒(méi)有包住while循環(huán),線程在sleep阻塞狀態(tài)時(shí)蹬跃,收到interrupt信號(hào)匙瘪,拋出異常,然后被catch住炬转,無(wú)法退出while循環(huán)結(jié)束線程辆苔,所以代碼會(huì)一直運(yùn)行直到num <= 10000

public class CantInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍數(shù)");
                }
                num++;
                
                // 收到interrupt`信號(hào),復(fù)原中斷標(biāo)志位扼劈,拋出異常驻啤,但是無(wú)法退出while循環(huán),所以會(huì)繼續(xù)運(yùn)行
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        // 等待線程完全啟動(dòng)
        Thread.sleep(5000);
        // 發(fā)送中斷通知
        thread.interrupt();
    }
}

Thread.interrupted()Thread.currentThread().isInterrupted()的區(qū)別

查看源碼易知荐吵,都是返回當(dāng)前線程的中斷標(biāo)志位骑冗,Thread.interrupted()會(huì)復(fù)原標(biāo)志位,Thread.currentThread().isInterrupted()不會(huì)

    /** 
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method. 
     * Thread.interrupted()方法檢測(cè)當(dāng)前線程是否已經(jīng)中斷先煎,這個(gè)方法會(huì)將中斷標(biāo)志位interrupted status清除復(fù)位
     * 
     * In other words, if this method were to be called twice in succession, the second call would return false
     * 換句話說(shuō)贼涩,如果該方法被調(diào)用兩次,第二次會(huì)返回false
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    /**
     * Thread.currentThread().isInterrupted()返回線程的中斷標(biāo)志位薯蝎,
     * 與上面代碼的區(qū)別是參數(shù)ClearInterrupted為false遥倦,即不清除標(biāo)志位
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }

停止線程的最佳實(shí)踐

由于Runnable.run()方法簽名不允許拋出異常,所以只能catch住占锯,所以需要傳遞中斷袒哥。總之消略,無(wú)論如何堡称,都不應(yīng)屏蔽中斷

為什么不擴(kuò)大try-catch范圍 ?

public class RightWayStopThreadInProd implements Runnable {

    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("...");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                // 阻塞狀態(tài)受到中斷信號(hào)艺演,jvm會(huì)復(fù)位中斷標(biāo)志位却紧,
                // 這里設(shè)置中斷標(biāo)志位為false桐臊,用于傳遞中斷,使得while條件可以結(jié)束線程
                Thread.currentThread().interrupt();
                //保存日志晓殊、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    // 業(yè)務(wù)方法
    private void throwInMethod() throws InterruptedException {
            Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

上面代碼依賴sleep檢查中斷標(biāo)志位断凶,如果沒(méi)有調(diào)用sleep,應(yīng)該怎么寫(xiě)挺物?沒(méi)有sleep更簡(jiǎn)單懒浮,直接while條件判斷中斷標(biāo)志位即可

響應(yīng)中斷的方法列表

響應(yīng)中斷的意思是這這些方法的執(zhí)行中飘弧,如果中斷信號(hào)過(guò)來(lái)了识藤,是可以感知到的。我們可以使用下面的方法讓線程進(jìn)入阻塞狀態(tài)次伶,為了使線程從阻塞狀態(tài)恢復(fù)痴昧,就可以使用interrupt()方法中斷線程

Object.wait()/wait(long)/wait(long, int)
Thread.sleep()/sleep(long)/sleep(long, int)
Thread.join()/join(long)/join(long, int)
java.util.concurrent.BlockingQueue.take()/put(E)
java.util.concurrent.locks.Lock.lockInterruptibly()
java.util.concurrent.CountDownLatch.await()
java.util.concurrent.CyclicBarrier.await()
java.util.concurrent.Exchanger.exchange(V)
java.nio.channels.InterruptibleChannel
java.nio.channels.Selector

為什么要使用interrupt來(lái)停止線程,有什么好處冠王?

被中斷的線程有如何響應(yīng)中斷的權(quán)利赶撰,因?yàn)榫€程的某些代碼可能是非常重要的,我們必須要等待線程處理完后柱彻,再由線程自己主動(dòng)去中止豪娜,或者線程不想中止也是可以的,不應(yīng)該魯莽的使用stop哟楷,而應(yīng)該使用interrupt方法發(fā)送中斷信號(hào)瘤载,這樣使得線程代碼更加安全,數(shù)據(jù)的完整性也得到了保障卖擅。

錯(cuò)誤的線程停止方法

stop():過(guò)期方法鸣奔,悟空說(shuō)會(huì)導(dǎo)致數(shù)據(jù)不完整,但是加synchronized可以解決此問(wèn)題惩阶,具體原因不知

suspend()

resume()

volatile設(shè)置標(biāo)記位:

彩蛋:如何分析 native 方法

  1. 查看Thread.interrupt()源碼挎狸,發(fā)現(xiàn)底層是調(diào)用native方法private native void interrupt0();
    public void interrupt() {
        // ... 
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();       // 調(diào)用native方法interrupt0
    }
  1. 進(jìn)入Github查看OpenJDK代碼庫(kù),點(diǎn)擊<kbd>FindFile</kbd>搜索Thread.c文件断楷,JDK native 方法的源碼都在同類(lèi)名的 .c 文件中
    20200107061222.png
  1. 找到native方法對(duì)應(yīng)的方法名JVM_IsInterrupted锨匆,在本倉(cāng)庫(kù)中搜索方法名JVM_IsInterrupted,發(fā)現(xiàn)在jvm.cpp中定義了該方法
// Thread.c文件中可以知道Native方法interrupt0對(duì)應(yīng)的本地方法是JVM_Interrupt
static JNINativeMethod methods[] = {
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
};
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");     // 綁定對(duì)應(yīng)方法

  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
  1. 上面代碼調(diào)用了Thread::interrupt(thr)冬筒,我們查看thread.cpp源碼恐锣,找到該方法
void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}
  1. 找到os::interrupt(thread)源碼在os_windows.cpp中,然后分析源碼
void os::interrupt(Thread* thread) {
  assert(!thread->is_Java_thread() || Thread::current() == thread || Threads_lock->owned_by_self(),
         "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
  osthread->set_interrupted(true);
  // More than one thread can get here with the same value of osthread,
  // resulting in multiple notifications.  We do, however, want the store
  // to interrupted() to be visible to other threads before we post
  // the interrupt event.
  OrderAccess::release();
  SetEvent(osthread->interrupt_event());
  // For JSR166:  unpark after setting status
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}

面試題 5:如何停止一個(gè)線程账千?

使用interrupt發(fā)送中斷通知侥蒙,

面試題 6:如何處理不可中斷的阻塞?

4. 線程狀態(tài)(核心4)

Thread的內(nèi)部類(lèi)State源碼如下

public enum State {
    // 更多線程狀態(tài)的描述信息見(jiàn)源碼注釋
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

線程一共有 6 種狀態(tài)匀奏,Java線程在運(yùn)行的生命周期中會(huì)處于下表所示的6種不同的狀態(tài)鞭衩,同一時(shí)刻,線程只能處于其中的一個(gè)狀態(tài)

狀態(tài)名稱 說(shuō)明
NEW 新建狀態(tài),線程被創(chuàng)建论衍,但還沒(méi)有調(diào)用start()方法
RUNNABLE (可)運(yùn)行狀態(tài)瑞佩,Java線程將操作系統(tǒng)中的就緒Ready和運(yùn)行Running兩種狀態(tài)合稱為“可運(yùn)行Runnable狀態(tài)”,此狀態(tài)的線程可能正在執(zhí)行坯台,也有可能正在等待CPU為它分配執(zhí)行時(shí)間
BLOCKED 阻塞狀態(tài)炬丸,表示線程在等待著一個(gè)排他鎖,
WAITING 等待狀態(tài)蜒蕾,表示當(dāng)前線程需要等待其他線程顯式喚醒或中斷稠炬,這種狀態(tài)的線程不會(huì)被CPU分配執(zhí)行時(shí)間。wait()咪啡,join()等方法會(huì)讓線程進(jìn)入無(wú)限期的等待狀態(tài)
TIME_WAITING 計(jì)時(shí)等待狀態(tài)首启,表示當(dāng)前線程需要等待其他線程喚醒或中斷,但是需要設(shè)置最長(zhǎng)等待時(shí)間撤摸,等待超時(shí)會(huì)進(jìn)入RUNNABLE狀態(tài)毅桃,這種狀態(tài)的線程也不會(huì)被CPU分配執(zhí)行時(shí)間
TERMINATED 終止?fàn)顟B(tài),表示當(dāng)前線程已經(jīng)執(zhí)行完畢

線程 6 種狀態(tài)的轉(zhuǎn)換圖如下所示准夷,可以知道钥飞,

  1. start0()會(huì)將線程狀態(tài)從 NEW 修改到 RUNNABLE,Debug 觀察this.getState()可知
  2. NEW衫嵌、RUNNABLE读宙、TERMINATED三種狀態(tài)只能從前往后,不可逆渐扮,
  3. BLOCKED论悴、WAITING、TIME_WAITING三種狀態(tài)都可以與RUNNABLE相互轉(zhuǎn)換墓律。

當(dāng)線程狀態(tài)到達(dá)TERMINATED膀估,如果還想執(zhí)行任務(wù),需要重新創(chuàng)建線程耻讽,復(fù)用Runnable實(shí)現(xiàn)類(lèi)即可

sadfq1qwrewq.png
20191229065809.png

阻塞狀態(tài)

一般習(xí)慣而言察纯,把BLOCKED(被阻塞),WAITING(等待)针肥,TIME_WAITING(計(jì)時(shí)等待)都稱為阻塞狀態(tài)

面試題 7:線程的生命周期是什么饼记,線程有哪幾種狀態(tài)?
根據(jù)上面的線程生命周期圖進(jìn)行描述慰枕,線程有 6 種狀態(tài)具则,轉(zhuǎn)換關(guān)系和轉(zhuǎn)換條件。

面試題 7:為什么Java線程沒(méi)有Running狀態(tài)具帮?
https://mp.weixin.qq.com/s?__biz=MzU4MDUyMDQyNQ==&mid=2247484215&idx=1&sn=2323d16b0e867baa5a152e926d398e57&chksm=fd54d3b1ca235aa79bf80a0c5c0c4659915bf994ec5a238b7aae1e6a18184c39909c6677bb43&scene=0&xtrack=1&key=af995e53853344b5f6d8f74ee64850b4f80f7182aa04658a4591811bed1f1d6723dcc5882656872d4cac4e1369685386b12d54789f77e4f6a994a154e6a4ee88343110a64f21b9a4d2ae7932c0c51a14&ascene=1&uin=ODEzMzE3OTc%3D&devicetype=Windows+10&version=62060833&lang=zh_CN&pass_ticket=%2FmxvoMvaRIx8ELIFwoajPh1MtdFTGO7bueEeQ6tKZPE%3D

5. 線程的方法(核心5)

Object.wait() 釋放調(diào)用對(duì)象的鎖博肋,進(jìn)入WAITING狀態(tài)

Object.notify() 喚醒同一對(duì)象一個(gè)WAITING/TIMED_WAITING狀態(tài)的線程低斋,用戶無(wú)法指定具體喚醒的線程

Object.notifyAll() 喚醒同一對(duì)象所有WAITING/TIMED_WAITING狀態(tài)的線程,

Thread.sleep() 進(jìn)入WAITING狀態(tài)匪凡,不釋放鎖膊畴,因?yàn)椴会尫沛i,所以sleep都是有參方法病游,需要設(shè)置時(shí)間邮旷,否則會(huì)持有鎖永久等待

5.1 wait/notify 實(shí)現(xiàn)線程通信

線程 t1 調(diào)用了object.wait()進(jìn)入等待狀態(tài)肚豺,線程 t2 調(diào)用object.notify()/notifyAll()喚醒 t1藐俺。

需要注意的是object.notify()/notifyAll()喚醒的是調(diào)用了object.wait()的線程每界,需要保證是同一個(gè)對(duì)象制圈,并且先waitnotify够吩。

前提: 由同一個(gè)lock對(duì)象調(diào)用wait嘀倒、notify方法采盒。

  1. 當(dāng)線程A執(zhí)行wait方法時(shí)唧龄,該線程會(huì)被掛起
  2. 當(dāng)線程B執(zhí)行notify方法時(shí)兼砖,會(huì)喚醒一個(gè)被掛起的線程A

面試題:lock對(duì)象、線程A和線程B三者是一種什么關(guān)系既棺?

根據(jù)上面的結(jié)論讽挟,可以想象一個(gè)場(chǎng)景:

  1. lock對(duì)象維護(hù)了一個(gè)等待隊(duì)列l(wèi)ist
  2. 線程A中執(zhí)行l(wèi)ock的wait方法,把線程A保存到list中
  3. 線程B中執(zhí)行l(wèi)ock的notify方法丸冕,從等待隊(duì)列中取出線程A繼續(xù)執(zhí)行
public class Wait {
    public static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            // 獲取object的monitor鎖
            synchronized (object) {
                System.out.println("線程" + Thread.currentThread().getName() +"開(kāi)始執(zhí)行了");
                try {
                    // 調(diào)用wait耽梅,釋放object的monitor鎖
                    // wait方法必須在synchronized中調(diào)用
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("線程" + Thread.currentThread().getName() + "獲取到了鎖");
            }
        };

        Runnable r2 = () -> {
            // 線程1釋放鎖后,進(jìn)入同步塊
            synchronized (object) {
                // 喚醒線程1胖烛,執(zhí)行完畢后眼姐,線程1開(kāi)始執(zhí)行
                object.notify();
                System.out.println("線程" + Thread.currentThread().getName() + "調(diào)用了notify");
            }
        };
        Thread t1 = new Thread(r, "t1");
        Thread t2 = new Thread(r2, "t2");
        t1.start();
        // 等待線程1啟動(dòng), 這樣才能保證先wait后notify
        Thread.sleep(200);    
        t2.start();
    }
}

wait()、notify()佩番、notifyAll()方法需要在synchronized塊中調(diào)用众旗,即必須獲取對(duì)象Monitor鎖,否則會(huì)拋出IllegalMonitorStateException異常

// wait方法必須在synchronized塊中調(diào)用趟畏,否則會(huì)報(bào)異常IllegalMonitorStateException
public class WaitException {
    public static Object object = new Object();

    public static void main(String[] args) {

        Thread t = new Thread(() -> {
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("...");
        });

        // 啟動(dòng)線程贡歧,會(huì)拋出IllegalMonitorStateException
        t.start();
    }
}

面試題 6:如何處理不可中斷的阻塞?

5.2 生產(chǎn)者消費(fèi)者模型

查看實(shí)例代碼可知:

  1. 生產(chǎn)者消費(fèi)者模型有三個(gè)組成部分:倉(cāng)庫(kù)赋秀、生產(chǎn)者利朵、消費(fèi)者

  2. 倉(cāng)庫(kù)有一個(gè)屬性容量maxsize,兩個(gè)功能生產(chǎn)put和消費(fèi)take

  3. puttake必須是線程安全的猎莲,防止多個(gè)生產(chǎn)者生產(chǎn)產(chǎn)品數(shù)量超出倉(cāng)庫(kù)maxsize

  4. 生產(chǎn)者put時(shí)當(dāng)倉(cāng)庫(kù)滿了進(jìn)入等待狀態(tài)(調(diào)用wait)绍弟,不再生產(chǎn),等待消費(fèi)者消費(fèi)并喚醒自己著洼;消費(fèi)者take時(shí)倉(cāng)庫(kù)空了進(jìn)入等待狀態(tài)樟遣,不再消費(fèi)姥份,等待生產(chǎn)者生產(chǎn)后并喚醒自己

面試題 7:兩個(gè)線程交替打印 0-100 的奇偶數(shù),即 A 線程只打印奇數(shù)年碘,B 線程只打印偶數(shù)

思路1:synchronized關(guān)鍵字澈歉,缺點(diǎn)是奇數(shù)線程釋放鎖后并不一定是偶數(shù)線程拿到鎖,會(huì)多次進(jìn)入無(wú)用循環(huán)屿衅,性能較差

public class PrintOddEvenSync {
    private static Object lock = new Object();
    private static int count = 0;

    public static void main(String[] args) {
        new Thread(() -> {
           while (count < 100) {
               synchronized (lock) {
                   if((count & 1) == 0) {
                       System.out.println(Thread.currentThread().getName() + ": " + count);
                       count++;
                   }
               }
           }
        }, "偶數(shù)Even").start();

        // 奇數(shù)線程
        new Thread(() -> {
            while (count < 100) {
                synchronized (lock) {
                    if((count & 1) == 1) {
                        System.out.println(Thread.currentThread().getName() + ": " + count);
                        count++;
                    }
                }
            }
        }, "奇數(shù)Odd").start();
    }
}

思路2:wait/notify埃难,線程A打印偶數(shù)數(shù)字之后,喚醒另一個(gè)線程涤久,自己進(jìn)入等待狀態(tài)涡尘;線程B打印奇數(shù)數(shù)字之后,喚醒另一個(gè)線程响迂,自己進(jìn)入等待狀態(tài)考抄。

A線程打印完后喚醒了其他線程,還未進(jìn)入狀態(tài)蔗彤,此時(shí)CPU切換到了線程B川梅,打印數(shù)字,喚醒其他線程然遏,但是此時(shí)A還沒(méi)有進(jìn)入WAITING狀態(tài)贫途,就會(huì)導(dǎo)致永久等待。所以需要synchronized保證同一時(shí)刻只有一個(gè)線程在打印待侵,當(dāng)然丢早,wait/notify也只能在同步方法中調(diào)用

public class PrintOddEvenWait {
    private static Object lock = new Object();
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            while (count < 100) {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + ": " + count);
                    count++;
                    // 打印之后,喚醒其他線程
                    lock.notify();
                    if(count < 100) {
                        try {
                            // 進(jìn)入等待狀態(tài)
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        new Thread(r, "偶數(shù)").start();
        // 使用sleep保證偶數(shù)線程先啟動(dòng)秧倾,或者使用CountDownLatch
        Thread.sleep(100);
        new Thread(r, "奇數(shù)").start();
    }
}

面試題 8:手寫(xiě)生產(chǎn)者消費(fèi)者設(shè)計(jì)模式怨酝?

見(jiàn)上面 生產(chǎn)者消費(fèi)者模型,查看示例代碼

面試題 9:wait和sleep有什么區(qū)別那先,為什么 wait 需要在同步代碼塊中使用农猬,而 sleep 不需要?

  1. 區(qū)別見(jiàn) wait 和 sleep 的區(qū)別
  2. wait釋放鎖的前提是獲取了對(duì)象的獨(dú)占鎖Monitor胃榕,調(diào)用wait()之后盛险,當(dāng)前線程又立即釋放掉鎖,線程隨后進(jìn)入WAIT_SET(等待池)中勋又。正如wait方法的注釋所說(shuō):This method should only be called by a thread that is the owner of this object's monitor
  3. wait 在執(zhí)行之后需要其他線程去 notify 喚醒苦掘,但是 wait 不一定能保證在 notify 之前執(zhí)行線程切換執(zhí)行),如果 notify 先執(zhí)行楔壤,wait 后執(zhí)行就不能釋放鎖鹤啡,可能會(huì)導(dǎo)致永久等待或死鎖,所以在 synchronized 同步代碼塊中使用是為了保證 wait/notify的先后順序

面試題 10:為什么線程通信方法 wait/notify/notifyAll 被定義在Object中蹲嚣,而sleep定義在Thread類(lèi)中递瑰?
wait/notify/notifyAll 屬于鎖操作祟牲,而鎖狀態(tài)標(biāo)志保存在對(duì)象頭中Mark Word,所以應(yīng)該定義在Object中抖部。

sleep 是線程操作说贝,所以定義在 Thread 類(lèi)中。

面試題 11:wait 方法是屬于 Object 的慎颗,那調(diào)用 Thread.wait 會(huì)怎么樣乡恕?

面試題 12:如何選擇 notify 和 notifyAll?

喚醒一個(gè)線程還是喚醒全部線程俯萎,notify 無(wú)法指定喚醒的線程

面試題 13:notifyAll 會(huì)喚醒所有線程傲宜,但是只有一個(gè)線程能獲取到鎖,那其他線程怎么辦夫啊?

其他線程會(huì)進(jìn)入 BLOCKED 阻塞狀態(tài)函卒,這類(lèi)似于起初多個(gè)線程獲取鎖,獲取不到的線程會(huì)進(jìn)入 BLOCKED 狀態(tài)等待鎖釋放

面試題 13:可以用suspend和resume來(lái)阻塞線程嗎撇眯?

這兩個(gè)方法已經(jīng)過(guò)時(shí)了报嵌,推薦使用wait/和notify來(lái)阻塞喚醒線程

sleep 方法

作用: 我只想讓線程在預(yù)期時(shí)間執(zhí)行,其他時(shí)候不要占用 CPU 資源叛本;例如定時(shí)檢查等

下面演示了sleep不釋放鎖

public class SleepDontReleaseLock implements Runnable {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }

    @Override
    public void run() {
        lock.lock();
        System.out.println("線程" + Thread.currentThread().getName() + "獲取到了鎖");
        try {
            Thread.sleep(5000);
            System.out.println("線程" + Thread.currentThread().getName() + "已經(jīng)蘇醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 一定要解鎖
            lock.unlock();
        }
    }
}

sleep 的優(yōu)雅寫(xiě)法

// 休眠3小時(shí)25分1秒沪蓬,休眠時(shí)間小于0直接忽略,而sleep會(huì)拋出異常
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(25);
TimeUnit.SECONDS.sleep(1);

wait 和 sleep 的區(qū)別

  • 相同
  1. wait 和 sleep 都可以使線程進(jìn)入(廣義)阻塞狀態(tài)
  2. wait 和 sleep 都是可中斷方法来候,被中斷后會(huì)拋出InterruptException
  • 不同
  1. wait 是 Object 的方法,而 sleep 是 Thread 特有的方法
  2. wait 方法的調(diào)用必須在同步方法中進(jìn)行逸雹,而 sleep 不需要
  3. 線程在同步方法中執(zhí)行 wait 時(shí)會(huì)釋放 monitor 鎖营搅,而 sleep 并不會(huì)釋放 monitor 鎖
  4. sleep 方法短暫休眠后會(huì)主動(dòng)退出阻塞,而 wait(沒(méi)有指定時(shí)間)則需要等待其他線程中斷或喚醒

join 方法

作用: 因?yàn)樾碌木€程加入了梆砸,我們需要等待他執(zhí)行完成转质,如果線程 M 中執(zhí)行了t1.join()方法,表示當(dāng)前線程 M 等待線程 t1 執(zhí)行完畢后才開(kāi)始執(zhí)行線程 M帖世,查看示例代碼

注意: Main 線程中調(diào)用子線程的join方法休蟹,表示 Main 線程需要等待子線程執(zhí)行完畢,Main 線程會(huì)進(jìn)入WAITING狀態(tài)日矫,而非子線程赂弓,這點(diǎn)與sleepwait不同哪轿,驗(yàn)證方法見(jiàn)示例代碼盈魁。有些資料說(shuō)join會(huì)進(jìn)入BLOCKED狀態(tài)是錯(cuò)誤的

因?yàn)檫M(jìn)入WAITING狀態(tài)的是Main線程,所以中斷WAITING狀態(tài)需要在子線程中調(diào)用main.interrupt()窃诉,具體見(jiàn)示例代碼

原理: 查看下面Thread類(lèi)的源碼杨耙,可知join()方法底層調(diào)用的是wait()赤套,由于每個(gè)線程執(zhí)行完后都會(huì)喚醒等待在該線程對(duì)象上的其他線程(源碼在 Thread.cpp),所以子線程執(zhí)行完后會(huì)喚醒 Main 線程珊膜。

    // Thread類(lèi)join()方法的源碼
    public final void join() throws InterruptedException {
        join(0);
    }

    // 同步方法容握,t1.join()表示獲得了t1對(duì)象的鎖,
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                // 參數(shù)為0表示進(jìn)入WAITING狀態(tài)车柠,直到其他線程喚醒
                // 調(diào)用者為線程對(duì)象a唯沮,a線程執(zhí)行完后會(huì)喚醒等待在a上的其他線程
                wait(0);    
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                // join參數(shù)不為0,進(jìn)入TIMED-WAITING狀態(tài)表示等待一段時(shí)間或其他線程喚醒
                wait(delay);    
                now = System.currentTimeMillis() - base;
            }
        }
    }
// Thread.cpp源碼堪遂,可知線程執(zhí)行完畢會(huì)喚醒其他線程
static void ensure_join(JavaThread* thread) {

  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  
  // to complete once we've done the notify_all below
  java_lang_Thread::set_thread(threadObj(), NULL);
  lock.notify_all(thread);      // 線程執(zhí)行完畢介蛉,喚醒等待在thread對(duì)象上的其他線程
  thread->clear_pending_exception();
}

通過(guò)上面的源碼,我們可以知道溶褪,join 底層原理就是 wait()币旧,所以我們可以自己實(shí)現(xiàn)以下join方法

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                // 子線程執(zhí)行完畢,會(huì)自動(dòng)調(diào)用notifyAll喚醒其他線程猿妈,源碼Thread.cpp中
                System.out.println("子線程執(zhí)行完畢吹菱,喚醒主線程");
            }
        });

        t.start();
        System.out.println("等待子線程運(yùn)行完畢");
        
        // 下面三行代碼與 t.join() 等價(jià),獲取線程t的Monitor鎖彭则,調(diào)用wait方法
        synchronized (t) {
            t.wait();
        }
        System.out.println("所有子線程執(zhí)行完畢鳍刷,開(kāi)始執(zhí)行Main線程");
    }

CountDownLatch 與 CylicBarrier

作用于join 類(lèi)似且更加強(qiáng)大,參考《java并發(fā)編程的藝術(shù)》

面試題 14:在 join 期間俯抖,線程會(huì)處于那種狀態(tài)输瓜?(wait/notify,CountDownLatch都可以引到這個(gè)問(wèn)題上來(lái))

  1. join期間芬萍,主線程處于WAITING狀態(tài)尤揣,可以通過(guò) debug 或 mainThread.getState 驗(yàn)證
  2. 因?yàn)?code>join底層是調(diào)用 wait() 方法,所以會(huì)處于WAITING狀態(tài)
  3. wait()方法獲取的Monitor鎖是線程對(duì)象的鎖柬祠,子線程執(zhí)行完會(huì)喚醒主線程

yield 方法

作用: 釋放我的 CPU 時(shí)間片北戏,線程狀態(tài)依然是RUNNABLE

一般開(kāi)發(fā)中不會(huì)使用yield漫蛔,但是JUC中AQS嗜愈,ConcurrentHashMap,F(xiàn)utuerTask等都會(huì)使用到yield方法

sleep 會(huì)讓出調(diào)度權(quán)莽龟,而yield雖然讓出了調(diào)度權(quán)蠕嫁,但也隨時(shí)可能被調(diào)度

其他Thread 方法

Thread.currentThread() 獲取正在執(zhí)行的線程對(duì)象
getState() 獲取線程狀態(tài)
getName() 獲取線程名稱
interrupt() 發(fā)送中斷通知
isInterrupted() 獲取中斷標(biāo)志位
public Thread(Runnable target, String threadName) 構(gòu)造方法,可以設(shè)置線程名稱

6. 線程的屬性

線程屬性 說(shuō)明
ID 每個(gè)線程都有自己的ID轧房,用于標(biāo)識(shí)不同的線程拌阴,不允許被修改
名稱 Name 在開(kāi)發(fā)過(guò)程中更容易區(qū)分不同線程,方便調(diào)試奶镶、定位問(wèn)題
守護(hù)線程 isDaemon true表示是守護(hù)線程迟赃,false表示是用戶線程
優(yōu)先級(jí) Priority 告訴線程調(diào)度器陪拘,用戶哪個(gè)線程多運(yùn)行,哪個(gè)少運(yùn)行

線程ID

查看 Thread 源碼可知纤壁,線程ID從1開(kāi)始左刽,不允許被修改。

第一個(gè)是Main線程酌媒,因?yàn)?Main 是入口方法欠痴,同時(shí)還要啟動(dòng)若干個(gè)線程,如Finalizer線程用來(lái)執(zhí)行對(duì)象的finalize()方法雨席,Reference Handler線程用于處理GC相關(guān)

    // 線程ID
    private static long threadSeqNumber;

    // 生成線程ID
    private static synchronized long nextThreadID() {
        // 先++后return陡厘,所以第一個(gè)線程Main線程ID為1
        return ++threadSeqNumber;
    }

線程名稱

查看 Thread 源碼可知云茸,若未指定線程名稱丙躏,則默認(rèn)使用 Thread+數(shù)字 作為線程名稱

    // 線程的構(gòu)造方法苗膝,若未指定線程名稱,則默認(rèn)使用 Thread+X 作為線程名稱
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

守護(hù)線程

作用: 為用戶線程提供服務(wù)的線程稱為守護(hù)線程乎完,JVM 中沒(méi)有了非Daemon線程网持,JVM需要退出宜岛,JVM中的所有Deamon線程都需要立即終止

特性:

  1. 線程類(lèi)型默認(rèn)繼承自父線程
  2. 除了Main線程,被JVM啟動(dòng)都是守護(hù)線程功舀,用戶啟動(dòng)的當(dāng)然都是用戶線程
  3. 守護(hù)線程不影響JVM的退出,用戶線程執(zhí)行完后身弊,JVM就會(huì)退出辟汰。

線程優(yōu)先級(jí)

線程優(yōu)先級(jí)共有 10 個(gè)級(jí)別,默認(rèn)為 5阱佛。但是程序設(shè)計(jì)不應(yīng)該依賴于優(yōu)先級(jí),因?yàn)椴煌牟僮飨到y(tǒng)對(duì)優(yōu)先級(jí)的處理不一樣扶踊,比如 Windows 中線程只有7個(gè)優(yōu)先級(jí)分井,Linux 中會(huì)忽略線程優(yōu)先級(jí)瘫辩;

面試題 15:如何利用線程優(yōu)先級(jí)幫助程序運(yùn)行弧械,有哪些禁忌画饥?

因?yàn)椴煌僮飨到y(tǒng)對(duì)優(yōu)先級(jí)的處理不同衔彻,不一定能生效椒涯,比如Win有7個(gè)級(jí)別,而Linux會(huì)忽略線程優(yōu)先級(jí)

7 線程的異常

主線程可以輕松發(fā)現(xiàn)異常哈扮,而子線程發(fā)生異常卻很難發(fā)現(xiàn)

子線程異常無(wú)法用傳統(tǒng)方法捕獲

如何全局處理異常包各,為什么要全局處理,不處理可以嗎?
run 方法是否可以拋出異常砚亭?如果拋出異常,線程狀態(tài)會(huì)怎么樣署尤?
線程中如何處理某個(gè)未處理異常混坞?

8 線程安全

什么是線程安全厨诸?

當(dāng)多個(gè)線程訪問(wèn)一個(gè)對(duì)象時(shí)颗管,如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行绽族,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果坠韩,那這個(gè)對(duì)象就是線程安全的 ---- Brin Goetz

9 Java 內(nèi)存模型JMM

彩蛋:自頂向下的好處

先講適用場(chǎng)景,再講怎么用熊赖,最后講原理嫂粟。問(wèn)題興趣驅(qū)動(dòng),與傳統(tǒng)的教育方式相反

直觀的理解玩裙,感性的認(rèn)識(shí)溶诞,有助于加深理解,最后帶著好奇心去分析源碼
《計(jì)算機(jī)網(wǎng)絡(luò) 自頂向下方法》

為什么需要JMM(Java Memory Model)决侈?

因?yàn)椴煌?CPU 平臺(tái)的機(jī)器指令千差萬(wàn)別螺垢,無(wú)法保證Java 代碼到 CPU 指令翻譯的準(zhǔn)確無(wú)誤展父,所以需要JMM來(lái)統(tǒng)一規(guī)范,讓多線程運(yùn)行的結(jié)果可預(yù)期

并發(fā)編程有三個(gè)問(wèn)題:

  1. 緩存導(dǎo)致的可見(jiàn)性問(wèn)題 - happen-before規(guī)則肺魁,volatile也能禁用CPU本地緩存
  2. 編譯優(yōu)化帶來(lái)的重排序問(wèn)題 - volatile語(yǔ)義增強(qiáng)禁止重排序
  3. 線程切換帶來(lái)的原子性問(wèn)題 - 互斥鎖來(lái)禁止線程切換

所以為了解決上述問(wèn)題,JMM 分為三個(gè)部分:重排序东亦、可見(jiàn)性、原子性

本章節(jié)詳細(xì)內(nèi)容參考《Java并發(fā)編程的藝術(shù)》第3章

9.1 重排序

重排序帶來(lái)的問(wèn)題

先來(lái)看一段代碼瘸恼,由于 CPU 執(zhí)行多線程時(shí)不斷切換,有可能得到 4 中結(jié)果

  1. x=1酌住,y=0 t2先執(zhí)行,t1再執(zhí)行
  2. x=0, y=1 t1先執(zhí)行狰域,t2再執(zhí)行
  3. x=1, y=1 t1給a賦值完后 CPU 切換到 t2執(zhí)行
  4. x=0, y=0 由于內(nèi)存重排序孩饼,t1對(duì)a進(jìn)行了賦值a=1椰弊,但沒(méi)有將該數(shù)據(jù)刷新到主存,導(dǎo)致t2執(zhí)行y=a時(shí)從主存拿到的a==0瓤鼻,所以最終y=0秉版;同理,t2 執(zhí)行的b=1也沒(méi)有刷新到主存茬祷,這就是重排序帶來(lái)的問(wèn)題沐飘。
public class OutOfOrderDemo {
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
                    a = 1;
                    x = b;
        });
        Thread t2 = new Thread(() -> {
            b = 1;
            y = a;
        });

        // 啟動(dòng)線程,交換線程的啟動(dòng)順序牲迫,會(huì)得到不同的x,y
        t1.start();
        t2.start();
        // 等待兩個(gè)線程執(zhí)行完畢
        t1.join();
        t2.join();
        System.out.println("x = " + x + ", y = " + y);
    }
}
20200110054241.png

[圖片上傳失敗...(image-8d82da-1583382844738)]
上圖處理器A和處理器B可以同時(shí)把共享變量a=1,b=1寫(xiě)入自己的緩沖區(qū)(A1,B1)借卧,然后從主存中讀取另一個(gè)共享變量b=0,a=0(A2,B2),最后才把自己寫(xiě)緩沖區(qū)保存的臟數(shù)據(jù)x=0,y=0刷新到了主存中(A3,B3)盹憎,雖然處理器A的執(zhí)行順序是A1->A2,但內(nèi)存操作實(shí)際發(fā)生的順序是A2->A1铐刘,此時(shí)陪每,處理器A的內(nèi)存操作就被重排序了。(詳見(jiàn)Java并發(fā)編程的藝術(shù)P25)

為了演示x=0, y=0的情況镰吵,可以查看示例代碼檩禾,需要多次運(yùn)行,運(yùn)行結(jié)果如下圖所示

20200110140511.png

重排序分為三種情況:

  1. 編譯器優(yōu)化:包括JVM疤祭,JIT編譯器
  2. CPU 指令重排:
  3. 內(nèi)存重排序
并發(fā)編程的藝術(shù)P24.png

重排序的好處

20200110051036.png

如上圖所示盼产,CPU 對(duì)代碼進(jìn)行了重排序,使得原先 9 條指令優(yōu)化為了 7 條指令勺馆,提高了 CPU 的處理速度戏售。其中 Load 表示從內(nèi)存讀取到CPU,Set 表示賦值草穆,Store 表示存儲(chǔ)到內(nèi)存

9.2 可見(jiàn)性

因?yàn)镃PU 有多級(jí)緩存灌灾,導(dǎo)致讀的數(shù)據(jù)會(huì)過(guò)期。

高速緩存的容量比主內(nèi)存小悲柱,但是速度僅次于寄存器锋喜,所以CPU和主內(nèi)存之間就多了Cache層

線程間對(duì)于共享變量的可見(jiàn)性問(wèn)題不是直接由多核引起的,而是多級(jí)緩存引起的


20200110122111.png

由上圖可知豌鸡,CPU 有多級(jí)緩存嘿般,JMM將共享的內(nèi)存L3 Cache段标、RAM抽象為主存,將核心獨(dú)有的內(nèi)存registers(寄存器)博个、L1 chache怀樟、L2 cache抽象為本地內(nèi)存(工作內(nèi)存)

為什么需要多級(jí)緩存?

為了提高CPU的執(zhí)行速度盆佣,因?yàn)镃PU的速度遠(yuǎn)遠(yuǎn)高于內(nèi)存往堡,所以需要將內(nèi)存中的數(shù)據(jù)提前讀取到緩存中。

比如我們找一本書(shū)的過(guò)程共耍,書(shū)桌上有常用的書(shū)虑灰,數(shù)量少,找書(shū)速度快痹兜,如果書(shū)桌上找不到穆咐,那么我們就去校圖書(shū)館找,數(shù)量較大速度一般字旭,如果還找不到对湃,就去市圖書(shū)館去找,數(shù)量極大速度最慢遗淳。書(shū)桌就是一級(jí)緩存拍柒,校圖書(shū)館就是二級(jí)緩存,市圖書(shū)館是內(nèi)存屈暗,這三者容量依次升高拆讯,查找速度依次降低。雖然一級(jí)緩存速度最高养叛,但也不能指望提高一級(jí)緩存的大小來(lái)提高緩存讀取速度种呐,因?yàn)榫彺嬖酱螅檎宜俣仍铰?/p>

另外將內(nèi)存中的數(shù)據(jù)讀到內(nèi)存中弃甥,但是CPU需要的數(shù)據(jù)不一定在緩存中爽室,這里涉及緩存命中(此處添加緩存命中文章的鏈接)的知識(shí)

主存與本地內(nèi)存的關(guān)系

JMM 有以下規(guī)定:

  1. 所有變量都存儲(chǔ)在主存中,同時(shí)每個(gè)線程也有自己的本地內(nèi)存潘飘,工作內(nèi)存中的變量?jī)?nèi)容是主存中的拷貝
  2. 線程不能直接讀寫(xiě)主存中的變量肮之,只能操作本地內(nèi)存中的變量,然后再同步到主存中
  3. 主存是多個(gè)線程共享的卜录,但線程間不共享本地內(nèi)存戈擒,如果線程間需要通信,必須借助主存中專來(lái)完成

Happen-Before (先行發(fā)生)原則

如果說(shuō)操作A Happen-Before 于操作B艰毒,其實(shí)就是說(shuō)操作B之前筐高,操作A的影響對(duì)于操作B可見(jiàn)

Happen-before 的概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性happen-before僅要求第一個(gè)操作執(zhí)行結(jié)果對(duì)第二個(gè)操作可見(jiàn),且前一個(gè)操作實(shí)際執(zhí)行時(shí)間排在第二個(gè)操作之前(the first is visible to and ordered before the second)

在JMM中柑土,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn)蜀肘,那么這兩個(gè)操作之間必須要存在happen-before關(guān)系,詳細(xì)見(jiàn)《并發(fā)編程藝術(shù)》p26稽屏,《深入淺出JVM》p376扮宠,一共有 9 個(gè)規(guī)則,其中最重要的是前 4 條規(guī)則

  1. 單線程原則:一個(gè)線程中的每個(gè)操作狐榔,對(duì)于該線程中的任意后續(xù)動(dòng)作可見(jiàn)坛增,也就是說(shuō)都在本地內(nèi)存運(yùn)行,不存在可見(jiàn)性問(wèn)題薄腻,又叫程序順序原則
  2. Monitor鎖原則(synchronized和Lock):對(duì)一個(gè)鎖的解鎖收捣,對(duì)于后續(xù)其他線程同一個(gè)鎖的加鎖可見(jiàn),這里的“后續(xù)”指的是時(shí)間上的先后順序庵楷,又叫管程鎖定原則罢艾,
  3. volatile 變量原則:對(duì)于一個(gè)volatile變量的寫(xiě),對(duì)于后續(xù)其他線程對(duì)volatile變量的讀可見(jiàn)尽纽,也就是說(shuō)volatile變量的寫(xiě)會(huì)直接刷新到主存咐蚯,這里的“后續(xù)”指的是時(shí)間上的先后順序
  4. 傳遞性原則:如果A happen-before B,B happen-before C弄贿,那么A happen-before C仓蛆,最常用到的一個(gè)原則
  5. start()原則:如果線程A執(zhí)行ThreadB.start()啟動(dòng)線程B,那么A線程ThreadB.start()及之前的操作對(duì)于線程B的所有操作可見(jiàn)
  6. 線程終止原則:線程中的所有操作挎春,對(duì)于此線程的終止檢測(cè)可見(jiàn),我們可以通過(guò)Thread.join()方法結(jié)束豆拨,Thread.isAlive()的返回值等手段直奋,檢測(cè)到線程終止運(yùn)行
  7. join()原則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的所有操作施禾,對(duì)于線程A可見(jiàn)脚线,實(shí)際開(kāi)發(fā)中,我們也是用join()方法來(lái)獲取線程B中的執(zhí)行結(jié)果弥搞,這一條規(guī)則其實(shí)是線程終止原則的細(xì)化部分
  8. 線程中斷規(guī)則:對(duì)線程A 調(diào)用ThreadB.interrupt()方法邮绿,對(duì)于線程B檢測(cè)中斷isInterrupted可見(jiàn)
  9. 對(duì)象終結(jié)原則:一個(gè)對(duì)象的初始化完成,對(duì)于該對(duì)象的finalize()方法可見(jiàn)
20200110125749.png

上圖中由程序順序原則可知攀例,1 happen-before 2船逮,3 happen-before 4,由volatile 變量原則 可知粤铭,2 happen-before 3挖胃,再結(jié)合傳遞性原則,可知 1 happen-before 4
另外還有一個(gè)重要的并發(fā)工具類(lèi)原則:

  1. 線程安全的容器,如CurrenthashMapput操作酱鸭,對(duì)于后續(xù)get操作可見(jiàn)
  2. CountDownLatchcountDown()操作吗垮,對(duì)于后續(xù)await()可見(jiàn)
  3. Semaphorerelease()釋放許可證操作,對(duì)于后續(xù)acquire()獲取許可證操作可見(jiàn)
  4. CyclicBarrier的最后一個(gè)線程到達(dá)屏障時(shí)凹髓,對(duì)于所有被攔截的線程await()可見(jiàn)
  5. Futurecall()操作執(zhí)行結(jié)果烁登,對(duì)于后續(xù)get()操作可見(jiàn),詳細(xì)見(jiàn)示例
  6. 線程池

面試題: 對(duì)于一個(gè)鎖的unlock操作蔚舀,對(duì)于后續(xù)的lock操作可見(jiàn)饵沧。因?yàn)槿绻豢梢?jiàn),其他線程就沒(méi)法獲取鎖了蝗敢。那對(duì)于一個(gè)鎖的lock操作捷泞,對(duì)于后續(xù)的unlock肯定也是可見(jiàn)的,那么這個(gè)由什么保證呢寿谴?

對(duì)于一個(gè)鎖的unlock操作凸丸,對(duì)于后續(xù)的lock操作可見(jiàn)性锭,隱含條件是對(duì)其他線程后續(xù)的lock操作可見(jiàn)。而對(duì)于一個(gè)鎖的lock操作和unlock操作,肯定是在同一個(gè)線程里捣卤,可以用單線程原則解釋

面試題:volatile 變量的寫(xiě) happen-before volatile 變量的讀是怎么保證的?

插入內(nèi)存屏障尸红,每個(gè)volatile寫(xiě)操作的前面都會(huì)插入StoreStore屏障谁榜,后面都會(huì)插入一個(gè)StoreLoad屏障,如下圖所示

[圖片上傳失敗...(image-381312-1583382844738)]

StoreStore屏障保證在volatile寫(xiě)之前的普通寫(xiě)操作狼犯,已經(jīng)對(duì)任意處理器可見(jiàn)了余寥。因?yàn)?code>StoreStore屏障把上面的所有普通寫(xiě)刷新到了內(nèi)存
詳細(xì)見(jiàn)(詳細(xì)見(jiàn)《Java并發(fā)編程的藝術(shù)》3.4.4)

volatile 關(guān)鍵字

volatile是一種同步機(jī)制,比synchronized或者Lock相關(guān)類(lèi)更輕量悯森,因?yàn)槭褂胿olatile不會(huì)發(fā)生上下文切換等開(kāi)銷(xiāo)很大的行為宋舷。

需要注意volatile做不到synchronized那樣的原子保護(hù)

不適應(yīng)場(chǎng)景:i++

不是一個(gè)原子操作,volatile無(wú)法保證原子性瓢姻,導(dǎo)致出錯(cuò)

使用場(chǎng)景1:boolean flag

如果一個(gè)共享變量自始至終只被各個(gè)線程賦值祝蝠,沒(méi)有其他操作,則可以使用volatile來(lái)替代synchronized和原子變量幻碱,因?yàn)橘x值本身就是原子操作绎狭,而volatile又保證了可見(jiàn)性,所以線程安全

使用場(chǎng)景2:作為刷新之前變量的觸發(fā)器

volatile能保證之前的操作全部刷新到主存

下面的代碼褥傍,變量 v 的作用就是觸發(fā)器儡嘶,當(dāng)v=true時(shí),前面的代碼(x=42)的執(zhí)行結(jié)果已經(jīng)刷新到了主存

面試題:線程A執(zhí)行完writer()后恍风,線程B執(zhí)行reader()社付,下面代碼注釋部分x會(huì)是多少呢承疲?為什么

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;                 // 1
    v = true;               // 2
  }
  public void reader() {
    if (v == true) {        // 3
      // 這里x會(huì)是多少呢?
      int i = x;            // 4
    }
  }
}

根據(jù)happen-before單線程原則鸥咖,1 happen-before 2燕鸽,3 happen-before 4;volatile變量原則啼辣,2 happen-before 3啊研;再結(jié)合傳遞性原則,1 happen-before 4鸥拧,所以得到i=42

20200111071300.png

在舊的內(nèi)存模型中党远,當(dāng)1和2之間沒(méi)有數(shù)據(jù)依賴關(guān)系時(shí),1和2之間就可能被重排序富弦。其結(jié)果就是線程B執(zhí)行到4時(shí)沟娱,不一定能看到線程A對(duì)共享變量x的修改,x可能為0腕柜。
JSR133之前對(duì)下面代碼重排序不進(jìn)行限制济似,JSR133增強(qiáng)了volatile的內(nèi)存語(yǔ)義,嚴(yán)格限制編譯器和處理器對(duì)volatile變量與普通變量的重排序盏缤,所以x=42砰蠢。(詳細(xì)見(jiàn)《Java并發(fā)編程的藝術(shù)》3.4.5)

x=45; // 1
v=true; // 2

volatile的兩點(diǎn)作用

可見(jiàn)性:讀一個(gè)volatile變量之前,需要先使相應(yīng)的本地緩存失效唉铜,這樣就必須到主內(nèi)存讀取最新值台舱,寫(xiě)一個(gè)volatile屬性會(huì)立即刷入到主內(nèi)存

禁止指令重排序:解決單例雙重鎖亂序問(wèn)題

volatile小結(jié)

  1. volatile屬性的讀寫(xiě)操作都是無(wú)鎖的,它不能替代synchronized潭流,因?yàn)樗鼪](méi)有提供原子性和互斥性竞惋。因?yàn)闊o(wú)鎖,不需要花費(fèi)時(shí)間在獲取鎖和釋放鎖灰嫉,所以說(shuō)volatile是低成本的

  2. volatile只能作用于屬性碰声,我們用volatile修飾屬性,這樣能禁止重排序

  3. volatile提供了可見(jiàn)性熬甫,任何一個(gè)線程對(duì)其的修改對(duì)其他線程立即可見(jiàn)。volatile屬性不會(huì)使用本地緩存蔓罚,始終從主存中讀取和寫(xiě)入

  4. volatile 提供了Happen-before保證椿肩,對(duì)volatile變量v的寫(xiě)入happen-before所有其他線程后續(xù)對(duì)v的讀取

  5. volatile 可以使得 long 和double 變量的賦值是原子操作

保證可見(jiàn)性的幾種方法

volatile、synchronized豺谈、Lock郑象、并發(fā)集合、Thread.join()茬末、Thread.start()

Happen-before

面試題:有一個(gè)共享變量 abc厂榛,在一個(gè)線程里設(shè)置了 abc 的值 abc=3盖矫,你思考一下,有哪些辦法可以讓其他線程能夠看到abc==3击奶?

見(jiàn)極客時(shí)間

synchronized

synchronized不僅保證了原子性辈双,還保證了可見(jiàn)性

9.3 原子性

一系列的操作,要么全部執(zhí)行成功柜砾,要么全部不執(zhí)行湃望,不會(huì)出現(xiàn)執(zhí)行一般的情況

Java中的原子操作

  • 基本類(lèi)型(int,byte痰驱,boolean证芭,short,char担映,float)的賦值操作废士,除了long和double,i++不是原子操作

  • 所有引用的賦值操作蝇完,不管是32位還是64位機(jī)器

  • java.concurrent.Atomic.*包中所有類(lèi)的原子操作

long和double的原子性

  1. 有可能出現(xiàn)線程安全問(wèn)題官硝,32位機(jī)器上對(duì)64位的long/double類(lèi)型變量進(jìn)行讀寫(xiě)操作,可能出現(xiàn)線程安全問(wèn)題四敞。Java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long和double類(lèi)型變量的寫(xiě)操作具有原子性泛源,所以存在不是原子操作的可能

  2. 不需要加volatile,JSR對(duì)于商用的JVM忿危,強(qiáng)烈建議將load, store, read, write四個(gè)操作實(shí)現(xiàn)為原子操作达箍,而且目前各平臺(tái)下的商用JVM都將其實(shí)現(xiàn)為了原子操作。因此實(shí)際編程中不需要把long铺厨,double類(lèi)型修飾為volatile變量缎玫。

詳細(xì)見(jiàn)《深入JVM》

全同步的HashMap的線程安全問(wèn)題,待補(bǔ)充

原子操作的實(shí)現(xiàn)原理

見(jiàn)《Java并發(fā)編程藝術(shù)》2.3原子操作的實(shí)現(xiàn)原理解滓。
處理器使用一下兩種方法實(shí)現(xiàn)原子操作
1.使用總線LOCK#信號(hào)保證原子性
2.通過(guò)緩存鎖定保證原子性

Java 使用CAS實(shí)現(xiàn)原子操作
Atomic的原理就是CAS赃磨,CAS操作會(huì)帶來(lái)三個(gè)問(wèn)題:1. ABA 2. 循環(huán)時(shí)間長(zhǎng)開(kāi)銷(xiāo)大 3. 只能保證一個(gè)共享變量的原子操作

面試題:volatile修飾的變量 i,能保證i++操作線程安全嗎?

經(jīng)過(guò)示例代碼github.jmm.NOVolaitile驗(yàn)證洼裤,不能保證線程安全邻辉,因?yàn)?code>i++不是原子操作,需要4步才能完成腮鞍,而volatile僅能解決重排序和可見(jiàn)性問(wèn)題值骇,原子性問(wèn)題需要鎖或CAS(原子類(lèi))來(lái)解決,詳細(xì)見(jiàn)《碼書(shū)》p230

9.4 單例模式

為什么需要單例模式?

  1. 節(jié)省內(nèi)存CPU資源移国,如果創(chuàng)建一個(gè)需要耗費(fèi)大量?jī)?nèi)存吱瘩、大量計(jì)算(耗費(fèi)CPU資源),大量耗時(shí)(從DB讀取數(shù)據(jù))的對(duì)象迹缀,我們使用單例模式使碾,可以節(jié)省內(nèi)存CPU資源
  2. 保證結(jié)果正確蜜徽,如多線程統(tǒng)計(jì)訪問(wèn)人數(shù),需要一個(gè)全局的計(jì)數(shù)器實(shí)例票摇,如果創(chuàng)建了多個(gè)計(jì)數(shù)器拘鞋,就會(huì)統(tǒng)計(jì)錯(cuò)誤,需要代碼層面限制創(chuàng)建多個(gè)計(jì)數(shù)器對(duì)象實(shí)例
  3. 方便管理兄朋,如日期工具類(lèi)掐禁、字符串工具類(lèi),我們不需要?jiǎng)?chuàng)建多個(gè)工具類(lèi)對(duì)象實(shí)例颅和,只會(huì)耗費(fèi)內(nèi)存傅事,一般工具類(lèi)都是類(lèi).靜態(tài)方法調(diào)用,也需要代碼層面限制創(chuàng)建工具類(lèi)實(shí)例峡扩,如java.lang.Math類(lèi)的構(gòu)造方法都是私有的蹭越,java.lang.Runtime也是單例模式

單例模式適用場(chǎng)景

  1. 無(wú)狀態(tài)的工具類(lèi):如日志工具類(lèi),不管在哪里適用教届,只需要它記錄日志信息响鹃,并不需要它的實(shí)例對(duì)象上存儲(chǔ)任何狀態(tài),故我們只需要一個(gè)實(shí)例對(duì)象即可案训。spring無(wú)狀態(tài)bean买置,
  2. 全局信息類(lèi):比如一個(gè)類(lèi)用來(lái)統(tǒng)計(jì)網(wǎng)站的訪問(wèn)次數(shù),我們不希望有的訪問(wèn)記錄在對(duì)象A上强霎,有的記錄在對(duì)象B上忿项,我們就讓這個(gè)類(lèi)成為單例

spirng框架使用單例模式

對(duì)于最常用的spring框架來(lái)說(shuō),我們經(jīng)常用spring來(lái)幫我們管理一些無(wú)狀態(tài)的bean城舞,其默認(rèn)設(shè)置為單例轩触,這樣在整個(gè)spring框架的運(yùn)行過(guò)程中,即使被多個(gè)線程訪問(wèn)和調(diào)用家夺,這些“無(wú)狀態(tài)”的bean就只會(huì)存在一個(gè)脱柱,為他們服務(wù)。那么“無(wú)狀態(tài)”bean指的是什么呢拉馋?

無(wú)狀態(tài):當(dāng)前我們托管給spring框架管理的javabean主要有service榨为、mybatis的mapper、一些utils煌茴,這些bean中一般都是與當(dāng)前線程會(huì)話狀態(tài)無(wú)關(guān)的随闺,沒(méi)有自己的屬性,只是在方法中會(huì)處理相應(yīng)的邏輯景馁,每個(gè)線程調(diào)用的都是自己的方法,在自己的方法棧中逗鸣。

有狀態(tài):指的是每個(gè)用戶有自己特有的一個(gè)實(shí)例合住,在用戶的生存期內(nèi)绰精,bean保持了用戶的信息,即“有狀態(tài)”透葛;一旦用戶滅亡(調(diào)用結(jié)束或?qū)嵗Y(jié)束)笨使,bean的生命期也告結(jié)束。即每個(gè)用戶最初都會(huì)得到一個(gè)初始的bean僚害,因此在將一些bean如User這些托管給spring管理時(shí)硫椰,需要設(shè)置為prototype多例,因?yàn)楸热鐄ser萨蚕,每個(gè)線程會(huì)話進(jìn)來(lái)時(shí)操作的user對(duì)象都不同靶草,因此需要設(shè)置為多例。

優(yōu)勢(shì):

  1. 減少了新生成實(shí)例的消耗岳遥,spring會(huì)通過(guò)反射或者cglib來(lái)生成bean實(shí)例這都是耗性能的操作奕翔,其次給對(duì)象分配內(nèi)存也會(huì)涉及復(fù)雜算法;
  2. 減少jvm垃圾回收浩蓉;
  3. 可以快速獲取到bean派继;

劣勢(shì):

單例的bean一個(gè)最大的劣勢(shì)就是要時(shí)刻注意線程安全的問(wèn)題,因?yàn)橐坏┯芯€程間共享數(shù)據(jù)變很可能引發(fā)問(wèn)題捻艳。

log4j中的單例模式

在使用log4j框架時(shí)也注意到了其使用的是單例驾窟,當(dāng)然也為了保證單個(gè)線程對(duì)日志文件的讀寫(xiě)時(shí)不出問(wèn)題。如果是多例认轨,那么后面的實(shí)例日志操作會(huì)覆蓋之前的日志文件

參考慕課網(wǎng)《Java設(shè)計(jì)模式》單例模式的實(shí)踐完善本小節(jié)

單例模式的8種實(shí)現(xiàn)

  1. 餓漢式
  2. 懶漢式
  3. 雙重檢查
  4. 靜態(tài)內(nèi)部類(lèi)
  5. 枚舉

雙重檢查單例模式

雙重檢查單例模式的實(shí)現(xiàn)如下所示绅络,INSTANCE = new DoubleCheckSingleton();不是原子操作,分為三個(gè)步驟:

  1. 分配堆內(nèi)存
  2. 對(duì)象初始化好渠,調(diào)用構(gòu)造方法創(chuàng)建對(duì)象
  3. 將對(duì)象賦值給INSTANCE變量

其中步驟2和步驟三可能出現(xiàn)重排序昨稼,如果執(zhí)行順序?yàn)?32,當(dāng)執(zhí)行到步驟2時(shí)拳锚,另一個(gè)線程進(jìn)入假栓,發(fā)現(xiàn)INSTANCE不為NULL,則會(huì)返回一個(gè)未初始化完畢的實(shí)例對(duì)象

[圖片上傳失敗...(image-35cd16-1583382844738)]

/**
 * 雙重檢查單例模式, 推薦使用
 * 線程安全, 延遲加載, 效率高
 */
public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton INSTANCE;
    /**
     * 構(gòu)造函數(shù)私有, 避免破壞單例
     */
    private DoubleCheckSingleton(){};

    /**
     * 獲取單例對(duì)象, 需要兩次if檢查, 故稱為雙重檢查
     * 解決了LazyUnSyncSingleton線程不安全的問(wèn)題, 解決了LazySyncSingleton后續(xù)獲取對(duì)象的效率低的問(wèn)題
     *
     * 但是 new DoubleCheckSingleton() 不是一個(gè)原子操作, 當(dāng)另一個(gè)線程進(jìn)入第一次檢查if(null == INSTANCE), 會(huì)返回一個(gè)未初始化完成的實(shí)例對(duì)象
     * 所以需要volatile 來(lái)禁止重排序
     */
    public  static DoubleCheckSingleton getInstance() {
        if(null == INSTANCE) {
            synchronized (DoubleCheckSingleton.class) {
                if(null == INSTANCE) {
                    // 不是原子操作,需要volatile禁止重排序
                    INSTANCE = new  DoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

靜態(tài)內(nèi)部類(lèi)單例模式

實(shí)現(xiàn)原理見(jiàn)JVM書(shū)霍掺,懶加載匾荆,用JVM類(lèi)加載特性保證線程安全

https://blog.csdn.net/mnb65482/article/details/80458571
單例模式靜態(tài)內(nèi)部類(lèi)原理

枚舉單例模式

《Effective Java》說(shuō)使用單元素的枚舉是實(shí)現(xiàn)單例模式的最佳方法

  1. 寫(xiě)法簡(jiǎn)單
  2. 線程安全
  3. 避免反序列化破壞單例
  4. 避免反射攻擊

驗(yàn)證反射是否能夠破壞枚舉模式,示例代碼如下杆烁,會(huì)報(bào)NoSuchMethodException異常牙丽,詳細(xì)原理見(jiàn)參考文檔12

    EnumSingleton singleton1=EnumSingleton.INSTANCE;
    EnumSingleton singleton2=EnumSingleton.INSTANCE;
    System.out.println("正常情況下,實(shí)例化兩個(gè)實(shí)例是否相同:"+(singleton1==singleton2));
    Constructor<EnumSingleton> constructor= null;
    constructor = EnumSingleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    EnumSingleton singleton3= null;
    singleton3 = constructor.newInstance();
         System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
    System.out.println("通過(guò)反射攻擊單例模式情況下兔魂,實(shí)例化兩個(gè)實(shí)例是否相同:"+(singleton1==singleton3));

面試題:雙重檢查單例模式的特點(diǎn)

優(yōu)點(diǎn):線程安全烤芦,延遲加載,獲取對(duì)象效率高

  1. 為什么要double-check析校?
    • synchronized修飾方法線程安全但后續(xù)獲取實(shí)例效率低
    • synchronized縮小范圍构罗,單check線程不安全

為了兼顧線程安全和后續(xù)獲取實(shí)例的效率铜涉,衍生出來(lái)雙重檢測(cè)單例模式

  1. 為什么要用volatile?

    • 新建對(duì)象不是原子操作遂唧,需要分類(lèi)內(nèi)存芙代,調(diào)用構(gòu)造方法,賦值操作三部分

    • 重排序可能會(huì)使得賦值操作早于調(diào)用構(gòu)造方法盖彭,出現(xiàn)NPE纹烹,所以需要volatile禁止重排序

實(shí)踐
? tsp中使用單例模式,沒(méi)有使用volatile召边,且創(chuàng)建對(duì)象后還要調(diào)用set方法袋狞,不是原子操作盛正,會(huì)出現(xiàn)線程安全問(wèn)題咸产,解決辦法見(jiàn)印象筆記

面試題:?jiǎn)卫J降淖罴褜?shí)現(xiàn)是什么听想?

  1. Effective Java中說(shuō)枚舉是單例的最佳實(shí)現(xiàn)
  2. 寫(xiě)法簡(jiǎn)單
  3. 線程安全
  4. 避免反序列化破壞單例

面試題: 單例模式實(shí)現(xiàn)有幾種,各有哪些優(yōu)缺點(diǎn)贱鼻?

靜態(tài)內(nèi)部類(lèi)的實(shí)現(xiàn)方式可以引申到JVM類(lèi)加載

雙重檢查的實(shí)現(xiàn)方式可以引申到并發(fā)宴卖、鎖、volatile邻悬、重排序症昏、原子操作等知識(shí)

枚舉類(lèi)的實(shí)現(xiàn)方式可以引申到反編譯,枚舉的原理父丰,反序列化

面試題:什么是JMM肝谭,JMM為了解決什么問(wèn)題?

面試題:Java內(nèi)存模型
Happen-before volatile 主存和本地緩存

面試題:volatile和synchronized的異同

面試題: 什么是原子操作蛾扇,i++攘烛、創(chuàng)建對(duì)象、賦值镀首、long類(lèi)型的寫(xiě)是不是原子操作坟漱,怎么解決?

面試題:什么是內(nèi)存可見(jiàn)性更哄?
[圖片上傳失敗...(image-f637ef-1583382844739)]

面試題:64位的double和long寫(xiě)入操作是原子操作嗎芋齿?

  1. 有可能,Java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long和double類(lèi)型變量的寫(xiě)操作具有原子性成翩,所以存在不是原子操作的可能
  2. 不需要加volatile觅捆,JSR對(duì)于商用的JVM,強(qiáng)烈建議將load, store, read, write四個(gè)操作實(shí)現(xiàn)為原子操作麻敌,而且目前各平臺(tái)下的商用JVM都將其實(shí)現(xiàn)為了原子操作栅炒。因此不需要把long,double類(lèi)型修飾為volatile變量。

10 死鎖

考考你

  1. 寫(xiě)一個(gè)必然死鎖的例子(百度面試題)
  2. 發(fā)生死鎖必須滿足哪些條件?
  3. 如何定位死鎖?
  4. 有哪些解決死鎖問(wèn)題的策略
  5. 講講經(jīng)典的哲學(xué)家就餐問(wèn)題
  6. 實(shí)際工程中如何避免死鎖?
  7. 什么是活躍性問(wèn)題? 活鎖赢赊、饑餓和死鎖有什么區(qū)別棒呛?

死鎖是什么?

當(dāng)兩個(gè)(或更多)線程(或進(jìn)程)相互持有對(duì)方所需要的資源域携,又不主動(dòng)釋放,導(dǎo)致所有線程都無(wú)法繼續(xù)運(yùn)行鱼喉,陷入無(wú)盡的阻塞秀鞭,這就是死鎖

10.1 死鎖的影響

數(shù)據(jù)庫(kù)中:兩個(gè)事務(wù)互相持有對(duì)方需要的資源,檢測(cè)到死鎖后會(huì)放棄事務(wù)扛禽,然后指派一個(gè)事務(wù)先放棄锋边,釋放資源,另一個(gè)事務(wù)執(zhí)行后再執(zhí)行該事務(wù)

JVM中:無(wú)法自動(dòng)處理编曼,因?yàn)椴淮_定線程的重要性豆巨,所以JVM無(wú)法指派一個(gè)線程先放棄。但是JVM提供檢測(cè)死鎖的功能掐场,將處理權(quán)利交給程序員

死鎖代碼


上述代碼運(yùn)行會(huì)進(jìn)入死鎖往扔,一直等待無(wú)法結(jié)束,手動(dòng)停止線程后會(huì)打印如下信息

Process finished with exit code 130(interrupted by signal 2:SIGINT)

正常退出exit code為0熊户,但是不確定130是否是死鎖退出的標(biāo)志

死鎖產(chǎn)生的四大條件

  1. 互斥條件:共享資源X和Y只能被一個(gè)線程占有
  2. 占有且等待條件:線程T1已經(jīng)獲得共享資源X萍膛,在等待共享資源Y時(shí),不釋放X
  3. 不剝奪條件:不能剝奪線程T1已經(jīng)獲得的共享資源
  4. 循環(huán)等待條件:線程T1等待T2占有的資源嚷堡;線程T2等待T1占有的資源

10.2 定位死鎖

jstack [pid]:通過(guò)jps獲得進(jìn)程pid蝗罗,然后使用jstack命令,獲取死鎖信息如下

[圖片上傳失敗...(image-e408d-1583382844739)]

ThreadMXBean:通過(guò)代碼檢測(cè)死鎖蝌戒,只能檢測(cè)當(dāng)前進(jìn)程中的死鎖

ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("發(fā)現(xiàn)死鎖" + threadInfo.getThreadName());
            }
        }

10.3 修復(fù)死鎖

修復(fù)死鎖的思路就是破壞死鎖產(chǎn)生的四大條件串塑,常用的解決方案有3種:

  1. 避免策略:哲學(xué)家就餐的換手方案,轉(zhuǎn)賬換序方案
  2. 檢測(cè)與恢復(fù)策略:定時(shí)檢測(cè)是否存在死鎖北苟,如果有就剝奪某一個(gè)資源桩匪,來(lái)打開(kāi)死鎖
  3. 鴕鳥(niǎo)策略:如果死鎖發(fā)生概率極低,可以直接忽略它粹淋,直到死鎖發(fā)生的時(shí)候再人工修復(fù)

避免策略

思路:避免相反的獲取鎖的順序吸祟。如轉(zhuǎn)賬時(shí)需要獲取轉(zhuǎn)出賬戶、轉(zhuǎn)入賬戶兩把鎖桃移,但是實(shí)際上不在乎獲取鎖的順序屋匕,當(dāng)兩把鎖都獲取到了才能進(jìn)行轉(zhuǎn)賬操作。所以可以避免兩個(gè)線程產(chǎn)生死鎖借杰。

通過(guò)hashcode來(lái)決定獲取鎖的順序过吻,hashcode相同時(shí)需要“加時(shí)賽”,如果對(duì)象鎖有主鍵,則利用主鍵替代hashcode更方便

下面轉(zhuǎn)賬代碼中纤虽,兩個(gè)線程互相轉(zhuǎn)賬乳绕,操作前要獲取兩個(gè)Account對(duì)象鎖,分別是from和to逼纸,哪個(gè)鎖的hash值小洋措,哪個(gè)鎖先被獲取,這樣兩個(gè)線程都先請(qǐng)求獲取hash值小的鎖杰刽,獲取到了才能獲取另一把鎖菠发,這樣就不會(huì)產(chǎn)生死鎖了。破壞了死鎖產(chǎn)生條件中循環(huán)等待條件贺嫂。即使是更加復(fù)雜的多人隨機(jī)轉(zhuǎn)賬產(chǎn)生滓鸠,因?yàn)?strong>無(wú)法形成閉環(huán)了,也不會(huì)產(chǎn)生死鎖第喳。


哲學(xué)家就餐問(wèn)題

哲學(xué)家就餐問(wèn)題本質(zhì)是一個(gè)死鎖問(wèn)題糜俗,解決哲學(xué)家就餐問(wèn)題,就是解決死鎖問(wèn)題曲饱。

問(wèn)題描述:五個(gè)哲學(xué)家在一張桌子上吃飯悠抹,兩人之間有一只筷子,共5只筷子扩淀,哲學(xué)家就餐流程:

  1. 先拿起左手邊的一只筷子

  2. 然后拿起右手邊的一只筷子

  3. 如果筷子正在被別人使用锌钮,那就等待別人用完

  4. 拿到兩只筷子后開(kāi)始吃飯,吃完后將筷子放回

死鎖:每個(gè)哲學(xué)家都拿著左手的筷子引矩,永遠(yuǎn)都在等待右邊的筷子梁丘,就會(huì)陷入一直等待的狀態(tài)

解決辦法:

  1. 服務(wù)員檢查(避免策略):每個(gè)哲學(xué)家拿左手邊筷子前先詢問(wèn)服務(wù)員,當(dāng)服務(wù)員發(fā)現(xiàn)其他4個(gè)哲學(xué)家都有且僅有左手邊筷子時(shí)旺韭,即這個(gè)哲學(xué)家拿起左手筷子就會(huì)死鎖氛谜,為了防止死鎖,服務(wù)員不允許這個(gè)哲學(xué)家拿左手邊筷子区端。
  2. 改變一個(gè)哲學(xué)家拿筷子的順序(避免策略):因?yàn)槎寄玫搅俗笫诌吙曜又德荚谡?qǐng)求右手邊筷子,形成了一個(gè)閉環(huán)织盼,只需要改變其中一個(gè)哲學(xué)家拿筷子的順序杨何,即可打破這個(gè)閉環(huán)
  3. 餐票(避免策略):因?yàn)?個(gè)人同時(shí)拿到左手筷子會(huì)發(fā)生死鎖,所以只提供4張餐票沥邻,即最多有4個(gè)人拿筷子就餐危虱,即可避免死鎖問(wèn)題
  4. 領(lǐng)導(dǎo)調(diào)節(jié)(檢測(cè)與恢復(fù)策略):與避免策略不同,該策略不避免你發(fā)生死鎖唐全,當(dāng)發(fā)生死鎖后埃跷,檢測(cè)出死鎖蕊玷,領(lǐng)導(dǎo)命令其中一個(gè)人放下筷子,破壞了死鎖形成四個(gè)條件中的不剝奪條件弥雹,解決了死鎖問(wèn)題

死鎖檢測(cè)算法:每次獲取鎖都有記錄垃帅,檢查鎖的調(diào)用鏈路圖,如果存在環(huán)路剪勿,則說(shuō)形成了死鎖

/**
 * 描述:     演示哲學(xué)家就餐問(wèn)題導(dǎo)致的死鎖
 * 解決辦法: 改變一個(gè)哲學(xué)家拿筷子的順序
 */
public class DiningPhilosophers {
    // 哲學(xué)家類(lèi)
    public static class Philosopher implements Runnable {

        private Object leftChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        private Object rightChopstick;

        @Override
        public void run() {
            try {
                // 先拿左邊筷子,再拿右邊筷子,吃完后放下筷子
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        // 總共5個(gè)哲學(xué)家,5只筷子
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            // 如果是最后一位哲學(xué)家, 則先拿右手邊筷子, 避免了環(huán)路的形成
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲學(xué)家" + (i + 1) + "號(hào)").start();
        }
    }
}

?

檢測(cè)與恢復(fù)策略

檢測(cè):每次獲取鎖都有記錄贸诚,定期檢查鎖的調(diào)用鏈路圖,如果存在環(huán)路厕吉,則說(shuō)形成了死鎖赦颇,一旦出現(xiàn)死鎖,就用死鎖恢復(fù)機(jī)制進(jìn)行恢復(fù)赴涵。

恢復(fù)方法有兩種:

線程終止,逐個(gè)終止線程订讼,直至死鎖消除

資源搶占髓窜,把已經(jīng)分發(fā)的鎖給收回來(lái),讓線程回退幾步欺殿,這樣就不用結(jié)束整個(gè)線程

??? 兩種恢復(fù)方法實(shí)際如何操作并不清楚

10.4 如何避免死鎖

  1. 設(shè)置超時(shí)時(shí)間

Lock.tryLock(long timeout, TimeUnit unit) 嘗試獲取鎖寄纵,獲取成功返回true,超時(shí)后放棄返回false

示例代碼如下:



synchronized不具備嘗試鎖的能力

獲取鎖失敳彼铡:打印日志程拭,發(fā)送報(bào)警信息、重啟等

  1. 多使用并發(fā)類(lèi)而不是自己設(shè)計(jì)鎖
  2. 盡量降低鎖的粒度
  3. 同步代碼塊優(yōu)于同步方法棍潘,自己制定鎖對(duì)象更好
  4. 線程設(shè)置一個(gè)有意義名稱恃鞋,debug和排查問(wèn)題事半功倍,框架和JDK都遵守這個(gè)規(guī)則
  5. 盡量避免鎖的嵌套
  6. 分配資源前先看能不能收回來(lái):銀行家算法
  7. 盡量不要多個(gè)功能用同一把鎖:專鎖專用

10.5 活鎖

會(huì)導(dǎo)致程序無(wú)法順利進(jìn)行亦歉,統(tǒng)稱為活躍性問(wèn)題恤浪。死鎖是最常見(jiàn)的活躍性問(wèn)題,活鎖(LiveLock)和饑餓都是活躍性問(wèn)題肴楷。

什么是活鎖水由?

雖然線程并沒(méi)有阻塞,也始終在運(yùn)行赛蔫,但是程序卻得不到進(jìn)展砂客,因?yàn)榫€程始終在做同樣的事。

活鎖對(duì)應(yīng)到哲學(xué)家就餐問(wèn)題:

五個(gè)哲學(xué)家都拿到了左邊的筷子呵恢,都在等待右邊的的筷子鞠值,最多等待5分鐘,如果拿不到右邊的筷子渗钉,就放下手中的筷子齿诉,再等五分鐘,又同時(shí)拿起左手邊的筷子

工程中的活鎖實(shí)例

消息隊(duì)列中的消息如果處理失敗,不能放在隊(duì)列開(kāi)頭重試粤剧,應(yīng)該放到隊(duì)列尾部歇竟,設(shè)置重試次數(shù),如果還是失敗抵恋,可以考慮保存到數(shù)據(jù)庫(kù)或?qū)懙轿募?/p>

10.6 饑餓

當(dāng)線程需要某些資源(例如CPU)焕议,但是始終得不到,稱為饑餓弧关。

面試題:寫(xiě)一個(gè)必然死鎖的例子盅安,生產(chǎn)中什么場(chǎng)景會(huì)產(chǎn)生死鎖?

線程a獲得鎖1世囊,請(qǐng)求鎖2,别瞭;線程b獲得鎖2,請(qǐng)求鎖1

面試題:發(fā)生死鎖必須滿足哪些條件株憾?

四大條件:互斥條件蝙寨、占有且等待條件、不剝奪條件嗤瞎、循環(huán)等待條件

面試題:如何定位死鎖墙歪?發(fā)現(xiàn)死鎖的原理是什么?

jstack命令贝奇、jconsole虹菲、ThreadMXBean都可以定位死鎖

發(fā)現(xiàn)死鎖的原理是根據(jù)鎖的調(diào)用鏈圖,形成閉環(huán)則說(shuō)明形成了死鎖

面試題:有哪些解決死鎖的策略掉瞳?

避免策略:哲學(xué)家就餐的換手方案毕源,轉(zhuǎn)賬換序方案,根據(jù)賬戶ID確定獲取鎖的順序

檢測(cè)與恢復(fù)策略:一段時(shí)間檢測(cè)是否有死鎖陕习,如果有就剝奪某一個(gè)資源脑豹,來(lái)打開(kāi)死鎖

面試題:哲學(xué)家就餐問(wèn)題

哲學(xué)家就餐死鎖問(wèn)題有四種解決辦法:服務(wù)員檢查(避免策略)、改變一個(gè)哲學(xué)家拿筷子的順序(避免策略)衡查、餐票(避免策略)瘩欺、領(lǐng)導(dǎo)調(diào)節(jié)(檢測(cè)與恢復(fù)策略)

面試題:實(shí)際工程中如何避免死鎖?

  1. 設(shè)置等待鎖的超市時(shí)間 Lock.tryLock()

  2. 多使用并發(fā)類(lèi)而不是自己設(shè)計(jì)鎖

    ...共8點(diǎn)拌牲,見(jiàn)上方詳解

面試題:什么是活躍性問(wèn)題? 活鎖俱饿、饑餓和死鎖有什么區(qū)別?

11 總結(jié)

使用錨點(diǎn)整體面試題

<a name="q1">面試題1</a>

<a href="#q1">跳轉(zhuǎn)到面試題1</a>

12 面試題歸納與套路

單例塌忽,分析雙重鎖拍埠,引出線程安全和和volatile禁止重排序,結(jié)合TSP實(shí)踐說(shuō)明單例模式土居。再引出Spring單例枣购,再引出cglib嬉探,再引出ThreadLocal,結(jié)合注解記錄日志說(shuō)明ThreadLocal

分析靜態(tài)內(nèi)部類(lèi)棉圈,引出類(lèi)加載方式

分析原子操作涩堤,引出i++不是線程安全,引出Atomic分瘾,引出CAS

分析原子操作胎围,引出鎖,解釋sync'原理德召,引出死鎖白魂,結(jié)合WG死鎖檢測(cè)實(shí)踐,再引出死鎖調(diào)用鏈形成閉環(huán)檢測(cè)死鎖上岗,再引出哲學(xué)家就餐問(wèn)題

都有哪些方法會(huì)拋出InterruptedException?

參考文檔

  1. Java并發(fā)核心知識(shí)體系精講 - 視頻教程
  2. 線程8大核心基礎(chǔ) - 思維導(dǎo)圖
  3. 配套高頻并發(fā)面試題匯總 - 持續(xù)更新
  4. OpenJDK 在線代碼:class 目錄是 Java 代碼福荸,native 目錄是 C++ 代碼
  5. OpenJDK 源碼 - Github
  6. Java線程源碼解析之 start
  7. Java線程面試題
  8. JVM源碼分析之Object.wait/notify實(shí)現(xiàn) - 占小狼
  9. 123個(gè)Java并發(fā)面試題
  10. Java 單例模式詳解
  11. Java單例模式的5種寫(xiě)法
  12. 枚舉實(shí)現(xiàn)單例模式的原理

結(jié)合極客時(shí)間、汪文君并發(fā)肴掷、Java并發(fā)編程的藝術(shù)敬锐,微信收藏、印象筆記捆等、簡(jiǎn)書(shū)筆記等完善,不完善總結(jié)相當(dāng)于白學(xué)了
學(xué)完后過(guò)一下參考文檔9面試題续室,檢驗(yàn)一下學(xué)習(xí)成果栋烤。

局部性原理筆記

錯(cuò)誤總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市挺狰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖钝鸽,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隙轻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡瞳购,警方通過(guò)查閱死者的電腦和手機(jī)话侄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)学赛,“玉大人年堆,你說(shuō)我怎么就攤上這事≌到剑” “怎么了变丧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)绢掰。 經(jīng)常有香客問(wèn)我痒蓬,道長(zhǎng)童擎,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任攻晒,我火速辦了婚禮顾复,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炎辨。我一直安慰自己捕透,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布碴萧。 她就那樣靜靜地躺著乙嘀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪破喻。 梳的紋絲不亂的頭發(fā)上虎谢,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音曹质,去河邊找鬼婴噩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛羽德,可吹牛的內(nèi)容都是我干的几莽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宅静,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼章蚣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起姨夹,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤纤垂,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后磷账,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體峭沦,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年逃糟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吼鱼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绰咽,死狀恐怖蛉抓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剃诅,我是刑警寧澤巷送,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站矛辕,受9級(jí)特大地震影響笑跛,放射性物質(zhì)發(fā)生泄漏付魔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一飞蹂、第九天 我趴在偏房一處隱蔽的房頂上張望几苍。 院中可真熱鬧,春花似錦陈哑、人聲如沸妻坝。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刽宪。三九已至,卻和暖如春界酒,著一層夾襖步出監(jiān)牢的瞬間圣拄,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工毁欣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庇谆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓凭疮,卻偏偏與公主長(zhǎng)得像饭耳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子执解,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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

  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類(lèi) 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,964評(píng)論 1 18
  • 本文主要講了java中多線程的使用方法寞肖、線程同步、線程數(shù)據(jù)傳遞材鹦、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法逝淹、概述等耕姊。 首先講...
    李欣陽(yáng)閱讀 2,458評(píng)論 1 15
  • 文章來(lái)源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,489評(píng)論 0 1
  • 林炳文Evankaka原創(chuàng)作品桶唐。轉(zhuǎn)載自http://blog.csdn.net/evankaka 本文主要講了ja...
    ccq_inori閱讀 656評(píng)論 0 4
  • 不管你是新程序員還是老手,你一定在面試中遇到過(guò)有關(guān)線程的問(wèn)題茉兰。Java語(yǔ)言一個(gè)重要的特點(diǎn)就是內(nèi)置了對(duì)并發(fā)的支持尤泽,讓...
    堯淳閱讀 1,597評(píng)論 0 25