Java併發程式設計八股文!

程式設計師大彬發表於2021-09-26

Java併發

大家好,我是大彬。最近在面試,看了很多面經,將常見的Java併發程式設計常見面試題總結了一下,如果對你有幫助,可以收藏和點贊後續還會繼續更新新的面試題目哦!

文章目錄如下:

首先給大家分享一個github倉庫,上面放了200多本經典的計算機書籍,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

github地址:https://github.com/Tyson0314/...

如果github訪問不了,可以訪問gitee倉庫。

gitee地址:https://gitee.com/tysondai/ja...

執行緒池

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

為什麼使用執行緒池?

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。統一管理執行緒,避免系統建立大量同類執行緒而導致消耗完記憶體。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

執行緒池執行原理?

建立新的執行緒需要獲取全域性鎖,通過這種設計可以儘量避免獲取全域性鎖,當 ThreadPoolExecutor 完成預熱之後(當前執行的執行緒數大於等於 corePoolSize),提交的大部分任務都會被放到 BlockingQueue。

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

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

執行緒池引數有哪些?

ThreadPoolExecutor 的通用建構函式:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:當有新任務時,如果執行緒池中執行緒數沒有達到執行緒池的基本大小,則會建立新的執行緒執行任務,否則將任務放入阻塞佇列。當執行緒池中存活的執行緒數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
  • maximumPoolSize:當阻塞佇列填滿時,如果執行緒池中執行緒數沒有超過最大執行緒數,則會建立新的執行緒執行任務。否則根據拒絕策略處理新任務。非核心執行緒類似於臨時借來的資源,這些執行緒在空閒時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
  • BlockingQueue:儲存等待執行的任務。
  • keepAliveTime:非核心執行緒空閒後,保持存活的時間,此引數只對非核心執行緒有效。設定為0,表示多餘的空閒執行緒會被立即終止。
  • TimeUnit:時間單位

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
  • ThreadFactory:每當執行緒池建立一個新的執行緒時,都是通過執行緒工廠方法來完成的。在 ThreadFactory 中只定義了一個方法 newThread,每當執行緒池需要建立新執行緒就會呼叫它。

    public class MyThreadFactory implements ThreadFactory {
        private final String poolName;
        
        public MyThreadFactory(String poolName) {
            this.poolName = poolName;
        }
        
        public Thread newThread(Runnable runnable) {
            return new MyAppThread(runnable, poolName);//將執行緒池名字傳遞給建構函式,用於區分不同執行緒池的執行緒
        }
    }
  • RejectedExecutionHandler:當佇列和執行緒池都滿了時,根據拒絕策略處理新任務。

    AbortPolicy:預設的策略,直接丟擲RejectedExecutionException
    DiscardPolicy:不處理,直接丟棄
    DiscardOldestPolicy:將等待佇列隊首的任務丟棄,並執行當前任務
    CallerRunsPolicy:由呼叫執行緒處理該任務

執行緒池大小怎麼設定?

如果執行緒池執行緒數量太小,當有大量請求需要處理,系統響應比較慢影響體驗,甚至會出現任務佇列大量堆積任務導致OOM。

如果執行緒池執行緒數量過大,大量執行緒可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換(cpu給執行緒分配時間片,當執行緒的cpu時間片用完後儲存狀態,以便下次繼續執行),從 而增加執行緒的執行時間,影響了整體執行效率。

CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將執行緒數設定為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個執行緒是為了防止某些原因導致的任務暫停(執行緒阻塞,如io操作,等待鎖,執行緒sleep)而帶來的影響。一旦某個執行緒被阻塞,釋放了cpu資源,而在這種情況下多出來的一個執行緒就可以充分利用 CPU 的空閒時間。

I/O 密集型任務(2N): 系統會用大部分的時間來處理 I/O 操作,而執行緒等待 I/O 操作會被阻塞,釋放 cpu資源,這時就可以將 CPU 交出給其它執行緒使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些執行緒,具體的計算方法:最佳執行緒數 = CPU核心數 (1/CPU利用率) = CPU核心數 (1 + (I/O耗時/CPU耗時)),一般可設定為2N

執行緒池的型別有哪些?適用場景?

常見的執行緒池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。這幾個都是 ExecutorService (執行緒池)例項。

FixedThreadPool

固定執行緒數的執行緒池。任何時間點,最多隻有 nThreads 個執行緒處於活動狀態執行任務。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用無界佇列 LinkedBlockingQueue(佇列容量為 Integer.MAX_VALUE),執行中的執行緒池不會拒絕任務,即不會呼叫RejectedExecutionHandler.rejectedExecution()方法。

maxThreadPoolSize 是無效引數,故將它的值設定為與 coreThreadPoolSize 一致。

keepAliveTime 也是無效引數,設定為0L,因為此執行緒池裡所有執行緒都是核心執行緒,核心執行緒不會被回收(除非設定了executor.allowCoreThreadTimeOut(true))。

適用場景:適用於處理CPU密集型的任務,確保CPU在長期被工作執行緒使用的情況下,儘可能的少的分配執行緒,即適用執行長期的任務。需要注意的是,FixedThreadPool 不會拒絕任務,在任務比較多的時候會導致 OOM。

SingleThreadExecutor

只有一個執行緒的執行緒池。

public static ExecutionService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用無界佇列 LinkedBlockingQueue。執行緒池只有一個執行的執行緒,新來的任務放入工作佇列,執行緒處理完任務就迴圈從佇列裡獲取任務執行。保證順序的執行各個任務。

適用場景:適用於序列執行任務的場景,一個任務一個任務地執行。在任務比較多的時候也是會導致 OOM。

CachedThreadPool

根據需要建立新執行緒的執行緒池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

如果主執行緒提交任務的速度高於執行緒處理任務的速度時,CachedThreadPool 會不斷建立新的執行緒。極端情況下,這樣會導致耗盡 cpu 和記憶體資源。

使用沒有容量的SynchronousQueue作為執行緒池工作佇列,當執行緒池有空閒執行緒時,SynchronousQueue.offer(Runnable task)提交的任務會被空閒執行緒處理,否則會建立新的執行緒處理任務。

適用場景:用於併發執行大量短期的小任務。CachedThreadPool允許建立的執行緒數量為 Integer.MAX_VALUE ,可能會建立大量執行緒,從而導致 OOM。

ScheduledThreadPoolExecutor

在給定的延遲後執行任務,或者定期執行任務。在實際專案中基本不會被用到,因為有其他方案選擇比如quartz

使用的任務佇列 DelayQueue 封裝了一個 PriorityQueuePriorityQueue 會對佇列中的任務進行排序,時間早的任務先被執行(即ScheduledFutureTasktime 變數小的先執行),如果time相同則先提交的任務會被先執行(ScheduledFutureTasksquenceNumber 變數小的先執行)。

執行週期任務步驟:

  1. 執行緒從 DelayQueue 中獲取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任務是指 ScheduledFutureTask的 time 大於等於當前系統的時間;
  2. 執行這個 ScheduledFutureTask
  3. 修改 ScheduledFutureTask 的 time 變數為下次將要被執行的時間;
  4. 把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

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

程式執行緒

程式是指一個記憶體中執行的應用程式,每個程式都有自己獨立的一塊記憶體空間,一個程式中可以啟動多個執行緒。
執行緒是比程式更小的執行單位,它是在一個程式中獨立的控制流,一個程式可以啟動多個執行緒,每條執行緒並行執行不同的任務。

執行緒的生命週期

初始(NEW):執行緒被構建,還沒有呼叫 start()。

執行(RUNNABLE):包括作業系統的就緒和執行兩種狀態。

阻塞(BLOCKED):一般是被動的,在搶佔資源中得不到資源,被動的掛起在記憶體,等待資源釋放將其喚醒。執行緒被阻塞會釋放CPU,不釋放記憶體。

等待(WAITING):進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。

超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。

終止(TERMINATED):表示該執行緒已經執行完畢。

圖片來源:Java併發程式設計的藝術

講一下執行緒中斷?

執行緒中斷即執行緒執行過程中被其他執行緒給打斷了,它與 stop 最大的區別是:stop 是由系統強制終止執行緒,而執行緒中斷則是給目標執行緒傳送一箇中斷訊號,如果目標執行緒沒有接收執行緒中斷的訊號並結束執行緒,執行緒則不會終止,具體是否退出或者執行其他邏輯取決於目標執行緒。

執行緒中斷三個重要的方法:

1、java.lang.Thread#interrupt

呼叫目標執行緒的interrupt()方法,給目標執行緒發一箇中斷訊號,執行緒被打上中斷標記。

2、java.lang.Thread#isInterrupted()

判斷目標執行緒是否被中斷,不會清除中斷標記。

3、java.lang.Thread#interrupted

判斷目標執行緒是否被中斷,會清除中斷標記。

private static void test2() {
    Thread thread = new Thread(() -> {
        while (true) {
            Thread.yield();

            // 響應中斷
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Java技術棧執行緒被中斷,程式退出。");
                return;
            }
        }
    });
    thread.start();
    thread.interrupt();
}

建立執行緒有哪幾種方式?

  • 通過擴充套件Thread類來建立多執行緒
  • 通過實現Runnable介面來建立多執行緒,可實現執行緒間的資源共享
  • 實現Callable介面,通過FutureTask介面建立執行緒。
  • 使用Executor框架來建立執行緒池。

繼承 Thread 建立執行緒程式碼如下。run()方法是由jvm建立完作業系統級執行緒後回撥的方法,不可以手動呼叫,手動呼叫相當於呼叫普通方法。

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:15
 */
public class MyThread extends Thread {
    public MyThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread() + ":" + i);
        }
    }

    public static void main(String[] args) {
        MyThread mThread1 = new MyThread();
        MyThread mThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();
    }
}

Runnable 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:04
 */
public class RunnableTest {
    public static  void main(String[] args){
        Runnable1 r = new Runnable1();
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("主執行緒:["+Thread.currentThread().getName()+"]");
    }
}

class Runnable1 implements Runnable{
    @Override
    public void run() {
        System.out.println("當前執行緒:"+Thread.currentThread().getName());
    }
}

實現Runnable介面比繼承Thread類所具有的優勢:

  1. 資源共享,適合多個相同的程式程式碼的執行緒去處理同一個資源
  2. 可以避免java中的單繼承的限制
  3. 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類

Callable 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:21
 */
public class CallableTest {
    public static void main(String[] args) {
        Callable1 c = new Callable1();

        //非同步計算的結果
        FutureTask<Integer> result = new FutureTask<>(c);

        new Thread(result).start();

        try {
            //等待任務完成,返回結果
            int sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}

class Callable1 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

使用 Executor 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:44
 */
public class ExecutorsTest {
    public static void main(String[] args) {
        //獲取ExecutorService例項,生產禁用,需要手動建立執行緒池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //提交任務
        executorService.submit(new RunnableDemo());
    }
}

class RunnableDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("大彬");
    }
}

什麼是執行緒死鎖?

多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於執行緒被無限期地阻塞,因此程式不可能正常終止。

如下圖所示,執行緒 A 持有資源 2,執行緒 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個執行緒就會互相等待而進入死鎖狀態。

下面通過例子說明執行緒死鎖,程式碼來自併發程式設計之美。

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "執行緒 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "執行緒 2").start();
    }
}

程式碼輸出如下:

Thread[執行緒 1,5,main]get resource1
Thread[執行緒 2,5,main]get resource2
Thread[執行緒 1,5,main]waiting get resource2
Thread[執行緒 2,5,main]waiting get resource1

執行緒 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過 Thread.sleep(1000);讓執行緒 A 休眠 1s 為的是讓執行緒 B 得到執行然後獲取到 resource2 的監視器鎖。執行緒 A 和執行緒 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個執行緒就會陷入互相等待的狀態,這也就產生了死鎖。

執行緒死鎖怎麼產生?怎麼避免?

死鎖產生的四個必要條件

  • 互斥:一個資源每次只能被一個程式使用(資源獨立)
  • 請求與保持:一個程式因請求資源而阻塞時,對已獲得的資源保持不放(不釋放鎖)
  • 不剝奪:程式已獲得的資源,在未使用之前,不能強行剝奪(搶奪資源)
  • 迴圈等待:若干程式之間形成一種頭尾相接的迴圈等待的資源關閉(死迴圈)

避免死鎖的方法:

  • 第一個條件 "互斥" 是不能破壞的,因為加鎖就是為了保證互斥
  • 一次性申請所有的資源,破壞 "佔有且等待" 條件
  • 佔有部分資源的執行緒進一步申請其他資源時,如果申請不到,主動釋放它佔有的資源,破壞 "不可搶佔" 條件
  • 按序申請資源,破壞 "迴圈等待" 條件

執行緒run和start的區別?

呼叫 start() 方法是用來啟動執行緒的,輪到該執行緒執行時,會自動呼叫 run();直接呼叫 run() 方法,無法達到啟動多執行緒的目的,相當於主執行緒線性執行 Thread 物件的 run() 方法。
一個執行緒對線的 start() 方法只能呼叫一次,多次呼叫會丟擲 java.lang.IllegalThreadStateException 異常;run() 方法沒有限制。

執行緒都有哪些方法?

join

Thread.join(),在main中建立了thread執行緒,在main中呼叫了thread.join()/thread.join(long millis),main執行緒放棄cpu控制權,執行緒進入WAITING/TIMED_WAITING狀態,等到thread執行緒執行完才繼續執行main執行緒。

public final void join() throws InterruptedException {
    join(0);
}

yield

Thread.yield(),一定是當前執行緒呼叫此方法,當前執行緒放棄獲取的CPU時間片,但不釋放鎖資源,由執行狀態變為就緒狀態,讓OS再次選擇執行緒。作用:讓相同優先順序的執行緒輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由使用者指定暫停多長時間。

public static native void yield(); //static方法

sleep

Thread.sleep(long millis),一定是當前執行緒呼叫此方法,當前執行緒進入TIMED_WAITING狀態,讓出cpu資源,但不釋放物件鎖,指定時間到後又恢復執行。作用:給其它執行緒執行機會的最佳方式。

public static native void sleep(long millis) throws InterruptedException;//static方法

volatile底層原理

volatile是輕量級的同步機制,volatile保證變數對所有執行緒的可見性,不保證原子性。

  1. 當對volatile變數進行寫操作的時候,JVM會向處理器傳送一條LOCK字首的指令,將該變數所在快取行的資料寫回系統記憶體。
  2. 由於快取一致性協議,每個處理器通過嗅探在匯流排上傳播的資料來檢查自己的快取是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行置為無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取中。

MESI(快取一致性協議):當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,就會從記憶體重新讀取。

volatile關鍵字的兩個作用:

  1. 保證了不同執行緒對共享變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
  2. 禁止進行指令重排序。

指令重排序是JVM為了優化指令,提高程式執行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度。Java編譯器會在生成指令系列時在適當的位置會插入記憶體屏障指令來禁止處理器重排序。插入一個記憶體屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。對一個volatile欄位進行寫操作,Java記憶體模型將在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都重新整理到記憶體。

AQS原理

AQS,AbstractQueuedSynchronizer,抽象佇列同步器,定義了一套多執行緒訪問共享資源的同步器框架,許多併發工具的實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一個volatile的int型別的成員變數state來表示同步狀態,通過CAS修改同步狀態的值。當執行緒呼叫 lock 方法時 ,如果 state=0,說明沒有任何執行緒佔有共享資源的鎖,可以獲得鎖並將 state=1。如果 state=1,則說明有執行緒目前正在使用共享變數,其他執行緒必須加入同步佇列進行等待。

private volatile int state;//共享變數,使用volatile修飾保證執行緒可見性

同步器依賴內部的同步佇列(一個FIFO雙向佇列)來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態(獨佔或共享 )構造成為一個節點(Node)並將其加入同步佇列並進行自旋,當同步狀態釋放時,會把首節中的後繼節點對應的執行緒喚醒,使其再次嘗試獲取同步狀態。

synchronized的用法有哪些?

  1. 修飾普通方法:作用於當前物件例項,進入同步程式碼前要獲得當前物件例項的鎖
  2. 修飾靜態方法:作用於當前類,進入同步程式碼前要獲得當前類物件的鎖,synchronized關鍵字加到static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖
  3. 修飾程式碼塊:指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖

Synchronized的作用有哪些?

原子性:確保執行緒互斥的訪問同步程式碼;
可見性:保證共享變數的修改能夠及時可見,其實是通過Java記憶體模型中的 “對一個變數unlock 操作之前,必須要同步到主記憶體中;如果對一個變數進行lock操作,則將會清空工作記憶體中此變數的值,在執行引擎使用此變數前,需要重新從主記憶體中load操作或assign操作初始化變數值” 來保證的;
有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”。

synchronized 底層實現原理?

synchronized 同步程式碼塊的實現是通過 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在於每個Java物件的物件頭中, synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因) 的持有權。

其內部包含一個計數器,當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在 執行 monitorexit 指令後,將鎖計數器設為0
,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。

ReentrantLock 是如何實現可重入性的?

ReentrantLock 內部自定義了同步器 Sync,在加鎖的時候通過 CAS 演算法,將執行緒物件放到一個雙向連結串列中,每次獲取鎖的時候,檢查當前維護的那個執行緒 ID 和當前請求的執行緒 ID 是否 一致,如果一致,同步狀態加1,表示鎖被當前執行緒獲取了多次。

ReentrantLock和synchronized區別

  1. 使用synchronized關鍵字實現同步,執行緒執行完同步程式碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
  2. synchronized是非公平鎖,ReentrantLock可以設定為公平鎖。
  3. ReentrantLock上等待獲取鎖的執行緒是可中斷的,執行緒可以放棄等待鎖。而synchonized會無限期等待下去。
  4. ReentrantLock 可以設定超時獲取鎖。在指定的截止時間之前獲取鎖,如果截止時間到了還沒有獲取到鎖,則返回。
  5. ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的獲取鎖,呼叫該方法後立刻返回,如果能夠獲取則返回true,否則返回false。

wait()和sleep()的區別

相同點:

  1. 使當前執行緒暫停執行,把機會交給其他執行緒
  2. 任何執行緒在等待期間被中斷都會丟擲InterruptedException

不同點:

  1. wait() 是Object超類中的方法;而sleep()是執行緒Thread類中的方法
  2. 對鎖的持有不同,wait()會釋放鎖,而sleep()並不釋放鎖
  3. 喚醒方法不完全相同,wait() 依靠notify或者notifyAll 、中斷、達到指定時間來喚醒;而sleep()到達指定時間被喚醒
  4. 呼叫obj.wait()需要先獲取物件的鎖,而 Thread.sleep()不用

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

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

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

Runnable和 Callable有什麼區別?

  • Callable介面方法是call(),Runnable的方法是run();
  • Callable介面call方法有返回值,支援泛型,Runnable介面run方法無返回值。
  • Callable介面call()方法允許丟擲異常;而Runnable介面run()方法不能繼續上拋異常;

volatile和synchronized的區別是什麼?

  1. volatile只能使用在變數上;而synchronized可以在類,變數,方法和程式碼塊上。
  2. volatile至保證可見性;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序;synchronized不會。
  4. volatile不會造成阻塞;synchronized會。

執行緒執行順序怎麼控制?

假設有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次

樂觀鎖一定就是好的嗎?

樂觀鎖避免了悲觀鎖獨佔物件的現象,提高了併發效能,但它也有缺點:

  • 樂觀鎖只能保證一個共享變數的原子操作。如果多一個或幾個變數,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管物件數量多少及物件顆粒度大小。
  • 長時間自旋可能導致開銷大。假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷。
  • ABA 問題。CAS 的核心思想是通過比對記憶體值與預期值是否一樣而判 斷記憶體值是否被改過,但這個判斷邏輯不嚴謹,假如記憶體值原來是 A, 後來被一條執行緒改為 B,最後又被改成了 A,則 CAS 認為此記憶體值並 沒有發生改變,但實際上是有被其他執行緒改過的,這種情況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變數更新都把版本號加一。

守護執行緒是什麼?

守護執行緒是執行在後臺的一種特殊程式。它獨立於控制終端並且週期性地執行某種任務或等待處理某些 發生的事件。在 Java 中垃圾回收執行緒就是特殊的守護執行緒。

執行緒間通訊方式

volatile

volatile是輕量級的同步機制,volatile保證變數對所有執行緒的可見性,不保證原子性。

synchronized

保證執行緒對變數訪問的可見性和排他性。

等待通知機制

wait/notify為 Object 物件的方法,呼叫wait/notify需要先獲得物件的鎖。物件呼叫wait之後執行緒釋放鎖,將執行緒放到物件的等待佇列,當通知執行緒呼叫此物件的notify()方法後,等待執行緒並不會立即從wait返回,需要等待通知執行緒釋放鎖(通知執行緒執行完同步程式碼塊),等待佇列裡的執行緒獲取鎖,獲取鎖成功才能從wait()方法返回,即從wait方法返回前提是執行緒獲得鎖。

等待通知機制依託於同步機制,目的是確保等待執行緒從wait方法返回時能感知到通知執行緒對物件的變數值的修改。

ThreadLocal

執行緒本地變數。當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒。

ThreadLocal原理

每個執行緒都有一個ThreadLocalMap(ThreadLocal內部類),Map中元素的鍵為ThreadLocal,而值對應執行緒的變數副本。

呼叫threadLocal.set()-->呼叫getMap(Thread)-->返回當前執行緒的ThreadLocalMap<ThreadLocal, value>-->map.set(this, value),this是ThreadLocal

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

呼叫get()-->呼叫getMap(Thread)-->返回當前執行緒的ThreadLocalMap<ThreadLocal, value>-->map.getEntry(this),返回value

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

threadLocals的型別ThreadLocalMap的鍵為ThreadLocal物件,因為每個執行緒中可有多個threadLocal變數,如longLocal和stringLocal。

public class ThreadLocalDemo {
    ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {
        longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {
        return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {
            threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal 並不是用來解決共享資源的多執行緒訪問的問題,因為每個執行緒中的資源只是副本,並不共享。因此ThreadLocal適合作為執行緒上下文變數,簡化執行緒內傳參。

ThreadLocal記憶體洩漏的原因?

每個Thread都有⼀個ThreadLocalMap的內部屬性,map的key是ThreaLocal,定義為弱引用,value是強引用型別。GC的時候會⾃動回收key,而value的回收取決於Thread物件的生命週期。一般會通過執行緒池的方式複用Thread物件節省資源,這也就導致了Thread物件的生命週期比較長,這樣便一直存在一條強引用鏈的關係:Thread --> ThreadLocalMap-->Entry-->Value,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致記憶體洩漏。

image-20200715235804982

解決⽅法:每次使⽤完ThreadLocal就調⽤它的remove()⽅法,手動將對應的鍵值對刪除,從⽽避免記憶體洩漏。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal使用場景有哪些?

ThreadLocal 適用場景:每個執行緒需要有自己單獨的例項,且需要在多個方法中共享例項,即同時滿足例項線上程間的隔離與方法間的共享。比如Java web應用中,每個執行緒有自己單獨的 Session 例項,就可以使用ThreadLocal來實現。

鎖的分類

公平鎖與非公平鎖

按照執行緒訪問順序獲取物件鎖。synchronized 是非公平鎖, Lock 預設是非公平鎖,可以設定為公平鎖,公平鎖會影響效能。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

共享式與獨佔式鎖

共享式與獨佔式的最主要區別在於:同一時刻獨佔式只能有一個執行緒獲取同步狀態,而共享式在同一時刻可以有多個執行緒獲取同步狀態。例如讀操作可以有多個執行緒同時進行,而寫操作同一時刻只能有一個執行緒進行寫操作,其他操作都會被阻塞。

悲觀鎖與樂觀鎖

悲觀鎖,每次訪問資源都會加鎖,執行完同步程式碼釋放鎖,synchronized 和 ReentrantLock 屬於悲觀鎖。

樂觀鎖,不會鎖定資源,所有的執行緒都能訪問並修改同一個資源,如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。樂觀鎖最常見的實現就是CAS。

樂觀鎖一般來說有以下2種方式:

  1. 使用資料版本記錄機制實現,這是樂觀鎖最常用的一種實現方式。給資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的version欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
  2. 使用時間戳。資料庫表增加一個欄位,欄位型別使用時間戳(timestamp),和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。

適用場景:

  • 悲觀鎖適合寫操作多的場景。
  • 樂觀鎖適合讀操作多的場景,不加鎖可以提升讀操作的效能。

CAS

什麼是CAS?

CAS全稱 Compare And Swap,比較與交換,是樂觀鎖的主要實現方式。CAS 在不使用鎖的情況下實現多執行緒之間的變數同步。ReentrantLock 內部的 AQS 和原子類內部都使用了 CAS。

CAS演算法涉及到三個運算元:

  • 需要讀寫的記憶體值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

只有當 V 的值等於 A 時,才會使用原子方式用新值B來更新V的值,否則會繼續重試直到成功更新值。

以 AtomicInteger 為例,AtomicInteger 的 getAndIncrement()方法底層就是CAS實現,關鍵程式碼是 compareAndSwapInt(obj, offset, expect, update),其含義就是,如果obj內的valueexpect相等,就證明沒有其他執行緒改變過這個變數,那麼就更新它為update,如果不相等,那就會繼續重試直到成功更新值。

CAS存在的問題?

CAS 三大問題:

  1. ABA問題。CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面新增版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從A-B-A變成了1A-2B-3A

    JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,原子更新帶有版本號的引用型別。

  2. 迴圈時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
  3. 只能保證一個共享變數的原子操作。對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的。

    Java從1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,可以把多個變數放在一個物件裡來進行CAS操作。

併發工具

在JDK的併發包裡提供了幾個非常有用的併發工具類。CountDownLatch、CyclicBarrier和Semaphore工具類提供了一種併發流程控制的手段。

CountDownLatch

CountDownLatch用於某個執行緒等待其他執行緒執行完任務再執行,與thread.join()功能類似。常見的應用場景是開啟多個執行緒同時執行某個任務,等到所有任務執行完再執行特定操作,如彙總統計結果。

public class CountDownLatchDemo {
    static final int N = 4;
    static CountDownLatch latch = new CountDownLatch(N);

    public static void main(String[] args) throws InterruptedException {

       for(int i = 0; i < N; i++) {
            new Thread(new Thread1()).start();
       }

       latch.await(1000, TimeUnit.MILLISECONDS); //呼叫await()方法的執行緒會被掛起,它會等待直到count值為0才繼續執行;等待timeout時間後count值還沒變為0的話就會繼續執行
       System.out.println("task finished");
    }

    static class Thread1 implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "starts working");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        }
    }
}

執行結果:

Thread-0starts working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task finished

CyclicBarrier

CyclicBarrier(同步屏障),用於一組執行緒互相等待到某個狀態,然後這組執行緒再同時執行。

public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

引數parties指讓多少個執行緒或者任務等待至某個狀態;引數barrierAction為當這些執行緒都達到某個狀態時會執行的內容。

public class CyclicBarrierTest {
    // 請求的數量
    private static final int threadCount = 10;
    // 需要同步的執行緒數量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {
        // 建立執行緒池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
        System.out.println("threadnum:" + threadnum + "is ready");
        try {
            /**等待60秒,保證子執行緒完全執行結束*/
            cyclicBarrier.await(60, TimeUnit.SECONDS);
        } catch (Exception e) {
            System.out.println("-----CyclicBarrierException------");
        }
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

執行結果如下,可以看出CyclicBarrier是可以重用的:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

當四個執行緒都到達barrier狀態後,會從四個執行緒中選擇一個執行緒去執行Runnable。

CyclicBarrier和CountDownLatch區別

CyclicBarrier 和 CountDownLatch 都能夠實現執行緒之間的等待。

CountDownLatch用於某個執行緒等待其他執行緒執行完任務再執行。CyclicBarrier用於一組執行緒互相等待到某個狀態,然後這組執行緒再同時執行。
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可用於處理更為複雜的業務場景。

Semaphore

Semaphore類似於鎖,它用於控制同時訪問特定資源的執行緒數量,控制併發執行緒數。

public class SemaphoreDemo {
    public static void main(String[] args) {
        final int N = 7;
        Semaphore s = new Semaphore(3);
        for(int i = 0; i < N; i++) {
            new Worker(s, i).start();
        }
    }

    static class Worker extends Thread {
        private Semaphore s;
        private int num;
        public Worker(Semaphore s, int num) {
            this.s = s;
            this.num = num;
        }

        @Override
        public void run() {
            try {
                s.acquire();
                System.out.println("worker" + num +  " using the machine");
                Thread.sleep(1000);
                System.out.println("worker" + num +  " finished the task");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果如下,可以看出並非按照執行緒訪問順序獲取資源的鎖,即

worker0 using the machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 finished the task

原子類

基本型別原子類

使用原子的方式更新基本型別

  • AtomicInteger:整型原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean :布林型原子類

AtomicInteger 類常用的方法:

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設定新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設定為輸入值(update)
public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。

AtomicInteger 類主要利用 CAS (compare and swap) 保證原子操作,從而避免加鎖的高開銷。

陣列型別原子類

使用原子的方式更新陣列裡的某個元素

  • AtomicIntegerArray:整形陣列原子類
  • AtomicLongArray:長整形陣列原子類
  • AtomicReferenceArray :引用型別陣列原子類

AtomicIntegerArray 類常用方法:

public final int get(int i) //獲取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的當前的值,並將其設定為新值:newValue
public final int getAndIncrement(int i)//獲取 index=i 位置元素的值,並讓該位置的元素自增
public final int getAndDecrement(int i) //獲取 index=i 位置元素的值,並讓該位置的元素自減
public final int getAndAdd(int i, int delta) //獲取 index=i 位置元素的值,並加上預期的值
boolean compareAndSet(int i, int expect, int update) //如果輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設定為輸入值(update)
public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。

引用型別原子類

  • AtomicReference:引用型別原子類
  • AtomicStampedReference:帶有版本號的引用型別原子類。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
  • AtomicMarkableReference :原子更新帶有標記的引用型別。該類將 boolean 標記與引用關聯起來

給大家分享一個github倉庫,上面放了200多本經典的計算機書籍,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

github地址:https://github.com/Tyson0314/...

如果github訪問不了,可以訪問gitee倉庫。

gitee地址:https://gitee.com/tysondai/ja...

Java併發

大家好,我是大彬。最近在面試,看了很多面經,將常見的Java併發程式設計常見面試題總結了一下,如果對你有幫助,可以收藏和點贊後續還會繼續更新新的面試題目哦!

文章目錄如下:

執行緒池

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

為什麼使用執行緒池?

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。統一管理執行緒,避免系統建立大量同類執行緒而導致消耗完記憶體。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

執行緒池執行原理?

建立新的執行緒需要獲取全域性鎖,通過這種設計可以儘量避免獲取全域性鎖,當 ThreadPoolExecutor 完成預熱之後(當前執行的執行緒數大於等於 corePoolSize),提交的大部分任務都會被放到 BlockingQueue。

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

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

執行緒池引數有哪些?

ThreadPoolExecutor 的通用建構函式:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:當有新任務時,如果執行緒池中執行緒數沒有達到執行緒池的基本大小,則會建立新的執行緒執行任務,否則將任務放入阻塞佇列。當執行緒池中存活的執行緒數總是大於 corePoolSize 時,應該考慮調大 corePoolSize。
  • maximumPoolSize:當阻塞佇列填滿時,如果執行緒池中執行緒數沒有超過最大執行緒數,則會建立新的執行緒執行任務。否則根據拒絕策略處理新任務。非核心執行緒類似於臨時借來的資源,這些執行緒在空閒時間超過 keepAliveTime 之後,就應該退出,避免資源浪費。
  • BlockingQueue:儲存等待執行的任務。
  • keepAliveTime:非核心執行緒空閒後,保持存活的時間,此引數只對非核心執行緒有效。設定為0,表示多餘的空閒執行緒會被立即終止。
  • TimeUnit:時間單位

    TimeUnit.DAYS
    TimeUnit.HOURS
    TimeUnit.MINUTES
    TimeUnit.SECONDS
    TimeUnit.MILLISECONDS
    TimeUnit.MICROSECONDS
    TimeUnit.NANOSECONDS
  • ThreadFactory:每當執行緒池建立一個新的執行緒時,都是通過執行緒工廠方法來完成的。在 ThreadFactory 中只定義了一個方法 newThread,每當執行緒池需要建立新執行緒就會呼叫它。

    public class MyThreadFactory implements ThreadFactory {
        private final String poolName;
        
        public MyThreadFactory(String poolName) {
            this.poolName = poolName;
        }
        
        public Thread newThread(Runnable runnable) {
            return new MyAppThread(runnable, poolName);//將執行緒池名字傳遞給建構函式,用於區分不同執行緒池的執行緒
        }
    }
  • RejectedExecutionHandler:當佇列和執行緒池都滿了時,根據拒絕策略處理新任務。

    AbortPolicy:預設的策略,直接丟擲RejectedExecutionException
    DiscardPolicy:不處理,直接丟棄
    DiscardOldestPolicy:將等待佇列隊首的任務丟棄,並執行當前任務
    CallerRunsPolicy:由呼叫執行緒處理該任務

執行緒池大小怎麼設定?

如果執行緒池執行緒數量太小,當有大量請求需要處理,系統響應比較慢影響體驗,甚至會出現任務佇列大量堆積任務導致OOM。

如果執行緒池執行緒數量過大,大量執行緒可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換(cpu給執行緒分配時間片,當執行緒的cpu時間片用完後儲存狀態,以便下次繼續執行),從 而增加執行緒的執行時間,影響了整體執行效率。

CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將執行緒數設定為 N(CPU 核心數)+1,比 CPU 核心數多出來的一個執行緒是為了防止某些原因導致的任務暫停(執行緒阻塞,如io操作,等待鎖,執行緒sleep)而帶來的影響。一旦某個執行緒被阻塞,釋放了cpu資源,而在這種情況下多出來的一個執行緒就可以充分利用 CPU 的空閒時間。

I/O 密集型任務(2N): 系統會用大部分的時間來處理 I/O 操作,而執行緒等待 I/O 操作會被阻塞,釋放 cpu資源,這時就可以將 CPU 交出給其它執行緒使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些執行緒,具體的計算方法:最佳執行緒數 = CPU核心數 (1/CPU利用率) = CPU核心數 (1 + (I/O耗時/CPU耗時)),一般可設定為2N

執行緒池的型別有哪些?適用場景?

常見的執行緒池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。這幾個都是 ExecutorService (執行緒池)例項。

FixedThreadPool

固定執行緒數的執行緒池。任何時間點,最多隻有 nThreads 個執行緒處於活動狀態執行任務。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用無界佇列 LinkedBlockingQueue(佇列容量為 Integer.MAX_VALUE),執行中的執行緒池不會拒絕任務,即不會呼叫RejectedExecutionHandler.rejectedExecution()方法。

maxThreadPoolSize 是無效引數,故將它的值設定為與 coreThreadPoolSize 一致。

keepAliveTime 也是無效引數,設定為0L,因為此執行緒池裡所有執行緒都是核心執行緒,核心執行緒不會被回收(除非設定了executor.allowCoreThreadTimeOut(true))。

適用場景:適用於處理CPU密集型的任務,確保CPU在長期被工作執行緒使用的情況下,儘可能的少的分配執行緒,即適用執行長期的任務。需要注意的是,FixedThreadPool 不會拒絕任務,在任務比較多的時候會導致 OOM。

SingleThreadExecutor

只有一個執行緒的執行緒池。

public static ExecutionService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用無界佇列 LinkedBlockingQueue。執行緒池只有一個執行的執行緒,新來的任務放入工作佇列,執行緒處理完任務就迴圈從佇列裡獲取任務執行。保證順序的執行各個任務。

適用場景:適用於序列執行任務的場景,一個任務一個任務地執行。在任務比較多的時候也是會導致 OOM。

CachedThreadPool

根據需要建立新執行緒的執行緒池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

如果主執行緒提交任務的速度高於執行緒處理任務的速度時,CachedThreadPool 會不斷建立新的執行緒。極端情況下,這樣會導致耗盡 cpu 和記憶體資源。

使用沒有容量的SynchronousQueue作為執行緒池工作佇列,當執行緒池有空閒執行緒時,SynchronousQueue.offer(Runnable task)提交的任務會被空閒執行緒處理,否則會建立新的執行緒處理任務。

適用場景:用於併發執行大量短期的小任務。CachedThreadPool允許建立的執行緒數量為 Integer.MAX_VALUE ,可能會建立大量執行緒,從而導致 OOM。

ScheduledThreadPoolExecutor

在給定的延遲後執行任務,或者定期執行任務。在實際專案中基本不會被用到,因為有其他方案選擇比如quartz

使用的任務佇列 DelayQueue 封裝了一個 PriorityQueuePriorityQueue 會對佇列中的任務進行排序,時間早的任務先被執行(即ScheduledFutureTasktime 變數小的先執行),如果time相同則先提交的任務會被先執行(ScheduledFutureTasksquenceNumber 變數小的先執行)。

執行週期任務步驟:

  1. 執行緒從 DelayQueue 中獲取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任務是指 ScheduledFutureTask的 time 大於等於當前系統的時間;
  2. 執行這個 ScheduledFutureTask
  3. 修改 ScheduledFutureTask 的 time 變數為下次將要被執行的時間;
  4. 把這個修改 time 之後的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

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

程式執行緒

程式是指一個記憶體中執行的應用程式,每個程式都有自己獨立的一塊記憶體空間,一個程式中可以啟動多個執行緒。
執行緒是比程式更小的執行單位,它是在一個程式中獨立的控制流,一個程式可以啟動多個執行緒,每條執行緒並行執行不同的任務。

執行緒的生命週期

初始(NEW):執行緒被構建,還沒有呼叫 start()。

執行(RUNNABLE):包括作業系統的就緒和執行兩種狀態。

阻塞(BLOCKED):一般是被動的,在搶佔資源中得不到資源,被動的掛起在記憶體,等待資源釋放將其喚醒。執行緒被阻塞會釋放CPU,不釋放記憶體。

等待(WAITING):進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。

超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。

終止(TERMINATED):表示該執行緒已經執行完畢。

圖片來源:Java併發程式設計的藝術

講一下執行緒中斷?

執行緒中斷即執行緒執行過程中被其他執行緒給打斷了,它與 stop 最大的區別是:stop 是由系統強制終止執行緒,而執行緒中斷則是給目標執行緒傳送一箇中斷訊號,如果目標執行緒沒有接收執行緒中斷的訊號並結束執行緒,執行緒則不會終止,具體是否退出或者執行其他邏輯取決於目標執行緒。

執行緒中斷三個重要的方法:

1、java.lang.Thread#interrupt

呼叫目標執行緒的interrupt()方法,給目標執行緒發一箇中斷訊號,執行緒被打上中斷標記。

2、java.lang.Thread#isInterrupted()

判斷目標執行緒是否被中斷,不會清除中斷標記。

3、java.lang.Thread#interrupted

判斷目標執行緒是否被中斷,會清除中斷標記。

private static void test2() {
    Thread thread = new Thread(() -> {
        while (true) {
            Thread.yield();

            // 響應中斷
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Java技術棧執行緒被中斷,程式退出。");
                return;
            }
        }
    });
    thread.start();
    thread.interrupt();
}

建立執行緒有哪幾種方式?

  • 通過擴充套件Thread類來建立多執行緒
  • 通過實現Runnable介面來建立多執行緒,可實現執行緒間的資源共享
  • 實現Callable介面,通過FutureTask介面建立執行緒。
  • 使用Executor框架來建立執行緒池。

繼承 Thread 建立執行緒程式碼如下。run()方法是由jvm建立完作業系統級執行緒後回撥的方法,不可以手動呼叫,手動呼叫相當於呼叫普通方法。

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:15
 */
public class MyThread extends Thread {
    public MyThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread() + ":" + i);
        }
    }

    public static void main(String[] args) {
        MyThread mThread1 = new MyThread();
        MyThread mThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        mThread1.start();
        mThread2.start();
        myThread3.start();
    }
}

Runnable 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:04
 */
public class RunnableTest {
    public static  void main(String[] args){
        Runnable1 r = new Runnable1();
        Thread thread = new Thread(r);
        thread.start();
        System.out.println("主執行緒:["+Thread.currentThread().getName()+"]");
    }
}

class Runnable1 implements Runnable{
    @Override
    public void run() {
        System.out.println("當前執行緒:"+Thread.currentThread().getName());
    }
}

實現Runnable介面比繼承Thread類所具有的優勢:

  1. 資源共享,適合多個相同的程式程式碼的執行緒去處理同一個資源
  2. 可以避免java中的單繼承的限制
  3. 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類

Callable 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:21
 */
public class CallableTest {
    public static void main(String[] args) {
        Callable1 c = new Callable1();

        //非同步計算的結果
        FutureTask<Integer> result = new FutureTask<>(c);

        new Thread(result).start();

        try {
            //等待任務完成,返回結果
            int sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}

class Callable1 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

使用 Executor 建立執行緒程式碼

/**
 * @author: 程式設計師大彬
 * @time: 2021-09-11 10:44
 */
public class ExecutorsTest {
    public static void main(String[] args) {
        //獲取ExecutorService例項,生產禁用,需要手動建立執行緒池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //提交任務
        executorService.submit(new RunnableDemo());
    }
}

class RunnableDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("大彬");
    }
}

什麼是執行緒死鎖?

多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於執行緒被無限期地阻塞,因此程式不可能正常終止。

如下圖所示,執行緒 A 持有資源 2,執行緒 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個執行緒就會互相等待而進入死鎖狀態。

下面通過例子說明執行緒死鎖,程式碼來自併發程式設計之美。

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "執行緒 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "執行緒 2").start();
    }
}

程式碼輸出如下:

Thread[執行緒 1,5,main]get resource1
Thread[執行緒 2,5,main]get resource2
Thread[執行緒 1,5,main]waiting get resource2
Thread[執行緒 2,5,main]waiting get resource1

執行緒 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過 Thread.sleep(1000);讓執行緒 A 休眠 1s 為的是讓執行緒 B 得到執行然後獲取到 resource2 的監視器鎖。執行緒 A 和執行緒 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個執行緒就會陷入互相等待的狀態,這也就產生了死鎖。

執行緒死鎖怎麼產生?怎麼避免?

死鎖產生的四個必要條件

  • 互斥:一個資源每次只能被一個程式使用(資源獨立)
  • 請求與保持:一個程式因請求資源而阻塞時,對已獲得的資源保持不放(不釋放鎖)
  • 不剝奪:程式已獲得的資源,在未使用之前,不能強行剝奪(搶奪資源)
  • 迴圈等待:若干程式之間形成一種頭尾相接的迴圈等待的資源關閉(死迴圈)

避免死鎖的方法:

  • 第一個條件 "互斥" 是不能破壞的,因為加鎖就是為了保證互斥
  • 一次性申請所有的資源,破壞 "佔有且等待" 條件
  • 佔有部分資源的執行緒進一步申請其他資源時,如果申請不到,主動釋放它佔有的資源,破壞 "不可搶佔" 條件
  • 按序申請資源,破壞 "迴圈等待" 條件

執行緒run和start的區別?

呼叫 start() 方法是用來啟動執行緒的,輪到該執行緒執行時,會自動呼叫 run();直接呼叫 run() 方法,無法達到啟動多執行緒的目的,相當於主執行緒線性執行 Thread 物件的 run() 方法。
一個執行緒對線的 start() 方法只能呼叫一次,多次呼叫會丟擲 java.lang.IllegalThreadStateException 異常;run() 方法沒有限制。

執行緒都有哪些方法?

join

Thread.join(),在main中建立了thread執行緒,在main中呼叫了thread.join()/thread.join(long millis),main執行緒放棄cpu控制權,執行緒進入WAITING/TIMED_WAITING狀態,等到thread執行緒執行完才繼續執行main執行緒。

public final void join() throws InterruptedException {
    join(0);
}

yield

Thread.yield(),一定是當前執行緒呼叫此方法,當前執行緒放棄獲取的CPU時間片,但不釋放鎖資源,由執行狀態變為就緒狀態,讓OS再次選擇執行緒。作用:讓相同優先順序的執行緒輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。Thread.yield()不會導致阻塞。該方法與sleep()類似,只是不能由使用者指定暫停多長時間。

public static native void yield(); //static方法

sleep

Thread.sleep(long millis),一定是當前執行緒呼叫此方法,當前執行緒進入TIMED_WAITING狀態,讓出cpu資源,但不釋放物件鎖,指定時間到後又恢復執行。作用:給其它執行緒執行機會的最佳方式。

public static native void sleep(long millis) throws InterruptedException;//static方法

volatile底層原理

volatile是輕量級的同步機制,volatile保證變數對所有執行緒的可見性,不保證原子性。

  1. 當對volatile變數進行寫操作的時候,JVM會向處理器傳送一條LOCK字首的指令,將該變數所在快取行的資料寫回系統記憶體。
  2. 由於快取一致性協議,每個處理器通過嗅探在匯流排上傳播的資料來檢查自己的快取是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行置為無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取中。

MESI(快取一致性協議):當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,就會從記憶體重新讀取。

volatile關鍵字的兩個作用:

  1. 保證了不同執行緒對共享變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
  2. 禁止進行指令重排序。

指令重排序是JVM為了優化指令,提高程式執行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度。Java編譯器會在生成指令系列時在適當的位置會插入記憶體屏障指令來禁止處理器重排序。插入一個記憶體屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。對一個volatile欄位進行寫操作,Java記憶體模型將在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都重新整理到記憶體。

AQS原理

AQS,AbstractQueuedSynchronizer,抽象佇列同步器,定義了一套多執行緒訪問共享資源的同步器框架,許多併發工具的實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一個volatile的int型別的成員變數state來表示同步狀態,通過CAS修改同步狀態的值。當執行緒呼叫 lock 方法時 ,如果 state=0,說明沒有任何執行緒佔有共享資源的鎖,可以獲得鎖並將 state=1。如果 state=1,則說明有執行緒目前正在使用共享變數,其他執行緒必須加入同步佇列進行等待。

private volatile int state;//共享變數,使用volatile修飾保證執行緒可見性

同步器依賴內部的同步佇列(一個FIFO雙向佇列)來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態(獨佔或共享 )構造成為一個節點(Node)並將其加入同步佇列並進行自旋,當同步狀態釋放時,會把首節中的後繼節點對應的執行緒喚醒,使其再次嘗試獲取同步狀態。

synchronized的用法有哪些?

  1. 修飾普通方法:作用於當前物件例項,進入同步程式碼前要獲得當前物件例項的鎖
  2. 修飾靜態方法:作用於當前類,進入同步程式碼前要獲得當前類物件的鎖,synchronized關鍵字加到static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖
  3. 修飾程式碼塊:指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖

Synchronized的作用有哪些?

原子性:確保執行緒互斥的訪問同步程式碼;
可見性:保證共享變數的修改能夠及時可見,其實是通過Java記憶體模型中的 “對一個變數unlock 操作之前,必須要同步到主記憶體中;如果對一個變數進行lock操作,則將會清空工作記憶體中此變數的值,在執行引擎使用此變數前,需要重新從主記憶體中load操作或assign操作初始化變數值” 來保證的;
有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”。

synchronized 底層實現原理?

synchronized 同步程式碼塊的實現是通過 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在於每個Java物件的物件頭中, synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因) 的持有權。

其內部包含一個計數器,當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在 執行 monitorexit 指令後,將鎖計數器設為0
,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。

ReentrantLock 是如何實現可重入性的?

ReentrantLock 內部自定義了同步器 Sync,在加鎖的時候通過 CAS 演算法,將執行緒物件放到一個雙向連結串列中,每次獲取鎖的時候,檢查當前維護的那個執行緒 ID 和當前請求的執行緒 ID 是否 一致,如果一致,同步狀態加1,表示鎖被當前執行緒獲取了多次。

ReentrantLock和synchronized區別

  1. 使用synchronized關鍵字實現同步,執行緒執行完同步程式碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
  2. synchronized是非公平鎖,ReentrantLock可以設定為公平鎖。
  3. ReentrantLock上等待獲取鎖的執行緒是可中斷的,執行緒可以放棄等待鎖。而synchonized會無限期等待下去。
  4. ReentrantLock 可以設定超時獲取鎖。在指定的截止時間之前獲取鎖,如果截止時間到了還沒有獲取到鎖,則返回。
  5. ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的獲取鎖,呼叫該方法後立刻返回,如果能夠獲取則返回true,否則返回false。

wait()和sleep()的區別

相同點:

  1. 使當前執行緒暫停執行,把機會交給其他執行緒
  2. 任何執行緒在等待期間被中斷都會丟擲InterruptedException

不同點:

  1. wait() 是Object超類中的方法;而sleep()是執行緒Thread類中的方法
  2. 對鎖的持有不同,wait()會釋放鎖,而sleep()並不釋放鎖
  3. 喚醒方法不完全相同,wait() 依靠notify或者notifyAll 、中斷、達到指定時間來喚醒;而sleep()到達指定時間被喚醒
  4. 呼叫obj.wait()需要先獲取物件的鎖,而 Thread.sleep()不用

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

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

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

Runnable和 Callable有什麼區別?

  • Callable介面方法是call(),Runnable的方法是run();
  • Callable介面call方法有返回值,支援泛型,Runnable介面run方法無返回值。
  • Callable介面call()方法允許丟擲異常;而Runnable介面run()方法不能繼續上拋異常;

volatile和synchronized的區別是什麼?

  1. volatile只能使用在變數上;而synchronized可以在類,變數,方法和程式碼塊上。
  2. volatile至保證可見性;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序;synchronized不會。
  4. volatile不會造成阻塞;synchronized會。

執行緒執行順序怎麼控制?

假設有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次

樂觀鎖一定就是好的嗎?

樂觀鎖避免了悲觀鎖獨佔物件的現象,提高了併發效能,但它也有缺點:

  • 樂觀鎖只能保證一個共享變數的原子操作。如果多一個或幾個變數,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管物件數量多少及物件顆粒度大小。
  • 長時間自旋可能導致開銷大。假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷。
  • ABA 問題。CAS 的核心思想是通過比對記憶體值與預期值是否一樣而判 斷記憶體值是否被改過,但這個判斷邏輯不嚴謹,假如記憶體值原來是 A, 後來被一條執行緒改為 B,最後又被改成了 A,則 CAS 認為此記憶體值並 沒有發生改變,但實際上是有被其他執行緒改過的,這種情況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變數更新都把版本號加一。

守護執行緒是什麼?

守護執行緒是執行在後臺的一種特殊程式。它獨立於控制終端並且週期性地執行某種任務或等待處理某些 發生的事件。在 Java 中垃圾回收執行緒就是特殊的守護執行緒。

執行緒間通訊方式

volatile

volatile是輕量級的同步機制,volatile保證變數對所有執行緒的可見性,不保證原子性。

synchronized

保證執行緒對變數訪問的可見性和排他性。

等待通知機制

wait/notify為 Object 物件的方法,呼叫wait/notify需要先獲得物件的鎖。物件呼叫wait之後執行緒釋放鎖,將執行緒放到物件的等待佇列,當通知執行緒呼叫此物件的notify()方法後,等待執行緒並不會立即從wait返回,需要等待通知執行緒釋放鎖(通知執行緒執行完同步程式碼塊),等待佇列裡的執行緒獲取鎖,獲取鎖成功才能從wait()方法返回,即從wait方法返回前提是執行緒獲得鎖。

等待通知機制依託於同步機制,目的是確保等待執行緒從wait方法返回時能感知到通知執行緒對物件的變數值的修改。

ThreadLocal

執行緒本地變數。當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒。

ThreadLocal原理

每個執行緒都有一個ThreadLocalMap(ThreadLocal內部類),Map中元素的鍵為ThreadLocal,而值對應執行緒的變數副本。

呼叫threadLocal.set()-->呼叫getMap(Thread)-->返回當前執行緒的ThreadLocalMap<ThreadLocal, value>-->map.set(this, value),this是ThreadLocal

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

呼叫get()-->呼叫getMap(Thread)-->返回當前執行緒的ThreadLocalMap<ThreadLocal, value>-->map.getEntry(this),返回value

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

threadLocals的型別ThreadLocalMap的鍵為ThreadLocal物件,因為每個執行緒中可有多個threadLocal變數,如longLocal和stringLocal。

public class ThreadLocalDemo {
    ThreadLocal<Long> longLocal = new ThreadLocal<>();

    public void set() {
        longLocal.set(Thread.currentThread().getId());
    }
    public Long get() {
        return longLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        threadLocalDemo.set();
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(() -> {
            threadLocalDemo.set();
            System.out.println(threadLocalDemo.get());
        }
        );

        thread.start();
        thread.join();

        System.out.println(threadLocalDemo.get());
    }
}

ThreadLocal 並不是用來解決共享資源的多執行緒訪問的問題,因為每個執行緒中的資源只是副本,並不共享。因此ThreadLocal適合作為執行緒上下文變數,簡化執行緒內傳參。

ThreadLocal記憶體洩漏的原因?

每個Thread都有⼀個ThreadLocalMap的內部屬性,map的key是ThreaLocal,定義為弱引用,value是強引用型別。GC的時候會⾃動回收key,而value的回收取決於Thread物件的生命週期。一般會通過執行緒池的方式複用Thread物件節省資源,這也就導致了Thread物件的生命週期比較長,這樣便一直存在一條強引用鏈的關係:Thread --> ThreadLocalMap-->Entry-->Value,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致記憶體洩漏。

image-20200715235804982

解決⽅法:每次使⽤完ThreadLocal就調⽤它的remove()⽅法,手動將對應的鍵值對刪除,從⽽避免記憶體洩漏。

currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal使用場景有哪些?

ThreadLocal 適用場景:每個執行緒需要有自己單獨的例項,且需要在多個方法中共享例項,即同時滿足例項線上程間的隔離與方法間的共享。比如Java web應用中,每個執行緒有自己單獨的 Session 例項,就可以使用ThreadLocal來實現。

鎖的分類

公平鎖與非公平鎖

按照執行緒訪問順序獲取物件鎖。synchronized 是非公平鎖, Lock 預設是非公平鎖,可以設定為公平鎖,公平鎖會影響效能。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

共享式與獨佔式鎖

共享式與獨佔式的最主要區別在於:同一時刻獨佔式只能有一個執行緒獲取同步狀態,而共享式在同一時刻可以有多個執行緒獲取同步狀態。例如讀操作可以有多個執行緒同時進行,而寫操作同一時刻只能有一個執行緒進行寫操作,其他操作都會被阻塞。

悲觀鎖與樂觀鎖

悲觀鎖,每次訪問資源都會加鎖,執行完同步程式碼釋放鎖,synchronized 和 ReentrantLock 屬於悲觀鎖。

樂觀鎖,不會鎖定資源,所有的執行緒都能訪問並修改同一個資源,如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。樂觀鎖最常見的實現就是CAS。

樂觀鎖一般來說有以下2種方式:

  1. 使用資料版本記錄機制實現,這是樂觀鎖最常用的一種實現方式。給資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的version欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
  2. 使用時間戳。資料庫表增加一個欄位,欄位型別使用時間戳(timestamp),和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。

適用場景:

  • 悲觀鎖適合寫操作多的場景。
  • 樂觀鎖適合讀操作多的場景,不加鎖可以提升讀操作的效能。

CAS

什麼是CAS?

CAS全稱 Compare And Swap,比較與交換,是樂觀鎖的主要實現方式。CAS 在不使用鎖的情況下實現多執行緒之間的變數同步。ReentrantLock 內部的 AQS 和原子類內部都使用了 CAS。

CAS演算法涉及到三個運算元:

  • 需要讀寫的記憶體值 V。
  • 進行比較的值 A。
  • 要寫入的新值 B。

只有當 V 的值等於 A 時,才會使用原子方式用新值B來更新V的值,否則會繼續重試直到成功更新值。

以 AtomicInteger 為例,AtomicInteger 的 getAndIncrement()方法底層就是CAS實現,關鍵程式碼是 compareAndSwapInt(obj, offset, expect, update),其含義就是,如果obj內的valueexpect相等,就證明沒有其他執行緒改變過這個變數,那麼就更新它為update,如果不相等,那就會繼續重試直到成功更新值。

CAS存在的問題?

CAS 三大問題:

  1. ABA問題。CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面新增版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從A-B-A變成了1A-2B-3A

    JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,原子更新帶有版本號的引用型別。

  2. 迴圈時間長開銷大。CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
  3. 只能保證一個共享變數的原子操作。對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的。

    Java從1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,可以把多個變數放在一個物件裡來進行CAS操作。

併發工具

在JDK的併發包裡提供了幾個非常有用的併發工具類。CountDownLatch、CyclicBarrier和Semaphore工具類提供了一種併發流程控制的手段。

CountDownLatch

CountDownLatch用於某個執行緒等待其他執行緒執行完任務再執行,與thread.join()功能類似。常見的應用場景是開啟多個執行緒同時執行某個任務,等到所有任務執行完再執行特定操作,如彙總統計結果。

public class CountDownLatchDemo {
    static final int N = 4;
    static CountDownLatch latch = new CountDownLatch(N);

    public static void main(String[] args) throws InterruptedException {

       for(int i = 0; i < N; i++) {
            new Thread(new Thread1()).start();
       }

       latch.await(1000, TimeUnit.MILLISECONDS); //呼叫await()方法的執行緒會被掛起,它會等待直到count值為0才繼續執行;等待timeout時間後count值還沒變為0的話就會繼續執行
       System.out.println("task finished");
    }

    static class Thread1 implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "starts working");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                latch.countDown();
            }
        }
    }
}

執行結果:

Thread-0starts working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task finished

CyclicBarrier

CyclicBarrier(同步屏障),用於一組執行緒互相等待到某個狀態,然後這組執行緒再同時執行。

public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

引數parties指讓多少個執行緒或者任務等待至某個狀態;引數barrierAction為當這些執行緒都達到某個狀態時會執行的內容。

public class CyclicBarrierTest {
    // 請求的數量
    private static final int threadCount = 10;
    // 需要同步的執行緒數量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {
        // 建立執行緒池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
        System.out.println("threadnum:" + threadnum + "is ready");
        try {
            /**等待60秒,保證子執行緒完全執行結束*/
            cyclicBarrier.await(60, TimeUnit.SECONDS);
        } catch (Exception e) {
            System.out.println("-----CyclicBarrierException------");
        }
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

執行結果如下,可以看出CyclicBarrier是可以重用的:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

當四個執行緒都到達barrier狀態後,會從四個執行緒中選擇一個執行緒去執行Runnable。

CyclicBarrier和CountDownLatch區別

CyclicBarrier 和 CountDownLatch 都能夠實現執行緒之間的等待。

CountDownLatch用於某個執行緒等待其他執行緒執行完任務再執行。CyclicBarrier用於一組執行緒互相等待到某個狀態,然後這組執行緒再同時執行。
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可用於處理更為複雜的業務場景。

Semaphore

Semaphore類似於鎖,它用於控制同時訪問特定資源的執行緒數量,控制併發執行緒數。

public class SemaphoreDemo {
    public static void main(String[] args) {
        final int N = 7;
        Semaphore s = new Semaphore(3);
        for(int i = 0; i < N; i++) {
            new Worker(s, i).start();
        }
    }

    static class Worker extends Thread {
        private Semaphore s;
        private int num;
        public Worker(Semaphore s, int num) {
            this.s = s;
            this.num = num;
        }

        @Override
        public void run() {
            try {
                s.acquire();
                System.out.println("worker" + num +  " using the machine");
                Thread.sleep(1000);
                System.out.println("worker" + num +  " finished the task");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果如下,可以看出並非按照執行緒訪問順序獲取資源的鎖,即

worker0 using the machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 finished the task

原子類

基本型別原子類

使用原子的方式更新基本型別

  • AtomicInteger:整型原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean :布林型原子類

AtomicInteger 類常用的方法:

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設定新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設定為輸入值(update)
public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。

AtomicInteger 類主要利用 CAS (compare and swap) 保證原子操作,從而避免加鎖的高開銷。

陣列型別原子類

使用原子的方式更新陣列裡的某個元素

  • AtomicIntegerArray:整形陣列原子類
  • AtomicLongArray:長整形陣列原子類
  • AtomicReferenceArray :引用型別陣列原子類

AtomicIntegerArray 類常用方法:

public final int get(int i) //獲取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的當前的值,並將其設定為新值:newValue
public final int getAndIncrement(int i)//獲取 index=i 位置元素的值,並讓該位置的元素自增
public final int getAndDecrement(int i) //獲取 index=i 位置元素的值,並讓該位置的元素自減
public final int getAndAdd(int i, int delta) //獲取 index=i 位置元素的值,並加上預期的值
boolean compareAndSet(int i, int expect, int update) //如果輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設定為輸入值(update)
public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。

引用型別原子類

  • AtomicReference:引用型別原子類
  • AtomicStampedReference:帶有版本號的引用型別原子類。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
  • AtomicMarkableReference :原子更新帶有標記的引用型別。該類將 boolean 標記與引用關聯起來

小夥伴們覺得有用的話,點贊加收藏,支援一下!

相關文章