1、為什么要使用多線程
選擇多線程的原因,就是因?yàn)榭臁Ee個(gè)例子:
如果要把1000塊磚搬到樓頂 ,假設(shè)到樓頂有幾個(gè)電梯,你覺(jué)得用一個(gè)電梯搬運(yùn)快,還是同時(shí)用幾個(gè)電梯同時(shí)搬運(yùn)快呢?這個(gè)電梯就可以理解為線程。
所以,我們使用多線程就是因?yàn)椋?在正確的場(chǎng)景下,設(shè)置恰當(dāng)數(shù)目的線程,可以用來(lái)程提高序的運(yùn)行速率。更專業(yè)點(diǎn)講,就是充分地利用CPU和I/O的利用率,提升程序運(yùn)行速率。
當(dāng)然,有利就有弊,多線程場(chǎng)景下,我們要保證線程安全,就需要考慮加鎖。加鎖如果不恰當(dāng),就很很耗性能。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
視頻教程:https://doc.iocoder.cn/video/
2. 創(chuàng)建線程有幾種方式?
Java中創(chuàng)建線程主要有以下這幾種方式:
定義Thread類的子類,并重寫該類的run方法
定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法
定義Callable接口的實(shí)現(xiàn)類,并重寫該接口的call()方法,一般配合Future使用
線程池的方式
2.1 定義Thread類的子類,并重寫該類的run方法
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?{ ????????Thread?thread?=?new?MyThread(); ????????thread.start(); ????} } class?MyThread?extends?Thread?{ ????@Override ????public?void?run()?{ ????????System.out.println("關(guān)注公?眾號(hào):芋道源碼"); ????} }
?
?
2.2 定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?{ ????????MyRunnable?myRunnable?=?new?MyRunnable(); ????????Thread?thread?=?new?Thread(myRunnable); ????????thread.start(); ????} } class?MyRunnable?implements?Runnable?{ ????@Override ????public?void?run()?{ ????????System.out.println("關(guān)注公眾號(hào):芋道源碼"); ????} } //運(yùn)行結(jié)果: 關(guān)注公眾號(hào):芋道源碼
?
?
2.3 定義Callable接口的實(shí)現(xiàn)類,并重寫該接口的call()方法
如果想要執(zhí)行的線程有返回,可以使用Callable。
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?throws?ExecutionException,?InterruptedException?{ ????????MyThreadCallable?mc?=?new?MyThreadCallable(); ????????FutureTask?ft?=?new?FutureTask<>(mc); ????????Thread?thread?=?new?Thread(ft); ????????thread.start(); ????????System.out.println(ft.get()); ????} } class?MyThreadCallable?implements?Callable?{ ????@Override ????public?String?call()throws?Exception?{ ????????return?"關(guān)注公眾號(hào):芋道源碼"; ????} } //運(yùn)行結(jié)果: 關(guān)注公眾號(hào):芋道源碼
?
?
2.4 線程池的方式
日常開發(fā)中,我們一般都是用線程池的方式執(zhí)行異步任務(wù)。
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?throws?Exception?{ ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1, ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue(20),?new?CustomizableThreadFactory("Tianluo-Thread-pool")); ????????executorOne.execute(()?->?{ ????????????System.out.println("關(guān)注公眾號(hào):芋道源碼"); ????????}); ????????//關(guān)閉線程池 ????????executorOne.shutdown(); ????} }
?
?
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
3. start()方法和run()方法的區(qū)別
其實(shí)start和run的主要區(qū)別如下:
start方法可以啟動(dòng)一個(gè)新線程,run方法只是類的一個(gè)普通方法而已,如果直接調(diào)用run方法,程序中依然只有主線程這一個(gè)線程。
start方法實(shí)現(xiàn)了多線程,而run方法沒(méi)有實(shí)現(xiàn)多線程。
start不能被重復(fù)調(diào)用,而run方法可以。
start方法中的run代碼可以不執(zhí)行完,就繼續(xù)執(zhí)行下面的代碼,也就是說(shuō)進(jìn)行了線程切換 。然而,如果直接調(diào)用run方法,就必須等待其代碼全部執(zhí)行完才能繼續(xù)執(zhí)行下面的代碼。
大家可以結(jié)合代碼例子來(lái)看看哈~
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args){ ????????Thread?t=new?Thread(){ ????????????public?void?run(){ ????????????????pong(); ????????????} ????????}; ????????t.start(); ????????t.run(); ????????t.run(); ????????System.out.println("好的,馬上去關(guān)注:關(guān)注公眾號(hào):芋道源碼"+?Thread.currentThread().getName()); ????} ????static?void?pong(){ ????????System.out.println("關(guān)注公眾號(hào):芋道源碼"+?Thread.currentThread().getName()); ????} } //輸出 關(guān)注公眾號(hào):芋道源碼main 關(guān)注公眾號(hào):芋道源碼main 好的,馬上去關(guān)注:關(guān)注公眾號(hào):芋道源碼main 關(guān)注公眾號(hào):芋道源碼Thread-0
?
?
4. 線程和進(jìn)程的區(qū)別
進(jìn)程是運(yùn)行中的應(yīng)用程序,線程是進(jìn)程的內(nèi)部的一個(gè)執(zhí)行序列
進(jìn)程是資源分配的最小單位,線程是CPU調(diào)度的最小單位。
一個(gè)進(jìn)程可以有多個(gè)線程。線程又叫做輕量級(jí)進(jìn)程,多個(gè)線程共享進(jìn)程的資源
進(jìn)程間切換代價(jià)大,線程間切換代價(jià)小
進(jìn)程擁有資源多,線程擁有資源少地址
進(jìn)程是存在地址空間的,而線程本身無(wú)地址空間,線程的地址空間是包含在進(jìn)程中的
舉個(gè)例子:
你打開QQ,開了一個(gè)進(jìn)程;打開了迅雷,也開了一個(gè)進(jìn)程。
在QQ的這個(gè)進(jìn)程里,傳輸文字開一個(gè)線程、傳輸語(yǔ)音開了一個(gè)線程、彈出對(duì)話框又開了一個(gè)線程。
所以運(yùn)行某個(gè)軟件,相當(dāng)于開了一個(gè)進(jìn)程。在這個(gè)軟件運(yùn)行的過(guò)程里(在這個(gè)進(jìn)程里),多個(gè)工作支撐的完成QQ的運(yùn)行,那么這“多個(gè)工作”分別有一個(gè)線程。
所以一個(gè)進(jìn)程管著多個(gè)線程。
通俗的講:“進(jìn)程是爹媽,管著眾多的線程兒子”...
5. 說(shuō)一下 Runnable 和 Callable有什么區(qū)別?
Runnable接口中的run()方法沒(méi)有返回值,是void類型,它做的事情只是純粹地去執(zhí)行run()方法中的代碼而已;
Callable接口中的call()方法是有返回值的,是一個(gè)泛型。它一般配合Future、FutureTask一起使用,用來(lái)獲取異步執(zhí)行的結(jié)果。
Callable接口call()方法允許拋出異常;而Runnable接口run()方法不能繼續(xù)上拋異常;
大家可以看下它倆的API:
?
?
?@FunctionalInterface public?interface?Callable?{ ????/** ?????*?支持泛型V,有返回值,允許拋出異常 ?????*/ ????V?call()?throws?Exception; } @FunctionalInterface public?interface?Runnable?{ ????/** ?????*??沒(méi)有返回值,不能繼續(xù)上拋異常 ?????*/ ????public?abstract?void?run(); }
?
?
為了方便大家理解,寫了一個(gè)demo,小伙伴們可以看看哈:
?
?
/* ?*??@Author?關(guān)注公眾號(hào):芋道源碼 ?*??@date?2022-07-11 ?*/ public?class?CallableRunnableTest?{ ????public?static?void?main(String[]?args)?{ ????????ExecutorService?executorService?=?Executors.newFixedThreadPool(5); ????????Callable?callable?=new?Callable ()?{ ????????????@Override ????????????public?String?call()?throws?Exception?{ ????????????????return?"你好,callable,關(guān)注公眾號(hào):芋道源碼"; ????????????} ????????}; ????????//支持泛型 ????????Future ?futureCallable?=?executorService.submit(callable); ????????try?{ ????????????System.out.println("獲取callable的返回結(jié)果:"+futureCallable.get()); ????????}?catch?(InterruptedException?e)?{ ????????????e.printStackTrace(); ????????}?catch?(ExecutionException?e)?{ ????????????e.printStackTrace(); ????????} ????????Runnable?runnable?=?new?Runnable()?{ ????????????@Override ????????????public?void?run()?{ ????????????????System.out.println("你好呀,runnable,關(guān)注公眾號(hào):芋道源碼"); ????????????} ????????}; ????????Future>?futureRunnable?=?executorService.submit(runnable); ????????try?{ ????????????System.out.println("獲取runnable的返回結(jié)果:"+futureRunnable.get()); ????????}?catch?(InterruptedException?e)?{ ????????????e.printStackTrace(); ????????}?catch?(ExecutionException?e)?{ ????????????e.printStackTrace(); ????????} ????????executorService.shutdown(); ????} } //運(yùn)行結(jié)果 獲取callable的返回結(jié)果:你好,callable,關(guān)注公眾號(hào):芋道源碼 你好呀,runnable,關(guān)注公眾號(hào):芋道源碼 獲取runnable的返回結(jié)果:null
?
?
6. 聊聊volatile作用,原理
volatile關(guān)鍵字是Java虛擬機(jī)提供的的最輕量級(jí)的同步機(jī)制。它作為一個(gè)修飾符,用來(lái)修飾變量。它保證變量對(duì)所有線程可見性,禁止指令重排,但是不保證原子性 。
我們先來(lái)一起回憶下java內(nèi)存模型(jmm):
Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型,來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
Java內(nèi)存模型規(guī)定所有的變量都是存在主內(nèi)存當(dāng)中,每個(gè)線程都有自己的工作內(nèi)存。這里的變量包括實(shí)例變量和靜態(tài)變量,但是不包括局部變量,因?yàn)榫植孔兞渴蔷€程私有的。
線程的工作內(nèi)存保存了被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接操作主內(nèi)存。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存。
volatile變量,保證新值能立即同步回主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新,所以我們說(shuō)volatile保證了多線程操作變量的可見性。
volatile保證可見性和禁止指令重排,都跟內(nèi)存屏障有關(guān)。我們來(lái)看一段volatile使用的demo代碼:
?
?
/** ?*?關(guān)注公眾號(hào):芋道源碼 ?**/ public?class?Singleton?{?? ???private?volatile?static?Singleton?instance;?? ???private?Singleton?(){}?? ???public?static?Singleton?getInstance()?{?? ???if?(instance?==?null)?{?? ???????synchronized?(Singleton.class)?{?? ???????if?(instance?==?null)?{?? ???????????instance?=?new?Singleton();?? ???????}?? ???????}?? ???}?? ???return?instance;?? ???}?? }??
?
?
編譯后,對(duì)比有volatile關(guān)鍵字和沒(méi)有volatile關(guān)鍵字時(shí)所生成的匯編代碼,發(fā)現(xiàn)有volatile關(guān)鍵字修飾時(shí),會(huì)多出一個(gè)lock addl $0x0,(%esp),即多出一個(gè)lock前綴指令,lock指令相當(dāng)于一個(gè)內(nèi)存屏障
lock指令相當(dāng)于一個(gè)內(nèi)存屏障,它保證以下這幾點(diǎn):
重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
將本處理器的緩存寫入內(nèi)存
如果是寫入動(dòng)作,會(huì)導(dǎo)致其他處理器中對(duì)應(yīng)的緩存無(wú)效。
第2點(diǎn)和第3點(diǎn)就是保證volatile保證可見性的體現(xiàn)嘛,第1點(diǎn)就是禁止指令重排的體現(xiàn) 。
內(nèi)存屏障四大分類:(Load 代表讀取指令,Store代表寫入指令)
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
有些小伙伴,可能對(duì)這個(gè)還是有點(diǎn)疑惑,內(nèi)存屏障這玩意太抽象了。我們照著代碼看下吧:
內(nèi)存屏障保證前面的指令先執(zhí)行,所以這就保證了禁止了指令重排啦,同時(shí)內(nèi)存屏障保證緩存寫入內(nèi)存和其他處理器緩存失效,這也就保證了可見性,哈哈~有關(guān)于volatile的底層實(shí)現(xiàn),我們就討論到這哈~
7. 說(shuō)說(shuō)并發(fā)與并行的區(qū)別?
并發(fā)和并行最開始都是操作系統(tǒng) 中的概念,表示的是CPU執(zhí)行多個(gè)任務(wù)的方式。
順序:上一個(gè)開始執(zhí)行的任務(wù)完成后,當(dāng)前任務(wù)才能開始執(zhí)行
并發(fā):無(wú)論上一個(gè)開始執(zhí)行的任務(wù)是否完成,當(dāng)前任務(wù)都可以開始執(zhí)行
(即 A B 順序執(zhí)行的話,A 一定會(huì)比 B 先完成,而并發(fā)執(zhí)行則不一定。)
串行:有一個(gè)任務(wù)執(zhí)行單元,從物理上就只能一個(gè)任務(wù)、一個(gè)任務(wù)地執(zhí)行
并行:有多個(gè)任務(wù)執(zhí)行單元,從物理上就可以多個(gè)任務(wù)一起執(zhí)行
(即在任意時(shí)間點(diǎn)上,串行執(zhí)行時(shí)必然只有一個(gè)任務(wù)在執(zhí)行,而并行則不一定。)
知乎有個(gè)很有意思的回答 ,大家可以看下:
你吃飯吃到一半,電話來(lái)了,你一直到吃完了以后才去接,這就說(shuō)明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來(lái)了,你停了下來(lái)接了電話,接完后繼續(xù)吃飯,這說(shuō)明你支持并發(fā)。
你吃飯吃到一半,電話來(lái)了,你一邊打電話一邊吃飯,這說(shuō)明你支持并行。
并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力,不一定要同時(shí)。并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力。所以我認(rèn)為它們最關(guān)鍵的點(diǎn)就是:是否是同時(shí) 。
來(lái)源:知乎
8.synchronized 的實(shí)現(xiàn)原理以及鎖優(yōu)化?
synchronized是Java中的關(guān)鍵字,是一種同步鎖。synchronized關(guān)鍵字可以作用于方法或者代碼塊。
一般面試時(shí)。可以這么回答:
8.1 monitorenter、monitorexit、ACC_SYNCHRONIZED
如果synchronized 作用于代碼塊 ,反編譯可以看到兩個(gè)指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)同步;如果作用synchronized作用于方法 ,反編譯可以看到ACCSYNCHRONIZED標(biāo)記,JVM通過(guò)在方法訪問(wèn)標(biāo)識(shí)符(flags)中加入ACCSYNCHRONIZED來(lái)實(shí)現(xiàn)同步功能。
同步代碼塊是通過(guò)monitorenter和monitorexit來(lái)實(shí)現(xiàn),當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得monitor鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)行到monitorexit的時(shí)候則要釋放鎖。
同步方法是通過(guò)中設(shè)置ACCSYNCHRONIZED標(biāo)志來(lái)實(shí)現(xiàn),當(dāng)線程執(zhí)行有ACCSYNCHRONI標(biāo)志的方法,需要獲得monitor鎖。每個(gè)對(duì)象都與一個(gè)monitor相關(guān)聯(lián),線程可以占有或者釋放monitor。
8.2 monitor監(jiān)視器
monitor是什么呢?操作系統(tǒng)的管程(monitors)是概念原理,ObjectMonitor是它的原理實(shí)現(xiàn)。
在Java虛擬機(jī)(HotSpot)中,Monitor(管程)是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:
?
?
?ObjectMonitor()?{ ????_header???????=?NULL; ????_count????????=?0;?//?記錄個(gè)數(shù) ????_waiters??????=?0, ????_recursions???=?0; ????_object???????=?NULL; ????_owner????????=?NULL; ????_WaitSet??????=?NULL;??//?處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet ????_WaitSetLock??=?0?; ????_Responsible??=?NULL?; ????_succ?????????=?NULL?; ????_cxq??????????=?NULL?; ????FreeNext??????=?NULL?; ????_EntryList????=?NULL?;??//?處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表 ????_SpinFreq?????=?0?; ????_SpinClock????=?0?; ????OwnerIsThread?=?0?; ??}
?
?
ObjectMonitor中幾個(gè)關(guān)鍵字段的含義如圖所示:
8.3 Java Monitor 的工作機(jī)理
想要獲取monitor的線程,首先會(huì)進(jìn)入_EntryList隊(duì)列。
當(dāng)某個(gè)線程獲取到對(duì)象的monitor后,進(jìn)入Owner區(qū)域,設(shè)置為當(dāng)前線程,同時(shí)計(jì)數(shù)器count加1。
如果線程調(diào)用了wait()方法,則會(huì)進(jìn)入WaitSet隊(duì)列。它會(huì)釋放monitor鎖,即將owner賦值為null,count自減1,進(jìn)入WaitSet隊(duì)列阻塞等待。
如果其他線程調(diào)用 notify() / notifyAll() ,會(huì)喚醒WaitSet中的某個(gè)線程,該線程再次嘗試獲取monitor鎖,成功即進(jìn)入Owner區(qū)域。
同步方法執(zhí)行完畢了,線程退出臨界區(qū),會(huì)將monitor的owner設(shè)為null,并釋放監(jiān)視鎖。
8.4 對(duì)象與monitor關(guān)聯(lián)
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header),實(shí)例數(shù)據(jù)(Instance Data)和對(duì)象填充(Padding) 。
對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Class Pointer(類型指針) 。
Mark Word 是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等。
重量級(jí)鎖,指向互斥量的指針。其實(shí)synchronized是重量級(jí)鎖,也就是說(shuō)Synchronized的對(duì)象鎖,Mark Word鎖標(biāo)識(shí)位為10,其中指針指向的是Monitor對(duì)象的起始地址。
9. 線程有哪些狀態(tài)?
線程有6個(gè)狀態(tài),分別是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated。
轉(zhuǎn)換關(guān)系圖如下:
New:線程對(duì)象創(chuàng)建之后、但還沒(méi)有調(diào)用start()方法,就是這個(gè)狀態(tài)。
?
?
/** ?*?關(guān)注公眾號(hào):芋道源碼 ?*/ public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?{ ????????Thread?thread?=?new?Thread(); ????????System.out.println(thread.getState()); ????} } //運(yùn)行結(jié)果: NEW
?
?
Runnable:它包括就緒(ready)和運(yùn)行中(running)兩種狀態(tài)。如果調(diào)用start方法,線程就會(huì)進(jìn)入Runnable狀態(tài)。它表示我這個(gè)線程可以被執(zhí)行啦(此時(shí)相當(dāng)于ready狀態(tài)),如果這個(gè)線程被調(diào)度器分配了CPU時(shí)間,那么就可以被執(zhí)行(此時(shí)處于running狀態(tài))。
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?{ ????????Thread?thread?=?new?Thread(); ????????thread.start(); ????????System.out.println(thread.getState()); ????} } //運(yùn)行結(jié)果: RUNNABLE
?
?
Blocked:阻塞的(被同步鎖或者IO鎖阻塞)。表示線程阻塞于鎖,線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(等待獲取鎖 )時(shí)的狀態(tài)。比如前面有一個(gè)臨界區(qū)的代碼需要執(zhí)行,那么線程就需要等待,它就會(huì)進(jìn)入這個(gè)狀態(tài)。它一般是從RUNNABLE狀態(tài)轉(zhuǎn)化過(guò)來(lái)的。如果線程獲取到鎖,它將變成RUNNABLE狀態(tài)
?
?
Thread?t?=?new?Thread(new?Runnable?{ ????void?run()?{ ????????synchronized?(lock)?{?//?阻塞于這里,變?yōu)锽locked狀態(tài) ????????????//?dothings ????????}? ????} }); t.getState();?//新建之前,還沒(méi)開始調(diào)用start方法,處于New狀態(tài) t.start();?//調(diào)用start方法,就會(huì)進(jìn)入Runnable狀態(tài)
?
?
WAITING : 永久等待狀態(tài),進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動(dòng)作(比如通知)。處于該狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無(wú)限期等待的狀態(tài)。一般Object.wait。
?
?
Thread?t?=?new?Thread(new?Runnable?{ ????void?run()?{ ????????synchronized?(lock)?{?//?Blocked ????????????//?dothings ????????????while?(!condition)?{ ????????????????lock.wait();?//?into?Waiting ????????????} ????????}? ????} }); t.getState();?//?New t.start();?//?Runnable
?
?
TIMED_WATING: 等待指定的時(shí)間重新被喚醒的狀態(tài)。有一個(gè)計(jì)時(shí)器在里面計(jì)算的,最常見就是使用Thread.sleep方法觸發(fā),觸發(fā)后,線程就進(jìn)入了Timed_waiting狀態(tài),隨后會(huì)由計(jì)時(shí)器觸發(fā),再進(jìn)入Runnable狀態(tài)。
?
?
Thread?t?=?new?Thread(new?Runnable?{ ????void?run()?{ ????????Thread.sleep(1000);?//?Timed_waiting ????} }); t.getState();?//?New t.start();?//?Runnable
?
?
終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完成。
再來(lái)看個(gè)代碼demo吧:
?
?
/** ?*?關(guān)注公眾號(hào):芋道源碼 ?*/ public?class?ThreadTest?{ ????private?static?Object?object?=?new?Object(); ????public?static?void?main(String[]?args)?throws?Exception?{ ????????Thread?thread?=?new?Thread(new?Runnable()?{ ????????????@Override ????????????public?void?run()?{ ????????????????try?{ ????????????????????for(int?i?=?0;?i1000;?i++){ ????????????????????????System.out.print(""); ????????????????????} ????????????????????Thread.sleep(500); ????????????????????synchronized?(object){ ????????????????????????object.wait(); ????????????????????} ????????????????}?catch?(InterruptedException?e)?{ ????????????????????e.printStackTrace(); ????????????????} ????????????} ????????}); ????????Thread?thread1?=?new?Thread(new?Runnable()?{ ????????????@Override ????????????public?void?run()?{ ????????????????try?{ ????????????????????synchronized?(object){ ????????????????????????Thread.sleep(1000); ????????????????????} ????????????????????Thread.sleep(1000); ????????????????????synchronized?(object){ ????????????????????????object.notify(); ????????????????????} ????????????????}?catch?(InterruptedException?e)?{ ????????????????????e.printStackTrace(); ????????????????} ????????????} ????????}); ???????? ????????System.out.println("1"+thread.getState()); ????????thread.start(); ????????thread1.start(); ????????System.out.println("2"+thread.getState()); ????????while?(thread.isAlive()){ ????????????System.out.println("---"+thread.getState()); ????????????Thread.sleep(100); ????????} ????????System.out.println("3"+thread.getState()); ????} } 運(yùn)行結(jié)果: 1NEW 2RUNNABLE ---RUNNABLE ---TIMED_WAITING ---TIMED_WAITING ---TIMED_WAITING ---TIMED_WAITING ---BLOCKED ---BLOCKED ---BLOCKED ---BLOCKED ---BLOCKED ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING
?
?
10. synchronized 和 ReentrantLock 的區(qū)別?
Synchronized是依賴于JVM實(shí)現(xiàn)的,而ReenTrantLock是API實(shí)現(xiàn)的。
在Synchronized優(yōu)化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級(jí)鎖(自旋鎖)后,兩者性能就差不多了。
Synchronized的使用比較方便簡(jiǎn)潔,它由編譯器去保證鎖的加鎖和釋放。而ReenTrantLock需要手工聲明來(lái)加鎖和釋放鎖,最好在finally中聲明釋放鎖。
ReentrantLock可以指定是公平鎖還是?公平鎖。?synchronized只能是?公平鎖。
ReentrantLock可響應(yīng)中斷、可輪回,而Synchronized是不可以響應(yīng)中斷的
11. wait(),notify()和 suspend(), resume()之間的區(qū)別
wait()方法使得線程進(jìn)入阻塞等待狀態(tài),并且釋放鎖
notify()喚醒一個(gè)處于等待狀態(tài)的線程,它一般跟wait()方法配套使用。
suspend()使得線程進(jìn)入阻塞狀態(tài),并且不會(huì)自動(dòng)恢復(fù),必須對(duì)應(yīng)的resume()被調(diào)用,才能使得線程重新進(jìn)入可執(zhí)行狀態(tài)。suspend()方法很容易引起死鎖問(wèn)題。
resume()方法跟suspend()方法配套使用。
suspend()不建議使用 ,因?yàn)閟uspend()方法在調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資 源(比如鎖),而是占有著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問(wèn)題。
12. CAS?CAS 有什么缺陷,如何解決?
CAS,全稱是Compare and Swap,翻譯過(guò)來(lái)就是比較并交換;
CAS涉及3個(gè)操作數(shù),內(nèi)存地址值V,預(yù)期原值A(chǔ),新值B;如果內(nèi)存位置的值V與預(yù)期原A值相匹配,就更新為新值B,否則不更新
CAS有什么缺陷?
ABA 問(wèn)題
并發(fā)環(huán)境下,假設(shè)初始條件是A,去修改數(shù)據(jù)時(shí),發(fā)現(xiàn)是A就會(huì)執(zhí)行修改。但是看到的雖然是A,中間可能發(fā)生了A變B,B又變回A的情況。此時(shí)A已經(jīng)非彼A,數(shù)據(jù)即使成功修改,也可能有問(wèn)題。
可以通過(guò)AtomicStampedReference 解決ABA問(wèn)題 ,它,一個(gè)帶有標(biāo)記的原子引用類,通過(guò)控制變量值的版本來(lái)保證CAS的正確性。
循環(huán)時(shí)間長(zhǎng)開銷
自旋CAS,如果一直循環(huán)執(zhí)行,一直不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開銷。很多時(shí)候,CAS思想體現(xiàn),是有個(gè)自旋次數(shù)的,就是為了避開這個(gè)耗時(shí)問(wèn)題~
只能保證一個(gè)變量的原子操作。
CAS 保證的是對(duì)一個(gè)變量執(zhí)行操作的原子性,如果對(duì)多個(gè)變量操作時(shí),CAS 目前無(wú)法直接保證操作的原子性的。可以通過(guò)這兩個(gè)方式解決這個(gè)問(wèn)題:1. 使用互斥鎖來(lái)保證原子性;2.將多個(gè)變量封裝成對(duì)象,通過(guò)AtomicReference來(lái)保證原子性。
13. 說(shuō)說(shuō)CountDownLatch與 CyclicBarrier 區(qū)別
CountDownLatch和CyclicBarrier都用于讓線程等待,達(dá)到一定條件時(shí)再運(yùn)行。主要區(qū)別是:
CountDownLatch:一個(gè)或者多個(gè)線程,等待其他多個(gè)線程完成某件事情之后才能執(zhí)行;
CyclicBarrier:多個(gè)線程互相等待,直到到達(dá)同一個(gè)同步點(diǎn),再繼續(xù)一起執(zhí)行。
舉個(gè)例子吧:
CountDownLatch:假設(shè)老師跟同學(xué)約定周末在公園門口集合,等人齊了再發(fā)門票。那么,發(fā)門票(這個(gè)主線程),需要等各位同學(xué)都到齊(多個(gè)其他線程都完成),才能執(zhí)行。
CyclicBarrier:多名短跑運(yùn)動(dòng)員要開始田徑比賽,只有等所有運(yùn)動(dòng)員準(zhǔn)備好,裁判才會(huì)鳴槍開始,這時(shí)候所有的運(yùn)動(dòng)員才會(huì)疾步如飛。
14. 什么是多線程環(huán)境下的偽共享
14.1 什么是偽共享?
CPU的緩存是以緩存行(cache line)為單位進(jìn)行緩存的,當(dāng)多個(gè)線程修改相互獨(dú)立的變量,而這些變量又處于同一個(gè)緩存行時(shí)就會(huì)影響彼此的性能。這就是偽共享
現(xiàn)代計(jì)算機(jī)計(jì)算模型:
CPU執(zhí)行速度比內(nèi)存速度快好幾個(gè)數(shù)量級(jí),為了提高執(zhí)行效率,現(xiàn)代計(jì)算機(jī)模型演變出CPU、緩存(L1,L2,L3),內(nèi)存的模型。
CPU執(zhí)行運(yùn)算時(shí),如先從L1緩存查詢數(shù)據(jù),找不到再去L2緩存找,依次類推,直到在內(nèi)存獲取到數(shù)據(jù)。
為了避免頻繁從內(nèi)存獲取數(shù)據(jù),聰明的科學(xué)家設(shè)計(jì)出緩存行,緩存行大小為64字節(jié)。
也正是因?yàn)?strong>緩存行的存在 ,就導(dǎo)致了偽共享問(wèn)題,如圖所示:
假設(shè)數(shù)據(jù)a、b被加載到同一個(gè)緩存行。
當(dāng)線程1修改了a的值,這時(shí)候CPU1就會(huì)通知其他CPU核,當(dāng)前緩存行(Cache line)已經(jīng)失效。
這時(shí)候,如果線程2發(fā)起修改b,因?yàn)榫彺嫘幸呀?jīng)失效了,所以「core2 這時(shí)會(huì)重新從主內(nèi)存中讀取該 Cache line 數(shù)據(jù)」。讀完后,因?yàn)樗薷腷的值,那么CPU2就通知其他CPU核,當(dāng)前緩存行(Cache line)又已經(jīng)失效。
醬紫,如果同一個(gè)Cache line的內(nèi)容被多個(gè)線程讀寫,就很容易產(chǎn)生相互競(jìng)爭(zhēng),頻繁回寫主內(nèi)存,會(huì)大大降低性能。
14.2 如何解決偽共享問(wèn)題
既然偽共享是因?yàn)橄嗷オ?dú)立的變量存儲(chǔ)到相同的Cache line導(dǎo)致的,一個(gè)緩存行大小是64字節(jié)。那么,我們就可以使用空間換時(shí)間 的方法,即數(shù)據(jù)填充的方式 ,把獨(dú)立的變量分散到不同的Cache line~
來(lái)看個(gè)例子:
?
?
/** ?*?更多干貨內(nèi)容,關(guān)注公眾號(hào):芋道源碼 ?*/ public?class?FalseShareTest??{ ????public?static?void?main(String[]?args)?throws?InterruptedException?{ ????????Rectangle?rectangle?=?new?Rectangle(); ????????long?beginTime?=?System.currentTimeMillis(); ????????Thread?thread1?=?new?Thread(()?->?{ ????????????for?(int?i?=?0;?i?100000000;?i++)?{ ????????????????rectangle.a?=?rectangle.a?+?1; ????????????} ????????}); ????????Thread?thread2?=?new?Thread(()?->?{ ????????????for?(int?i?=?0;?i?100000000;?i++)?{ ????????????????rectangle.b?=?rectangle.b?+?1; ????????????} ????????}); ????????thread1.start(); ????????thread2.start(); ????????thread1.join(); ????????thread2.join(); ????????System.out.println("執(zhí)行時(shí)間"?+?(System.currentTimeMillis()?-?beginTime)); ????} } class?Rectangle?{ ????volatile?long?a; ????volatile?long?b; } //運(yùn)行結(jié)果: 執(zhí)行時(shí)間2815
?
?
一個(gè)long類型是8字節(jié),我們?cè)谧兞縜和b之間不上7個(gè)long類型變量呢,輸出結(jié)果是啥呢?如下:
?
?
class?Rectangle?{ ????volatile?long?a; ????long?a1,a2,a3,a4,a5,a6,a7; ????volatile?long?b; } //運(yùn)行結(jié)果 執(zhí)行時(shí)間1113
?
?
可以發(fā)現(xiàn)利用填充數(shù)據(jù)的方式,讓讀寫的變量分割到不同緩存行,可以很好挺高性能~
15. Fork/Join框架的理解
Fork/Join框架是Java7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。
Fork/Join框架需要理解兩個(gè)點(diǎn),「分而治之」和「工作竊取算法」。
分而治之
以上Fork/Join框架的定義,就是分而治之思想的體現(xiàn)啦
工作竊取算法
把大任務(wù)拆分成小任務(wù),放到不同隊(duì)列執(zhí)行,交由不同的線程分別執(zhí)行時(shí)。有的線程優(yōu)先把自己負(fù)責(zé)的任務(wù)執(zhí)行完了,其他線程還在慢慢悠悠處理自己的任務(wù),這時(shí)候?yàn)榱顺浞痔岣咝剩托枰ぷ鞅I竊算法啦~
工作盜竊算法就是,「某個(gè)線程從其他隊(duì)列中竊取任務(wù)進(jìn)行執(zhí)行的過(guò)程」。一般就是指做得快的線程(盜竊線程)搶慢的線程的任務(wù)來(lái)做,同時(shí)為了減少鎖競(jìng)爭(zhēng),通常使用雙端隊(duì)列,即快線程和慢線程各在一端。
16. 聊聊ThreadLocal原理?
ThreadLocal的內(nèi)存結(jié)構(gòu)圖
為了對(duì)ThreadLocal有個(gè)宏觀的認(rèn)識(shí),我們先來(lái)看下ThreadLocal的內(nèi)存結(jié)構(gòu)圖
從內(nèi)存結(jié)構(gòu)圖,我們可以看到:
Thread類中,有個(gè)ThreadLocal.ThreadLocalMap 的成員變量。
ThreadLocalMap內(nèi)部維護(hù)了Entry數(shù)組,每個(gè)Entry代表一個(gè)完整的對(duì)象,key是ThreadLocal本身,value是ThreadLocal的泛型對(duì)象值。
關(guān)鍵源碼分析
對(duì)照著關(guān)鍵源碼來(lái)看,更容易理解一點(diǎn)哈~
首先看下Thread類的源碼,可以看到成員變量ThreadLocalMap的初始值是為null
?
?
public?class?Thread?implements?Runnable?{ ???//ThreadLocal.ThreadLocalMap是Thread的屬性 ???ThreadLocal.ThreadLocalMap?threadLocals?=?null; }
?
?
成員變量ThreadLocalMap的關(guān)鍵源碼如下:
?
?
static?class?ThreadLocalMap?{ ???? ????static?class?Entry?extends?WeakReference>?{ ????????/**?The?value?associated?with?this?ThreadLocal.?*/ ????????Object?value; ????????Entry(ThreadLocal>?k,?Object?v)?{ ????????????super(k); ????????????value?=?v; ????????} ????} ????//Entry數(shù)組 ????private?Entry[]?table; ???? ????//?ThreadLocalMap的構(gòu)造器,ThreadLocal作為key ????ThreadLocalMap(ThreadLocal>?firstKey,?Object?firstValue)?{ ????????table?=?new?Entry[INITIAL_CAPACITY]; ????????int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1); ????????table[i]?=?new?Entry(firstKey,?firstValue); ????????size?=?1; ????????setThreshold(INITIAL_CAPACITY); ????} }
?
?
ThreadLocal類中的關(guān)鍵set()方法:
?
?
?public?void?set(T?value)?{ ????????Thread?t?=?Thread.currentThread();?//獲取當(dāng)前線程t ????????ThreadLocalMap?map?=?getMap(t);??//根據(jù)當(dāng)前線程獲取到ThreadLocalMap ????????if?(map?!=?null)??//如果獲取的ThreadLocalMap對(duì)象不為空 ????????????map.set(this,?value);?//K,V設(shè)置到ThreadLocalMap中 ????????else ????????????createMap(t,?value);?//創(chuàng)建一個(gè)新的ThreadLocalMap ????} ???? ?????ThreadLocalMap?getMap(Thread?t)?{ ???????return?t.threadLocals;?//返回Thread對(duì)象的ThreadLocalMap屬性 ????} ????void?createMap(Thread?t,?T?firstValue)?{?//調(diào)用ThreadLocalMap的構(gòu)造函數(shù) ????????t.threadLocals?=?new?ThreadLocalMap(this,?firstValue);?this表示當(dāng)前類ThreadLocal ????} ????
?
?
ThreadLocal類中的關(guān)鍵get()方法
?
?
????public?T?get()?{ ????????Thread?t?=?Thread.currentThread();//獲取當(dāng)前線程t ????????ThreadLocalMap?map?=?getMap(t);//根據(jù)當(dāng)前線程獲取到ThreadLocalMap ????????if?(map?!=?null)?{?//如果獲取的ThreadLocalMap對(duì)象不為空 ????????????//由this(即ThreadLoca對(duì)象)得到對(duì)應(yīng)的Value,即ThreadLocal的泛型值 ????????????ThreadLocalMap.Entry?e?=?map.getEntry(this); ????????????if?(e?!=?null)?{ ????????????????@SuppressWarnings("unchecked") ????????????????T?result?=?(T)e.value;? ????????????????return?result; ????????????} ????????} ????????return?setInitialValue();?//初始化threadLocals成員變量的值 ????} ???? ?????private?T?setInitialValue()?{ ????????T?value?=?initialValue();?//初始化value的值 ????????Thread?t?=?Thread.currentThread();? ????????ThreadLocalMap?map?=?getMap(t);?//以當(dāng)前線程為key,獲取threadLocals成員變量,它是一個(gè)ThreadLocalMap ????????if?(map?!=?null) ????????????map.set(this,?value);??//K,V設(shè)置到ThreadLocalMap中 ????????else ????????????createMap(t,?value);?//實(shí)例化threadLocals成員變量 ????????return?value; ????}
?
?
所以怎么回答ThreadLocal的實(shí)現(xiàn)原理 ?如下,最好是能結(jié)合以上結(jié)構(gòu)圖一起說(shuō)明哈~
Thread線程類有一個(gè)類型為ThreadLocal.ThreadLocalMap的實(shí)例變量threadLocals,即每個(gè)線程都有一個(gè)屬于自己的ThreadLocalMap。
ThreadLocalMap內(nèi)部維護(hù)著Entry數(shù)組,每個(gè)Entry代表一個(gè)完整的對(duì)象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
并發(fā)多線程場(chǎng)景下,每個(gè)線程Thread,在往ThreadLocal里設(shè)置值的時(shí)候,都是往自己的ThreadLocalMap里存,讀也是以某個(gè)ThreadLocal作為引用,在自己的map里找對(duì)應(yīng)的key,從而可以實(shí)現(xiàn)了線程隔離 。
17. TreadLocal為什么會(huì)導(dǎo)致內(nèi)存泄漏呢?
弱引用導(dǎo)致的內(nèi)存泄漏呢?
key是弱引用,GC回收會(huì)影響ThreadLocal的正常工作嘛?
ThreadLocal內(nèi)存泄漏的demo
17.1 弱引用導(dǎo)致的內(nèi)存泄漏呢?
我們先來(lái)看看TreadLocal的引用示意圖哈:
關(guān)于ThreadLocal內(nèi)存泄漏,網(wǎng)上比較流行的說(shuō)法是這樣的:
ThreadLocalMap使用ThreadLocal的弱引用 作為key,當(dāng)ThreadLocal變量被手動(dòng)設(shè)置為null,即一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用來(lái)引用它,當(dāng)系統(tǒng)GC時(shí),ThreadLocal一定會(huì)被回收。這樣的話,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話(比如線程池的核心線程),這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreaLocalMap -> Entry -> value -> Object 永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄漏。
當(dāng)ThreadLocal變量被手動(dòng)設(shè)置為null后的引用鏈圖:
實(shí)際上,ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況。所以也加上了一些防護(hù)措施:即在ThreadLocal的get,set,remove方法,都會(huì)清除線程ThreadLocalMap里所有key為null的value。
源代碼中,是有體現(xiàn)的,如ThreadLocalMap的set方法:
?
?
??private?void?set(ThreadLocal>?key,?Object?value)?{ ??????Entry[]?tab?=?table; ??????int?len?=?tab.length; ??????int?i?=?key.threadLocalHashCode?&?(len-1); ??????for?(Entry?e?=?tab[i]; ????????????e?!=?null; ????????????e?=?tab[i?=?nextIndex(i,?len)])?{ ??????????ThreadLocal>?k?=?e.get(); ??????????if?(k?==?key)?{ ??????????????e.value?=?value; ??????????????return; ??????????} ???????????//如果k等于null,則說(shuō)明該索引位之前放的key(threadLocal對(duì)象)被回收了,這通常是因?yàn)橥獠繉hreadLocal變量置為null, ???????????//又因?yàn)閑ntry對(duì)threadLocal持有的是弱引用,一輪GC過(guò)后,對(duì)象被回收。 ????????????//這種情況下,既然用戶代碼都已經(jīng)將threadLocal置為null,那么也就沒(méi)打算再通過(guò)該對(duì)象作為key去取到之前放入threadLocalMap的value,?因此ThreadLocalMap中會(huì)直接替換調(diào)這種不新鮮的entry。 ??????????if?(k?==?null)?{ ??????????????replaceStaleEntry(key,?value,?i); ??????????????return; ??????????} ????????} ????????tab[i]?=?new?Entry(key,?value); ????????int?sz?=?++size; ????????//觸發(fā)一次Log2(N)復(fù)雜度的掃描,目的是清除過(guò)期Entry?? ????????if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold) ??????????rehash(); ????}
?
?
如ThreadLocal的get方法:
?
?
??public?T?get()?{ ????Thread?t?=?Thread.currentThread(); ????ThreadLocalMap?map?=?getMap(t); ????if?(map?!=?null)?{ ????????//去ThreadLocalMap獲取Entry,方法里面有key==null的清除邏輯 ????????ThreadLocalMap.Entry?e?=?map.getEntry(this); ????????if?(e?!=?null)?{ ????????????@SuppressWarnings("unchecked") ????????????T?result?=?(T)e.value; ????????????return?result; ????????} ????} ????return?setInitialValue(); } private?Entry?getEntry(ThreadLocal>?key)?{ ????????int?i?=?key.threadLocalHashCode?&?(table.length?-?1); ????????Entry?e?=?table[i]; ????????if?(e?!=?null?&&?e.get()?==?key) ?????????????return?e; ????????else ??????????//里面有key==null的清除邏輯 ??????????return?getEntryAfterMiss(key,?i,?e); ????} ???????? private?Entry?getEntryAfterMiss(ThreadLocal>?key,?int?i,?Entry?e)?{ ????????Entry[]?tab?=?table; ????????int?len?=?tab.length; ????????while?(e?!=?null)?{ ????????????ThreadLocal>?k?=?e.get(); ????????????if?(k?==?key) ????????????????return?e; ????????????//?Entry的key為null,則表明沒(méi)有外部引用,且被GC回收,是一個(gè)過(guò)期Entry ????????????if?(k?==?null) ????????????????expungeStaleEntry(i);?//刪除過(guò)期的Entry ????????????else ????????????????i?=?nextIndex(i,?len); ????????????e?=?tab[i]; ????????} ????????return?null; ????}
?
?
17.2 key是弱引用,GC回收會(huì)影響ThreadLocal的正常工作嘛?
有些小伙伴可能有疑問(wèn),ThreadLocal的key既然是弱引用 .會(huì)不會(huì)GC貿(mào)然把key回收掉,進(jìn)而影響ThreadLocal的正常使用?
弱引用 :具有弱引用的對(duì)象擁有更短暫的生命周期。如果一個(gè)對(duì)象只有弱引用存在了,則下次GC將會(huì)回收掉該對(duì)象 (不管當(dāng)前內(nèi)存空間足夠與否)
其實(shí)不會(huì)的,因?yàn)橛蠺hreadLocal變量引用著它,是不會(huì)被GC回收的,除非手動(dòng)把ThreadLocal變量設(shè)置為null,我們可以跑個(gè)demo來(lái)驗(yàn)證一下:
?
?
??public?class?WeakReferenceTest?{ ????public?static?void?main(String[]?args)?{ ????????Object?object?=?new?Object(); ????????WeakReference
?
?
結(jié)論就是,小伙伴放下這個(gè)疑惑了,哈哈~
17.3 ThreadLocal內(nèi)存泄漏的demo
給大家來(lái)看下一個(gè)內(nèi)存泄漏的例子,其實(shí)就是用線程池,一直往里面放對(duì)象
?
?
public?class?ThreadLocalTestDemo?{ ????private?static?ThreadLocal?tianLuoThreadLocal?=?new?ThreadLocal<>(); ????public?static?void?main(String[]?args)?throws?InterruptedException?{ ????????ThreadPoolExecutor?threadPoolExecutor?=?new?ThreadPoolExecutor(5,?5,?1,?TimeUnit.MINUTES,?new?LinkedBlockingQueue<>()); ????????for?(int?i?=?0;?i?10;?++i)?{ ????????????threadPoolExecutor.execute(new?Runnable()?{ ????????????????@Override ????????????????public?void?run()?{ ????????????????????System.out.println("創(chuàng)建對(duì)象:"); ????????????????????TianLuoClass?tianLuoClass?=?new?TianLuoClass(); ????????????????????tianLuoThreadLocal.set(tianLuoClass); ????????????????????tianLuoClass?=?null;?//將對(duì)象設(shè)置為?null,表示此對(duì)象不在使用了 ???????????????????//?tianLuoThreadLocal.remove(); ????????????????} ????????????}); ????????????Thread.sleep(1000); ????????} ????} ????static?class?TianLuoClass?{ ????????//?100M ????????private?byte[]?bytes?=?new?byte[100?*?1024?*?1024]; ????} } 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: Exception?in?thread?"pool-1-thread-4"?java.lang.OutOfMemoryError:?Java?heap?space ?at?com.example.dto.ThreadLocalTestDemo$TianLuoClass. (ThreadLocalTestDemo.java:33) ?at?com.example.dto.ThreadLocalTestDemo$1.run(ThreadLocalTestDemo.java:21) ?at?java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ?at?java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ?at?java.lang.Thread.run(Thread.java:748)
?
?
運(yùn)行結(jié)果出現(xiàn)了OOM,tianLuoThreadLocal.remove();加上后,則不會(huì)OOM。
?
?
創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: 創(chuàng)建對(duì)象: ......
?
?
我們這里沒(méi)有手動(dòng)設(shè)置 tianLuoThreadLocal變量為null,但是還是會(huì)內(nèi)存泄漏 。因?yàn)槲覀兪褂昧司€程池,線程池有很長(zhǎng)的生命周期,因此線程池會(huì)一直持有tianLuoClass對(duì)象的value值,即使設(shè)置tianLuoClass = null;引用還是存在的。這就好像,你把一個(gè)個(gè)對(duì)象object放到一個(gè)list列表里,然后再單獨(dú)把object設(shè)置為null的道理是一樣的,列表的對(duì)象還是存在的。
?
?
????public?static?void?main(String[]?args)?{ ????????List
?
?
所以內(nèi)存泄漏就這樣發(fā)生啦,最后內(nèi)存是有限的,就拋出了OOM了。如果我們加上threadLocal.remove();,則不會(huì)內(nèi)存泄漏。為什么呢?因?yàn)閠hreadLocal.remove();會(huì)清除Entry,源碼如下:
?
?
????private?void?remove(ThreadLocal>?key)?{ ??????Entry[]?tab?=?table; ??????int?len?=?tab.length; ??????int?i?=?key.threadLocalHashCode?&?(len-1); ??????for?(Entry?e?=?tab[i]; ??????????e?!=?null; ??????????e?=?tab[i?=?nextIndex(i,?len)])?{ ??????????if?(e.get()?==?key)?{ ??????????????//清除entry ??????????????e.clear(); ????????????expungeStaleEntry(i); ????????????return; ????????} ????} }
?
?
18 為什么ThreadLocalMap 的 key 是弱引用,設(shè)計(jì)理念是?
通過(guò)閱讀ThreadLocal的源碼,我們是可以看到Entry的Key是設(shè)計(jì)為弱引用的(ThreadLocalMap使用ThreadLocal的弱引用作為Key的)。為什么要設(shè)計(jì)為弱引用呢?
我們先來(lái)回憶一下四種引用:
強(qiáng)引用 :我們平時(shí)new了一個(gè)對(duì)象就是強(qiáng)引用,例如 Object obj = new Object();即使在內(nèi)存不足的情況下,JVM寧愿拋出OutOfMemory錯(cuò)誤也不會(huì)回收這種對(duì)象。
軟引用 :如果一個(gè)對(duì)象只具有軟引用,則內(nèi)存空間足夠,垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。
弱引用 :具有弱引用的對(duì)象擁有更短暫的生命周期。如果一個(gè)對(duì)象只有弱引用存在了,則下次GC將會(huì)回收掉該對(duì)象 (不管當(dāng)前內(nèi)存空間足夠與否)。
虛引用 :如果一個(gè)對(duì)象僅持有虛引用,那么它就和沒(méi)有任何引用一樣,在任何時(shí)候都可能被垃圾回收器回收。虛引用主要用來(lái)跟蹤對(duì)象被垃圾回收器回收的活動(dòng)。
我們先來(lái)看看官方文檔,為什么要設(shè)計(jì)為弱引用:
?
?
To?help?deal?with?very?large?and?long-lived?usages,?the?hash?table?entries?use?WeakReferences?for?keys. 為了應(yīng)對(duì)非常大和長(zhǎng)時(shí)間的用途,哈希表使用弱引用的?key。
?
?
我再把ThreadLocal的引用示意圖搬過(guò)來(lái):
下面我們分情況討論:
如果Key使用強(qiáng)引用:當(dāng)ThreadLocal的對(duì)象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用的話,如果沒(méi)有手動(dòng)刪除,ThreadLocal就不會(huì)被回收,會(huì)出現(xiàn)Entry的內(nèi)存泄漏問(wèn)題。
如果Key使用弱引用:當(dāng)ThreadLocal的對(duì)象被回收了,因?yàn)門hreadLocalMap持有ThreadLocal的弱引用,即使沒(méi)有手動(dòng)刪除,ThreadLocal也會(huì)被回收。value則在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
因此可以發(fā)現(xiàn),使用弱引用作為Entry的Key,可以多一層保障:弱引用ThreadLocal不會(huì)輕易內(nèi)存泄漏,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
實(shí)際上,我們的內(nèi)存泄漏的根本原因是,不再被使用的Entry,沒(méi)有從線程的ThreadLocalMap中刪除。一般刪除不再使用的Entry有這兩種方式:
一種就是,使用完ThreadLocal,手動(dòng)調(diào)用remove(),把Entry從ThreadLocalMap中刪除
另外一種方式就是:ThreadLocalMap的自動(dòng)清除機(jī)制去清除過(guò)期Entry.(ThreadLocalMap的get(),set()時(shí)都會(huì)觸發(fā)對(duì)過(guò)期Entry的清除)
19. 如何保證父子線程間的共享ThreadLocal數(shù)據(jù)
我們知道ThreadLocal是線程隔離的,如果我們希望父子線程共享數(shù)據(jù),如何做到呢?可以使用InheritableThreadLocal。先來(lái)看看demo:
?
?
public?class?InheritableThreadLocalTest?{ ???public?static?void?main(String[]?args)?{ ???????ThreadLocal?threadLocal?=?new?ThreadLocal<>(); ???????InheritableThreadLocal ?inheritableThreadLocal?=?new?InheritableThreadLocal<>(); ???????threadLocal.set("關(guān)注公眾號(hào):芋道源碼"); ???????inheritableThreadLocal.set("關(guān)注公眾號(hào):芋道源碼"); ???????Thread?thread?=?new?Thread(()->{ ???????????System.out.println("ThreadLocal?value?"?+?threadLocal.get()); ???????????System.out.println("InheritableThreadLocal?value?"?+?inheritableThreadLocal.get()); ???????}); ???????thread.start(); ??????? ???} } //運(yùn)行結(jié)果 ThreadLocal?value?null InheritableThreadLocal?value?關(guān)注公眾號(hào):芋道源碼
?
?
可以發(fā)現(xiàn),在子線程中,是可以獲取到父線程的 InheritableThreadLocal 類型變量的值,但是不能獲取到 ThreadLocal 類型變量的值。
獲取不到ThreadLocal 類型的值,我們可以好理解,因?yàn)樗蔷€程隔離的嘛。InheritableThreadLocal 是如何做到的呢?原理是什么呢?
在Thread類中,除了成員變量threadLocals之外,還有另一個(gè)成員變量:inheritableThreadLocals。它們兩類型是一樣的:
?
?
public?class?Thread?implements?Runnable?{ ???ThreadLocalMap?threadLocals?=?null; ???ThreadLocalMap?inheritableThreadLocals?=?null; ?}
?
?
Thread類的init方法中,有一段初始化設(shè)置:
?
?
?private?void?init(ThreadGroup?g,?Runnable?target,?String?name, ??????????????????????long?stackSize,?AccessControlContext?acc, ??????????????????????boolean?inheritThreadLocals)?{ ?????? ????????...... ????????if?(inheritThreadLocals?&&?parent.inheritableThreadLocals?!=?null) ????????????this.inheritableThreadLocals?= ????????????????ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); ????????/*?Stash?the?specified?stack?size?in?case?the?VM?cares?*/ ????????this.stackSize?=?stackSize; ????????/*?Set?thread?ID?*/ ????????tid?=?nextThreadID(); ????} ?static?ThreadLocalMap?createInheritedMap(ThreadLocalMap?parentMap)?{ ????????return?new?ThreadLocalMap(parentMap); ????}
?
?
可以發(fā)現(xiàn),當(dāng)parent的inheritableThreadLocals不為null時(shí),就會(huì)將parent的inheritableThreadLocals,賦值給前線程的inheritableThreadLocals。說(shuō)白了,就是如果當(dāng)前線程的inheritableThreadLocals不為null,就從父線程哪里拷貝過(guò)來(lái)一個(gè)過(guò)來(lái),類似于另外一個(gè)ThreadLocal,但是數(shù)據(jù)從父線程那里來(lái)的。有興趣的小伙伴們可以在去研究研究源碼~
20. 如何保證多線程下 i++ 結(jié)果正確?
使用循環(huán)CAS,實(shí)現(xiàn)i++原子操作
使用鎖機(jī)制,實(shí)現(xiàn)i++原子操作
使用synchronized,實(shí)現(xiàn)i++原子操作
舉個(gè)簡(jiǎn)單的例子,如下:
?
?
/** ?*??關(guān)注公眾號(hào):芋道源碼? ?*??非常多干貨 ?*/ public?class?AtomicIntegerTest?{ ????private?static?AtomicInteger?atomicInteger?=?new?AtomicInteger(0); ????public?static?void?main(String[]?args)?throws?InterruptedException?{ ????????testIAdd(); ????} ????private?static?void?testIAdd()?throws?InterruptedException?{ ????????//創(chuàng)建線程池 ????????ExecutorService?executorService?=?Executors.newFixedThreadPool(2); ????????for?(int?i?=?0;?i?1000;?i++)?{ ????????????executorService.execute(()?->?{ ????????????????for?(int?j?=?0;?j?2;?j++)?{ ????????????????????//自增并返回當(dāng)前值 ????????????????????int?andIncrement?=?atomicInteger.incrementAndGet(); ????????????????????System.out.println("線程:"?+?Thread.currentThread().getName()?+?"?count="?+?andIncrement); ????????????????} ????????????}); ????????} ????????executorService.shutdown(); ????????Thread.sleep(100); ????????System.out.println("最終結(jié)果是?:"?+?atomicInteger.get()); ????} ???? }
?
?
運(yùn)行結(jié)果:
?
?
... 線程:pool-1-thread-1?count=1997 線程:pool-1-thread-1?count=1998 線程:pool-1-thread-1?count=1999 線程:pool-1-thread-2?count=315 線程:pool-1-thread-2?count=2000 最終結(jié)果是?:2000
?
?
21. 如何檢測(cè)死鎖?怎么預(yù)防死鎖?死鎖四個(gè)必要條件
死鎖是指多個(gè)線程因競(jìng)爭(zhēng)資源而造成的一種互相等待的僵局。如圖感受一下:
死鎖的四個(gè)必要條件:
互斥:一次只有一個(gè)進(jìn)程可以使用一個(gè)資源。其他進(jìn)程不能訪問(wèn)已分配給其他進(jìn)程的資源。
占有且等待:當(dāng)一個(gè)進(jìn)程在等待分配得到其他資源時(shí),其繼續(xù)占有已分配得到的資源。
非搶占:不能強(qiáng)行搶占進(jìn)程中已占有的資源。
循環(huán)等待:存在一個(gè)封閉的進(jìn)程鏈,使得每個(gè)資源至少占有此鏈中下一個(gè)進(jìn)程所需要的一個(gè)資源。
如何預(yù)防死鎖?
加鎖順序(線程按順序辦事)
加鎖時(shí)限 (線程請(qǐng)求所加上權(quán)限,超時(shí)就放棄,同時(shí)釋放自己占有的鎖)
死鎖檢測(cè)
22. 如果線程過(guò)多,會(huì)怎樣?
使用多線程可以提升程序性能。但是如果使用過(guò)多的線程,則適得其反。
過(guò)多的線程會(huì)影響程序的系統(tǒng)。
一方面,線程的啟動(dòng)和銷毀,都是需要開銷的。
其次,過(guò)多的并發(fā)線程也會(huì)導(dǎo)致共享有限資源的開銷增大。過(guò)多的線程,還會(huì)導(dǎo)致內(nèi)存泄漏,筆者在以前公司,看到一個(gè)生產(chǎn)問(wèn)題:一個(gè)第三方的包是使用new Thread來(lái)實(shí)現(xiàn)的,使用完沒(méi)有恰當(dāng)回收銷毀,最后引發(fā)內(nèi)存泄漏問(wèn)題。
因此,我們平時(shí)盡量使用線程池來(lái)管理線程。同時(shí)還需要設(shè)置恰當(dāng)?shù)木€程數(shù)。
23. 聊聊happens-before原則
在Java語(yǔ)言中,有一個(gè)先行發(fā)生原則(happens-before)。它包括八大規(guī)則,如下:
程序次序規(guī)則 :在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
管程鎖定規(guī)則 :一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作
volatile變量規(guī)則 :對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作
線程啟動(dòng)規(guī)則 :Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作
線程終止規(guī)則 :線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
線程中斷規(guī)則 :對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
對(duì)象終結(jié)規(guī)則 :一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始
傳遞性 :如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
24. 如何實(shí)現(xiàn)兩個(gè)線程間共享數(shù)據(jù)
可以通過(guò)類變量直接將數(shù)據(jù)放到主存中
通過(guò)并發(fā)的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù)
使用volatile變量或者鎖
調(diào)用atomic類(如AtomicInteger)
25. LockSupport作用是?
LockSupport是一個(gè)工具類。它的主要作用是掛起和喚醒線程 。該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)。它的主要方法是
?
?
public?static?void?park(Object?blocker);?//?暫停指定線程 public?static?void?unpark(Thread?thread);?//?恢復(fù)指定的線程 public?static?void?park();?//?無(wú)期限暫停當(dāng)前線程
?
?
看個(gè)代碼的例子:
?
?
public?class?LockSupportTest?{ ????private?static?Object?object?=?new?Object(); ????static?MyThread?thread?=?new?MyThread("線程芋道源碼"); ????public?static?class?MyThread?extends?Thread?{ ????????public?MyThread(String?name)?{ ????????????super(name); ????????} ????????@Override?public?void?run()?{ ????????????synchronized?(object)?{ ????????????????System.out.println("線程名字:?"?+?Thread.currentThread()); ????????????????try?{ ????????????????????Thread.sleep(2000L); ????????????????}?catch?(InterruptedException?e)?{ ????????????????????e.printStackTrace(); ????????????????} ????????????????LockSupport.park(); ????????????????if?(Thread.currentThread().isInterrupted())?{ ????????????????????System.out.println("線程被中斷了"); ????????????????} ????????????????System.out.println("繼續(xù)執(zhí)行"); ????????????} ????????} ????} ????public?static?void?main(String[]?args)?{ ????????thread.start(); ????????LockSupport.unpark(thread); ????????System.out.println("恢復(fù)線程調(diào)用"); ????} } //output 恢復(fù)線程調(diào)用 線程名字:Thread[線程芋道源碼,5,main] 繼續(xù)執(zhí)行
?
?
因?yàn)閠hread線程內(nèi)部有休眠2秒的操作,所以u(píng)npark方法的操作肯定先于park方法的調(diào)用。為什么thread線程最終仍然可以結(jié)束,是因?yàn)閜ark和unpark會(huì)對(duì)每個(gè)線程維持一個(gè)許可證(布爾值)
26 ?線程池如何調(diào)優(yōu),如何確認(rèn)最佳線程數(shù)?
?
?
最佳線程數(shù)目?=?((線程等待時(shí)間+線程CPU時(shí)間)/線程CPU時(shí)間?)*?CPU數(shù)目
?
?
我們的服務(wù)器CPU核數(shù)為8核,一個(gè)任務(wù)線程cpu耗時(shí)為20ms,線程等待(網(wǎng)絡(luò)IO、磁盤IO)耗時(shí)80ms,那最佳線程數(shù)目:( 80 + 20 )/20 * 8 = 40。也就是設(shè)置 40個(gè)線程數(shù)最佳。
27. 為什么要用線程池?
線程池:一個(gè)管理線程的池子。線程池可以:
管理線程,避免增加創(chuàng)建線程和銷毀線程的資源損耗。
提高響應(yīng)速度。
重復(fù)利用線程。
28. Java的線程池執(zhí)行原理
線程池的執(zhí)行原理如下:
為了形象描述線程池執(zhí)行,打個(gè)比喻:
核心線程比作公司正式員工
非核心線程比作外包員工
阻塞隊(duì)列比作需求池
提交任務(wù)比作提需求
29. 聊聊線程池的核心參數(shù)
我們先來(lái)看看ThreadPoolExecutor的構(gòu)造函數(shù)
?
?
public?ThreadPoolExecutor(int?corePoolSize,?int?maximumPoolSize, ???long?keepAliveTime, ???TimeUnit?unit, ???BlockingQueue?workQueue, ???ThreadFactory?threadFactory, ???RejectedExecutionHandler?handler)?
?
?
corePoolSize:線程池核心線程數(shù)最大值
maximumPoolSize:線程池最大線程數(shù)大小
keepAliveTime:線程池中非核心線程空閑的存活時(shí)間大小
unit:線程空閑存活時(shí)間單位
workQueue:存放任務(wù)的阻塞隊(duì)列
threadFactory:用于設(shè)置創(chuàng)建線程的工廠,可以給創(chuàng)建的線程設(shè)置有意義的名字,可方便排查問(wèn)題。
handler:線城池的飽和策略事件,主要有四種類型拒絕策略。
四種拒絕策略
AbortPolicy(拋出一個(gè)異常,默認(rèn)的)
DiscardPolicy(直接丟棄任務(wù))
DiscardOldestPolicy(丟棄隊(duì)列里最老的任務(wù),將當(dāng)前這個(gè)任務(wù)繼續(xù)提交給線程池)
CallerRunsPolicy(交給線程池調(diào)用所在的線程進(jìn)行處理)
幾種工作阻塞隊(duì)列
ArrayBlockingQueue(用數(shù)組實(shí)現(xiàn)的有界阻塞隊(duì)列,按FIFO排序量)
LinkedBlockingQueue(基于鏈表結(jié)構(gòu)的阻塞隊(duì)列,按FIFO排序任務(wù),容量可以選擇進(jìn)行設(shè)置,不設(shè)置的話,將是一個(gè)無(wú)邊界的阻塞隊(duì)列)
DelayQueue(一個(gè)任務(wù)定時(shí)周期的延遲執(zhí)行的隊(duì)列)
PriorityBlockingQueue(具有優(yōu)先級(jí)的無(wú)界阻塞隊(duì)列)
SynchronousQueue(一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列,每個(gè)插入操作必須等到另一個(gè)線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài))
30.當(dāng)提交新任務(wù)時(shí),異常如何處理?
我們先來(lái)看一段代碼:
?
?
??ExecutorService?threadPool?=?Executors.newFixedThreadPool(5); ??for?(int?i?=?0;?i?5;?i++)?{ ??????threadPool.submit(()?->?{ ??????????System.out.println("current?thread?name"?+?Thread.currentThread().getName()); ??????????Object?object?=?null; ??????????System.out.print("result##?"+object.toString()); ??????}); ??}
?
?
顯然,這段代碼會(huì)有異常,我們?cè)賮?lái)看看執(zhí)行結(jié)果
雖然沒(méi)有結(jié)果輸出,但是沒(méi)有拋出異常,所以我們無(wú)法感知任務(wù)出現(xiàn)了異常,所以需要添加try/catch。如下圖:
OK,線程的異常處理,我們可以直接try...catch捕獲。
31. AQS組件,實(shí)現(xiàn)原理
AQS,即AbstractQueuedSynchronizer,是構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架,它使用了一個(gè)int成員變量表示同步狀態(tài),通過(guò)內(nèi)置的FIFO隊(duì)列來(lái)完成資源獲取線程的排隊(duì)工作。可以回答以下這幾個(gè)關(guān)鍵點(diǎn)哈:
state 狀態(tài)的維護(hù)。
CLH隊(duì)列
ConditionObject通知
模板方法設(shè)計(jì)模式
獨(dú)占與共享模式。
自定義同步器。
AQS全家桶的一些延伸,如:ReentrantLock等。
31.1 state 狀態(tài)的維護(hù)
state,int變量,鎖的狀態(tài),用volatile修飾,保證多線程中的可見性。
getState()和setState()方法采用final修飾,限制AQS的子類重寫它們兩。
compareAndSetState()方法采用樂(lè)觀鎖思想的CAS算法操作確保線程安全,保證狀態(tài) 設(shè)置的原子性。
31.2 CLH隊(duì)列
CLH 同步隊(duì)列,全英文Craig, Landin, and Hagersten locks。是一個(gè)FIFO雙向隊(duì)列,其內(nèi)部通過(guò)節(jié)點(diǎn)head和tail記錄隊(duì)首和隊(duì)尾元素,隊(duì)列元素的類型為Node。AQS依賴它來(lái)完成同步狀態(tài)state的管理,當(dāng)前線程如果獲取同步狀態(tài)失敗時(shí),AQS則會(huì)將當(dāng)前線程已經(jīng)等待狀態(tài)等信息構(gòu)造成一個(gè)節(jié)點(diǎn)(Node)并將其加入到CLH同步隊(duì)列,同時(shí)會(huì)阻塞當(dāng)前線程,當(dāng)同步狀態(tài)釋放時(shí),會(huì)把首節(jié)點(diǎn)喚醒(公平鎖),使其再次嘗試獲取同步狀態(tài)。
31.3 ConditionObject通知
我們都知道,synchronized控制同步的時(shí)候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以實(shí)現(xiàn)等待/通知模式。而Lock呢?它提供了條件Condition接口,配合await(),signal(),signalAll() 等方法也可以實(shí)現(xiàn)等待/通知機(jī)制。ConditionObject實(shí)現(xiàn)了Condition接口,給AQS提供條件變量的支持
ConditionObject隊(duì)列與CLH隊(duì)列的愛(ài)恨情仇:
調(diào)用了await()方法的線程,會(huì)被加入到conditionObject等待隊(duì)列中,并且喚醒CLH隊(duì)列中head節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)。
線程在某個(gè)ConditionObject對(duì)象上調(diào)用了singnal()方法后,等待隊(duì)列中的firstWaiter會(huì)被加入到AQS的CLH隊(duì)列中,等待被喚醒。
當(dāng)線程調(diào)用unLock()方法釋放鎖時(shí),CLH隊(duì)列中的head節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)(在本例中是firtWaiter),會(huì)被喚醒。
31.4 模板方法設(shè)計(jì)模式
模板方法模式:在一個(gè)方法中定義一個(gè)算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟。
AQS的典型設(shè)計(jì)模式就是模板方法設(shè)計(jì)模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生實(shí)現(xiàn),就體現(xiàn)出這個(gè)設(shè)計(jì)模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,給子類實(shí)現(xiàn)自定義的同步器。
31.5 獨(dú)占與共享模式。
獨(dú)占式: 同一時(shí)刻僅有一個(gè)線程持有同步狀態(tài),如ReentrantLock。又可分為公平鎖和非公平鎖。
共享模式:多個(gè)線程可同時(shí)執(zhí)行,如Semaphore/CountDownLatch等都是共享式的產(chǎn)物。
31.6 自定義同步器
你要實(shí)現(xiàn)自定義鎖的話,首先需要確定你要實(shí)現(xiàn)的是獨(dú)占鎖還是共享鎖,定義原子變量state的含義,再定義一個(gè)內(nèi)部類去繼承AQS,重寫對(duì)應(yīng)的模板方法即可啦
32 Semaphore原理
Semaphore ,我們也把它叫做信號(hào)量 。可以用來(lái)控制同時(shí)訪問(wèn)特定資源的線程數(shù)量 ,通過(guò)協(xié)調(diào)各個(gè)線程,以保證合理的使用資源。
我們可以把它簡(jiǎn)單的理解成我們停車場(chǎng)入口立著的那個(gè)顯示屏,每當(dāng)有一輛車進(jìn)入停車場(chǎng)顯示屏就會(huì)顯示剩余車位減1,每有一輛車從停車場(chǎng)出去,顯示屏上顯示的剩余車輛就會(huì)加1,當(dāng)顯示屏上的剩余車位為0時(shí),停車場(chǎng)入口的欄桿就不會(huì)再打開,車輛就無(wú)法進(jìn)入停車場(chǎng)了,直到有一輛車從停車場(chǎng)出去為止。
32.1 Semaphore使用demo
我們就以停車場(chǎng)的例子,來(lái)實(shí)現(xiàn)demo。
假設(shè)停車場(chǎng)最多可以停20輛車,現(xiàn)在有100輛要進(jìn)入停車場(chǎng)。
我們很容易寫出以下代碼;
?
?
public?class?SemaphoreTest?{ ????private??static?Semaphore?semaphore=new?Semaphore(20); ????public?static?void?main(String[]?args)?{ ????????? ????????ExecutorService?executorService=?Executors.newFixedThreadPool(200); ????????//模擬100輛車要來(lái) ????????for?(int?i?=?0;?i?100;?i++)?{ ????????????executorService.execute(()->{ ????????????????System.out.println("===="+Thread.currentThread().getName()+"準(zhǔn)備進(jìn)入停車場(chǎng)=="); ????????????????//車位判斷 ????????????????if?(semaphore.availablePermits()?==?0)?{ ????????????????????System.out.println("車輛不足,請(qǐng)耐心等待"); ????????????????} ????????????????try?{ ????????????????????//獲取令牌嘗試進(jìn)入停車場(chǎng) ????????????????????semaphore.acquire(); ????????????????????System.out.println("===="?+?Thread.currentThread().getName()?+?"成功進(jìn)入停車場(chǎng)"); ????????????????????//模擬車輛在停車場(chǎng)停留的時(shí)間 ????????????????????Thread.sleep(new?Random().nextInt(20000)); ????????????????????System.out.println("===="?+?Thread.currentThread().getName()?+?"駛出停車場(chǎng)"); ????????????????????//釋放令牌,騰出停車場(chǎng)車位 ????????????????????semaphore.release(); ?????????????????}?catch?(InterruptedException?e)?{ ????????????????????e.printStackTrace(); ????????????????} ????????????}); ????????????//線程池關(guān)閉?????????? ????????????executorService.shutdown(); ????????} ????} }
?
?
32.2 Semaphore原理
我們來(lái)看下實(shí)現(xiàn)的原理是怎樣的。
Semaphore構(gòu)造函數(shù)
可用令牌數(shù)
獲取令牌
釋放令牌
Semaphore構(gòu)造函數(shù)
?
?
Semaphore?semaphore=new?Semaphore(20);
?
?
它會(huì)創(chuàng)建一個(gè)非公平的鎖的同步阻塞隊(duì)列,并且把初始令牌數(shù)量(20)賦值給同步隊(duì)列的state,這個(gè)state就是AQS的哈。
?
?
?//構(gòu)造函數(shù),創(chuàng)建一個(gè)非公平的鎖的同步阻塞隊(duì)列 ?public?Semaphore(int?permits)?{ ????sync?=?new?NonfairSync(permits); } ???? NonfairSync(int?permits)?{ ????super(permits); } //把令牌數(shù)量賦值給同步隊(duì)列的state Sync(int?permits)?{ ????setState(permits); }
?
?
2.可用令牌數(shù)
這個(gè)availablePermits,獲取的就是state值。剛開始為20,所以肯定不會(huì)為0嘛。
?
?
semaphore.availablePermits(); public?int?availablePermits()?{ ??return?sync.getPermits(); } final?int?getPermits()?{ ??return?getState(); }
?
?
獲取令牌
接著我們?cè)倏聪芦@取令牌的API
?
?
semaphore.acquire();
?
?
獲取1個(gè)令牌
?
?
??public?void?acquire()?throws?InterruptedException?{ ????sync.acquireSharedInterruptibly(1); ??} ?? ????public?final?void?acquireSharedInterruptibly(int?arg) ????????throws?InterruptedException?{ ????if?(Thread.interrupted()) ????????throw?new?InterruptedException(); ??????//嘗試獲取令牌,arg為獲取令牌個(gè)數(shù) ????if?(tryAcquireShared(arg)?0) ????????// ????????doAcquireSharedInterruptibly(arg); ??}
?
?
嘗試獲取令牌,使用了CAS算法。
?
?
final?int?nonfairTryAcquireShared(int?acquires)?{ ????????????for?(;;)?{ ????????????????int?available?=?getState(); ????????????????int?remaining?=?available?-?acquires; ????????????????if?(remaining?0?|| ????????????????????compareAndSetState(available,?remaining)) ????????????????????return?remaining; ????????????} ????????}
?
?
可獲取令牌的話,就創(chuàng)建節(jié)點(diǎn),加入阻塞隊(duì)列;重雙向鏈表的head,tail節(jié)點(diǎn)關(guān)系,清空無(wú)效節(jié)點(diǎn);掛起當(dāng)前節(jié)點(diǎn)線程
?
?
????private?void?doAcquireSharedInterruptibly(int?arg) ????????throws?InterruptedException?{ ????????//創(chuàng)建節(jié)點(diǎn)加入阻塞隊(duì)列 ????????final?Node?node?=?addWaiter(Node.SHARED); ????????boolean?failed?=?true; ????????try?{ ????????????for?(;;)?{ ????????????????final?Node?p?=?node.predecessor(); ????????????????if?(p?==?head)?{ ????????????????????//返回鎖的state ????????????????????int?r?=?tryAcquireShared(arg); ????????????????????if?(r?>=?0)?{ ????????????????????????setHeadAndPropagate(node,?r); ????????????????????????p.next?=?null;?//?help?GC ????????????????????????failed?=?false; ????????????????????????return; ????????????????????} ????????????????} ????????????????//重組雙向鏈表,清空無(wú)效節(jié)點(diǎn),掛起當(dāng)前線程 ????????????????if?(shouldParkAfterFailedAcquire(p,?node)?&& ????????????????????parkAndCheckInterrupt()) ????????????????????throw?new?InterruptedException(); ????????????} ????????}?finally?{ ????????????if?(failed) ????????????????cancelAcquire(node); ????????} ????}
?
?
釋放令牌
?
?
?semaphore.release(); ? ??/** ?????*?釋放令牌 ?????*/ public?void?release()?{ ????sync.releaseShared(1); } ??public?final?boolean?releaseShared(int?arg)?{ ?????????//釋放共享鎖 ????????if?(tryReleaseShared(arg))?{ ????????????//喚醒所有共享節(jié)點(diǎn)線程 ????????????doReleaseShared(); ????????????return?true; ????????} ????????return?false; ????}
?
?
3 synchronized做了哪些優(yōu)化?什么是偏向鎖?什么是自旋鎖?鎖租化?
在JDK1.6之前,synchronized的實(shí)現(xiàn)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級(jí)鎖。從JDK6開始,HotSpot虛擬機(jī)開發(fā)團(tuán)隊(duì)對(duì)Java中的鎖進(jìn)行優(yōu)化,如增加了適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等優(yōu)化策略 ,提升了synchronized的性能。
偏向鎖:在無(wú)競(jìng)爭(zhēng)的情況下,只是在Mark Word里存儲(chǔ)當(dāng)前線程指針,CAS操作都不做。
輕量級(jí)鎖:在沒(méi)有多線程競(jìng)爭(zhēng)時(shí),相對(duì)重量級(jí)鎖,減少操作系統(tǒng)互斥量帶來(lái)的性能消耗。但是,如果存在鎖競(jìng)爭(zhēng),除了互斥量本身開銷,還額外有CAS操作的開銷。
自旋鎖:減少不必要的CPU上下文切換。在輕量級(jí)鎖升級(jí)為重量級(jí)鎖時(shí),就使用了自旋加鎖的方式
鎖粗化:將多個(gè)連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個(gè)范圍更大的鎖。
鎖消除:虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。
34 什么是上下文切換?
什么是CPU上下文?
CPU 寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計(jì)數(shù)器,則是用來(lái)存儲(chǔ) CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境,因此叫做CPU上下文。
什么是CPU上下文切換?
它是指,先把前一個(gè)任務(wù)的CPU上下文(也就是CPU寄存器和程序計(jì)數(shù)器)保存起來(lái),然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。
一般我們說(shuō)的上下文切換 ,就是指內(nèi)核(操作系統(tǒng)的核心)在CPU上對(duì)進(jìn)程或者線程進(jìn)行切換 。進(jìn)程從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過(guò)系統(tǒng)調(diào)用來(lái)完成。系統(tǒng)調(diào)用的過(guò)程,會(huì)發(fā)生CPU上下文的切換。
所以大家有時(shí)候會(huì)聽到這種說(shuō)法,線程的上下文切換 。它指,CPU資源的分配采用了時(shí)間片輪轉(zhuǎn) ,即給每個(gè)線程分配一個(gè)時(shí)間片,線程在時(shí)間片內(nèi)占用 CPU 執(zhí)行任務(wù)。當(dāng)線程使用完時(shí)間片后,就會(huì)處于就緒狀態(tài)并讓出 CPU 讓其他線程占用,這就是線程的上下文切換。看個(gè)圖,可能會(huì)更容易理解一點(diǎn)
35.為什么wait(),notify(),notifyAll()在對(duì)象中,而不在Thread類中
鎖只是個(gè)一個(gè)標(biāo)記,存在對(duì)象頭里面。
下面從面向?qū)ο蠛陀^察者模式角度來(lái)分析。
面向?qū)ο蟮慕嵌龋何覀兛梢园褀ait和notify直接理解為get和set方法。wait和notify方法都是對(duì)對(duì)象的鎖進(jìn)行操作,那么自然這些方法應(yīng)該屬于對(duì)象。舉例來(lái)說(shuō),門對(duì)象上有鎖屬性,開鎖和關(guān)鎖的方法應(yīng)該屬于門對(duì)象,而不應(yīng)該屬于人對(duì)象。
從觀察者模式的角度:對(duì)象是被觀察者,線程是觀察者。被觀察者的狀態(tài)如果發(fā)生變化,理應(yīng)有被觀察者去輪詢通知觀察者,否則的話,觀察者怎么知道notify方法應(yīng)該在哪個(gè)時(shí)刻調(diào)用?n個(gè)觀察者的notify又如何做到同時(shí)調(diào)用?
來(lái)源:知乎 ?https://www.zhihu.com/question/321674476
36. 線程池中 submit()和 execute()方法有什么區(qū)別?
execute和submit都屬于線程池的方法,execute只能提交Runnable類型的任務(wù),而submit既能提交Runnable類型任務(wù)也能提交Callable類型任務(wù)。
execute會(huì)直接拋出任務(wù)執(zhí)行時(shí)的異常,submit會(huì)吃掉異常,可通過(guò)Future的get方法將任務(wù)執(zhí)行時(shí)的異常重新拋出。
execute所屬頂層接口是Executor,submit所屬頂層接口是ExecutorService,實(shí)現(xiàn)類ThreadPoolExecutor重寫了execute方法,抽象類AbstractExecutorService重寫了submit方法。
37 AtomicInteger 的原理?
AtomicInteger的底層,是基于CAS實(shí)現(xiàn)的。我們可以看下AtomicInteger的添加方法。如下
?
?
????public?final?int?getAndIncrement()?{ ????????return?unsafe.getAndAddInt(this,?valueOffset,?1); ????} ????通過(guò)Unsafe類的實(shí)例來(lái)進(jìn)行添加操作 ????public?final?int?getAndAddInt(Object?var1,?long?var2,?int?var4)?{ ????????int?var5; ????????do?{ ????????????var5?=?this.getIntVolatile(var1,?var2); ????????}?while(!this.compareAndSwapInt(var1,?var2,?var5,?var5?+?var4));//使用了CAS算法實(shí)現(xiàn) ????????return?var5; ????}
?
?
注意:compareAndSwapInt是一個(gè)native方法哈,它是基于CAS來(lái)操作int類型的變量。并且,其它的原子操作類基本也大同小異。
38 Java中用到的線程調(diào)度算法是什么?
我們知道有兩種調(diào)度模型:分時(shí)調(diào)度和搶占式調(diào)度 。
分時(shí)調(diào)度模型:讓所有的線程輪流獲得cpu的使用權(quán),并且平均分配每個(gè)線程占用的 CPU 的時(shí)間片。
搶占式調(diào)度:優(yōu)先讓可運(yùn)行池中優(yōu)先級(jí)高的線程占用CPU,如果可運(yùn)行池中的線程優(yōu)先級(jí)相同,那么就隨機(jī)選擇一個(gè)線程,使其占用CPU。處于運(yùn)行狀態(tài)的線程會(huì)一直運(yùn)行,直至它不得不放棄 CPU。
Java默認(rèn)的線程調(diào)度算法是搶占式。即線程用完CPU之后,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行。
39. shutdown() 和 shutdownNow()的區(qū)別
shutdownNow()能立即停止線程池,正在跑的和正在等待的任務(wù)都停下了。這樣做立即生效,但是風(fēng)險(xiǎn)也比較大。
shutdown()只是關(guān)閉了提交通道,用submit()是無(wú)效的;而內(nèi)部的任務(wù)該怎么跑還是怎么跑,跑完再?gòu)氐淄V咕€程池。
40 說(shuō)說(shuō)幾種常見的線程池及使用場(chǎng)景?
幾種常用線程池:
newFixedThreadPool (固定數(shù)目線程的線程池)
newCachedThreadPool(可緩存線程的線程池)
newSingleThreadExecutor(單線程的線程池)
newScheduledThreadPool(定時(shí)及周期執(zhí)行的線程池)
40.1 newFixedThreadPool
?
?
??public?static?ExecutorService?newFixedThreadPool(int?nThreads,?ThreadFactory?threadFactory)?{ ????????return?new?ThreadPoolExecutor(nThreads,?nThreads, ??????????????????????????????????????0L,?TimeUnit.MILLISECONDS, ??????????????????????????????????????new?LinkedBlockingQueue(), ??????????????????????????????????????threadFactory); ????}
?
?
核心線程數(shù)和最大線程數(shù)大小一樣
沒(méi)有所謂的非空閑時(shí)間,即keepAliveTime為0
阻塞隊(duì)列為無(wú)界隊(duì)列LinkedBlockingQueue
使用場(chǎng)景
FixedThreadPool 適用于處理CPU密集型的任務(wù),確保CPU在長(zhǎng)期被工作線程使用的情況下,盡可能的少的分配線程,即適用執(zhí)行長(zhǎng)期的任務(wù)。
40.2 newCachedThreadPool
?
?
?public?static?ExecutorService?newCachedThreadPool(ThreadFactory?threadFactory)?{ ????????return?new?ThreadPoolExecutor(0,?Integer.MAX_VALUE, ??????????????????????????????????????60L,?TimeUnit.SECONDS, ??????????????????????????????????????new?SynchronousQueue(), ??????????????????????????????????????threadFactory); ????}
?
?
核心線程數(shù)為0
最大線程數(shù)為Integer.MAX_VALUE
阻塞隊(duì)列是SynchronousQueue
非核心線程空閑存活時(shí)間為60秒
使用場(chǎng)景
當(dāng)提交任務(wù)的速度大于處理任務(wù)的速度時(shí),每次提交一個(gè)任務(wù),就必然會(huì)創(chuàng)建一個(gè)線程。極端情況下會(huì)創(chuàng)建過(guò)多的線程,耗盡 CPU 和內(nèi)存資源。由于空閑 60 秒的線程會(huì)被終止,長(zhǎng)時(shí)間保持空閑的 CachedThreadPool 不會(huì)占用任何資源。
40.3 newSingleThreadExecutor 單線程的線程池
?
?
??public?static?ExecutorService?newSingleThreadExecutor(ThreadFactory?threadFactory)?{ ????????return?new?FinalizableDelegatedExecutorService ????????????(new?ThreadPoolExecutor(1,?1, ????????????????????????????????????0L,?TimeUnit.MILLISECONDS, ????????????????????????????????????new?LinkedBlockingQueue(), ????????????????????????????????????threadFactory)); ????}
?
?
核心線程數(shù)為1
最大線程數(shù)也為1
阻塞隊(duì)列是LinkedBlockingQueue
keepAliveTime為0
使用場(chǎng)景
適用于串行執(zhí)行任務(wù)的場(chǎng)景,一個(gè)任務(wù)一個(gè)任務(wù)地執(zhí)行。
40.4 newScheduledThreadPool
?
?
??public?ScheduledThreadPoolExecutor(int?corePoolSize)?{ ????????super(corePoolSize,?Integer.MAX_VALUE,?0,?NANOSECONDS, ??????????????new?DelayedWorkQueue()); ????}
?
?
最大線程數(shù)為Integer.MAX_VALUE
阻塞隊(duì)列是DelayedWorkQueue
keepAliveTime為0
scheduleAtFixedRate() :按某種速率周期執(zhí)行
scheduleWithFixedDelay():在某個(gè)延遲后執(zhí)行
使用場(chǎng)景
周期性執(zhí)行任務(wù)的場(chǎng)景,需要限制線程數(shù)量的場(chǎng)景
41 什么是FutureTask
FutureTask是一種可以取消的異步的計(jì)算任務(wù)。它的計(jì)算是通過(guò)Callable實(shí)現(xiàn)的,可以把它理解為是可以返回結(jié)果的Runnable。
使用FutureTask的優(yōu)點(diǎn):
可以獲取線程執(zhí)行后的返回結(jié)果;
提供了超時(shí)控制功能。
它實(shí)現(xiàn)了Runnable接口和Future接口,底層基于生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)。
FutureTask用于在異步操作場(chǎng)景中,F(xiàn)utureTask作為生產(chǎn)者(執(zhí)行FutureTask的線程)和消費(fèi)者(獲取FutureTask結(jié)果的線程)的橋梁,如果生產(chǎn)者先生產(chǎn)出了數(shù)據(jù),那么消費(fèi)者get時(shí)能會(huì)直接拿到結(jié)果;如果生產(chǎn)者還未產(chǎn)生數(shù)據(jù),那么get時(shí)會(huì)一直阻塞或者超時(shí)阻塞,一直到生產(chǎn)者產(chǎn)生數(shù)據(jù)喚醒阻塞的消費(fèi)者為止。
42 java中interrupt(),interrupted()和 isInterrupted()的區(qū)別
interrupt 它是真正觸發(fā)中斷的方法。
interrupted是Thread中的一個(gè)類方法,它也調(diào)用了isInterrupted(true)方法,不過(guò)它傳遞的參數(shù)是true,表示將會(huì)清除中斷標(biāo)志位。
isInterrupted是Thread類中的一個(gè)實(shí)例方法,可以判斷實(shí)例線程是否被中斷。。
?
?
????public?void?interrupt()?{ ????????if?(this?!=?Thread.currentThread()) ????????????checkAccess(); ????????synchronized?(blockerLock)?{ ????????????Interruptible?b?=?blocker; ????????????if?(b?!=?null)?{ ????????????????interrupt0();???????????//?Just?to?set?the?interrupt?flag ????????????????b.interrupt(this); ????????????????return; ????????????} ????????} ????????interrupt0(); ????} ???? ???public?static?boolean?interrupted()?{ ????????return?currentThread().isInterrupted(true); ????} ????public?boolean?isInterrupted()?{ ????????return?isInterrupted(false); ????}
?
?
43 有三個(gè)線程T1,T2,T3, 怎么確保它們按順序執(zhí)行
可以使用join方法解決這個(gè)問(wèn)題。比如在線程A中,調(diào)用線程B的join方法表示的意思就是 :A等待B線程執(zhí)行完畢后(釋放CPU執(zhí)行權(quán)),在繼續(xù)執(zhí)行。
代碼如下:
?
?
public?class?ThreadTest?{ ????public?static?void?main(String[]?args)?{ ????????Thread?spring?=?new?Thread(new?SeasonThreadTask("春天")); ????????Thread?summer?=?new?Thread(new?SeasonThreadTask("夏天")); ????????Thread?autumn?=?new?Thread(new?SeasonThreadTask("秋天")); ????????try ????????{ ????????????//春天線程先啟動(dòng) ????????????spring.start(); ????????????//主線程等待線程spring執(zhí)行完,再往下執(zhí)行 ????????????spring.join(); ????????????//夏天線程再啟動(dòng) ????????????summer.start(); ????????????//主線程等待線程summer執(zhí)行完,再往下執(zhí)行 ????????????summer.join(); ????????????//秋天線程最后啟動(dòng) ????????????autumn.start(); ????????????//主線程等待線程autumn執(zhí)行完,再往下執(zhí)行 ????????????autumn.join(); ????????}?catch?(InterruptedException?e) ????????{ ????????????e.printStackTrace(); ????????} ????} } class?SeasonThreadTask?implements?Runnable{ ????private?String?name; ????public?SeasonThreadTask(String?name){ ????????this.name?=?name; ????} ????@Override ????public?void?run()?{ ????????for?(int?i?=?1;?i?<4;?i++)?{ ????????????System.out.println(this.name?+?"來(lái)了:?"?+?i?+?"次"); ????????????try?{ ????????????????Thread.sleep(100); ????????????}?catch?(InterruptedException?e)?{ ????????????????e.printStackTrace(); ????????????} ????????} ????} } 運(yùn)行結(jié)果: 春天來(lái)了:?1次 春天來(lái)了:?2次 春天來(lái)了:?3次 夏天來(lái)了:?1次 夏天來(lái)了:?2次 夏天來(lái)了:?3次 秋天來(lái)了:?1次 秋天來(lái)了:?2次 秋天來(lái)了:?3次
?
?
44 有哪些阻塞隊(duì)列
ArrayBlockingQueue ? ?一個(gè)由數(shù)組構(gòu)成的有界阻塞隊(duì)列
LinkedBlockingQueue ? ?一個(gè)由鏈表構(gòu)成的有界阻塞隊(duì)列
PriorityBlockingQueue ? 一個(gè)支持優(yōu)先級(jí)排序的無(wú)界阻塞隊(duì)列
DelayQueue ? ? ? ?一個(gè)使用優(yōu)先隊(duì)列實(shí)現(xiàn)的無(wú)界阻塞隊(duì)列。
SynchroniouQueue ? ? 一個(gè)不儲(chǔ)存元素的阻塞隊(duì)列
LinkedTransferQueue ? ?一個(gè)由鏈表結(jié)構(gòu)組成的無(wú)界阻塞隊(duì)列
LinkedBlockingDeque ? ?一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列
45 Java 中 ConcurrentHashMap 的并發(fā)度是什么?
并發(fā)度就是segment的個(gè)數(shù),通常是2的N次方。默認(rèn)是16
46 Java線程有哪些常用的調(diào)度方法?
46.1 線程休眠
Thread.sleep(long)方法,使線程轉(zhuǎn)到超時(shí)等待阻塞(TIMED_WAITING) 狀態(tài)。long參數(shù)設(shè)定睡眠的時(shí)間,以毫秒為單位。當(dāng)睡眠結(jié)束后,線程自動(dòng)轉(zhuǎn)為就緒(Runnable)狀態(tài)。
46.2 線程中斷
interrupt()表示中斷線程。需要注意的是,InterruptedException是線程自己從內(nèi)部拋出的,并不是interrupt()方法拋出的。對(duì)某一線程調(diào)用interrupt()時(shí),如果該線程正在執(zhí)行普通的代碼,那么該線程根本就不會(huì)拋出InterruptedException。但是,一旦該線程進(jìn)入到wait()/sleep()/join()后,就會(huì)立刻拋出InterruptedException。可以用isInterrupted()來(lái)獲取狀態(tài)。
46.3 線程等待
Object類中的wait()方法,會(huì)導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的notify()方法或notifyAll()喚醒方法。
46.4 線程讓步
Thread.yield()方法,暫停當(dāng)前正在執(zhí)行的線程對(duì)象,把執(zhí)行機(jī)會(huì)讓給相同或者更高優(yōu)先級(jí)的線程。
46.5 線程通知
Object的notify()方法,喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此對(duì)象上等待,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的,并在對(duì)實(shí)現(xiàn)做出決定時(shí)發(fā)生。
notifyAll(),則是喚醒在此對(duì)象監(jiān)視器上等待的所有線程。
47. ReentrantLock的加鎖原理
ReentrantLock,是可重入鎖,是JDK5中添加在并發(fā)包下的一個(gè)高性能的工具。它支持同一個(gè)線程在未釋放鎖的情況下重復(fù)獲取鎖。
47.1 ReentrantLock使用的模板
我們先來(lái)看下是ReentrantLock使用的模板:
?
?
???//實(shí)例化對(duì)象 ????ReentrantLock?lock?=?new?ReentrantLock(); ????//獲取鎖操作 ????lock.lock(); ????try?{ ????????//?執(zhí)行業(yè)務(wù)代碼邏輯 ????}?catch?(Exception?ex)?{ ????????//異常處理 ????}?finally?{ ????????//?解鎖操作 ????????lock.unlock(); ????}
?
?
47.2 什么是非公平鎖,什么是公平鎖?
ReentrantLock無(wú)參構(gòu)造函數(shù),默認(rèn)創(chuàng)建的是非公平鎖 ,如下:
?
?
public?ReentrantLock()?{ ????sync?=?new?NonfairSync(); }
?
?
而通過(guò)fair參數(shù)指定使用公平鎖(FairSync)還是非公平鎖(NonfairSync)
?
?
public?ReentrantLock(boolean?fair)?{ ????sync?=?fair???new?FairSync()?:?new?NonfairSync(); }
?
?
什么是公平鎖?
公平鎖:多個(gè)線程按照申請(qǐng)鎖的順序去獲得鎖,線程會(huì)直接進(jìn)入隊(duì)列去排隊(duì),永遠(yuǎn)都是隊(duì)列的第一位才能得到鎖。
優(yōu)點(diǎn):所有的線程都能得到資源,不會(huì)餓死在隊(duì)列中。
缺點(diǎn):吞吐量會(huì)下降很多,隊(duì)列里面除了第一個(gè)線程,其他的線程都會(huì)阻塞,cpu喚醒阻塞線程的開銷會(huì)很大。
什么是非公平鎖?
非公平鎖:多個(gè)線程去獲取鎖的時(shí)候,會(huì)直接去嘗試獲取,獲取不到,再去進(jìn)入等待隊(duì)列,如果能獲取到,就直接獲取到鎖。
優(yōu)點(diǎn):可以減少CPU喚醒線程的開銷,整體的吞吐效率會(huì)高點(diǎn),CPU也不必取喚醒所有線程,會(huì)減少喚起線程的數(shù)量。
缺點(diǎn):你們可能也發(fā)現(xiàn)了,這樣可能導(dǎo)致隊(duì)列中間的線程一直獲取不到鎖或者長(zhǎng)時(shí)間獲取不到鎖,導(dǎo)致餓死。
47.3 lock()加鎖流程
大家可以結(jié)合AQS + 公平鎖/非公平鎖 + CAS去講ReentrantLock的原理哈。
48. 線程間的通訊方式
48.1 volatile和synchronized關(guān)鍵字
volatile關(guān)鍵字用來(lái)修飾共享變量,保證了共享變量的可見性,任何線程需要讀取時(shí)都要到內(nèi)存中讀取(確保獲得最新值)。
synchronized關(guān)鍵字確保只能同時(shí)有一個(gè)線程訪問(wèn)方法或者變量,保證了線程訪問(wèn)的可見性和排他性。
48.2 等待/通知機(jī)制
等待/通知機(jī)制,是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B 調(diào)用了對(duì)象O的notify()或者notifyAll()方法,線程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而 執(zhí)行后續(xù)操作。
48.3 管道輸入/輸出流
管道輸入/輸出流和普通的文件輸入/輸出流或者網(wǎng)絡(luò)輸入/輸出流不同之處在于,它主要 用于線程之間的數(shù)據(jù)傳輸,而傳輸?shù)拿浇闉閮?nèi)存。
管道輸入/輸出流主要包括了如下4種具體實(shí)現(xiàn):PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向字節(jié),而后兩種面向字符。
48.4 join()方法
如果一個(gè)線程A執(zhí)行了thread.join()語(yǔ)句,其含義是:當(dāng)前線程A等待thread線程終止之后才 從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個(gè)具備超時(shí)特性的方法。這兩個(gè)超時(shí)方法表示,如果線程thread在給定的超時(shí) 時(shí)間里沒(méi)有終止,那么將會(huì)從該超時(shí)方法中返回。
48.5 ThreadLocal
ThreadLocal,即線程本地變量(每個(gè)線程都有自己唯一的一個(gè)哦),是一個(gè)以ThreadLocal對(duì)象為鍵、任意對(duì)象為值的存儲(chǔ)結(jié)構(gòu)。底層是一個(gè)ThreadLocalMap來(lái)存儲(chǔ)信息,key是弱引用,value是強(qiáng)引用,所以使用完畢后要及時(shí)清理(尤其使用線程池時(shí))。
49 ?寫出3條你遵循的多線程最佳實(shí)踐
多用同步類,少用wait,notify
少用鎖,應(yīng)當(dāng)縮小同步范圍
給線程一個(gè)自己的名字
多用并發(fā)集合少用同步集合
50. 為什么阿里發(fā)布的 Java開發(fā)手冊(cè)中強(qiáng)制線程池不允許使用 Executors 去創(chuàng)建?
這是因?yàn)椋琂DK開發(fā)者提供了線程池的實(shí)現(xiàn)類都是有坑的,如newFixedThreadPool和newCachedThreadPool都有內(nèi)存泄漏的坑。
[1]Semaphore 使用及原理: https://zhuanlan.zhihu.com/p/98593407
[2]Java線程間通信方式講解: http://www.codebaoku.com/it-java/it-java-227064.html
編輯:黃飛
?
評(píng)論
查看更多