java核心技术-多线程并发设计原理以及常见面试题
文章目錄
- 寫在前面
- 多線程回顧
- Thread和Runnable
- 面試官:為什么我們在項目中推薦使用使用Runnable方法而不用繼承Thread?
- 面試官:Callable為什么可以帶返回值,你知道底層原理?
- 面試題:線程了解?給我講講線程的幾種狀態(tài)?
- 面試題:你知道線程等待和阻塞的區(qū)別?
- 面試官:給我講講線程的生命周期?
- 面試官:如果我在代碼種連續(xù)調(diào)用兩次thread.start()會發(fā)生什么你知道?
- synchronized
- 聲:居然我們學(xué)習(xí)到鎖,給我講講鎖的本質(zhì)是什么?
- 聲:synchronized使用的幾種方式?
- 面試官:給我講講synchronized 實例鎖(Synchronized)和類鎖(Static Synchronized)有什么區(qū)別?
- 面試官:給我講講鎖是如何實現(xiàn)的?
- monitor enter
- monitor exit
- 面試題:synchronized拋出異常是如何保證能正常釋放鎖?
- 面試題:進(jìn)入synchronized獲取對象鎖后,調(diào)用Thread.sleep()方法會釋放鎖資源?
- wait和notify
- 筆試題:如何用wait和notify實現(xiàn)生產(chǎn)者消費者模式?
- 面試題:為什么wait()必須和synchronized一起使用?
- 面試題:為什么Java要把wait()和notify()放在如此Object類里面,而不是像sleep放在Thread中呢?
- 面試題:wait()的時候?qū)ο箧i會釋放鎖?
- 面試題:wait()和sleep()區(qū)別?
- interruptedException和interrupt()方法
- 聲:什么情況下拋出InterruptedException
- 面試官:給我說說輕量級阻塞和重量級阻塞
- 聲:你了解線程中斷后線程復(fù)位和被動復(fù)位?
- 聲:如何優(yōu)雅的關(guān)閉線程?
- 并發(fā)核心概念
- 并發(fā)與并行
- 同步
- 1.控制同步
- 面試題:如何控制多個線程執(zhí)行順序:給你三個線程如何順序打印數(shù)字?
- 2.數(shù)據(jù)訪問同步
- 不可變對象
- 面試官:String為什么設(shè)計成不可變對象?
- 原子操作和原子變量
- 并發(fā)問題
- 面試官:多線程場景會出現(xiàn)哪些并發(fā)問題?你項目中是如何解決的?
- 數(shù)據(jù)競爭
- 死鎖
- 面試題:什么叫死鎖?死鎖必須滿足哪些條件?如何定位死鎖問題?有哪些解決死鎖策略?
- 活鎖
- 資源不足
- JMM(java memory model)內(nèi)存模型
- 面試官:你知道JMM內(nèi)存模型、java內(nèi)存模型、jvm內(nèi)存模型區(qū)別是什么?
- 聲:什么是JMM?
- 我:聽著還是好復(fù)雜呀,那什么是內(nèi)存可見性?
- 我:什么是原子操作,我們應(yīng)該注意什么呢?
- 我:什么是指令重排序
- 面試官:給我講講jvm內(nèi)存模型以及jdk1.7和1.8版本有何區(qū)別?
- happen-before
- as if serial(串行)語義
- 面試官:什么是happen-before?
- voliate關(guān)鍵字
- final關(guān)鍵字
寫在前面
這是第一次嘗試用模擬對話體方式來敘述知識點,這樣換種方式來做筆記使我印象更深刻,也希望使讀者更容易理解(😊)!
多線程回顧
Thread和Runnable
我:這個使用還不簡單,我分分鐘就可以創(chuàng)建執(zhí)行線程給你看
1.繼承Thread
package net.dreamzuora;public class ThreadDemo extends Thread{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "threadImpl run");}}2.實現(xiàn)Runnable
package net.dreamzuora;public class RunnableDemo implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "runnableImpl run");} }3.帶返回值的Callable
package net.dreamzuora;import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit;public class CallableDemo implements Callable {@Overridepublic String call() throws Exception {TimeUnit.SECONDS.sleep(3);String p = Thread.currentThread().getName() + "callableImpl run";return p;} }測試方法:
@Testpublic void threadAndRunnable() throws ExecutionException, InterruptedException {new Thread(new RunnableDemo()).start();new Thread(new ThreadDemo()).start();//包裝返回對象FutureTask<String> futureTask = new FutureTask<String>(new CallableDemo());new Thread(futureTask).start();//同步阻塞String p = futureTask.get();System.out.println(p);}面試官:為什么我們在項目中推薦使用使用Runnable方法而不用繼承Thread?
1.Runnable 與 Thread 類的解耦
2.提高性能,不用繼承Thread每來個任務(wù)就必須創(chuàng)建一個線程,可以交給線程池提交任務(wù),而線程池有線程復(fù)用機(jī)制,可以一個線程執(zhí)行多個任務(wù)
3.從java語言特性分析:java單繼承多實現(xiàn),因此使用Runnable方式拓展性更強(qiáng),就比如利用FutureTask包裝Callable實現(xiàn)帶有返回值的任務(wù)。
面試官:你居然談到線程復(fù)用,那你可以給我講講線程復(fù)用原理?
面試官:Callable為什么可以帶返回值,你知道底層原理?
首先Callable接口只是定義了一個帶有返回值的方法,利用FutureTask包裝Callable返回對象,根據(jù)類繼承關(guān)系會發(fā)現(xiàn)FutureTask還是繼承了Runnable接口,只是對其進(jìn)行功能拓展。
jdk1.8的Future特性簡介及使用場景
聲:那你知道線程有哪些特征?
我:你說的那些東西,我工作當(dāng)中根本用不著有啥用?
聲:你忘記了你因為基礎(chǔ)薄弱被大廠虐的很慘?難道你不想進(jìn)阿里了?不想升職加薪了?
我:真香,我要好好學(xué)習(xí),那么什么是守護(hù)線程?main()主線程是守護(hù)線程?
聲:
main不是守護(hù)線程,首先我們來理解守護(hù)線程的概念,守護(hù)線程是程序運行后它默默的在背后提供服務(wù)支持,最典型的像GC的內(nèi)存回收保障程序正常運行,守護(hù)線程終止是被動的,只要非守護(hù)線程都退出之后守護(hù)線程就會被終止,非守護(hù)線程結(jié)束后意味著程序終止了。
我:我明白了,非守護(hù)線程(用戶線程)就是我們一般手動創(chuàng)建的線程,而守護(hù)線程一般都是java底層默認(rèn)提供的線程例如垃圾回收器、緩存管理器等用來執(zhí)行輔助任務(wù)的,那么我們怎么創(chuàng)建守護(hù)線程呢?
聲:你理解很到位,創(chuàng)建守護(hù)線程很簡單,Thread類給我們提供類 thread.setDaemon(true) 方法,但是你一般不會用它,如果我們使用守護(hù)線程進(jìn)行IO、業(yè)務(wù)邏輯操作,而它隨著用戶線程結(jié)束而結(jié)束,因此你無法保證服務(wù)正常執(zhí)行。
我:我現(xiàn)在明白線程等基本特征,那線程又有哪些狀態(tài)呢?
聲:切記,線程有6種狀態(tài),這個面試時候經(jīng)常會遇到,總共以下幾種:
New
new Thread() -> 新建狀態(tài)Runnable
new Thread().start() -> 執(zhí)行start()方法以后Runnable狀態(tài) 其實Runnable分為兩種狀態(tài),取決于獲取CPU分配時間片之前和之后兩種狀態(tài)1.就緒狀態(tài):等待操作系統(tǒng)分配CPU時間片段2.運行狀態(tài):獲取CPU分配時間片之后線程運行Blocked
高并發(fā)多線程場景下,執(zhí)行synchronied代碼塊,線程處于被阻塞狀態(tài)
Waiting
1.不帶時間參數(shù)的Object.wait() 2.不帶時間參數(shù)的Thread.join()不帶時間參數(shù) 3.LockSupport.park()方法聲:你知道LockSupport.park()方法?用在什么場景
我:不知道…
聲:剛才強(qiáng)調(diào)過,Blocked 僅僅針對 synchronized monitor 鎖,可是在 Java 中還有很多其他的鎖,比如 ReentrantLock,如果線程在獲取這種鎖時沒有搶到該鎖就會進(jìn)入 Waiting 狀態(tài),因為本質(zhì)上它執(zhí)行了 LockSupport.park() 方法,所以會進(jìn)入 Waiting 狀態(tài)。同樣,Object.wait() 和 Thread.join() 也會讓線程進(jìn)入 Waiting 狀態(tài)。
park函數(shù)作用
Java的LockSupport.park()實現(xiàn)分析
面試題:線程了解?給我講講線程的幾種狀態(tài)?
面試題:你知道線程等待和阻塞的區(qū)別?
Blocked 與 Waiting 的區(qū)別是 Blocked 在等待其他線程釋放 monitor 鎖,而 Waiting 則是在等待某個條件,比如 join 的線程執(zhí)行完畢,或者是 notify()/notifyAll() 。
Timed_waiting
1.設(shè)置了時間參數(shù)的 Thread.sleep(long millis) 方法; 2.設(shè)置了時間參數(shù)的 Object.wait(long timeout) 方法; 3.設(shè)置了時間參數(shù)的 Thread.join(long millis) 方法; 4.設(shè)置了時間參數(shù)的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。Terminated
再來看看最后一種狀態(tài),Terminated 終止?fàn)顟B(tài),要想進(jìn)入這個狀態(tài)有兩種可能。
1.run() 方法執(zhí)行完畢,線程正常退出。
2.出現(xiàn)一個沒有捕獲的異常,終止了 run() 方法,最終導(dǎo)致意外終止。
Thread類存儲線程狀態(tài)源碼:
Thread.State 類:
最后我們再看線程轉(zhuǎn)換的兩個注意點。
1.線程的狀態(tài)是需要按照箭頭方向來走的,比如線程從 New 狀態(tài)是不可以直接進(jìn)入 Blocked 狀態(tài)的,它需要先經(jīng)歷 Runnable 狀態(tài)。
2.線程生命周期不可逆:一旦進(jìn)入 Runnable 狀態(tài)就不能回到 New 狀態(tài);一旦被終止就不可能再有任何狀態(tài)的變化。所以一個線程只能有一次 New 和 Terminated 狀態(tài),只有處于中間狀態(tài)才可以相互轉(zhuǎn)換。
面試官:給我講講線程的生命周期?
我:可以參考我之前寫過的博客
面試官:如果我在代碼種連續(xù)調(diào)用兩次thread.start()會發(fā)生什么你知道?
synchronized
聲:居然我們學(xué)習(xí)到鎖,給我講講鎖的本質(zhì)是什么?
我:在多線程環(huán)境下訪問共享資源,這個資源可能是變量、對象、文件等,當(dāng)我們給資源加鎖以后,能夠保證一段時間只有一個線程去訪問,這樣就避免數(shù)據(jù)競爭問題。
聲:synchronized使用的幾種方式?
我:這個簡單,不就下面三種嗎?
面試官:給我講講synchronized 實例鎖(Synchronized)和類鎖(Static Synchronized)有什么區(qū)別?
如果synchronized作用在static方法中就是類鎖,類鎖是對該類下所有的對象實例都生效,而對象鎖只是針對實例加鎖
聲:那你知道synchronized對對象加鎖的原理?
首先我們可以把synchronized鎖理解成一個“對象”,居然是“對象”里面肯定有成員變量,其中“對象”有三個成員變量:
1.state:鎖是否被占用標(biāo)識符。
2.threadID:占用鎖的線程id。
3.threadIDList:保存正在等待、被阻塞獲取鎖的線程id,當(dāng)前占用鎖的線程被釋放以后,就從threadIdList中喚醒一個線程拿到鎖,循環(huán)往復(fù)。
synchronized(this)或者synchronized(obj)可以理解為就是將鎖“對象”
synchronized和加鎖的對象this/obj合二為一,怎么理解這句話呢?要訪問的共享資源是obj那么這把鎖就加載obj對象上,將鎖“對象”作為obj對象的成員變量,這樣意味著這個obj對象即是共享資源,同事具備鎖的“對象”
面試官:給我講講鎖是如何實現(xiàn)的?
在對象頭里有一塊數(shù)據(jù)叫Mark Word,其中包括兩個重要的字段:鎖標(biāo)志位、占用鎖的threadId。不同版本的JVM,對象頭的數(shù)據(jù)結(jié)構(gòu)不一樣。
我:你前面講的大白話我是聽懂了,但是我想知道同步代碼塊和同步方法是如何避免并發(fā)問題的?
聲:好,我先給你講講同步代碼塊實現(xiàn)原理
java代碼:
public class SynTest {public void synBlock() {synchronized (this) {System.out.println("dreamzuora");}} }讓我們看看匯編:javap -verbose SynTest.class
public void synBlock();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc #3 // String dreamzuora9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_113: monitorexit14: goto 2217: astore_218: aload_119: monitorexit20: aload_221: athrow22: return說到synchronized我們腦子立馬就想到monitor計數(shù)器、monitor enter、monitor exit,現(xiàn)在我結(jié)合上圖來回答怎么這三個直接是怎么結(jié)合使用的。
[畫圖能力不行,這個圖可能有些出入以下面文字為準(zhǔn)]
monitor enter
執(zhí)行 monitorenter 的線程嘗試獲得 monitor 的所有權(quán),會發(fā)生以下這三種情況之一:
a. 如果該 monitor 的計數(shù)為 0,則線程獲得該 monitor 并將其計數(shù)設(shè)置為 1。然后,該線程就是這個 monitor 的所有者。
b. 如果線程已經(jīng)擁有了這個 monitor ,則它將重新進(jìn)入,并且累加計數(shù)。
c. 如果其他線程已經(jīng)擁有了這個 monitor,那個這個線程就會被阻塞,直到這個 monitor 的計數(shù)變成為 0,代表這個 monitor 已經(jīng)被釋放了,于是當(dāng)前這個線程就會再次嘗試獲取這個 monitor。
monitor exit
monitorexit 的作用是將 monitor 的計數(shù)器減 1,直到減為 0 為止。代表這個 monitor 已經(jīng)被釋放了,已經(jīng)沒有任何線程擁有它了,也就代表著解鎖,所以,其他正在等待這個 monitor 的線程,此時便可以再次嘗試獲取這個 monitor 的所有權(quán)
面試題:synchronized拋出異常是如何保證能正常釋放鎖?
細(xì)心的同學(xué)能夠看到,反匯編后出現(xiàn)兩處monitorexit
13: monitorexit14: goto 2217: astore_218: aload_119: monitorexitJVM 要保證每個 monitorenter 必須有與之對應(yīng)的 monitorexit,monitorenter 指令被插入到同步代碼塊的開始位置,而 monitorexit 需要插入到方法正常結(jié)束處和異常處兩個地方,這樣就可以保證拋異常的情況下也能釋放鎖
我:那同步方法和同步代碼實現(xiàn)原理是一樣的?
同步方法:
同步方法匯編指令:
public synchronized void synMethod();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 16: 0聲:同步代碼塊和同步方法實現(xiàn)不一樣,同步方法并不是依靠 monitorenter 和 monitorexit 指令實現(xiàn)的,在同步方法中有一個ACC_SYNCHRONIZED標(biāo)記這個方法是同步方法,當(dāng)線程進(jìn)入該方法會先判斷該方法是否有這個標(biāo)記,如果有則需要先獲得 monitor 鎖,然后才能開始執(zhí)行方法,方法執(zhí)行之后再釋放 monitor 鎖。其他方面, synchronized 方法和剛才的 synchronized 代碼塊是很類似的,例如這時如果其他線程來請求執(zhí)行方法,也會因為無法獲得 monitor 鎖而被阻塞。
推薦一個大牛文章將synchronized底褲都扒了
面試題:進(jìn)入synchronized獲取對象鎖后,調(diào)用Thread.sleep()方法會釋放鎖資源?
我:調(diào)用Thread.sleep()后線程會出讓時間片段進(jìn)入WAITING狀態(tài),不會釋放鎖資源,代碼演示如下
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.concurrent.CountDownLatch;public class SynSleepDemo {Object obj = new Object();void a() {System.out.println("a:enter");synchronized (obj) {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("a:exit");}}void b() {System.out.println("b:enter");synchronized (obj) {System.out.println("B");}System.out.println("b:exit");}void c() {synchronized (obj) {System.out.println("C");}}@Testpublic void singleThread() throws InterruptedException {this.a();this.b();this.c();}@Testpublic void multiThread() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);new Thread(() -> a()).start();new Thread(() -> b()).start();new Thread(() -> c()).start();countDownLatch.await();}}控制臺輸出:
結(jié)論:可以看出三個線程同時只能有一個線程獲取對象鎖,而其中一個線程獲取鎖以后,并且執(zhí)行Thread.sleep()方法的時候,其他線程仍然是BLOCKED狀態(tài),等到線程1休眠3秒以后其他線程才開始執(zhí)行。
wait和notify
聲:你知道wait()方法作用?
我:在wait()調(diào)用之前必須先獲得對象鎖,并且必須與synchronized一起使用,wait()使當(dāng)前線程處于waiting狀態(tài),并且主動釋放對象鎖。
聲:你知道notify()和notifyAll()作用?
我:notify()或者notifyAll()也必須和synchronized一起使用,notify()用來喚醒調(diào)用wait()后處于waiting狀態(tài)的線程,當(dāng)有多個線程時隨機(jī)喚醒一個線程對其發(fā)出通知,但是并不會立馬被喚醒,需要等待正在執(zhí)行notify()的線程釋放鎖以后才可以。notifyAll()方法用來通知所有waiting()的線程。
筆試題:如何用wait和notify實現(xiàn)生產(chǎn)者消費者模式?
package net.dreamzuora.thread;import java.util.LinkedList; import java.util.Queue; import java.util.UUID;public class CustomMQ {public static void main(String[] args) {Queue<String> queue = new LinkedList<>();Producer producer = new Producer(queue);Consumer consumer = new Consumer(queue);new Thread(producer, "producer-thread-1").start();new Thread(producer, "producer-thread-2").start();new Thread(consumer, "consumer-thread-1").start();new Thread(consumer, "consumer-thread-2").start();}} class Producer implements Runnable{private Queue<String> queue;public Producer(Queue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){synchronized (queue){String uuid = UUID.randomUUID().toString();System.out.println("thread: " + Thread.currentThread().getName() + " producer: " + uuid);queue.add(uuid);queue.notify();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}} }class Consumer implements Runnable{private Queue<String> queue;public Consumer(Queue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){synchronized (queue){if (queue.isEmpty()){try {System.out.println(Thread.currentThread().getName() + "wait...");queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}String uuid = queue.poll();System.out.println("thread: " + Thread.currentThread().getName() + "consumer:" + uuid);}}} }代碼案例和詳細(xì)解釋可以看看這個大牛的博客
面試題:為什么wait()必須和synchronized一起使用?
面試題:為什么Java要把wait()和notify()放在如此Object類里面,而不是像sleep放在Thread中呢?
面試題:wait()的時候?qū)ο箧i會釋放鎖?
我:調(diào)用wait()線程不會出讓CPU時間片段,但是會釋放鎖,我可以做個案例,如下
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.Date; import java.util.concurrent.CountDownLatch;public class WaitDemo2 {Object obj = new Object();void a() {synchronized (obj) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("1");}void b() {synchronized (obj) {System.out.println("2");}}void c() {synchronized (obj) {System.out.println("3");}}@Testpublic void method() throws InterruptedException {this.a();this.b();this.c();}@Testpublic void thread() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);System.out.println(new Date());new Thread(() -> a()).start();new Thread(() -> b()).start();new Thread(() -> c()).start();Thread.sleep(5000);System.out.println(new Date());synchronized (obj){obj.notifyAll();}countDownLatch.await();} }控制臺:
結(jié)論:最先啟動的線程占用鎖調(diào)用wait()方法都進(jìn)入等待狀態(tài),其他線程仍然在執(zhí)行,而等待的線程到了五秒被notifyAll()喚醒才執(zhí)行,由此可以得出結(jié)論wait()方法時線程會釋放鎖。
面試題:wait()和sleep()區(qū)別?
wait()不會出讓CPU時間片段,會釋放鎖;sleep()出讓CPU時間片段,不釋放鎖。
interruptedException和interrupt()方法
當(dāng)其他線程通過調(diào)用當(dāng)前線程的 thread.interrupt() 方法,表示向當(dāng) 前線程打個招呼,告訴他可以中斷線程的執(zhí)行了,至于什么時候中斷,取決于當(dāng)前線程自己。 線程通過檢查是否被中斷來進(jìn)行相應(yīng),可以通過 Thread.currentThread().isInterrupted()來判斷是否被中斷。
interrupt() 方法并不像在 for 循環(huán)語句中使用 break 語句那樣干脆,馬上就停止循環(huán)。調(diào)用 interrupt() 方法僅僅是在當(dāng)前線程中打一個停止的標(biāo)記,并不是真的停止線程。
聲:什么情況下拋出InterruptedException
我:obj.wait()、Thread.sleep()、Thread.join()等方法申請了中斷異常才會拋出異常
面試官:給我說說輕量級阻塞和重量級阻塞
我:這個概念我都沒聽過~
聲:你基礎(chǔ)還是不行,遇到面試這種基礎(chǔ)題你都不會直接被pass掉了,我來給你講講。
聲:前面我們講過線程的幾種狀態(tài)還記得?(
我:腦海回想…NEW、RUNNABLE、BLOCKED、WAITING、Time_Waiting、SLEEP、time_sleep
聲:記憶不錯,首先我們要知道輕量級阻塞是處于waiting、time_waiting狀態(tài),而重量級阻塞是blocked狀態(tài)的,然后我們會發(fā)現(xiàn)只有線程進(jìn)入synchronized嘗試獲取類鎖或者對象鎖,多線程競爭情況線程進(jìn)入Blocked狀態(tài),而synchronized不會聲明interruptedException,因此此時即使調(diào)用thread.interrupt()正在blocked線程是無法感知中斷信息,由此可以確定synchronized修飾的代碼會使線程重量級阻塞,相反線程中使用wait()、sleep()等方法會聲明interruptedException,因此會響應(yīng)中斷,其屬于輕量級阻塞。
我:那居然synchronized是重量級阻塞不會響應(yīng)中斷,那我在synchronized中調(diào)用thread.sleep()方法是不是就無法接收到中斷信號?
聲:你問這個問題說明你還沒明白阻塞和等待線程狀態(tài),我們前面說過synchronized使線程BLOCKED是多線程競爭類鎖或者對象鎖時候,那些等待獲取鎖的線程進(jìn)入阻塞狀態(tài),而一旦獲取鎖以后就不再是阻塞狀態(tài)了,那么接下來執(zhí)行的thread.sleep()也是這樣能響應(yīng)中斷的呀!我給你舉個例子
console:
聲:你了解線程中斷后線程復(fù)位和被動復(fù)位?
1.Thread.interrupted()手動復(fù)位將中斷狀態(tài)true->false 和Thread.currentThread().isInterrupted()方法一起使用。
2.進(jìn)入InterruptedException異常前中斷狀態(tài)true->false
java線程的中斷的方式原理分析
聲:如何優(yōu)雅的關(guān)閉線程?
Thread.currentThread().stop():官方不推薦使用,強(qiáng)制殺死線程
Thread.currentThread().destroy():官方不推薦使用,搞不懂官方為什么會有這個方法
回到這節(jié)標(biāo)題,如何優(yōu)雅的關(guān)閉線程?
停止線程方法有三種
總結(jié):利用interrupted()方法關(guān)閉線程
并發(fā)核心概念
聲:這一章節(jié)可能比較枯燥,都是一些概念
并發(fā)與并行
并發(fā):在同一時間段同時運行多個任務(wù),單核CPU中運行多任務(wù)通過操作系統(tǒng)調(diào)度很快從一個任務(wù)運行切換到另一個任務(wù)
并行:在同一時刻同事運行多個任務(wù),多核CPU同時運行多任務(wù)
同步
聲:說到同步,我們可能會想到同步代碼塊,如果你知道同步代碼塊的作用,那么就好理解同步概念了,同步代碼塊是為了防止多線程并發(fā)訪問共享資源而導(dǎo)致線程安全問題,那么同步就是用來協(xié)調(diào)兩個或者多個任務(wù)能夠按照我們想要的順序去執(zhí)行,獲得我們預(yù)期想要的結(jié)果,那么這就是同步的概念。
聲:你能想到日常開發(fā)當(dāng)中都會用過同步機(jī)制?
我:synchronized
聲:恩,說的不錯,其實還有Semaphore等,現(xiàn)在讓我們學(xué)習(xí)兩種同步方法:
1.控制同步
當(dāng)前任務(wù)結(jié)束輸出作為下個任務(wù)輸入
例如:
面試題:如何控制多個線程執(zhí)行順序:給你三個線程如何順序打印數(shù)字?
順序打印代碼參考我之前總結(jié)的博客
package net.dreamzuora.thread;import org.junit.jupiter.api.Test;import java.util.concurrent.CountDownLatch;public class MultiThreadIncrement {int num = 1;Object obj = new Object();class Worker1 implements Runnable {Object object;public Worker1(Object object) {this.object = object;}@Overridepublic void run() {synchronized (obj) {while (num != 1) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "A");num = 2;obj.notify();}}}class Worker2 implements Runnable {Object obj;public Worker2(Object obj) {this.obj = obj;}@Overridepublic void run() {synchronized (obj) {while (num != 2) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "B");num = 3;obj.notifyAll();}}}class Worker3 implements Runnable {Object obj;public Worker3(Object obj) {this.obj = obj;}@Overridepublic void run() {synchronized (obj) {while (num != 3) {try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "C");}}}@Testpublic void main() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);MultiThreadIncrement multiThreadIncrement = new MultiThreadIncrement();new Thread(multiThreadIncrement.new Worker3(obj), "thread-3").start();new Thread(multiThreadIncrement.new Worker1(obj), "thread-1").start();new Thread(multiThreadIncrement.new Worker2(obj), "thread-2").start();countDownLatch.await();}}控制臺:
并發(fā)中有不同的同步機(jī)制,比較流行的有以下幾種
條件和通報條件)。一旦你通報了該條件,在等待它的任務(wù)中只有一個會繼續(xù)執(zhí)行。
2.數(shù)據(jù)訪問同步
當(dāng)兩個或更多任務(wù)訪問共享變量時,再任意時間里,只有一個任務(wù)可以訪問該變量。
不可變對象
面試官:String為什么設(shè)計成不可變對象?
我:節(jié)省內(nèi)存開銷、避免線程安全問題
原子操作和原子變量
聲:相信我們項目中用過很多原子操作或者原子變量吧?
我:的確用過很多,原子操作:像操作數(shù)據(jù)庫的時候我們會利用@Transaction事務(wù)要不成功要么失敗,像redis jedis.set(lockKey, requestId, “NX”, “EX”, expireTime)實現(xiàn)原子操作;原子變量:像利用voliate修飾的bool變量。
聲:你回答的不錯,讓我們明確一下他們的居然定義
原子操作是一種發(fā)生在瞬間的操作。在并發(fā)應(yīng)用程序中,可以通過一
個臨界段來實現(xiàn)原子操作,以便對整個操作采用同步機(jī)制。
原子變量是一種通過原子操作來設(shè)置和獲取其值的變量。可以使用某種同步機(jī)制來實現(xiàn)一個原子變
量,或者也可以使用CAS以無鎖方式來實現(xiàn)一個原子變量,而這種方式并不需要任何同步機(jī)制。
并發(fā)問題
面試官:多線程場景會出現(xiàn)哪些并發(fā)問題?你項目中是如何解決的?
數(shù)據(jù)競爭
我:最常見的,多線程訪問共享資源并對其進(jìn)行增刪改操作導(dǎo)致數(shù)據(jù)混亂問題,需要利用同步機(jī)制,或者CAS解決。
死鎖
我:多線程情況下每個線程都占用對方要使用的鎖卻沒有釋放,導(dǎo)致死鎖,死鎖問題有典型的哲學(xué)家就餐問題
面試題:什么叫死鎖?死鎖必須滿足哪些條件?如何定位死鎖問題?有哪些解決死鎖策略?
我的博客總結(jié)
活鎖
馬路中間有條小橋,只能容納一輛車經(jīng)過,橋兩頭開來兩輛車A和B,A比較禮貌,示意B先過,B也比較禮貌,示意A先過,結(jié)果兩人一直謙讓誰也過不去。(這個形象的比喻摘自:死鎖、活鎖、饑餓)
資源不足
多線程訪問共享資源時候需要先獲取鎖,如果有個線程持續(xù)占有鎖而其他線程會一直白白等待。解決方案:加入計時等待機(jī)制,減少CPU開銷
JMM(java memory model)內(nèi)存模型
我:之前在面試中面試官就會問給我講講jvm內(nèi)存模型,我巴拉巴拉答了一大堆,面試官說你這答的是jmm內(nèi)存模型啊
面試官:你知道JMM內(nèi)存模型、java內(nèi)存模型、jvm內(nèi)存模型區(qū)別是什么?
我:JMM內(nèi)存模型就是java內(nèi)存模型,而jvm內(nèi)存模型就程序計數(shù)器、堆、虛擬機(jī)棧、本地方法棧,那什么是JMM內(nèi)存模型呢?
聲:我相信大多數(shù)人都不知道他們區(qū)別,那我來給你講講
聲:什么是JMM?
JMM是和多線程相關(guān)的一組規(guī)范,需要各個JVM的實現(xiàn)遵守JMM規(guī)范,實現(xiàn)Java代碼在不同JVM運行都能得到相同結(jié)果,從 Java 代碼到 CPU 指令的這個轉(zhuǎn)化過程要遵守哪些和并發(fā)相關(guān)的原則和規(guī)范,這就是 JMM 的重點內(nèi)容
我:你說的概念都是什么鬼,聽不懂,能不能具體點,這種抽象概念誰記得住啊,老子面試怎么打?
聲:你這急性子,我還沒講完呢,得嘞,我來講講具體點的
JMM與處理器、緩存、并發(fā)、編譯器有關(guān),我想我們應(yīng)該都聽過CPU多級緩存、處理器優(yōu)化、指令重排序等導(dǎo)致結(jié)果不一致問題,JMM很好的解決了這些問題。
我想我們都使用過synchronized、volatile、Lock等同步工具和關(guān)鍵字,而它們的實現(xiàn)都遵循了JMM規(guī)范。
面試官問道JMM我們應(yīng)該立馬想到JMM最最重要的3個內(nèi)容:原子性、內(nèi)存可見性、指令重排序
我:聽著還是好復(fù)雜呀,那什么是內(nèi)存可見性?
我:什么是原子操作,我們應(yīng)該注意什么呢?
我:什么是指令重排序
面試官:給我講講jvm內(nèi)存模型以及jdk1.7和1.8版本有何區(qū)別?
我:jmm內(nèi)存劃分:程序計數(shù)器、堆、方法區(qū)、虛擬機(jī)棧、本地方法棧,jdk1.8中將廢除了永久代,引入了元空間
關(guān)于java內(nèi)存模型講解網(wǎng)上一大把,推薦一篇大牛博客總結(jié)的很好:Java內(nèi)存管理-JVM內(nèi)存模型以及JDK7和JDK8內(nèi)存模型對比總結(jié)
jvm的匯總之后會在jvm常見面試題博客中進(jìn)行詳細(xì)梳理
happen-before
聲: happen-before我想很多人并不熟悉,這節(jié)的內(nèi)容概念性比較多可能有點枯燥,耐心看喲!
as if serial(串行)語義
單線程重排序
對于單線程程序CPU和編譯器都可以對其重排序并不會導(dǎo)致結(jié)果不一致問題,這就是as if serial語義
多線程重排序
多線程場景中數(shù)據(jù)依賴復(fù)雜,編譯器和CP無法理解之間關(guān)系做出合理優(yōu)化,編譯器和CPU只能保證單線程的as if serial
面試官:什么是happen-before?
如果A happen-before B 也就是A的執(zhí)行結(jié)果對B可見,但是并不是意味著A一定要在B之前執(zhí)行,在多線程中AB執(zhí)行順序不確定,又由于指令重排序,因此很容易會出現(xiàn)并發(fā)問題,為了解決這種問題java定義了許多內(nèi)存可見性的約束:
voliate關(guān)鍵字
final關(guān)鍵字
總結(jié)
以上是生活随笔為你收集整理的java核心技术-多线程并发设计原理以及常见面试题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【转载】向量空间模型VSM及余弦计算
- 下一篇: 计数排序和桶排序 java代码实现