摘要:多線程能夠理解爲在同一個程序中可以同時運行多個不一樣的線程來執行不一樣的任務,這些線程能夠同時利用CPU的多個核心運行。
本文分享自華爲雲社區《對Java多線程的用法感到一頭亂麻?40個問題讓你快速掌握多線程的精髓》,原文做者:breakDraw 。html
多線程能夠理解爲在同一個程序中可以同時運行多個不一樣的線程來執行不一樣的任務,這些線程能夠同時利用CPU的多個核心運行。多線程編程可以最大限度的利用CPU的資源。本文將經過如下幾個方向爲你們講解多線程的用法。java
- 1.Thread類基礎
- 2.synchronized關鍵字
- 3.其餘的同步工具
- CountDownLatch
- FutureTask
- Semaphore
- CyclicBarrier
- Exchanger
- 原子類AtomicXXX
- 4.線程池
- 5.Thread狀態轉換
- 6.Volatile
- 7.線程羣組
1、Thread類基礎
Q: Thread的deprecated過時方法是哪3個?做用是啥
A:編程
- stop(), 終止線程的執行。
- suspend(), 暫停線程執行。
- resume(), 恢復線程執行。
Q: 廢棄stop的緣由是啥?
A:調用stop時,會直接終止線程並釋放線程上已鎖定的鎖,線程內部沒法感知,而且不會作線程內的catch操做!即線程內部不會處理stop後的爛攤子。若是其餘線程等在等着上面的鎖去取數據, 那麼拿到的多是1個半成品。數組
變成題目的話應該是下面這樣,問會輸出什麼?緩存
public class Test { public static void main(String[] args) throws InterruptedException { System.out.println("start"); Thread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.stop(); // thread.interrupt(); } } class MyThread extends Thread { public void run() { try { System.out.println("run"); Thread.sleep(5000); } catch (Exception e) { //處理爛攤子,清理資源 System.out.println("clear resource!"); } } }
答案是輸出 start和run,可是不會輸出clear resource安全
Q: stop的替代方法是什麼?
A: interrupt()。
調用thread.interrupt()終止時, 不會直接釋放鎖,可經過調用interrupt()或者捕捉sleep產生的中斷異常,來判斷是否被終止,並處理爛攤子。多線程
上題把thread.stop()改爲thread.interrupt(),在Thread.sleep()過程當中就會拋出interrupException(注意,InterrupExcetpion是sleep拋出的)所以就會輸出clear resource。若是沒有作sleep操做, 能夠用isInterrupted()來判斷本身這個線程是否被終止了,來作清理。併發
另外注意一下interrupt和isInterrupted的區別:異步
Q: suspend/resume的廢棄緣由是什麼?
A: :調用suspend不會釋放鎖。
若是線程A暫停後,他的resume是由線程B來調用的,可是線程B又依賴A裏的某個鎖,那麼就死鎖了。例以下面這個例子,就要知道會引起死鎖:ide
public class Test { public static Object lockObject = new Object(); public static void main(String[] args) throws InterruptedException { System.out.println("start"); Thread thread = new MyThread(); thread.start(); Thread.sleep(1000); System.out.println("主線程試圖佔用lockObject鎖資源"); synchronized (Test.lockObject) { // 用Test.lockObject作一些事 System.out.println("作一些事"); } System.out.println("恢復"); thread.resume(); } } class MyThread extends Thread { public void run() { try { synchronized (Test.lockObject) { System.out.println("佔用Test.lockObject"); suspend(); } System.out.println("MyThread釋放TestlockObject鎖資源"); } catch (Exception e){} } }
答案輸出
MyThread內部暫停後,外部的main由於無法拿到鎖,因此沒法執行後面的resume操做。
Q: 上題的suspend和resume能夠怎麼替換,來解決死鎖問題?
A: 能夠用wait和noitfy來處理(不過儘可能不要這樣設計,通常都是用run內部帶1個while循環的)
public class Test { public static Object lockObject = new Object(); //拿來作臨時鎖對象 public static void main(String[] args) throws InterruptedException { Thread thread = new MyThread(); thread.start(); Thread.sleep(1000); System.out.println("主線程試圖佔用lockObject鎖資源"); synchronized (Test.lockObject) { // 用Test.lockObject作一些事 System.out.println("作一些事"); } System.out.println("恢復"); synchronized (Test.lockObject) { Test.lockObject.notify(); } } } class MyThread extends Thread { public void run() { try { synchronized (Test.lockObject) { System.out.println("佔用Test.lockObject"); Test.lockObject.wait(); } System.out.println("MyThread釋放TestlockObject鎖資源"); } catch (Exception e){} } }
如此執行,結果正常:
Q: 下面這例子爲何會運行異常,拋出IllegalMonitorStateException錯誤?
public static void main(String[] args) throws InterruptedException { Thread thread = new MyThread(); thread.start(); thread.notify(); }
A: notify和wait的使用前提是必須持有這個對象的鎖, 即main代碼塊 須要先持有thread對象的鎖,才能使用notify去喚醒(wait同理)。
改爲下面就好了:
Thread thread = new MyThread(); thread.start(); synchronized (thread) { thread.notify(); }
Q: Thread.sleep()和Object.wait()的區別
A:sleep不會釋放對象鎖, 而wait會釋放對象鎖。
Q:Runnable接口和Callable的區別。
A: Callable能夠和Futrue配合,而且啓動線程時用的時call,可以拿到線程結束後的返回值,call方法還能拋出異常。
Q:thread.alive()表示線程當前是否處於活躍/可用狀態。
活躍狀態: 線程已經啓動且還沒有終止。線程處於正在運行或準備開始運行的狀態,就認爲線程是「存活的
thread.start()後,是否alive()必定返回true?
public class Main { public static void main(String[] args) { TestThread tt = new TestThread(); System.out.println("Begin == " + tt.isAlive()); tt.start(); System.out.println("end == " + tt.isAlive()); } }
A:不必定,有可能在打印時,線程已經運行結束了,或者start後,還未真正啓動起來(就是還沒進入到run中)
Q: 線程A以下:
public class A extends Thread { @Override public void run() { System.out.println("this.isAlive()=" + this.isAlive()); } }
把線程A做爲構造參數,傳給線程B
A a = new A(); Thread b = new Thread(a); b.start()
此時會打印什麼?
A:此時會打印false!
由於把a做爲構造參數傳入b中, b執行start時, 其實是在B線程中去調用了 A對象的run方法,而不是啓用了A線程。
若是改爲
A a = new A(); a.start()
那麼就會打印true了
Q:把FutureTask放進Thread中,並start後,會正常執行callable裏的內容嗎?
public static void main(String[] args) throws Exception { Callable<Integer> callable = () -> { System.out.println("call 100"); return 100; }; FutureTask<Integer> task = new FutureTask<>(callable); Thread thread = new Thread(task); thread.start(); }
A:能正常打印
2、synchronized關鍵字
- 便可做爲方法的修飾符,也能夠做爲代碼塊的修飾符
- 注意修飾方法時,並非這個方法上有鎖, 而是調用該方法時,須要取該方法所在對象上的鎖。
class A{ synchroized f(){ } }
即調用這個f(), 並非說f同一時刻只能進入一次,而是說進入f時,須要取到A上的鎖。
Q: 調用下面的f()時,會出現死鎖嗎?
class A{ synchroized f(){ t() } synchroized t(){ } }
A:不會。
1個線程內, 能夠重複進入1個對象的synchroized 塊。
- 原理:
當線程請求本身的鎖時。JVM會記下鎖的持有者,而且給這個鎖計數爲1。若是該線程再次請求本身的鎖,則能夠再次進入,計數爲2。退出時計數-1,直到所有退出時纔會釋放鎖。
Q:2個線程同時調用f1和f2會產生同步嗎?
class A{ private static synchronized void f1(){}; private synchronized void f2(){}; }
A:不會產生同步。兩者不是1個鎖。
f1是類鎖,等同於synchronized(A.class)
f2是對象鎖。
3、其餘的同步工具
CountDownLatch
final CountDownLatch latch = new CountDownLatch(2);
2是計數器初始值。
而後執行latch.await()時, 就會阻塞,直到其餘線程中把這個latch進行latch.countDown(),而且計數器下降至0。
- 和join的區別:
join阻塞時,是隻等待單個線程的完成
而CountDownLatch多是爲了等待多個線程
Q: countDownLatch的內部計數值能被重置嗎?
A:不能重置了。若是要從新計數必須從新new一個。畢竟他的類名就叫DownLatch
FutureTask
能夠理解爲一個支持有返回值的線程
FutureTask<Integer> task = new FutureTask<>(runable);
當調用task.get()時,就能能達到線程裏的返回值
Q:調用futrueTask.get()時,這個是阻塞方法嗎?若是是阻塞,何時會結束?
A:是阻塞方法。
- 線程跑完並返回結果
- 阻塞時間達到futrueTask.get(xxx)裏設定的xxx時間
- 線程出現異常InterruptedException或者ExecutionException
- 線程被取消,拋出CancellationException
Semaphore
信號量:就是操做系統裏常見的那個概念,java實現,用於各線程間進行資源協調。
用Semaphore(permits)構造一個包含permits個資源的信號量,而後某線程作了消費動做, 則執行semaphore.acquire(),則會消費一個資源,若是某線程作了生產動做,則執行semaphore.release(),則會釋放一個資源(即新增一個資源)
更詳細的信號量方法說明: https://blog.csdn.net/hanchao5272/article/details/79780045
Q: 信號量中,公平模式和非公平模式的區別?下面設成true就是公平模式
//new Semaphore(permits,fair):初始化許可證數量和是否公平模式的構造函數 semaphore = new Semaphore(5, true);
A:其實就是使用哪一種公平鎖仍是非公平鎖。
Java併發中的fairSync和NonfairSync主要區別爲:
- 若是當前線程不是鎖的佔有者,則NonfairSync並不判斷是否有等待隊列,直接使用compareAndSwap去進行鎖的佔用,即誰正好搶到,就給誰用!
- 若是當前線程不是鎖的佔有者,則FairSync則會判斷當前是否有等待隊列,若是有則將本身加到等待隊列尾,即嚴格的先到先得!
CyclicBarrier
柵欄,通常是在線程中去調用的。它的構造須要指定1個線程數量,和柵欄被破壞前要執行的操做,每當有1個線程調用barrier.await(),就會進入阻塞,同時barrier裏的線程計數-1。
當線程計數爲0時, 調用柵欄裏指定的那個操做後,而後破壞柵欄, 全部被阻塞在await上的線程繼續往下走。
Exchanger
我理解爲兩方柵欄,用於交換數據。
簡單說就是一個線程在完成必定的事務後,想與另外一個線程交換數據,則第一個先拿出數據的線程會一直等待第二個線程,直到第二個線程拿着數據到來時才能彼此交換對應數據。
原子類AtomicXXX
就是內部已實現了原子同步機制
Q:下面輸出什麼?(考察getAndAdd的用法)
AtomicInteger num = new AtomicInteger(1); System.out.println(num.getAndAdd(1)); System.out.println(num.get());
A:輸出一、2
顧名思義, getAndAdd(),那麼就是先get,再加, 相似於num++。
若是是addAndGet(),那麼就是++num
Q:AtomicReference和AtomicInteger的區別?
A:AtomicInteger是對整數的封裝,而AtomicReference則對應普通的對象引用。也就是它能夠保證你在修改對象引用時的線程安全性。便可能會有多個線程修改atomicReference裏包含的引用。
- 經典用法:
boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference)就是經典的CAS同步法
compreAndSet它會將將引用與預期值(引用)進行比較,若是它們相等,則在AtomicReference對象內設置一個新的引用。相似於一個非負責的自旋鎖。 - AtomicReferenceArray是原子數組, 能夠進行一些原子的數組操做例如 set(index, value),
java中已實現的所有原子類:
注意,沒有float,沒有short和byte。
4、線程池
Q: ThreadPoolExecutor線程池構造參數中,corePoolSize和maximumPoolSize有什麼區別?
A:當提交新線程到池中時
- 若是當前線程數 < corePoolSize,則會建立新線程
- 若是當前線程數=corePoolSize,則新線程被塞進一個隊列中等待。
- 若是隊列也被塞滿了,那麼又會開始新建線程來運行任務,避免任務阻塞或者丟棄
- 若是隊列滿了的狀況下, 線程總數超過了maxinumPoolSize,那麼就拋異常或者阻塞(取決於隊列性質)。
- 調用prestartCoreThread()可提早開啓一個空閒的核心線程
- 調用prestartAllCoreThreads(),可提早建立corePoolSize個核心線程。
Q: 線程池的keepalive參數是幹嗎的?
A:當線程數量在corePoolSize到maxinumPoolSize之間時, 若是有線程已跑完,且空閒時間超過keepalive時,則會被清除(注意只限於corePoolSize到maxinumPoolsize之間的線程)
Q: 線程池有哪三種隊列策略?
A:
- 握手隊列
至關於不排隊的隊列。可能形成線程數量無限增加直到超過maxinumPoolSize(至關於corePoolSize沒什麼用了,只以maxinumPoolSize作上限) - 無界隊列
隊列隊長無限,即線程數量達到corePoolSize時,後面的線程只會在隊列中等待。(至關於maxinumPoolSize沒什麼用了)
缺陷: 可能形成隊列無限增加以致於OOM - 有界隊列
Q: 線程池隊列已滿且maxinumPoolSize已滿時,有哪些拒絕策略?
A:
- AbortPolicy 默認策略:直接拋出RejectedExecutionException異常
- DiscardPolicy 丟棄策略: 直接丟了,什麼錯誤也不報
- DiscardOldestPolicy 丟棄隊頭策略: 即把最早入隊的人從隊頭扔出去,再嘗試讓該任務進入隊尾(隊頭任務心裏:不公平。。。。)
- CallerRunsPolicy 調用者處理策略: 交給調用者所在線程本身去跑任務(即誰調用的submit或者execute,他就本身去跑)
- 也能夠用實現自定義新的RejectedExecutionHandler
Q:有如下五種Executor提供的線程池,注意記憶一下他們的用途,就能理解內部的原理了。
- newCachedThreadPool: 緩存線程池
corePoolSize=0, maxinumPoolSize=+∞,隊列長度=0 ,所以線程數量會在corePoolSize到maxinumPoolSize之間一直靈活緩存和變更, 且不存在隊列等待的狀況,一來任務我就建立,用完了會釋放。
- newFixedThreadPool :定長線程池
corePoolSize= maxinumPoolSize=構造參數值, 隊列長度=+∞。所以不存在線程不夠時擴充的狀況 - newScheduledThreadPool :定時器線程池
提交定時任務用的,構造參數裏會帶定時器的間隔和單位。 其餘和FixedThreadPool相同,屬於定長線程池。 - newSingleThreadExecutor : 單線程池
corePoolSize=maxinumPoolSize=1, 隊列長度=+∞,只會跑一個任務, 因此其餘的任務都會在隊列中等待,所以會嚴格按照FIFO執行 - newWorkStealingPool(繼承自ForkJoinPool ): 並行線程池
若是你的任務執行時間很長,而且裏面的任務運行並行跑的,那麼他會把你的線程任務再細分到其餘的線程來分治。ForkJoinPool介紹: https://blog.csdn.net/m0_37542889/article/details/92640903
Q: submit和execute的區別是什麼?
A:
- execute只能接收Runnable類型的任務,而submit除了Runnable,還能接收Callable(Callable類型任務支持返回值)
- execute方法返回void, submit方法返回FutureTask。
- 異常方面, submit方法由於返回了futureTask對象,而當進行future.get()時,會把線程中的異常拋出,所以調用者能夠方便地處理異常。(若是是execute,只能用內部捕捉或者設置catchHandler)
Q:線程池中, shutdown、 shutdownNow、awaitTermination的區別?
A:
- shutdown: 中止接收新任務,等待全部池中已存在任務完成( 包括等待隊列中的線程 )。異步方法,即調用後立刻返回。
- shutdownNow: 中止接收新任務,並 中止全部正執行的task,返回還在隊列中的task列表 。
- awaitTermination: 僅僅是一個判斷方法,判斷當前線程池任務是否所有結束。通常用在shutdown後面,由於shutdown是異步方法,你須要知道何時才真正結束。
5、Thread狀態轉換
Q: 線程的6種狀態是:
A:
- New: 新建了線程,可是還沒調用start
- RUNNABLE: 運行, 就緒狀態包括在運行態中
- BLOCKED: 阻塞,通常是由於想拿鎖拿不到
- WAITING: 等待,通常是wait或者join以後
- TIMED_WAITING: 定時等待,即固定時間後可返回,通常是調用sleep或者wait(時間)的。
- TERMINATED: 終止狀態。
欣賞一幅好圖,能瞭解調用哪些方法會進入哪些狀態。
原圖連接
Q: java線程何時會進入阻塞(可能按多選題考):
A:
- sleep
- wati()掛起, 等待得到別的線程發送的Notify()消息
- 等待IO
- 等待鎖
6、Volatile
用volatile修飾成員變量時, 一旦有線程修改了變量,其餘線程可當即看到改變。
Q: 不用volatile修飾成員變量時, 爲何其餘線程會沒法當即看到改變?
A:線程能夠把變量保存在本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。
這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值。
Q: 用了volatile是否是就能夠不用加鎖啦?
A: 不行。
- 鎖並非只保證1個變量的互斥, 有時候是要保證幾個成員在連續變化時,讓其餘線程沒法干擾、讀取。
- 而volatile保證1個變量可變, 保證不了幾個變量同時變化時的原子性。
Q:展現一段《Java併發編程實戰》書裏的一個經典例子,在科目二考試裏也出現了,只是例子換了個皮。爲何下面這個例子可能會死循環,或者輸出0?
A:首先理解一下java重排序,能夠看一下這篇博文: https://www.cnblogs.com/coshaho/p/8093944.html
而後分析後面那2個奇怪的狀況是怎麼發生的。
- 永遠不輸出:
通過程序的指令排序,出現了這種狀況:
- ReaderThread在while裏讀取ready值, 此時是false, 因而存入了ReaderThread的寄存器。
- 主線程修改ready和number。
- ReaderThread沒有感知到ready的修改(對於ReaderThread線程,感知不到相關的指令,來讓他更新ready寄存器的值),所以進入死循環。
- 輸出0
通過程序的指令排序,出現了這種狀況:
1)主線程設置ready爲true
2)ReaderThread在while裏讀取ready值,是true,因而退出while循環
- ReaderThread讀取到number值, 此時number仍是初始化的值爲0,因而輸出0
- 主線程這時候才修改number=42,此時ReaderThread已經結束了!
上面這個問題,能夠用volatile或者加鎖。當你加了鎖時, 若是變量被寫了,會有指令去更新另外一個寄存器的值,所以就可見了。
7、線程羣組
爲了方便管理一批線程,咱們使用ThreadGroup來表示線程組,經過它對一批線程進行分類管理
使用方法:
Thread group = new ThreadGroup("group"); Thread thread = new Thread(gourp, ()->{..});
即thread除了Thread(Runable)這個構造方法外,還有個Thread(ThreadGroup, Runnable)構造方法
Q:在線程A中建立線程B, 他們屬於同一個線程組嗎
A:是的
線程組的一大做用是對同一個組線程進行統一的異常捕捉處理,避免每次新建線程時都要從新去setUncaghtExceptionHandler。即線程組自身能夠實現一個uncaughtException方法。
ThreadGroup group = new ThreadGroup("group") { @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println(thread.getName() + throwable.getMessage()); } }; }
線程若是拋出異常,且沒有在線程內部被捕捉,那麼此時線程異常的處理順序是什麼?相信不少人都看過下面這段話,好多講線程組的博客裏都這樣寫:
(1)首先看看當前線程組(ThreadGroup)有沒有父類的線程組,若是有,則使用父類的UncaughtException()方法。
(2)若是沒有,就看線程是否是調用setUncaughtExceptionHandler()方法創建Thread.setUncaughtExceptionHandler實例。若是創建,直接使用它的UncaughtException()方法處理異常。
(3)若是上述都不成立就看這個異常是否是ThreadDead實例,若是是,什麼都不作,若是不是,輸出堆棧追蹤信息(printStackTrace)。
來源:
https://blog.csdn.net/qq_43073128/article/details/90597006
https://blog.csdn.net/qq_43073128/article/details/88280469
好,別急着記,先看一下下面的題目,問輸出什麼:
Q:
// 父類線程組 static class GroupFather extends ThreadGroup { public GroupFather(String name) { super(name); } @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println("groupFather=" + throwable.getMessage()); } } public static void main(String[] args) { // 子類線程組 GroupFather groupSon = new GroupFather("groupSon") { @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println("groupSon=" + throwable.getMessage()); } }; Thread thread1 = new Thread(groupSon, ()->{ throw new RuntimeException("我異常了"); }); thread1.start(); }
A:一看(1),那是否是應該輸出groupFather?
錯錯錯,輸出的是groupSon這句話在不少地方能看到,但沒有去實踐過看過源碼的人就會這句話被誤導。實際上父線程組不是指類繼承關係上的線程組,而是指下面這樣的:
即指的是構造關係的有父子關係。若是子類的threadGroup沒有去實現uncaughtException方法,那麼就會去構造參數裏指定的父線程組去調用方法。
Q: 那我改爲構造關係上的父子關係,下面輸出什麼?
public static void main(String[] args) { // 父線程組 ThreadGroup groupFather = new ThreadGroup("groupFather") { @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println("groupFather=" + throwable.getMessage()); } }; // 子線程組,把groupFather做爲parent參數 ThreadGroup groupSon = new ThreadGroup(groupFather, "groupSon") { @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println("groupSon=" + throwable.getMessage()); } }; Thread thread1 = new Thread(groupSon, ()->{ throw new RuntimeException("我異常了"); }); thread1.start(); }
A:答案輸出
即只要子線程組有實現過,則會用子線程組裏的方法,而不是直接去找的父線程組!
Q:若是我讓本身作set捕捉器的操做呢?那下面這個輸出什麼?
public static void main(String[] args) { // 父線程組 ThreadGroup group = new ThreadGroup("group") { @Override public void uncaughtException(Thread thread, Throwable throwable) { System.out.println("group=" + throwable.getMessage()); } }; // 建一個線程,在線程組內 Thread thread1 = new Thread(group, () -> { throw new RuntimeException("我異常了"); }); // 本身設置setUncaughtExceptionHandler方法 thread1.setUncaughtExceptionHandler((t, e) -> { System.out.println("no gourp:" + e.getMessage()); }); thread1.start(); }
A:看以前的結論裏,彷佛是應該輸出線程組的異常?
可是結果卻輸出的是:
也就是說,若是線程對本身特意執行過setUncaughtExceptionHandler,那麼有優先對本身設置過的UncaughtExceptionHandler作處理。
那難道第(2)點這個是錯的嗎?確實錯了,實際上第二點應該指的是全局Thread的默認捕捉器,注意是全局的。實際上那段話出自ThreadGroup裏uncaughtException的源碼:
這裏就解釋了以前的那三點,可是該代碼中沒考慮線程自身設置了捕捉器
因此修改一下以前的總結一下線程的實際異常拋出判斷邏輯:
- 若是線程自身有進行過setUncaughtExceptionHandler,則使用本身設置的按個。
- 若是沒設置過,則看一下沒有線程組。並按照如下邏輯判斷:
若是線程組有覆寫過uncaughtException,則用覆寫過的uncaughtException
若是線程組沒有覆寫過,則去找父線程組(注意是構造體上的概念)的uncaughtException方法。 - 若是線程組以及父類都沒覆寫過uncaughtException, 則判斷是否用Thread.setDefaultUncaughtExceptionHandler(xxx)去設置全局的默認捕捉器,有的話則用全局默認
- 若是不是ThreadDeath線程, 則只打印堆棧。
- 若是是ThreadDeath線程,那麼就什麼也不處理。