1. 程式人生 > 實用技巧 >個人珍藏的80道多執行緒併發面試題(11-20答案解析)

個人珍藏的80道多執行緒併發面試題(11-20答案解析)

前言

個人珍藏的80道Java多執行緒/併發經典面試題,現在給出11-20的答案解析哈,並且上傳github哈~

https://github.com/whx123/JavaHome

個人珍藏的80道多執行緒併發面試題(1-10答案解析)

11、為什麼要用執行緒池?Java的執行緒池內部機制,引數作用,幾種工作阻塞佇列,執行緒池型別以及使用場景

回答這些點:

  • 為什麼要用執行緒池?
  • Java的執行緒池原理
  • 執行緒池核心引數
  • 幾種工作阻塞佇列
  • 執行緒池使用不當的問題
  • 執行緒池型別以及使用場景

為什麼要用執行緒池?

執行緒池:一個管理執行緒的池子。

  • 管理執行緒,避免增加建立執行緒和銷燬執行緒的資源損耗。
  • 提高響應速度。
  • 重複利用。

Java的執行緒池執行原理


為了形象描述執行緒池執行,打個比喻:

  • 核心執行緒比作公司正式員工
  • 非核心執行緒比作外包員工
  • 阻塞佇列比作需求池
  • 提交任務比作提需求

執行緒池核心引數

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 
  • corePoolSize: 執行緒池核心執行緒數最大值
  • maximumPoolSize: 執行緒池最大執行緒數大小
  • keepAliveTime: 執行緒池中非核心執行緒空閒的存活時間大小
  • unit: 執行緒空閒存活時間單位
  • workQueue: 存放任務的阻塞佇列
  • threadFactory: 用於設定建立執行緒的工廠,可以給建立的執行緒設定有意義的名字,可方便排查問題。
  • handler:線城池的飽和策略事件,主要有四種類型拒絕策略。

四種拒絕策略

  • AbortPolicy(丟擲一個異常,預設的)
  • DiscardPolicy(直接丟棄任務)
  • DiscardOldestPolicy(丟棄佇列裡最老的任務,將當前這個任務繼續提交給執行緒池)
  • CallerRunsPolicy(交給執行緒池呼叫所在的執行緒進行處理)

幾種工作阻塞佇列

  • ArrayBlockingQueue(用陣列實現的有界阻塞佇列,按FIFO排序量)
  • LinkedBlockingQueue(基於連結串列結構的阻塞佇列,按FIFO排序任務,容量可以選擇進行設定,不設定的話,將是一個無邊界的阻塞佇列)
  • DelayQueue(一個任務定時週期的延遲執行的佇列)
  • PriorityBlockingQueue(具有優先順序的無界阻塞佇列)
  • SynchronousQueue(一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態)

執行緒池使用不當的問題

執行緒池適用不當可能導致記憶體飆升問題哦

有興趣可以看我這篇文章哈:原始碼角度分析-newFixedThreadPool執行緒池導致的記憶體飆升問題

執行緒池型別以及使用場景

  • newFixedThreadPool

適用於處理CPU密集型的任務,確保CPU在長期被工作執行緒使用的情況下,儘可能的少的分配執行緒,即適用執行長期的任務。

  • newCachedThreadPool

用於併發執行大量短期的小任務。

  • newSingleThreadExecutor

適用於序列執行任務的場景,一個任務一個任務地執行。

  • newScheduledThreadPool

週期性執行任務的場景,需要限制執行緒數量的場景

  • newWorkStealingPool

建一個含有足夠多執行緒的執行緒池,來維持相應的並行級別,它會通過工作竊取的方式,使得多核的 CPU 不會閒置,總會有活著的執行緒讓 CPU 去執行,本質上就是一個 ForkJoinPool。)

有興趣可以看我這篇文章哈:面試必備:Java執行緒池解析

12、談談volatile關鍵字的理解

volatile是面試官非常喜歡問的一個問題,可以回答以下這幾點:

  • vlatile變數的作用
  • 現代計算機的記憶體模型(嗅探技術,MESI協議,匯流排)
  • Java記憶體模型(JMM)
  • 什麼是可見性?
  • 指令重排序
  • volatile的記憶體語義
  • as-if-serial
  • Happens-before
  • volatile可以解決原子性嘛?為什麼?
  • volatile底層原理,如何保證可見性和禁止指令重排(記憶體屏障)

vlatile變數的作用?

  • 保證變數對所有執行緒可見性
  • 禁止指令重排

現代計算機的記憶體模型

  • 其中快取記憶體包括L1,L2,L3快取~
  • 快取一致性協議,可以瞭解MESI協議
  • 匯流排(Bus)是計算機各種功能部件之間傳送資訊的公共通訊幹線,CPU和其他功能部件是通過匯流排通訊的。
  • 處理器使用嗅探技術保證它的內部快取、系統記憶體和其他處理器的快取資料在總線上保持一致。

Java記憶體模型(JMM)

什麼是可見性?

可見性就是當一個執行緒 修改一個共享變數時,另外一個執行緒能讀到這個修改的值。

指令重排序

指令重排是指在程式執行過程中,為了提高效能, 編譯器和CPU可能會對指令進行重新排序。

volatile的記憶體語義

  • 當寫一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體。
  • 當讀一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。

as-if-serial

如果在本執行緒內觀察,所有的操作都是有序的;即不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不會被改變。

double pi  = 3.14;    //A
double r   = 1.0;     //B
double area = pi * r * r; //C

步驟C依賴於步驟A和B,因為指令重排的存在,程式執行順訊可能是A->B->C,也可能是B->A->C,但是C不能在A或者B前面執行,這將違反as-if-serial語義。

Happens-before

Java語言中,有一個先行發生原則(happens-before):

  • 程式次序規則:在一個執行緒內,按照控制流順序,書寫在前面的操作先行發生於書寫在後面的操作。
  • 管程鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
  • volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作
  • 執行緒終止規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
  • 物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始
  • 傳遞性:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C

volatile可以解決原子性嘛?為什麼?

不可以,可以直接舉i++那個例子,原子性需要synchronzied或者lock保證

public class Test {
    public volatile int race = 0;
     
    public void increase() {
        race++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<100;j++)
                        test.increase();
                };
            }.start();
        }
        
        //等待所有累加執行緒結束
        while(Thread.activeCount()>1)  
            Thread.yield();
        System.out.println(test.race);
    }
}

volatile底層原理,如何保證可見性和禁止指令重排(記憶體屏障)

volatile 修飾的變數,轉成彙編程式碼,會發現多出一個lock字首指令。lock指令相當於一個記憶體屏障,它保證以下這幾點:

  • 1.重排序時不能把後面的指令重排序到記憶體屏障之前的位置
  • 2.將本處理器的快取寫入記憶體
  • 3.如果是寫入動作,會導致其他處理器中對應的快取無效。

2、3點保證可見性,第1點禁止指令重排~

有興趣的朋友可以看我這篇文章哈:Java程式設計師面試必備:Volatile全方位解析

13、AQS元件,實現原理

AQS,即AbstractQueuedSynchronizer,是構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作。可以回答以下這幾個關鍵點哈:

  • state 狀態的維護。
  • CLH佇列
  • ConditionObject通知
  • 模板方法設計模式
  • 獨佔與共享模式。
  • 自定義同步器。
  • AQS全家桶的一些延伸,如:ReentrantLock等。

state 狀態的維護

  • state,int變數,鎖的狀態,用volatile修飾,保證多執行緒中的可見性。
  • getState()和setState()方法採用final修飾,限制AQS的子類重寫它們兩。
  • compareAndSetState()方法採用樂觀鎖思想的CAS演算法操作確保執行緒安全,保證狀態
    設定的原子性。

對CAS有興趣的朋友,可以看下我這篇文章哈~
CAS樂觀鎖解決併發問題的一次實踐

CLH佇列

CLH(Craig, Landin, and Hagersten locks) 同步佇列 是一個FIFO雙向佇列,其內部通過節點head和tail記錄隊首和隊尾元素,佇列元素的型別為Node。AQS依賴它來完成同步狀態state的管理,當前執行緒如果獲取同步狀態失敗時,AQS則會將當前執行緒已經等待狀態等資訊構造成一個節點(Node)並將其加入到CLH同步佇列,同時會阻塞當前執行緒,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。

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),會被喚醒。

模板方法設計模式

什麼是模板設計模式?

在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。

AQS的典型設計模式就是模板方法設計模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生實現,就體現出這個設計模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,給子類實現自定義的同步器。

獨佔與共享模式

  • 獨佔式: 同一時刻僅有一個執行緒持有同步狀態,如ReentrantLock。又可分為公平鎖和非公平鎖。
  • 共享模式:多個執行緒可同時執行,如Semaphore/CountDownLatch等都是共享式的產物。

自定義同步器

你要實現自定義鎖的話,首先需要確定你要實現的是獨佔鎖還是共享鎖,定義原子變數state的含義,再定義一個內部類去繼承AQS,重寫對應的模板方法即可啦

AQS全家桶的一些延伸。

Semaphore,CountDownLatch,ReentrantLock

可以看下之前我這篇文章哈,AQS解析與實戰

14、什麼是多執行緒環境下的偽共享

  • 什麼是偽共享
  • 如何解決偽共享問題

什麼是偽共享

偽共享定義?

CPU的快取是以快取行(cache line)為單位進行快取的,當多個執行緒修改相互獨立的變數,而這些變數又處於同一個快取行時就會影響彼此的效能。這就是偽共享

現代計算機計算模型,大家都有印象吧?我之前這篇文章也有講過,有興趣可以看一下哈,Java程式設計師面試必備:Volatile全方位解析

  • 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的內容被多個執行緒讀寫,就很容易產生相互競爭,頻繁回寫主記憶體,會大大降低效能。

如何解決偽共享問題

既然偽共享是因為相互獨立的變數儲存到相同的Cache line導致的,一個快取行大小是64位元組。那麼,我們就可以使用空間換時間,即資料填充的方式,把獨立的變數分散到不同的Cache line~

共享記憶體demo例子:

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、 說一下 Runnable和 Callable有什麼區別?

  • Callable介面方法是call(),Runnable的方法是run();
  • Callable介面call方法有返回值,支援泛型,Runnable介面run方法無返回值。
  • Callable介面call()方法允許丟擲異常;而Runnable介面run()方法不能繼續上拋異常;
@FunctionalInterface
public interface Callable<V> {
    /**
     * 支援泛型V,有返回值,允許丟擲異常
     */
    V call() throws Exception;
}

@FunctionalInterface
public interface Runnable {
    /**
     *  沒有返回值,不能繼續上拋異常
     */
    public abstract void run();
}

看下demo程式碼吧,這樣應該好理解一點哈~

/*
 *  @Author 撿田螺的小男孩
 *  @date 2020-08-18
 */
public class CallableRunnableTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        Callable <String> callable =new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "你好,callable";
            }
        };

        //支援泛型
        Future<String> futureCallable = executorService.submit(callable);

        try {
            System.out.println(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(futureRunnable.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        executorService.shutdown();

    }
}

執行結果:

你好,callable
你好呀,runnable
null

16、wait(),notify()和suspend(),resume()之間的區別

  • wait() 使得執行緒進入阻塞等待狀態,並且釋放鎖
  • notify()喚醒一個處於等待狀態的執行緒,它一般跟wait()方法配套使用。
  • suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須對應的resume() 被呼叫,才能使得執行緒重新進入可執行狀態。suspend()方法很容易引起死鎖問題。
  • resume()方法跟suspend()方法配套使用。

suspend()不建議使用,suspend()方法在呼叫後,執行緒不會釋放已經佔有的資 源(比如鎖),而是佔有著資源進入睡眠狀態,這樣容易引發死鎖問題。

17.Condition介面及其實現原理

  • Condition介面與Object監視器方法對比
  • Condition介面使用demo
  • Condition實現原理

Condition介面與Object監視器方法對比

Java物件(Object),提供wait()、notify(),notifyAll() 系列方法,配合synchronized,可以實現等待/通知模式。而Condition介面配合Lock,通過await(),signal(),signalAll() 等方法,也可以實現類似的等待/通知機制。

對比項 物件監視方法 Condition
前置條件 獲得物件的鎖 呼叫Lock.lock()獲取鎖,呼叫Lock.newCondition()獲得Condition物件
呼叫方式 直接呼叫,object.wait() 直接呼叫,condition.await()
等待佇列數 1個 多個
當前執行緒釋放鎖並進入等待狀態 支援 支援
在等待狀態中不響應中斷 不支援 支援
當前執行緒釋放鎖並進入超時等待狀態 支援 支援
當前執行緒釋放鎖並進入等待狀態到將來的某個時間 不支援 支援
喚醒等待佇列中的一個執行緒 支援 支援
喚醒等待佇列中的全部執行緒 支援 支援

Condition介面使用demo

public class ConditionTest {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

Condition實現原理

其實,同步佇列和等待佇列中節點型別都是同步器的靜態內部類 AbstractQueuedSynchronizer.Node,接下來我們圖解一下Condition的實現原理~

等待佇列的基本結構圖

一個Condition包含一個等待佇列,Condition擁有首節點(firstWaiter)和尾節點 (lastWaiter)。當前執行緒呼叫Condition.await()方法,將會以當前執行緒構造節點,並將節點從尾部加入等待隊

AQS 結構圖

ConditionI是跟Lock一起結合使用的,底層跟同步器(AQS)相關。同步器擁有一個同步佇列和多個等待佇列~

等待

當呼叫await()方法時,相當於同步佇列的首節點(獲取了鎖的節點)移動到Condition的等待佇列中。

通知

呼叫Condition的signal()方法,將會喚醒在等待佇列中等待時間最長的節點(首節點),在
喚醒節點之前,會將節點移到同步佇列中。

18、執行緒池如何調優,最大數目如何確認?

在《Java Concurrency in Practice》一書中,有一個評估執行緒池執行緒大小的公式

Nthreads=NcpuUcpu(1+w/c)

  • Ncpu = CPU總核數
  • Ucpu =cpu使用率,0~1
  • W/C=等待時間與計算時間的比率

假設cpu 100%運轉,則公式為

Nthreads=Ncpu*(1+w/c)

估算的話,醬紫:

  • 如果是IO密集型應用(如資料庫資料互動、檔案上傳下載、網路資料傳輸等等),IO操作一般比較耗時,等待時間與計算時間的比率(w/c)會大於1,所以最佳執行緒數估計就是 Nthreads=Ncpu*(1+1)= 2Ncpu 。
  • 如果是CPU密集型應用(如演算法比較複雜的程式),最理想的情況,沒有等待,w=0,Nthreads=Ncpu。又對於計算密集型的任務,在擁有N個處理器的系統上,當執行緒池的大小為N+1時,通常能實現最優的效率。所以 Nthreads = Ncpu+1

有具體指參考呢?舉個例子

比如平均每個執行緒CPU執行時間為0.5s,而執行緒等待時間(非CPU執行時間,比如IO)為1.5s,CPU核心數為8,那麼根據上面這個公式估算得到:執行緒池大小=(1+1.5/05)*8 =32。

參考了網上這篇文章,寫得很棒,有興趣的朋友可以去看一下哈:

19、 假設有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

可以使用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次

20. LockSupport作用是?

  • LockSupport作用
  • park和unpark,與wait,notify的區別
  • Object blocker作用?

LockSupport是個工具類,它的主要作用是掛起和喚醒執行緒, 該工具類是建立鎖和其他同步類的基礎。

public static void park(); //掛起當前執行緒,呼叫unpark(Thread thread)或者當前執行緒被中斷,才能從park方法返回
public static void parkNanos(Object blocker, long nanos);  // 掛起當前執行緒,有超時時間的限制
public static void parkUntil(Object blocker, long deadline); // 掛起當前執行緒,直到某個時間
public static void park(Object blocker); //掛起當前執行緒
public static void unpark(Thread thread); // 喚醒當前thread執行緒

看個例子吧:

public class LockSupportTest {

    public static void main(String[] args) {

        CarThread carThread = new CarThread();
        carThread.setName("勞斯勞斯");
        carThread.start();

        try {
            Thread.currentThread().sleep(2000);
            carThread.park();
            Thread.currentThread().sleep(2000);
            carThread.unPark();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class CarThread extends Thread{

        private boolean isStop = false;

        @Override
        public void run() {

            System.out.println(this.getName() + "正在行駛中");

            while (true) {

                if (isStop) {
                    System.out.println(this.getName() + "車停下來了");
                    LockSupport.park(); //掛起當前執行緒
                }
                System.out.println(this.getName() + "車還在正常跑");

                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

        public void park() {
            isStop = true;
            System.out.println("停車啦,檢查酒駕");

        }

        public void unPark(){
            isStop = false;
            LockSupport.unpark(this); //喚醒當前執行緒
            System.out.println("老哥你沒酒駕,繼續開吧");
        }

    }
}

執行結果:

勞斯勞斯正在行駛中
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
停車啦,檢查酒駕
勞斯勞斯車停下來了
老哥你沒酒駕,繼續開吧
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑

LockSupport的park和unpark的實現,有點類似wait和notify的功能。但是

  • park不需要獲取物件鎖
  • 中斷的時候park不會丟擲InterruptedException異常,需要在park之後自行判斷中斷狀態
  • 使用park和unpark的時候,可以不用擔心park的時序問題造成死鎖
  • LockSupport不需要在同步程式碼塊裡
  • unpark卻可以喚醒一個指定的執行緒,notify只能隨機選擇一個執行緒喚醒

Object blocker作用?

方便線上程dump的時候看到具體的阻塞物件的資訊。

公眾號

參考與感謝