[TOC]
0 前言
為什么需要學(xué)習(xí)并發(fā)編程?
- 大廠JD硬性要求,也是高級(jí)工程師必經(jīng)之路奕污,幾乎所有的程序都需要并發(fā)和多線程
- 面試高頻出現(xiàn)萎羔,書(shū)籍、網(wǎng)絡(luò)博客內(nèi)容水平參差不齊碳默,知識(shí)點(diǎn)凌亂
- 眾多框架的原理和基礎(chǔ)贾陷,Spring 線程池、單例的應(yīng)用嘱根;數(shù)據(jù)庫(kù)的樂(lè)觀鎖思想髓废;Log4J2 對(duì)阻塞隊(duì)列的應(yīng)用
本門(mén)課程的優(yōu)點(diǎn)
- 系統(tǒng):成體系不容易忘記,思維導(dǎo)圖该抒,為什么->演示代碼->分析原理->得出結(jié)論
- 內(nèi)容豐富:線程 8 大核心基礎(chǔ)慌洪,java內(nèi)存模型,死鎖
- 分析面試題:答題思路,引申解答
- 分析本質(zhì):深入原理分析設(shè)計(jì)理念,interrupt與stop停止線程,wait必須在同步塊中使用贞远,JMM
- 學(xué)習(xí)方法:技術(shù)提高途徑爽醋、技術(shù)前沿動(dòng)態(tài)乌昔、業(yè)務(wù)中成長(zhǎng)、自頂向下學(xué)習(xí)
- 通俗易懂:近朱者赤-happen-before,森然火災(zāi)-線上事故,夫妻遷讓-死鎖
- 逐步迭代:從0開(kāi)始因痛,逐漸優(yōu)化,重視思路岸更,分析錯(cuò)誤代碼到修復(fù)問(wèn)題
- 案例演示豐富
- 習(xí)題檢驗(yàn):總結(jié)知識(shí)點(diǎn)鸵膏,檢驗(yàn)學(xué)習(xí)效果,知識(shí)卡防止走神
- 配套資料:思維導(dǎo)圖怎炊,知識(shí)點(diǎn)文檔谭企,面試題總結(jié)
線程八大核心
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 接口更好:
- 可擴(kuò)展责循,java 只能單繼承多實(shí)現(xiàn)糟港,繼承 Thread 類(lèi)后就不能繼承其他類(lèi),限制了可擴(kuò)展性院仿;而 Runnable 方式可以實(shí)現(xiàn)多個(gè)接口
- 節(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)資源
- 解耦,實(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)線程有幾種方式期犬?
- 有兩種方法,根據(jù) Thread 類(lèi)的注釋(或 Java 官方文檔)避诽,分別是實(shí)現(xiàn) Runnable 接口和繼承 Thread 類(lèi)
- 準(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ū)別) - 分析優(yōu)缺點(diǎn)棉安,可擴(kuò)展性、節(jié)約資源铸抑、解耦(參考上面兩種創(chuàng)建方式的對(duì)比)
- 分析常見(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
}
// ......
}
}
- 無(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í)器
- 匿名內(nèi)部類(lèi) 示例代碼
運(yùn)行示例代碼,會(huì)生成AnonymousInnerClassStyle2.class反編譯發(fā)現(xiàn)該類(lèi)繼承了 Thread 類(lèi)
- lambda 表達(dá)式 示例代碼
查看示例代碼 粥航,代碼中打印了 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)的哪種方式更好滤奈?
- 可擴(kuò)展性,Java 不支持多繼承撩满,繼承 Thread 類(lèi)后就不能繼承其他類(lèi)蜒程,限制了可擴(kuò)展性绅你,而實(shí)現(xiàn) Runnable 方式可以實(shí)現(xiàn)多個(gè)接口
- 代碼架構(gòu)角度,實(shí)現(xiàn) Runnable 接口解耦昭躺,一是具體的業(yè)務(wù)邏輯在
run()
方法中忌锯,二是控制線程生命周期是 Thread 類(lèi),兩個(gè)目的不一樣领炫,不建議寫(xiě)在一個(gè)類(lèi)中偶垮,應(yīng)該解耦。 - 節(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ì)路徑
-
宏觀
- 責(zé)任心昆禽,不要放過(guò)任何 Bug,找到原因并去解決蝇庭,因?yàn)楹芏?Bug 都需要非常深入的知識(shí)才能解決醉鳖,解決問(wèn)題的能力比學(xué)很多的知識(shí)更重要
- 主動(dòng),永遠(yuǎn)不要覺(jué)得自己的時(shí)間多余哮内,不斷重構(gòu)盗棵、優(yōu)化、學(xué)習(xí)北发、總結(jié)
- 敢于承擔(dān)纹因,對(duì)于沒(méi)碰過(guò)的技術(shù)難題,在一定調(diào)研后琳拨,敢于承擔(dān)瞭恰,讓工作充滿挑戰(zhàn),攻克難關(guān)的過(guò)程進(jìn)步飛速
- 關(guān)心產(chǎn)品和業(yè)務(wù)狱庇,不僅要寫(xiě)好代碼惊畏,更要在業(yè)務(wù)層面多思考
-
微觀
- 系統(tǒng)學(xué)習(xí)是牢,碎片化知識(shí)公眾號(hào)文章最容易忘記和一葉障目,要看經(jīng)典書(shū)籍的譯本
- 官方文檔驳棱,專家撰寫(xiě)不斷迭代,最權(quán)威的蜡豹,像線程實(shí)現(xiàn)方式百度結(jié)果有非常多的錯(cuò)誤
- 分析源碼较雕,隨著看源碼和官方文檔的次數(shù)增多趁怔,就會(huì)更熟悉更快湿硝,而baidu并不能達(dá)到這個(gè)效果
- 英文搜索,前面幾個(gè)不能解決問(wèn)題润努,再搜索 Google 和 StackOverflow图柏,用英文更容易找到正確答案如Annoymouses Class
- 多實(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ā)生什么情況子檀?
- 一個(gè)線程只能調(diào)用一次
start()
方法,否則會(huì)拋出IllegalThreadStateException乃戈,因?yàn)?code>start()方法的衛(wèi)語(yǔ)句會(huì)檢查線程狀態(tài)褂痰,線程狀態(tài)不為 NEW 則拋出異常 -
start()
是synchronized
修飾的線程安全方法,不存在線程已啟動(dòng)但線程狀態(tài) threadStatus 還未修改的情況症虑,所以也不用擔(dān)心被調(diào)用兩次 - 即使線程執(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章 - 為什么這么設(shè)計(jì)习贫?
問(wèn)題逛球? 線程結(jié)束后能不能再調(diào)用start,那什么時(shí)候結(jié)束沈条?
面試題 4:既然
start()
還是會(huì)調(diào)用run()
方法需忿,為什么我們不直接調(diào)用run()
方法呢?
- 因?yàn)檎{(diào)用
start()
方法才會(huì)真的啟動(dòng)一個(gè)新線程蜡歹,而調(diào)用run()
只是簡(jiǎn)單的調(diào)用方法 -
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í):
- 如果線程處于正掣渭活動(dòng)狀態(tài)瞻坝,那么會(huì)將該線程的中斷標(biāo)志位設(shè)置為 true,僅此而已
- 如果線程處于被阻塞狀態(tài)(例如處于sleep, wait, join 等狀態(tài))杏瞻,那么線程將立即退出被阻塞狀態(tài)所刀,并拋出一個(gè)InterruptedException異常。僅此而已捞挥。
- 被設(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ò)誤的線程停止方法
:過(guò)期方法鸣奔,悟空說(shuō)會(huì)導(dǎo)致數(shù)據(jù)不完整,但是加synchronized可以解決此問(wèn)題惩阶,具體原因不知stop()
:suspend()
:resume()
用volatile
設(shè)置標(biāo)記位:
彩蛋:如何分析 native 方法
- 查看
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
}
- 進(jìn)入Github查看OpenJDK代碼庫(kù),點(diǎn)擊<kbd>FindFile</kbd>搜索
Thread.c
文件断楷,JDK native 方法的源碼都在同類(lèi)名的 .c 文件中
20200107061222.png
- 找到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);
}
- 上面代碼調(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);
}
- 找到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)換圖如下所示准夷,可以知道钥飞,
-
start0()
會(huì)將線程狀態(tài)從 NEW 修改到 RUNNABLE,Debug 觀察this.getState()
可知 - NEW衫嵌、RUNNABLE读宙、TERMINATED三種狀態(tài)只能從前往后,不可逆渐扮,
- 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)即可
阻塞狀態(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)換條件。
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ì)象制圈,并且先wait
后notify
够吩。
前提: 由同一個(gè)lock對(duì)象調(diào)用wait嘀倒、notify方法采盒。
- 當(dāng)線程A執(zhí)行wait方法時(shí)唧龄,該線程會(huì)被掛起
- 當(dāng)線程B執(zhí)行notify方法時(shí)兼砖,會(huì)喚醒一個(gè)被掛起的線程A
面試題:lock對(duì)象、線程A和線程B三者是一種什么關(guān)系既棺?
根據(jù)上面的結(jié)論讽挟,可以想象一個(gè)場(chǎng)景:
- lock對(duì)象維護(hù)了一個(gè)等待隊(duì)列l(wèi)ist
- 線程A中執(zhí)行l(wèi)ock的wait方法,把線程A保存到list中
- 線程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í)例代碼可知:
生產(chǎn)者消費(fèi)者模型有三個(gè)組成部分:倉(cāng)庫(kù)赋秀、生產(chǎn)者利朵、消費(fèi)者
倉(cāng)庫(kù)有一個(gè)屬性容量
maxsize
,兩個(gè)功能生產(chǎn)put
和消費(fèi)take
put
和take
必須是線程安全的猎莲,防止多個(gè)生產(chǎn)者生產(chǎn)產(chǎn)品數(shù)量超出倉(cāng)庫(kù)maxsize
生產(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 不需要?
- 區(qū)別見(jiàn) wait 和 sleep 的區(qū)別
- 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
- 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ū)別
- 相同
- wait 和 sleep 都可以使線程進(jìn)入(廣義)阻塞狀態(tài)
- wait 和 sleep 都是可中斷方法来候,被中斷后會(huì)拋出InterruptException
- 不同
- wait 是 Object 的方法,而 sleep 是 Thread 特有的方法
- wait 方法的調(diào)用必須在同步方法中進(jìn)行逸雹,而 sleep 不需要
- 線程在同步方法中執(zhí)行 wait 時(shí)會(huì)釋放 monitor 鎖营搅,而 sleep 并不會(huì)釋放 monitor 鎖
- 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)與sleep
,wait
不同哪轿,驗(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))
-
join
期間芬萍,主線程處于WAITING
狀態(tài)尤揣,可以通過(guò) debug 或mainThread.getState
驗(yàn)證 - 因?yàn)?code>join底層是調(diào)用
wait()
方法,所以會(huì)處于WAITING
狀態(tài) -
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
線程都需要立即終止
特性:
- 線程類(lèi)型默認(rèn)繼承自父線程
- 除了Main線程,被JVM啟動(dòng)都是守護(hù)線程功舀,用戶啟動(dòng)的當(dāng)然都是用戶線程
- 守護(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)題:
- 緩存導(dǎo)致的可見(jiàn)性問(wèn)題 - happen-before規(guī)則肺魁,volatile也能禁用CPU本地緩存
- 編譯優(yōu)化帶來(lái)的重排序問(wèn)題 - volatile語(yǔ)義增強(qiáng)禁止重排序
- 線程切換帶來(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é)果
- x=1酌住,y=0 t2先執(zhí)行,t1再執(zhí)行
- x=0, y=1 t1先執(zhí)行狰域,t2再執(zhí)行
- x=1, y=1 t1給a賦值完后 CPU 切換到 t2執(zhí)行
- 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);
}
}
[圖片上傳失敗...(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é)果如下圖所示
重排序分為三種情況:
- 編譯器優(yōu)化:包括JVM疤祭,JIT編譯器
- CPU 指令重排:
- 內(nèi)存重排序
重排序的好處
如上圖所示盼产,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í)緩存引起的
由上圖可知豌鸡,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ī)定:
- 所有變量都存儲(chǔ)在主存中,同時(shí)每個(gè)線程也有自己的本地內(nèi)存潘飘,工作內(nèi)存中的變量?jī)?nèi)容是主存中的拷貝
- 線程不能直接讀寫(xiě)主存中的變量肮之,只能操作本地內(nèi)存中的變量,然后再同步到主存中
- 主存是多個(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ī)則
- 單線程原則:一個(gè)線程中的每個(gè)操作狐榔,對(duì)于該線程中的任意后續(xù)動(dòng)作可見(jiàn)坛增,也就是說(shuō)都在本地內(nèi)存運(yùn)行,不存在可見(jiàn)性問(wèn)題薄腻,又叫程序順序原則
-
Monitor
鎖原則(synchronized和Lock):對(duì)一個(gè)鎖的解鎖收捣,對(duì)于后續(xù)其他線程同一個(gè)鎖的加鎖可見(jiàn),這里的“后續(xù)”指的是時(shí)間上的先后順序庵楷,又叫管程鎖定原則罢艾, -
volatile
變量原則:對(duì)于一個(gè)volatile變量的寫(xiě),對(duì)于后續(xù)其他線程對(duì)volatile變量的讀可見(jiàn)尽纽,也就是說(shuō)volatile變量的寫(xiě)會(huì)直接刷新到主存咐蚯,這里的“后續(xù)”指的是時(shí)間上的先后順序 - 傳遞性原則:如果A happen-before B,B happen-before C弄贿,那么A happen-before C仓蛆,最常用到的一個(gè)原則
-
start()
原則:如果線程A執(zhí)行ThreadB.start()啟動(dòng)線程B,那么A線程ThreadB.start()及之前的操作對(duì)于線程B的所有操作可見(jiàn) -
線程終止原則:線程中的所有操作挎春,對(duì)于此線程的終止檢測(cè)可見(jiàn),我們可以通過(guò)
Thread.join()
方法結(jié)束豆拨,Thread.isAlive()
的返回值等手段直奋,檢測(cè)到線程終止運(yùn)行 -
join()
原則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的所有操作施禾,對(duì)于線程A可見(jiàn)脚线,實(shí)際開(kāi)發(fā)中,我們也是用join()
方法來(lái)獲取線程B中的執(zhí)行結(jié)果弥搞,這一條規(guī)則其實(shí)是線程終止原則的細(xì)化部分 -
線程中斷規(guī)則:對(duì)線程A 調(diào)用
ThreadB.interrupt()
方法邮绿,對(duì)于線程B檢測(cè)中斷isInterrupted
可見(jiàn) -
對(duì)象終結(jié)原則:一個(gè)對(duì)象的初始化完成,對(duì)于該對(duì)象的
finalize()
方法可見(jiàn)
上圖中由程序順序原則可知攀例,1 happen-before 2
船逮,3 happen-before 4
,由volatile
變量原則 可知粤铭,2 happen-before 3
挖胃,再結(jié)合傳遞性原則,可知 1 happen-before 4
另外還有一個(gè)重要的并發(fā)工具類(lèi)原則:
- 線程安全的容器,如
CurrenthashMap
的put
操作酱鸭,對(duì)于后續(xù)get
操作可見(jiàn) -
CountDownLatch
的countDown()
操作吗垮,對(duì)于后續(xù)await()
可見(jiàn) -
Semaphore
的release()
釋放許可證操作,對(duì)于后續(xù)acquire()
獲取許可證操作可見(jiàn) -
CyclicBarrier
的最后一個(gè)線程到達(dá)屏障時(shí)凹髓,對(duì)于所有被攔截的線程await()
可見(jiàn) -
Future
的call()
操作執(zhí)行結(jié)果烁登,對(duì)于后續(xù)get()
操作可見(jiàn),詳細(xì)見(jiàn)示例 - 線程池
面試題: 對(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
在舊的內(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é)
volatile屬性的讀寫(xiě)操作都是無(wú)鎖的,它不能替代synchronized潭流,因?yàn)樗鼪](méi)有提供原子性和互斥性竞惋。因?yàn)闊o(wú)鎖,不需要花費(fèi)時(shí)間在獲取鎖和釋放鎖灰嫉,所以說(shuō)volatile是低成本的
volatile只能作用于屬性碰声,我們用volatile修飾屬性,這樣能禁止重排序
volatile提供了可見(jiàn)性熬甫,任何一個(gè)線程對(duì)其的修改對(duì)其他線程立即可見(jiàn)。volatile屬性不會(huì)使用本地緩存蔓罚,始終從主存中讀取和寫(xiě)入
volatile 提供了Happen-before保證椿肩,對(duì)volatile變量v的寫(xiě)入happen-before所有其他線程后續(xù)對(duì)v的讀取
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的原子性
有可能出現(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ě)操作具有原子性泛源,所以存在不是原子操作的可能
不需要加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 單例模式
為什么需要單例模式?
- 節(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資源
- 保證結(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í)例
- 方便管理兄朋,如日期工具類(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)景
- 無(wú)狀態(tài)的工具類(lèi):如日志工具類(lèi),不管在哪里適用教届,只需要它記錄日志信息响鹃,并不需要它的實(shí)例對(duì)象上存儲(chǔ)任何狀態(tài),故我們只需要一個(gè)實(shí)例對(duì)象即可案训。spring無(wú)狀態(tài)bean买置,
- 全局信息類(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ì):
- 減少了新生成實(shí)例的消耗岳遥,spring會(huì)通過(guò)反射或者cglib來(lái)生成bean實(shí)例這都是耗性能的操作奕翔,其次給對(duì)象分配內(nèi)存也會(huì)涉及復(fù)雜算法;
- 減少jvm垃圾回收浩蓉;
- 可以快速獲取到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)
- 餓漢式
- 懶漢式
- 雙重檢查
- 靜態(tài)內(nèi)部類(lèi)
- 枚舉
雙重檢查單例模式
雙重檢查單例模式的實(shí)現(xiàn)如下所示绅络,INSTANCE = new DoubleCheckSingleton();
不是原子操作,分為三個(gè)步驟:
- 分配堆內(nèi)存
- 對(duì)象初始化好渠,調(diào)用構(gòu)造方法創(chuàng)建對(duì)象
- 將對(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)單例模式的最佳方法
- 寫(xiě)法簡(jiǎn)單
- 線程安全
- 避免反序列化破壞單例
- 避免反射攻擊
驗(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ì)象效率高
- 為什么要double-check析校?
- synchronized修飾方法線程安全但后續(xù)獲取實(shí)例效率低
- synchronized縮小范圍构罗,單check線程不安全
為了兼顧線程安全和后續(xù)獲取實(shí)例的效率铜涉,衍生出來(lái)雙重檢測(cè)單例模式
-
為什么要用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)是什么听想?
- Effective Java中說(shuō)枚舉是單例的最佳實(shí)現(xiàn)
- 寫(xiě)法簡(jiǎn)單
- 線程安全
- 避免反序列化破壞單例
面試題: 單例模式實(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ě)入操作是原子操作嗎芋齿?
- 有可能,Java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long和double類(lèi)型變量的寫(xiě)操作具有原子性成翩,所以存在不是原子操作的可能
- 不需要加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 死鎖
考考你
- 寫(xiě)一個(gè)必然死鎖的例子(百度面試題)
- 發(fā)生死鎖必須滿足哪些條件?
- 如何定位死鎖?
- 有哪些解決死鎖問(wèn)題的策略
- 講講經(jīng)典的哲學(xué)家就餐問(wèn)題
- 實(shí)際工程中如何避免死鎖?
- 什么是活躍性問(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)生的四大條件
- 互斥條件:共享資源X和Y只能被一個(gè)線程占有
- 占有且等待條件:線程T1已經(jīng)獲得共享資源X萍膛,在等待共享資源Y時(shí),不釋放X
- 不剝奪條件:不能剝奪線程T1已經(jīng)獲得的共享資源
- 循環(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種:
- 避免策略:哲學(xué)家就餐的換手方案,轉(zhuǎn)賬換序方案
- 檢測(cè)與恢復(fù)策略:定時(shí)檢測(cè)是否存在死鎖北苟,如果有就剝奪某一個(gè)資源桩匪,來(lái)打開(kāi)死鎖
- 鴕鳥(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é)家就餐流程:
先拿起左手邊的一只筷子
然后拿起右手邊的一只筷子
如果筷子正在被別人使用锌钮,那就等待別人用完
拿到兩只筷子后開(kāi)始吃飯,吃完后將筷子放回
死鎖:每個(gè)哲學(xué)家都拿著左手的筷子引矩,永遠(yuǎn)都在等待右邊的筷子梁丘,就會(huì)陷入一直等待的狀態(tài)
解決辦法:
- 服務(wù)員檢查(避免策略):每個(gè)哲學(xué)家拿左手邊筷子前先詢問(wèn)服務(wù)員,當(dāng)服務(wù)員發(fā)現(xiàn)其他4個(gè)哲學(xué)家都有且僅有左手邊筷子時(shí)旺韭,即這個(gè)哲學(xué)家拿起左手筷子就會(huì)死鎖氛谜,為了防止死鎖,服務(wù)員不允許這個(gè)哲學(xué)家拿左手邊筷子区端。
- 改變一個(gè)哲學(xué)家拿筷子的順序(避免策略):因?yàn)槎寄玫搅俗笫诌吙曜又德荚谡?qǐng)求右手邊筷子,形成了一個(gè)閉環(huán)织盼,只需要改變其中一個(gè)哲學(xué)家拿筷子的順序杨何,即可打破這個(gè)閉環(huán)
- 餐票(避免策略):因?yàn)?個(gè)人同時(shí)拿到左手筷子會(huì)發(fā)生死鎖,所以只提供4張餐票沥邻,即最多有4個(gè)人拿筷子就餐危虱,即可避免死鎖問(wèn)題
- 領(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 如何避免死鎖
- 設(shè)置超時(shí)時(shí)間
Lock.tryLock(long timeout, TimeUnit unit) 嘗試獲取鎖寄纵,獲取成功返回true,超時(shí)后放棄返回false
示例代碼如下:
synchronized不具備嘗試鎖的能力
獲取鎖失敳彼铡:打印日志程拭,發(fā)送報(bào)警信息、重啟等
- 多使用并發(fā)類(lèi)而不是自己設(shè)計(jì)鎖
- 盡量降低鎖的粒度
- 同步代碼塊優(yōu)于同步方法棍潘,自己制定鎖對(duì)象更好
- 線程設(shè)置一個(gè)有意義名稱恃鞋,debug和排查問(wèn)題事半功倍,框架和JDK都遵守這個(gè)規(guī)則
- 盡量避免鎖的嵌套
- 分配資源前先看能不能收回來(lái):銀行家算法
- 盡量不要多個(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í)際工程中如何避免死鎖?
設(shè)置等待鎖的超市時(shí)間 Lock.tryLock()
-
多使用并發(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?
參考文檔
- Java并發(fā)核心知識(shí)體系精講 - 視頻教程
- 線程8大核心基礎(chǔ) - 思維導(dǎo)圖
- 配套高頻并發(fā)面試題匯總 - 持續(xù)更新
- OpenJDK 在線代碼:class 目錄是 Java 代碼福荸,native 目錄是 C++ 代碼
- OpenJDK 源碼 - Github
- Java線程源碼解析之 start
- Java線程面試題
- JVM源碼分析之Object.wait/notify實(shí)現(xiàn) - 占小狼
- 123個(gè)Java并發(fā)面試題
- Java 單例模式詳解
- Java單例模式的5種寫(xiě)法
- 枚舉實(shí)現(xiàn)單例模式的原理
結(jié)合極客時(shí)間、汪文君并發(fā)肴掷、Java并發(fā)編程的藝術(shù)敬锐,微信收藏、印象筆記捆等、簡(jiǎn)書(shū)筆記等完善,不完善總結(jié)相當(dāng)于白學(xué)了
學(xué)完后過(guò)一下參考文檔9面試題续室,檢驗(yàn)一下學(xué)習(xí)成果栋烤。
局部性原理筆記