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