您好,登錄后才能下訂單哦!
本篇內容主要講解“Java多線程的原理和用法”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java多線程的原理和用法”吧!
摘要:多線程可以理解為在同一個程序中能夠同時運行多個不同的線程來執行不同的任務,這些線程可以同時利用CPU的多個核心運行。
多線程可以理解為在同一個程序中能夠同時運行多個不同的線程來執行不同的任務,這些線程可以同時利用CPU的多個核心運行。多線程編程能夠最大限度的利用CPU的資源。本文將通過以下幾個方向為大家講解多線程的用法。
1.Thread類基礎
2.synchronized關鍵字
3.其他的同步工具
CountDownLatch
FutureTask
Semaphore
CyclicBarrier
Exchanger
原子類AtomicXXX
4.線程池
5.Thread狀態轉換
6.Volatile
7.線程群組
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里的某個鎖,那么就死鎖了。例如下面這個例子,就要知道會引發死鎖:
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:能正常打印
即可作為方法的修飾符,也可以作為代碼塊的修飾符
注意修飾方法時,并不是這個方法上有鎖, 而是調用該方法時,需要取該方法所在對象上的鎖。
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是對象鎖。
final CountDownLatch latch = new CountDownLatch(2);
2是計數器初始值。
然后執行latch.await()時, 就會阻塞,直到其他線程中把這個latch進行latch.countDown(),并且計數器降低至0。
和join的區別:
join阻塞時,是只等待單個線程的完成
而CountDownLatch可能是為了等待多個線程
Q: countDownLatch的內部計數值能被重置嗎?
A:不能重置了。如果要重新計數必須重新new一個。畢竟他的類名就叫DownLatch
可以理解為一個支持有返回值的線程
FutureTask<Integer> task = new FutureTask<>(runable);
當調用task.get()時,就能能達到線程里的返回值
Q:調用futrueTask.get()時,這個是阻塞方法嗎?如果是阻塞,什么時候會結束?
A:是阻塞方法。
線程跑完并返回結果
阻塞時間達到futrueTask.get(xxx)里設定的xxx時間
線程出現異常InterruptedException或者ExecutionException
線程被取消,拋出CancellationException
信號量:就是操作系統里常見的那個概念,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則會判斷當前是否有等待隊列,如果有則將自己加到等待隊列尾,即嚴格的先到先得!
柵欄,一般是在線程中去調用的。它的構造需要指定1個線程數量,和柵欄被破壞前要執行的操作,每當有1個線程調用barrier.await(),就會進入阻塞,同時barrier里的線程計數-1。
當線程計數為0時, 調用柵欄里指定的那個操作后,然后破壞柵欄, 所有被阻塞在await上的線程繼續往下走。
我理解為兩方柵欄,用于交換數據。
簡單說就是一個線程在完成一定的事務后,想與另一個線程交換數據,則第一個先拿出數據的線程會一直等待第二個線程,直到第二個線程拿著數據到來時才能彼此交換對應數據。
就是內部已實現了原子同步機制
Q:下面輸出什么?(考察getAndAdd的用法)
AtomicInteger num = new AtomicInteger(1); System.out.println(num.getAndAdd(1)); System.out.println(num.get());
A:輸出1、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。
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是異步方法,你需要知道什么時候才真正結束。
Q: 線程的6種狀態是:
A:
New: 新建了線程,但是還沒調用start
RUNNABLE: 運行, 就緒狀態包括在運行態中
BLOCKED: 阻塞,一般是因為想拿鎖拿不到
WAITING: 等待,一般是wait或者join之后
TIMED_WAITING: 定時等待,即固定時間后可返回,一般是調用sleep或者wait(時間)的。
TERMINATED: 終止狀態。
欣賞一幅好圖,能了解調用哪些方法會進入哪些狀態。
原圖鏈接
Q: java線程什么時候會進入阻塞(可能按多選題考):
A:
sleep
wati()掛起, 等待獲得別的線程發送的Notify()消息
等待IO
等待鎖
用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或者加鎖。當你加了鎖時, 如果變量被寫了,會有指令去更新另一個寄存器的值,因此就可見了。
為了方便管理一批線程,我們使用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線程,那么就什么也不處理。
到此,相信大家對“Java多線程的原理和用法”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。