個人珍藏的80道多線程并發(fā)面試題(11-20答案解析)

前言

個人珍藏的80道Java多線程/并發(fā)經(jīng)典面試題,現(xiàn)在給出11-20的答案解析哈,并且上傳github哈~

https://github.com/whx123/JavaHome

個人珍藏的80道多線程并發(fā)面試題(1-10答案解析)

11、為什么要用線程池?Java的線程池內(nèi)部機(jī)制,參數(shù)作用,幾種工作阻塞隊列,線程池類型以及使用場景

回答這些點:

  • 為什么要用線程池?
  • Java的線程池原理
  • 線程池核心參數(shù)
  • 幾種工作阻塞隊列
  • 線程池使用不當(dāng)?shù)膯栴}
  • 線程池類型以及使用場景

為什么要用線程池?

線程池:一個管理線程的池子。

  • 管理線程,避免增加創(chuàng)建線程和銷毀線程的資源損耗。
  • 提高響應(yīng)速度。
  • 重復(fù)利用。

Java的線程池執(zhí)行原理

image

為了形象描述線程池執(zhí)行,打個比喻:

  • 核心線程比作公司正式員工
  • 非核心線程比作外包員工
  • 阻塞隊列比作需求池
  • 提交任務(wù)比作提需求


    image

線程池核心參數(shù)

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 
  • corePoolSize: 線程池核心線程數(shù)最大值
  • maximumPoolSize: 線程池最大線程數(shù)大小
  • keepAliveTime: 線程池中非核心線程空閑的存活時間大小
  • unit: 線程空閑存活時間單位
  • workQueue: 存放任務(wù)的阻塞隊列
  • threadFactory: 用于設(shè)置創(chuàng)建線程的工廠,可以給創(chuàng)建的線程設(shè)置有意義的名字,可方便排查問題。
  • handler:線城池的飽和策略事件,主要有四種類型拒絕策略。

四種拒絕策略

  • AbortPolicy(拋出一個異常,默認(rèn)的)
  • DiscardPolicy(直接丟棄任務(wù))
  • DiscardOldestPolicy(丟棄隊列里最老的任務(wù),將當(dāng)前這個任務(wù)繼續(xù)提交給線程池)
  • CallerRunsPolicy(交給線程池調(diào)用所在的線程進(jìn)行處理)

幾種工作阻塞隊列

  • ArrayBlockingQueue(用數(shù)組實現(xiàn)的有界阻塞隊列,按FIFO排序量)
  • LinkedBlockingQueue(基于鏈表結(jié)構(gòu)的阻塞隊列,按FIFO排序任務(wù),容量可以選擇進(jìn)行設(shè)置,不設(shè)置的話,將是一個無邊界的阻塞隊列)
  • DelayQueue(一個任務(wù)定時周期的延遲執(zhí)行的隊列)
  • PriorityBlockingQueue(具有優(yōu)先級的無界阻塞隊列)
  • SynchronousQueue(一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài))

線程池使用不當(dāng)?shù)膯栴}

線程池適用不當(dāng)可能導(dǎo)致內(nèi)存飆升問題哦

有興趣可以看我這篇文章哈:源碼角度分析-newFixedThreadPool線程池導(dǎo)致的內(nèi)存飆升問題

線程池類型以及使用場景

  • newFixedThreadPool

適用于處理CPU密集型的任務(wù),確保CPU在長期被工作線程使用的情況下,盡可能的少的分配線程,即適用執(zhí)行長期的任務(wù)。

  • newCachedThreadPool

用于并發(fā)執(zhí)行大量短期的小任務(wù)。

  • newSingleThreadExecutor

適用于串行執(zhí)行任務(wù)的場景,一個任務(wù)一個任務(wù)地執(zhí)行。

  • newScheduledThreadPool

周期性執(zhí)行任務(wù)的場景,需要限制線程數(shù)量的場景

  • newWorkStealingPool

建一個含有足夠多線程的線程池,來維持相應(yīng)的并行級別,它會通過工作竊取的方式,使得多核的 CPU 不會閑置,總會有活著的線程讓 CPU 去運行,本質(zhì)上就是一個 ForkJoinPool。)

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

12、談?wù)剉olatile關(guān)鍵字的理解

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

  • vlatile變量的作用
  • 現(xiàn)代計算機(jī)的內(nèi)存模型(嗅探技術(shù),MESI協(xié)議,總線)
  • Java內(nèi)存模型(JMM)
  • 什么是可見性?
  • 指令重排序
  • volatile的內(nèi)存語義
  • as-if-serial
  • Happens-before
  • volatile可以解決原子性嘛?為什么?
  • volatile底層原理,如何保證可見性和禁止指令重排(內(nèi)存屏障)

vlatile變量的作用?

  • 保證變量對所有線程可見性
  • 禁止指令重排

現(xiàn)代計算機(jī)的內(nèi)存模型

image
  • 其中高速緩存包括L1,L2,L3緩存~
  • 緩存一致性協(xié)議,可以了解MESI協(xié)議
  • 總線(Bus)是計算機(jī)各種功能部件之間傳送信息的公共通信干線,CPU和其他功能部件是通過總線通信的。
  • 處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)內(nèi)存和其他處理器的緩存數(shù)據(jù)在總線上保持一致。

Java內(nèi)存模型(JMM)

image

什么是可見性?

可見性就是當(dāng)一個線程 修改一個共享變量時,另外一個線程能讀到這個修改的值。

指令重排序

指令重排是指在程序執(zhí)行過程中,為了提高性能, 編譯器和CPU可能會對指令進(jìn)行重新排序。


image

volatile的內(nèi)存語義

  • 當(dāng)寫一個 volatile 變量時,JMM 會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。
  • 當(dāng)讀一個 volatile 變量時,JMM 會把該線程對應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。

as-if-serial

如果在本線程內(nèi)觀察,所有的操作都是有序的;即不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不會被改變。

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

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


image

Happens-before

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

  • 程序次序規(guī)則:在一個線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
  • 管程鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作
  • volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
  • 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作
  • 線程終止規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行
  • 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
  • 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
  • 傳遞性:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作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();
        }
        
        //等待所有累加線程結(jié)束
        while(Thread.activeCount()>1)  
            Thread.yield();
        System.out.println(test.race);
    }
}

volatile底層原理,如何保證可見性和禁止指令重排(內(nèi)存屏障)

volatile 修飾的變量,轉(zhuǎn)成匯編代碼,會發(fā)現(xiàn)多出一個lock前綴指令。lock指令相當(dāng)于一個內(nèi)存屏障,它保證以下這幾點:

  • 1.重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置
  • 2.將本處理器的緩存寫入內(nèi)存
  • 3.如果是寫入動作,會導(dǎo)致其他處理器中對應(yīng)的緩存無效。

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

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

13、AQS組件,實現(xiàn)原理

AQS,即AbstractQueuedSynchronizer,是構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架,它使用了一個int成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成資源獲取線程的排隊工作。可以回答以下這幾個關(guān)鍵點哈:

  • state 狀態(tài)的維護(hù)。
  • CLH隊列
  • ConditionObject通知
  • 模板方法設(shè)計模式
  • 獨占與共享模式。
  • 自定義同步器。
  • AQS全家桶的一些延伸,如:ReentrantLock等。

state 狀態(tài)的維護(hù)

  • state,int變量,鎖的狀態(tài),用volatile修飾,保證多線程中的可見性。
  • getState()和setState()方法采用final修飾,限制AQS的子類重寫它們兩。
  • compareAndSetState()方法采用樂觀鎖思想的CAS算法操作確保線程安全,保證狀態(tài)
    設(shè)置的原子性。

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

CLH隊列

image

CLH(Craig, Landin, and Hagersten locks) 同步隊列 是一個FIFO雙向隊列,其內(nèi)部通過節(jié)點head和tail記錄隊首和隊尾元素,隊列元素的類型為Node。AQS依賴它來完成同步狀態(tài)state的管理,當(dāng)前線程如果獲取同步狀態(tài)失敗時,AQS則會將當(dāng)前線程已經(jīng)等待狀態(tài)等信息構(gòu)造成一個節(jié)點(Node)并將其加入到CLH同步隊列,同時會阻塞當(dāng)前線程,當(dāng)同步狀態(tài)釋放時,會把首節(jié)點喚醒(公平鎖),使其再次嘗試獲取同步狀態(tài)。

ConditionObject通知

我們都知道,synchronized控制同步的時候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以實現(xiàn)等待/通知模式。而Lock呢?它提供了條件Condition接口,配合await(),signal(),signalAll() 等方法也可以實現(xiàn)等待/通知機(jī)制。ConditionObject實現(xiàn)了Condition接口,給AQS提供條件變量的支持 。

image

ConditionObject隊列與CLH隊列的愛恨情仇:

  • 調(diào)用了await()方法的線程,會被加入到conditionObject等待隊列中,并且喚醒CLH隊列中head節(jié)點的下一個節(jié)點。
  • 線程在某個ConditionObject對象上調(diào)用了singnal()方法后,等待隊列中的firstWaiter會被加入到AQS的CLH隊列中,等待被喚醒。
  • 當(dāng)線程調(diào)用unLock()方法釋放鎖時,CLH隊列中的head節(jié)點的下一個節(jié)點(在本例中是firtWaiter),會被喚醒。

模板方法設(shè)計模式

什么是模板設(shè)計模式?

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

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

獨占與共享模式

  • 獨占式: 同一時刻僅有一個線程持有同步狀態(tài),如ReentrantLock。又可分為公平鎖和非公平鎖。
  • 共享模式:多個線程可同時執(zhí)行,如Semaphore/CountDownLatch等都是共享式的產(chǎn)物。

自定義同步器

你要實現(xiàn)自定義鎖的話,首先需要確定你要實現(xiàn)的是獨占鎖還是共享鎖,定義原子變量state的含義,再定義一個內(nèi)部類去繼承AQS,重寫對應(yīng)的模板方法即可啦

AQS全家桶的一些延伸。

Semaphore,CountDownLatch,ReentrantLock

可以看下之前我這篇文章哈,AQS解析與實戰(zhàn)

14、什么是多線程環(huán)境下的偽共享

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

什么是偽共享

偽共享定義?

CPU的緩存是以緩存行(cache line)為單位進(jìn)行緩存的,當(dāng)多個線程修改相互獨立的變量,而這些變量又處于同一個緩存行時就會影響彼此的性能。這就是偽共享

現(xiàn)代計算機(jī)計算模型,大家都有印象吧?我之前這篇文章也有講過,有興趣可以看一下哈,Java程序員面試必備:Volatile全方位解析

image
  • CPU執(zhí)行速度比內(nèi)存速度快好幾個數(shù)量級,為了提高執(zhí)行效率,現(xiàn)代計算機(jī)模型演變出CPU、緩存(L1,L2,L3),內(nèi)存的模型。
  • CPU執(zhí)行運算時,如先從L1緩存查詢數(shù)據(jù),找不到再去L2緩存找,依次類推,直到在內(nèi)存獲取到數(shù)據(jù)。
  • 為了避免頻繁從內(nèi)存獲取數(shù)據(jù),聰明的科學(xué)家設(shè)計出緩存行,緩存行大小為64字節(jié)。

也正是因為緩存行,就導(dǎo)致偽共享問題的存在,如圖所示:


image

假設(shè)數(shù)據(jù)a、b被加載到同一個緩存行。

  • 當(dāng)線程1修改了a的值,這時候CPU1就會通知其他CPU核,當(dāng)前緩存行(Cache line)已經(jīng)失效。
  • 這時候,如果線程2發(fā)起修改b,因為緩存行已經(jīng)失效了,所以core2 這時會重新從主內(nèi)存中讀取該 Cache line 數(shù)據(jù)。讀完后,因為它要修改b的值,那么CPU2就通知其他CPU核,當(dāng)前緩存行(Cache line)又已經(jīng)失效。
  • 醬紫,如果同一個Cache line的內(nèi)容被多個線程讀寫,就很容易產(chǎn)生相互競爭,頻繁回寫主內(nèi)存,會大大降低性能。

如何解決偽共享問題

既然偽共享是因為相互獨立的變量存儲到相同的Cache line導(dǎo)致的,一個緩存行大小是64字節(jié)。那么,我們就可以使用空間換時間,即數(shù)據(jù)填充的方式,把獨立的變量分散到不同的Cache line~

共享內(nèi)存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("執(zhí)行時間" + (System.currentTimeMillis() - beginTime));
    }

}

class Rectangle {
    volatile long a;
    volatile long b;
}

運行結(jié)果:

執(zhí)行時間2815

一個long類型是8字節(jié),我們在變量a和b之間不上7個long類型變量呢,輸出結(jié)果是啥呢?如下:

class Rectangle {
    volatile long a;
    long a1,a2,a3,a4,a5,a6,a7;
    volatile long b;
}

運行結(jié)果:

執(zhí)行時間1113

可以發(fā)現(xiàn)利用填充數(shù)據(jù)的方式,讓讀寫的變量分割到不同緩存行,可以很好挺高性能~

15、 說一下 Runnable和 Callable有什么區(qū)別?

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

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

看下demo代碼吧,這樣應(yīng)該好理解一點哈~

/*
 *  @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();

    }
}

運行結(jié)果:

你好,callable
你好呀,runnable
null

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

  • wait() 使得線程進(jìn)入阻塞等待狀態(tài),并且釋放鎖
  • notify()喚醒一個處于等待狀態(tài)的線程,它一般跟wait()方法配套使用。
  • suspend()使得線程進(jìn)入阻塞狀態(tài),并且不會自動恢復(fù),必須對應(yīng)的resume() 被調(diào)用,才能使得線程重新進(jìn)入可執(zhí)行狀態(tài)。suspend()方法很容易引起死鎖問題。
  • resume()方法跟suspend()方法配套使用。

suspend()不建議使用,suspend()方法在調(diào)用后,線程不會釋放已經(jīng)占有的資 源(比如鎖),而是占有著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。

17.Condition接口及其實現(xiàn)原理

  • Condition接口與Object監(jiān)視器方法對比
  • Condition接口使用demo
  • Condition實現(xiàn)原理

Condition接口與Object監(jiān)視器方法對比

Java對象(Object),提供wait()、notify(),notifyAll() 系列方法,配合synchronized,可以實現(xiàn)等待/通知模式。而Condition接口配合Lock,通過await(),signal(),signalAll() 等方法,也可以實現(xiàn)類似的等待/通知機(jī)制。

對比項 對象監(jiān)視方法 Condition
前置條件 獲得對象的鎖 調(diào)用Lock.lock()獲取鎖,調(diào)用Lock.newCondition()獲得Condition對象
調(diào)用方式 直接調(diào)用,object.wait() 直接調(diào)用,condition.await()
等待隊列數(shù) 1個 多個
當(dāng)前線程釋放鎖并進(jìn)入等待狀態(tài) 支持 支持
在等待狀態(tài)中不響應(yīng)中斷 不支持 支持
當(dāng)前線程釋放鎖并進(jìn)入超時等待狀態(tài) 支持 支持
當(dāng)前線程釋放鎖并進(jìn)入等待狀態(tài)到將來的某個時間 不支持 支持
喚醒等待隊列中的一個線程 支持 支持
喚醒等待隊列中的全部線程 支持 支持

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實現(xiàn)原理

其實,同步隊列和等待隊列中節(jié)點類型都是同步器的靜態(tài)內(nèi)部類 AbstractQueuedSynchronizer.Node,接下來我們圖解一下Condition的實現(xiàn)原理~

等待隊列的基本結(jié)構(gòu)圖

image

一個Condition包含一個等待隊列,Condition擁有首節(jié)點(firstWaiter)和尾節(jié)點 (lastWaiter)。當(dāng)前線程調(diào)用Condition.await()方法,將會以當(dāng)前線程構(gòu)造節(jié)點,并將節(jié)點從尾部加入等待隊

AQS 結(jié)構(gòu)圖

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


image

等待

image

當(dāng)調(diào)用await()方法時,相當(dāng)于同步隊列的首節(jié)點(獲取了鎖的節(jié)點)移動到Condition的等待隊列中。

通知

image

調(diào)用Condition的signal()方法,將會喚醒在等待隊列中等待時間最長的節(jié)點(首節(jié)點),在
喚醒節(jié)點之前,會將節(jié)點移到同步隊列中。

18、線程池如何調(diào)優(yōu),最大數(shù)目如何確認(rèn)?

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

Nthreads=NcpuUcpu(1+w/c)**

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

假設(shè)cpu 100%運轉(zhuǎn),則公式為

Nthreads=Ncpu*(1+w/c)

估算的話,醬紫:

  • 如果是IO密集型應(yīng)用(如數(shù)據(jù)庫數(shù)據(jù)交互、文件上傳下載、網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)鹊龋?,IO操作一般比較耗時,等待時間與計算時間的比率(w/c)會大于1,所以最佳線程數(shù)估計就是 Nthreads=Ncpu*(1+1)= 2Ncpu 。
  • 如果是CPU密集型應(yīng)用(如算法比較復(fù)雜的程序),最理想的情況,沒有等待,w=0,Nthreads=Ncpu。又對于計算密集型的任務(wù),在擁有N個處理器的系統(tǒng)上,當(dāng)線程池的大小為N+1時,通常能實現(xiàn)最優(yōu)的效率。所以 Nthreads = Ncpu+1

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

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

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

19、 假設(shè)有T1、T2、T3三個線程,你怎樣保證T2在T1執(zhí)行完后執(zhí)行,T3在T2執(zhí)行完后執(zhí)行?

可以使用join方法解決這個問題。比如在線程A中,調(diào)用線程B的join方法表示的意思就是:A等待B線程執(zhí)行完畢后(釋放CPU執(zhí)行權(quán)),在繼續(xù)執(zhí)行。

代碼如下:

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執(zhí)行完,再往下執(zhí)行
            spring.join();
            //夏天線程再啟動
            summer.start();
            //主線程等待線程summer執(zhí)行完,再往下執(zhí)行
            summer.join();
            //秋天線程最后啟動
            autumn.start();
            //主線程等待線程autumn執(zhí)行完,再往下執(zhí)行
            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();
            }
        }
    }
}

運行結(jié)果:

春天來了: 1次
春天來了: 2次
春天來了: 3次
夏天來了: 1次
夏天來了: 2次
夏天來了: 3次
秋天來了: 1次
秋天來了: 2次
秋天來了: 3次

20. LockSupport作用是?

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

LockSupport是個工具類,它的主要作用是掛起和喚醒線程, 該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)。

public static void park(); //掛起當(dāng)前線程,調(diào)用unpark(Thread thread)或者當(dāng)前線程被中斷,才能從park方法返回
public static void parkNanos(Object blocker, long nanos);  // 掛起當(dāng)前線程,有超時時間的限制
public static void parkUntil(Object blocker, long deadline); // 掛起當(dāng)前線程,直到某個時間
public static void park(Object blocker); //掛起當(dāng)前線程
public static void unpark(Thread thread); // 喚醒當(dāng)前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(); //掛起當(dāng)前線程
                }
                System.out.println(this.getName() + "車還在正常跑");

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

            }
        }

        public void park() {
            isStop = true;
            System.out.println("停車?yán)玻瑱z查酒駕");

        }

        public void unPark(){
            isStop = false;
            LockSupport.unpark(this); //喚醒當(dāng)前線程
            System.out.println("老哥你沒酒駕,繼續(xù)開吧");
        }

    }
}

運行結(jié)果:

勞斯勞斯正在行駛中
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
停車?yán)玻瑱z查酒駕
勞斯勞斯車停下來了
老哥你沒酒駕,繼續(xù)開吧
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑
勞斯勞斯車還在正常跑

LockSupport的park和unpark的實現(xiàn),有點類似wait和notify的功能。但是

  • park不需要獲取對象鎖
  • 中斷的時候park不會拋出InterruptedException異常,需要在park之后自行判斷中斷狀態(tài)
  • 使用park和unpark的時候,可以不用擔(dān)心park的時序問題造成死鎖
  • LockSupport不需要在同步代碼塊里
  • unpark卻可以喚醒一個指定的線程,notify只能隨機(jī)選擇一個線程喚醒

Object blocker作用?

方便在線程dump的時候看到具體的阻塞對象的信息。

公眾號

image

參考與感謝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容