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