1. 程式人生 > >史上最全的Java並發面試題(珍藏版)

史上最全的Java並發面試題(珍藏版)

其它 優勢 空閑 null 基本操作 try web 任務 最好的

技術分享圖片

多線程

java中有幾種方法可以實現一個線程?

  • 繼承Thread類;
  • 實現Runnable接口;
  • 實現Callable接口通過FutureTask包裝器來創建Thread線程;
  • 使用ExecutorService、Callable、Future實現有返回結果的多線程(也就是使用了ExecutorService來管理前面的三種方式)。

如何停止一個正在運行的線程?

  1. 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。
  2. 使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期作廢的方法。
  3. 使用interrupt方法中斷線程。

notify()和notifyAll()有什麽區別?

如果線程調用了對象的 wait()方法,那麽線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。

當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭。

優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

sleep()和 wait()有什麽區別?

對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。

sleep()方法導致了程序暫停執行指定的時間,讓出cpu該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。在調用sleep()方法的過程中,線程不會釋放對象鎖。

當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備,獲取對象鎖進入運行狀態。

什麽是Daemon線程?它有什麽意義?

Java語言自己可以創建兩種進程“用戶線程”和“守護線程”

  • 用戶線程:就是我們平時創建的普通線程.
  • 守護線程:主要是用來服務用戶線程.

Daemon就是守護線程,他的意義是:

只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最後一個非守護線程結束時,守護線程隨著JVM一同結束工作。

Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。

什麽是可重入鎖(ReentrantLock)?

線程可以進入任何一個它已經擁有的鎖所同步著的代碼塊。

代碼設計如下:

public class Lock{
    Boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
                throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

當一個線程進入某個對象的一個synchronized的實例方法後,其它線程是否可進入此對象的其它方法?

如果其他方法前加了synchronized關鍵字,就不能,如果沒加synchronized,則能夠進去。

如果這個方法內部調用了wait(),則可以進入其他加synchronized的方法。

如果其他方法加了synchronized關鍵字,並且沒有調用wai方法,則不能。

synchronized和java.util.concurrent.locks.Lock的異同?

主要相同點:Lock能完成Synchronized所實現的所有功能。

主要不同點:Lock有比Synchronized更精確的線程予以和更好的性能。Synchronized會自動釋放鎖,但是Lock一定要求程序員手工釋放,並且必須在finally從句中釋放。

樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?

樂觀鎖是假設每次操作都不會沖突,若是遇到沖突失敗就重試直到成功;悲觀鎖是讓其他線程都等待,等鎖釋放完了再競爭鎖。

樂觀鎖實現方式:cas,volatile

悲觀鎖實現方式:synchronized,Lock

並發框架

SynchronizedMap和ConcurrentHashMap有什麽區別?

SynchronizedMap()和Hashtable一樣,實現上在調用map所有方法時,都對整個map進行同步。而ConcurrentHashMap的實現卻更加精細,它對map中的所有桶加了鎖。所以,只要有一個線程訪問map,其他線程就無法進入map,而如果一個線程在訪問ConcurrentHashMap某個桶時,其他線程,仍然可以對map執行某些操作。

所以,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優勢。同時,同步操作精確控制到桶,這樣,即使在遍歷map時,如果其他線程試圖對map進行數據修改,也不會拋出ConcurrentModificationException。

CopyOnWriteArrayList可以用於什麽應用場景?

CopyOnWriteArrayList的特性是針對讀操作,不做處理,和普通的ArrayList性能一樣。而在寫操作時,會先拷貝一份,實現新舊版本的分離,然後在拷貝的版本上進行修改操作,修改完後,將其更新至就版本中。

那麽他的使用場景就是:一個需要在多線程中操作,並且頻繁遍歷。其解決了由於長時間鎖定整個數組導致的性能問題,解決方案即寫時拷貝。

另外需要註意的是CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。

線程安全

什麽叫線程安全?servlet是線程安全嗎?

線程安全就是說多線程訪問同一代碼,不會產生不確定的結果。

在多線程環境中,當各線程不共享數據的時候,即都是私有(private)成員,那麽一定是線程安全的。但這種情況並不多見,在多數情況下需要共享數據,這時就需要進行適當的同步控制了。

線程安全一般都涉及到synchronized, 就是一段代碼同時只能有一個線程來操作 不然中間過程可能會產生不可預制的結果。

如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

同步有幾種實現方法?

1.同步方法

即有synchronized關鍵字修飾的方法。

由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。

2.同步代碼塊

即有synchronized關鍵字修飾的語句塊。

被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。

3.使用特殊域變量(volatile)實現線程同步

a.volatile關鍵字為域變量的訪問提供了一種免鎖機制,

b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新,

c.因此每次使用該域就要重新計算,而不是使用寄存器中的值

d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量

4.使用重入鎖實現線程同步

在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,並且擴展了其能力。

5.使用局部變量實現線程同步

volatile有什麽用?能否用一句話說明下volatile的應用場景?

作用是:作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值,即不是從寄存器裏取備份值,而是去該地址內存存儲的值。

一句話說明volatile的應用場景:

對變量的寫操作不依賴於當前值且該變量沒有包含在具有其他變量的不變式中。

請說明下java的內存模型。

Java內存模型的邏輯視圖

技術分享圖片

為了保證並發編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是內存模型。

為了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操作行為的規範。

通過這些規則來規範對內存的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與緩存有關、與並發有關、與編譯器也有關。

它解決了 CPU 多級緩存、處理器優化、指令重排等導致的內存訪問問題,保證了並發場景下的一致性、原子性和有序性。

內存模型解決並發問題主要采用兩種方式

  • 限制處理器優化
  • 使用內存屏障

關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種操作來完成:

  • lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占狀態。
  • unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
  • read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  • load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  • store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

如果要把一個變量從主內存中復制到工作內存,就需要按順尋地執行read和load操作,如果把變量從工作內存中同步回主內存中,就要按順序地執行store和write操作。Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之後必須同步到主內存中。
  • 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中。
  • 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進行lock操作,lock和unlock必須成對出現
  • 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作)。

並發容器和框架

如何讓一段程序並發的執行,並最終匯總結果?

使用CyclicBarrier 在多個關口處將多個線程執行結果匯總, CountDownLatch 在各線程執行完畢後向總線程匯報結果。

CountDownLatch : 一個線程(或者多個), 等待另外N個線程完成某個事情之後才能執行。

CyclicBarrier : N個線程相互等待,任何一個線程完成之前,所有的線程都必須等待。

這樣應該就清楚一點了,對於CountDownLatch來說,重點是那個“一個線程”, 是它在等待,而另外那N的線程在把“某個事情”做完之後可以繼續等待,可以終止。而對於CyclicBarrier來說,重點是那N個線程,他們之間任何一個沒有完成,所有的線程都必須等待。

從api上理解就是CountdownLatch有主要配合使用兩個方法countDown()和await(),countDown()是做事的線程用的方法,await()是等待事情完成的線程用個方法,這兩種線程是可以分開的(下面例子:CountdownLatchTest2),當然也可以是同一組線程;CyclicBarrier只有一個方法await(),指的是做事線程必須大家同時等待,必須是同一組線程的工作。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 各個線程執行完成後,主線程做總結性工作的例子
 * @author xuexiaolei
 * @version 2019年4月16日
 */
public class CountdownLatchTest2 {
    private final static int THREAD_NUM = 10;
    public static void main(String[] args) {
        CountDownLatch lock = new CountDownLatch(THREAD_NUM);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_NUM; i++) {
            exec.submit(new CountdownLatchTask(lock, "Thread-"+i));
        }
        try {
            lock.await();
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("大家都執行完成了,做總結性工作");
        exec.shutdown();
    }
    static class CountdownLatchTask implements Runnable{
        private final CountDownLatch lock;
        private final String threadName;
        CountdownLatchTask(CountDownLatch lock, String threadName) {
            this.lock = lock;
            this.threadName = threadName;
        }
        @Override public void run() {
            System.out.println(threadName + " 執行完成");
            lock.countDown();
        }
    }
}

CyclicBarrier例子:

import java.util.concurrent.*;
/**
 *
 * @author xuexiaolei
 * @version 2019年4月16日
 */
public class CyclicBarrierTest {
    private final static int THREAD_NUM = 10;
    public static void main(String[] args) {
        CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() {
            @Override public void run() {
                System.out.println("這階段大家都執行完成了,我總結一下,然後開始下一階段");
            }
        }
        );
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < THREAD_NUM; i++) {
            exec.submit(new CountdownLatchTask(lock, "Task-"+i));
        }
        exec.shutdown();
    }
    static class CountdownLatchTask implements Runnable{
        private final CyclicBarrier lock;
        private final String threadName;
        CountdownLatchTask(CyclicBarrier lock, String threadName) {
            this.lock = lock;
            this.threadName = threadName;
        }
        @Override public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(threadName + " 執行完成");
                try {
                    lock.await();
                }
                catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

如何合理的配置java線程池?如CPU密集型的任務,基本線程池應該配置多大?IO密集型的任務,基本線程池應該配置多大?用有界隊列好還是×××隊列好?任務非常多的時候,使用什麽阻塞隊列能獲取最好的吞吐量

雖然Exectors可以生成一些很常用的線程池,但畢竟在什麽情況下使用還是開發者最清楚的。在某些自己很清楚的使用場景下,java線程池還是推薦自己配置的。下面是java線程池的配置類的參數,我們逐一分析一下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler
  • corePoolSize - 池中所保存的線程數,包括空閑線程。
  • maximumPoolSize - 池中允許的最大線程數。
  • keepAliveTime - 當線程數大於核心時,此為終止前多余的空閑線程等待新任務的最長時間。
  • unit - keepAliveTime 參數的時間單位。
  • workQueue - 執行前用於保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務。用BlocingQueue的實現類都可以。
  • threadFactory - 執行程序創建新線程時使用的工廠。自定義線程工廠可以做一些額外的操作,比如統計生產的線程數等。
  • handler - 飽和策略,即超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。策略有:Abort終止並拋出異常,Discard悄悄拋棄任務,Discard-Oldest拋棄最老的任務策略,Caller-Runs將任務退回給調用者策略。

至於線程池應當配置多大的問題,一般有如下的經驗設置:

  • 如果是CPU密集型應用,則線程池大小設置為N+1。

  • 如果是IO密集型應用,則線程池大小設置為2N+1。

用有界隊列好還是×××隊列好?這種問題的答案肯定是視情況而定:

  • 有界隊列有助於避免資源耗盡的情況發生。但他帶來了新的問題:當隊列填滿後,新的任務怎麽辦?所以有界隊列適用於執行比較耗資源的任務,同時要設計好相應的飽和策略。

  • ×××隊列和有界隊列剛好相反,在資源無限的情況下可以一直接收新任務。適用於小任務,請求和處理速度相對持平的狀況。

  • 其實還有一種同步移交的隊列 SynchronousQueue ,這種隊列不存儲任務信息,直接將任務提交給線程池。可以理解為容量只有1的有界隊列,在特殊場景下有特殊作用,同樣得設計好相應的飽和策略。

如何使用阻塞隊列實現一個生產者和消費者模型?請寫代碼

下面這是一個完整的生產者消費者代碼例子,對比傳統的wait、nofity代碼,它更易於理解。

ProducerConsumerPattern.java如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ProducerConsumerPattern {
    public static void main(String args[]){
        //Creating shared object
        BlockingQueue sharedQueue = new LinkedBlockingQueue();
        //Creating Producer and Consumer Thread
        Thread prodThread = new Thread(new Producer(sharedQueue));
        Thread consThread = new Thread(new Consumer(sharedQueue));
        //Starting producer and Consumer thread
        prodThread.start();
        consThread.start();
    }
}

生產者,Producer.java如下:

class Producer implements Runnable {
    private final BlockingQueue sharedQueue;
    public Producer(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }
    @Override
        public void run() {
        for (int i=0; i<10; i++){
            try {
                System.out.println("Produced: " + i);
                sharedQueue.put(i);
            }
            catch (InterruptedException ex) {
                Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

消費者,Consumer.java如下所示:

class Consumer implements Runnable{
    private final BlockingQueue sharedQueue;
    public Consumer (BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }
    @Override
        public void run() {
        while(true){
            try {
                System.out.println("Consumed: "+ sharedQueue.take());
            }
            catch (InterruptedException ex) {
                Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}

Java中的鎖

如何實現樂觀鎖(CAS)?如何避免ABA問題?

CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麽處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的沖突檢查+數據更新的原理是一樣的。

這裏再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

ABA問題:

比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然後two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然後one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。

解決方法:通過版本號(version)的方式來解決,每次比較要比較數據的值和版本號兩項內容即可。

讀寫鎖可以用於什麽應用場景?

在多線程的環境下,對同一份數據進行讀寫,會涉及到線程安全的問題。比如在一個線程讀取數據的時候,另外一個線程在寫數據,而導致前後數據的不一致性;一個線程在寫數據的時候,另一個線程也在寫,同樣也會導致線程前後看到的數據的不一致性。

這時候可以在讀寫方法中加入互斥鎖,任何時候只能允許一個線程的一個讀或寫操作,而不允許其他線程的讀或寫操作,這樣是可以解決這樣以上的問題,但是效率卻大打折扣了。因為在真實的業務場景中,一份數據,讀取數據的操作次數通常高於寫入數據的操作,而線程與線程間的讀讀操作是不涉及到線程安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就行了。

對於以上這種情況,讀寫鎖是最好的解決方案!其中它的實現類:ReentrantReadWriteLock--顧名思義是可重入的讀寫鎖,允許多個讀線程獲得ReadLock,但只允許一個寫線程獲得WriteLock

讀寫鎖的機制:

  • "讀-讀" 不互斥
  • "讀-寫" 互斥
  • "寫-寫" 互斥

什麽時候應該使用可重入鎖?

可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數獲得鎖之後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。

在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖。

什麽場景下可以使用volatile替換synchronized?

狀態標誌:把簡單地volatile變量作為狀態標誌,來達成線程之間通訊的目的,省去了用synchronized還要wait,notify或者interrupt的編碼麻煩。

替換重量級鎖:如果某個變量僅是單次讀或者單次寫操作,沒有復合操作(i++,先檢查後判斷之類的)就可以用volatile替換synchronized。

並發工具

如何實現一個流控程序,用於控制請求的調用次數?

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
 * 阻塞訪問的線程,直到獲取了訪問令牌
 * @author xuexiaolei
 * @version 2017年11月15日
 */
public class FlowControl2 {
    private final static int MAX_COUNT = 10;
    private final Semaphore semaphore = new Semaphore(MAX_COUNT);
    private final ExecutorService exec = Executors.newCachedThreadPool();
    public void access(int i){
        exec.submit(new Runnable() {
            @Override public void run() {
                semaphore.acquireUninterruptibly();
                doSomething(i);
                semaphore.release();
            }
        }
        );
    }
    public void doSomething(int i){
        try {
            Thread.sleep(new Random().nextint(100));
            System.out.println(String.format("%s 通過線程:%s 訪問成功",i,Thread.currentThread().getName()));
        }
        catch (InterruptedException e) {
        }
    }
    public static void main(String[] args) {
        FlowControl2 web = new FlowControl2();
        for (int i = 0; i < 2000; i++) {
            web.access(i);
        }
    }
}

史上最全的Java並發面試題(珍藏版)