天天看點

四分鐘快速入門Java線程的六種狀态與流轉

作者:Java碼農之路

1.并行與并發有什麼差別?

并行和并發都是指多個任務同時執行的概念,但是它們之間有着明顯的差別。

  • 并行:多個任務在同一時刻同時運作,通常需要使用多個處理器或者多核處理器來實作。例如,一個計算機同時執行多個程式、多個線程或者多個程序時,就是采用并行的方式來處理任務,這樣能夠提高計算機的處理效率。
  • 并發:多個任務同時進行,但是這些任務的執行是交替進行的,即一個任務執行一段時間後,再執行另外一個任務。它是通過作業系統的協作排程來實作各個任務的切換,達到看上去同時進行的效果。例如,一個多線程程式中的多個線程就是同時運作的,但是因為 CPU 隻能處理一個線程,是以在任意時刻隻有一個線程在執行,線程之間會通過競争的方式來擷取 CPU 的時間片。
四分鐘快速入門Java線程的六種狀态與流轉

總的來說,雖然并行和并發都是多任務處理的方式,但是并行是采用多核處理器等硬體實作任務同步執行,而并發則是通過作業系統的排程算法來合理地配置設定系統資源,使得多個任務看上去同時執行。

2.說說什麼是程序和線程?

程序和線程是作業系統中的概念,用于描述程式運作時的執行實體。

程序:一個程式在執行過程中的一個執行個體,每個程序都有自己獨立的位址空間,也就是說它們不能直接共享記憶體。程序的特點包括:

  • 需要占用獨立的記憶體空間;
  • 可以并發地執行多個任務;
  • 程序之間需要通過程序間通信(IPC)來交換資料;

線程:程序中的一個執行單元,一個程序中可以包含多個線程,這些線程共享程序的記憶體空間。線程的特點包括:

  • 線程共享程序記憶體空間,可以友善、高效地通路變量;
  • 同一個程序中的多個線程可以并發地執行多個任務;
  • 線程之間切換開銷小,可以實作更細粒度的控制,例如 UI 線程控制界面重新整理,工作線程進行耗時的計算等。
四分鐘快速入門Java線程的六種狀态與流轉

線程相比于程序,線程的建立和銷毀開銷較小,上下文切換開銷也較小,是以線程是實作多任務并發的一種更加輕量級的方式。

3.說說線程有幾種建立方式?

Java中建立線程主要有三種方式:

四分鐘快速入門Java線程的六種狀态與流轉
  • 定義Thread類的子類,并重寫該類的run方法
/**
 * 繼承Thread-重寫run方法
 * Created by BaiLi
 */
public class BaiLiThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("一鍵三連");
    }
}           
  • 定義Runnable接口的實作類,并重寫該接口的run()方法
/**
 * 實作Runnable-重寫run()方法
 * Created by BaiLi
 */
public class BaiLiRunnable {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
    }
}
class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("一鍵三連");
    }
}           
  • 定義Callable接口的實作類,并重寫該接口的call()方法,一般配合FutureTask使用
/**
 * 實作Callable-重寫call()方法
 * Created by BaiLi
 */
public class BaiLiCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> ft = new FutureTask<String>(new MyCallable());
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}
class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "一鍵三連";
    }
}           

4.為什麼調用start()方法時會執行run()方法,那怎麼不直接調用run()方法?

JVM執行start方法,會先建立一個線程,由建立出來的新線程去執行thread的run方法,這才起到多線程的效果。

start()和run()的主要差別如下:

  • start方法可以啟動一個新線程,run方法隻是類的一個普通方法而已,如果直接調用run方法,程式中依然隻有主線程這一個線程。
  • start方法實作了多線程,而run方法沒有實作多線程。
  • start不能被重複調用,而run方法可以。
  • start方法中的run代碼可以不執行完,就繼續執行下面的代碼,也就是說進行了線程切換。然而,如果直接調用run方法,就必須等待其代碼全部執行完才能繼續執行下面的代碼。
/**
 * Created by BaiLi
 */
public class BaiLiDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName()+":一鍵三連"));
        thread.start();
        thread.run();
        thread.run();
        System.out.println(Thread.currentThread().getName()+":一鍵三連 + 分享");
    }
}           

5.線程有哪些常用的排程方法

四分鐘快速入門Java線程的六種狀态與流轉
import java.time.LocalTime;

/**
 * Created by BaiLi
 */
public class WaitDemo {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread thread1 = new Thread(() -> {
            try {
                synchronized (lock) {
                    System.out.println("線程進入永久等待"+ LocalTime.now());
                    lock.wait();
                    System.out.println("線程永久等待喚醒"+ LocalTime.now());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-1");

        Thread thread2 = new Thread(() -> {
            try {
                synchronized (lock) {
                    System.out.println("線程進入逾時等待"+ LocalTime.now());
                    lock.wait(5000);
                    System.out.println("線程逾時等待喚醒"+ LocalTime.now());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread-2");

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        synchronized (lock) {
            lock.notifyAll();
        }
        thread1.join();
        thread2.join();
    }
}           
public class YieldDemo extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running");
            Thread.yield(); // 調用 yield 方法,讓出 CPU 執行時間
        }
    }

    public static void main(String[] args) {
        YieldDemo demo = new YieldDemo();

        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);

        t1.start();
        t2.start();
    }
}           
/**
 * Created by BaiLi
 */
public class InterruptedDemo extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+isInterrupted());
        if(isInterrupted()){
            if(!interrupted()){
                System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+isInterrupted());
            }
        }
    }

    public static void main(String[] args) {
        InterruptedDemo interruptedDemo = new InterruptedDemo();
        interruptedDemo.start();

        interruptedDemo.interrupt();
        System.out.println(Thread.currentThread().getName()+":目前線程中斷狀态_"+Thread.interrupted());
    }
}
           

6.線程有幾種狀态?

四分鐘快速入門Java線程的六種狀态與流轉

線程在自身的生命周期中, 并不是固定地處于某個狀态,而是随着代碼的執行在不同的狀态之間進行切換,如下圖:

四分鐘快速入門Java線程的六種狀态與流轉

7.什麼是線程上下文切換?

線程上下文切換指的是在多線程運作時,作業系統從目前正在執行的線程中儲存其上下文(包括目前線程的寄存器、程式指針、棧指針等狀态資訊),并将這些資訊恢複到另一個等待執行的線程中,進而實作線程之間的切換。

四分鐘快速入門Java線程的六種狀态與流轉
四分鐘快速入門Java線程的六種狀态與流轉

8.線程間有哪些通信方式?

四分鐘快速入門Java線程的六種狀态與流轉

線程間通信是指在多線程程式設計中,各個線程之間共享資訊或者協同完成某一任務的過程。常用的線程間通信方式有以下幾種:

  • 共享變量:共享變量是指多個線程都可以通路和修改的變量,它們通常是在主線程中建立的。多個線程對同一個共享變量進行讀寫操作時,可能會出現競态條件導緻資料錯誤或程式異常。是以需要使用同步機制比如synchronized、Lock等來保證線程安全
  • 管道通信:管道是一種基于檔案描述符的通信機制,形成一個單向通信的資料流管道。它通常用于隻有兩個程序或線程之間的通信。其中一個程序将資料寫入到管道(管道的輸出端口),而另一個程序從管道的輸入端口讀取資料
  • 信号量:信号量是一種計數器,用于控制多個線程對資源的通路。當一個線程需要通路資源時,它需要申請擷取信号量,如果信号量的計數器值大于 0,則可以通路資源,否則該線程就會等待。當線程結束通路資源後,需要釋放信号量,并将計數器加1
  • 條件變量:條件變量是一種通知機制,用于在多個線程之間傳遞狀态資訊和控制資訊。當某個線程需要等待某個條件變量發生改變時,它可以調用 wait() 方法挂起,并且釋放所占用的鎖。當某個線程滿足條件後,可以調用 notify() 或者 signal() 方法來通知等待該條件變量的線程繼續執行
/**
 * 共享變量
 * 建立人:百裡
 */
public class BaiLiSharedMemoryDemo {
    public static void main(String[] args) {
        ArrayList<Integer> integers = new ArrayList<>();
        Thread producerThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (integers) {
                    integers.add(i);
                    System.out.println(Thread.currentThread().getName() + "_Producer:" + i);
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "ProducerThread");

        Thread consumeThread = new Thread(() -> {
            while (true) {
                synchronized (integers) {
                    if (!integers.isEmpty()) {
                        Integer integer = integers.remove(0);
                        System.out.println(Thread.currentThread().getName() + "_Consume:" + integer);
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "ConsumeThread");
        producerThread.start();
        consumeThread.start();
    }
}           
/**
 * 管道通信模式
 * 建立人:百裡
 */
public class BaiLiPipedStreamDemo {
    public static void main(String[] args) throws IOException {
        //輸出管道
        PipedOutputStream pipedOutputStream = new PipedOutputStream();
        //輸入管道
        PipedInputStream pipedInputStream = new PipedInputStream();

        pipedInputStream.connect(pipedOutputStream);

        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    pipedOutputStream.write(i);
                    System.out.println(Thread.currentThread().getName() + "_Produce: " + i);
                    Thread.sleep(2000);
                }
                pipedOutputStream.close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }, "ProducerThread");

        Thread consumeThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    while (true) {
                        int read = pipedInputStream.read();
                        if (read != -1) {
                            System.out.println(Thread.currentThread().getName() + "_Consume: " + read);
                        } else {
                            break;
                        }
                        Thread.sleep(1000);
                    }
                }
                pipedInputStream.close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }, "ConsumeThread");

        producerThread.start();
        consumeThread.start();
    }
}
           
/**
 * 信号量
 * 建立人:百裡
 */
public class BaiLiSemaphoreDemo {
    public static void main(String[] args) {
        // 執行個體化一個信号量對象,初始值為 0
        Semaphore semaphore = new Semaphore(0);

        // 建立生産者線程
        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "_Producer:" + i);
                    semaphore.release(); // 把信号量的計數器加 1
                    Thread.sleep(1000); //模拟停頓
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "ProducerThread");

        // 建立消費者線程
        Thread consumeThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    semaphore.acquire(); // 請求占有信号量,如果計數器不為 0,計數器減 1,否則線程阻塞等待
                    System.out.println(Thread.currentThread().getName() + "_Consume:" + i);
                    Thread.sleep(1000); //模拟停頓
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "ConsumeThread");

        producerThread.start();
        consumeThread.start();
    }
}           
/**
 * 條件變量|可重入鎖
 * 建立人:百裡
 */
public class BaiLIConditionDemo {
    public static void main(String[] args) {
        // 執行個體化一個可重入鎖對象
        ReentrantLock lock = new ReentrantLock();
        // 擷取該鎖對象的條件變量
        Condition condition = lock.newCondition();

        // 建立生産者線程
        Thread producerThread = new Thread(() -> {
            try {
                lock.lock(); // 擷取鎖對象
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " produce: " + i);
                    condition.signal(); // 喚醒處于等待狀态下的消費者線程
                    condition.await(); // 使目前線程處于等待狀态,并釋放鎖對象
                    Thread.sleep(1000);
                }
                condition.signal(); // 避免消費者線程一直等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 釋放鎖對象
            }
        }, "producer");

        // 建立消費者線程
        Thread consumerThread = new Thread(() -> {
            try {
                lock.lock(); // 擷取鎖對象
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " consume: " + i);
                    condition.signal(); // 喚醒處于等待狀态下的生産者線程
                    condition.await(); // 使目前線程處于等待狀态,并釋放鎖對象
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 釋放鎖對象
            }
        }, "consumer");

        // 啟動生産者和消費者線程
        producerThread.start();
        consumerThread.start();
    }
}           

9.ThreadLocal是什麼?

ThreadLocal也就是線程本地變量。如果你建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地拷貝,多個線程操作這個變量的時候,實際是操作自己本地記憶體裡面的變量,進而起到線程隔離的作用,避免了線程安全問題。

四分鐘快速入門Java線程的六種狀态與流轉

ThreadLocal是整個線程的全局變量,不是整個程式的全局變量。

/**
 * ThreadLocal
 * 建立人:百裡
 */
public class BaiLiThreadLocalDemo {
    //建立一個靜态的threadLocal變量,被所有線程共享
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            System.out.println(threadLocal.get());
            threadLocal.set(0);
            System.out.println(threadLocal.get());
        },"Thread-1");

        Thread thread2 = new Thread(() -> {
            System.out.println(threadLocal.get());
            threadLocal.set(1);
            System.out.println(threadLocal.get());
        },"Thread-2");

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
    }
}           

10.ThreadLocal怎麼實作?

  • ThreadLocal是Java中所提供的線程本地存儲機制,可以利用該機制将資料緩存在某個線程内部,該線程可以在任意時刻、任意方法中擷取緩存的資料
  • ThreadLocal底層是通過ThreadLocalMap來實作的,每個Thread對象(注意不是ThreadLocal對象)中都存在一個ThreadLocalMap,Map的key為ThreadLocal對象,Map的value為需要緩存的值
四分鐘快速入門Java線程的六種狀态與流轉

實作方式觀察ThreadLocal的set方法:

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;
}

ThreadLocal.ThreadLocalMap threadLocals = null;

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}           

11.ThreadLocal記憶體洩露是怎麼回事?

如果線上程池中使用ThreadLocal會造成記憶體洩漏,因為當ThreadLocal對象使用完之後,應該要把設定的key,value,也就是Entry對象進行回收,但線程池中的線程不會回收,而線程對象是通過強引用指向ThreadLocalMap,ThreadLocalMap也是通過強引用指向Entry對象,線程不被回收,Entry對象也就不會被回收,進而出現記憶體洩漏。

解決辦法是在使用了ThreadLocal對象之後,手動調用ThreadLocal的remove方法,手動清除Entry對象。

四分鐘快速入門Java線程的六種狀态與流轉
package communication;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 建立人:百裡
 */
public class BaiLiThreadLocalMemoryLeakDemo {
    private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<byte[]>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 100; i++) {
            executorService.execute(() -> {
                byte[] data = new byte[50240 * 10240];
                threadLocal.set(data);
                // 不調用 remove 方法,會導緻記憶體洩漏
                //threadLocal.remove();
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
    }
}           

12.ThreadLocalMap的結構

ThreadLocalMap雖然被稱為Map,但是其實它是沒有實作Map接口的,不過結構還是和HashMap比較類似的,主要關注的是兩個要素:元素數組和散列方法。

四分鐘快速入門Java線程的六種狀态與流轉
  • 元素數組一個table數組,存儲Entry類型的元素,Entry是ThreadLocal弱引用作為key,Object作為value的結構。
private Entry[] table;           
  • 散列方法就是怎麼把對應的key映射到table數組的相應下标,ThreadLocalMap用的是哈希取餘法,取出key的threadLocalHashCode,然後和table數組長度減一&運算(相當于取餘)
int i = key.threadLocalHashCode & (table.length - 1);           

補充一點每建立一個ThreadLocal對象,它就會新增0x61c88647,這個值很特殊,它是斐波那契數也叫黃金分割數。這樣帶來的好處就是hash分布非常均勻。

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}           

13.ThreadLocalMap怎麼解決Hash沖突的?

我們可能都知道HashMap使用了連結清單來解決沖突,也就是所謂的鍊位址法。

ThreadLocalMap内部使用的是開放位址法來解決 Hash沖突的問題。具體來說,當發生Hash沖突時,ThreadLocalMap會将目前插入的元素從沖突位置開始依次往後周遊,直到找到一個空閑的位置,然後把該元素放在這個空閑位置。這樣即使出現了Hash沖突,不會影響到已經插入的元素,而隻是會影響到新的插入操作。

四分鐘快速入門Java線程的六種狀态與流轉

查找的時候,先根據ThreadLocal對象的hash值找到對應的位置,然後比較該槽位Entry對象中的key是否和get的key一緻,如果不一緻就依次往後查找。

14.ThreadLocalMap擴容機制

ThreadLocalMap 的擴容機制和 HashMap 類似,也是在元素數量達到門檻值(預設為數組長度的 2/3)時進行擴容。具體來說,在 set() 方法中,如果目前元素數量已經達到了門檻值,就會調用 rehash() 方法,rehash()會先去清理過期的Entry,然後還要根據條件判斷size >= threshold - threshold / 4 也就是size >= threshold * 3/4來決定是否需要擴容:

private void rehash() {
    //清理過期Entry
    expungeStaleEntries();

    //擴容
    if (size >= threshold - threshold / 4)
        resize();
}

//清理過期Entry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}           

發現需要擴容時調用resize()方法,resize()方法首先将數組長度翻倍,然後建立一個新的數組newTab。接着周遊舊數組oldTab中的所有元素,散列方法重新計算位置,開放位址解決沖突,然後放到新的newTab,周遊完成之後,oldTab中所有的entry資料都已經放入到newTab中了,然後table引用指向newTab.

四分鐘快速入門Java線程的六種狀态與流轉

需要注意的是,新數組的長度始終是2的整數次幂,并且擴容後新數組的長度始終大于舊數組的長度。這是為了保證哈希函數計算出的位置在新數組中仍然有效。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}           

15.ThreadLocal怎麼進行父子線程通信

在Java多線程程式設計中,父子線程之間的資料傳遞和共享問題一直是一個非常重要的議題。如果不處理好資料的傳遞和共享,會導緻多線程程式的性能下降或者出現線程安全問題。ThreadLocal是Java提供的一種解決方案,可以非常好地解決父子線程資料共享和傳遞的問題。

那麼它是如何實作通信的了?在Thread類中存在InheritableThreadLocal變量,簡單的說就是使用InheritableThreadLocal來進行傳遞,當父線程的InheritableThreadLocal不為空時,就會将這個值傳到目前子線程的InheritableThreadLocal。

/**
 * ThreadLocal父子線程通信
 * 建立人:百裡
 */
public class BaiLiInheritableThreadLocalDemo {
    public static void main(String[] args) throws Exception {
        ThreadLocal threadLocal = new ThreadLocal<>();
        threadLocal.set("threadLocal");

        ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
        inheritableThreadLocal.set("分享 + inheritableThreadLocal");

        Thread t = new Thread(() -> {
            System.out.println("一鍵三連 + " + threadLocal.get());
            System.out.println("一鍵三連 + " + inheritableThreadLocal.get());
        });
        t.start();
    }
}           

16.說一下你對Java記憶體模型(JMM)的了解?

Java 記憶體模型(Java Memory Model)是一種規範,用于描述 Java 虛拟機(JVM)中多線程情況下,線程之間如何協同工作,如何共享資料,并保證多線程的操作在各個線程之間的可見性、有序性和原子性。

具體定義如下:

  • 所有的變量都存儲在主記憶體(Main Memory)中。
  • 每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體中存儲了該線程以讀/寫共享變量的拷貝副本。
  • 線程對變量的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體。
  • 不同的線程之間無法直接通路對方本地記憶體中的變量;線程間共享變量時,通過主記憶體來實作通信、協作和傳遞資訊。

Java記憶體模型的抽象圖:

四分鐘快速入門Java線程的六種狀态與流轉

在這個抽象的記憶體模型中,在兩個線程之間的通信(共享變量狀态變更)時,會進行如下兩個步驟:

  • 線程A把在本地記憶體更新後的共享變量副本的值,重新整理到主記憶體中。
  • 線程B在使用到該共享變量時,到主記憶體中去讀取線程A更新後的共享變量的值,并更新線程B本地記憶體的值。

17.說說你對原子性、可見性、有序性的了解?

原子性、有序性、可見性是并發程式設計中非常重要的基礎概念,JMM的很多技術都是圍繞着這三大特性展開。

  • 原子性:指一個操作是不可分割、不可中斷的,在執行過程中不會受到其他線程的幹擾,要麼全部執行,要麼就全不執行。即使是在多線程的環境下,一個操作也是原子性的執行完成。

線程切換會帶來原子性問題,示例:

int count = 0; //1
count++;       //2
int a = count; //3           

上面展示語句中,除了語句1是原子操作,其它兩個語句都不是原子性操作,下面我們來分析一下語句2

其實語句2在執行的時候,包含三個指令操作

  • 指令 1:首先,把變量 count 從記憶體加載到 CPU 的寄存器
  • 指令 2:然後,在寄存器中執行 +1 操作
  • 指令 3:最終,将結果寫入記憶體

對于上面的三條指令來說,如果線程 A 在指令 1 執行完後做線程切換,線程 A 和線程 B 按照下圖的序列執行,那麼我們會發現兩個線程都執行了 count+=1 的操作,但是得到的結果不是我們期望的 2,而是 1。

四分鐘快速入門Java線程的六種狀态與流轉
  • 可見性:指一個線程對共享變量的修改,對于其他線程應該是立即可見的,確定了各個線程之間對記憶體狀态的正确觀察。
  • 有序性:指程式執行的順序按照代碼的順序執行。在單線程的情況下,代碼執行順序與編寫的順序一緻。但在多線程環境中,由于時間片輪換,不同的線程可能會交替執行不同的代碼。

原子性、可見性、有序性都應該怎麼保證呢?

  • 原子性:JMM隻能保證基本的原子性,如果要保證一個代碼塊的原子性,需要使用synchronized。
  • 可見性:Java是利用volatile關鍵字來保證可見性的,除此之外,final和synchronized也能保證可見性。
  • 有序性:synchronized或者volatile都可以保證多線程之間操作的有序性。

18.說說什麼是指令重排?

在不影響單線程程式執行結果的前提下,計算機為了最大限度的發揮機器性能,對機器指令進行重排序優化。

四分鐘快速入門Java線程的六種狀态與流轉

從Java源代碼到最終實際執行的指令序列,會分别經曆下面3種重排序:

四分鐘快速入門Java線程的六種狀态與流轉
  • 編譯器重排序:編譯器在不改變單線程程式語義的前提下重新安排語句的執行順序。例如把變量緩存到寄存器中、提取公共子表達式等。
  • 指令級重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。例如亂序執行的 Load 和 Store 指令、分支預測以及指令突發等。
  • 記憶體系統重排序:由于資料讀寫過程中涉及到多個緩沖區,這使得加載和存儲的操作看上去可能是亂序執行,于是需要記憶體系統的重排序。例如寫入緩存中的操作順序,對于其他CPU的 Cache 來說是不可見的。

以雙重校驗鎖單例模式為例子,Singleton instance=new Singleton();對應的JVM指令分為三步:配置設定記憶體空間-->初始化對象--->對象指向配置設定的記憶體空間,但是經過了編譯器的指令重排序,第二步和第三步就可能會重排序。

四分鐘快速入門Java線程的六種狀态與流轉

JMM屬于語言級的記憶體模型,它確定在不同的編譯器和不同的處理器平台之上,通過禁止特定類型的編譯器重排序和指令級重排序,為程式員提供一緻的記憶體可見性保證。

19.指令重排有限制嗎?happens-before了解嗎?

指令重排也是有一些限制的,有兩個規則happens-before和as-if-serial來限制。

happens-before的定義:

  • 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果将對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
  • 兩個操作之間存在happens-before關系,并不意味着Java平台的具體實作必須要按照 happens-before關系指定的順序來執行。隻要沒有改變程式的執行結果,編譯器和處理器怎麼優化都可以。

happens-before的六大規則:

四分鐘快速入門Java線程的六種狀态與流轉
  • 程式順序規則:一個線程中的每個操作,happens-before于該線程中的任意後續操作。
四分鐘快速入門Java線程的六種狀态與流轉
/**
 * 順序性規則
 * 順序執行是針對代碼邏輯而言的,在實際執行的時候發生指令重排序但是并沒有改變源代碼的邏輯。
 * @author 百裡
 */
public class BaiLiHappenBeforeDemo {
    public static void main(String[] args) {
        double pi = 3.14; // A
        double r = 1.0; // B
        double area = pi * r * r; // C
        System.out.println(area);
    }
}           
  • 螢幕鎖規則:一個unlock操作之前對某個鎖的lock操作必須發生在該unlock操作之前
四分鐘快速入門Java線程的六種狀态與流轉
import java.util.concurrent.locks.ReentrantLock;

/**
 * 重排鎖的話,會導緻邏輯改變。
 * @author 百裡
 */
public class BaiLiHappenBeforeLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        // TODO 
        reentrantLock.unlock();
        
        reentrantLock.lock();
        // TODO
        reentrantLock.unlock();
    }
}           
  • volatile變量規則:對一個volatile變量的寫操作必須發生在該變量的讀操作之前。
  • 傳遞性規則:如果A happens-before B,且B happens-before C,那麼A happens-before C。
四分鐘快速入門Java線程的六種狀态與流轉

從圖中,我們可以看到:

  • “x=42”Happens-Before 寫變量“v=true”,這是規則1的内容;
  • 寫變量“v=true”Happens-Before 讀變量“v=true”,這是規則3的内容;
  • 再根據這個傳遞性規則,我們得到結果:“x=42”Happens-Before 讀變量“v=true”;

這意味着什麼呢?如果線程 B 讀到了“v=true”,那麼線程A設定的“x=42”對線程B是可見的。也就是說,線程B能看到“x == 42“。

/**
 * 傳遞性規則
 * @author 百裡
 */
public class BaiLiHappenBeforeVolatileDemo {
    int x = 0;
    volatile boolean v = false;
    public void writer() {
        x = 42;
        v = true;
    }
    public void reader() {
        if (v == true) {
            System.out.println(x);
        }
    }
}           
  • 線程啟動規則:如果線程A執行操作ThreadB.start()(啟動線程B),那麼A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
四分鐘快速入門Java線程的六種狀态與流轉

我們可以了解為:線程A啟動線程B之後,線程B能夠看到線程A在啟動線程B之前的操作。

  • 線程結束規則:如果線程A執行操作ThreadB.join()并成功傳回,那麼線程B中的任意操作 happens-before于 ThreadB.join()操作成功傳回後的線程A操作。
四分鐘快速入門Java線程的六種狀态與流轉

在Java語言裡面,Happens-Before的語義本質上是一種可見性,A Happens-Before B 意味着A事件對B事件來說是可見的,并且無論A事件和B事件是否發生在同一個線程裡。

20.as-if-serial又是什麼?單線程的程式一定是順序的嗎?

as-if-serial是指無論如何重排序都不會影響單線程程式的執行結果。這個原則的核心思想是編譯器和處理器等各個層面的優化,不能改變程式執行的意義。

四分鐘快速入門Java線程的六種狀态與流轉

A和C之間存在資料依賴關系,同時B和C之間也存在資料依賴關系。是以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程式的結果将會被改變)。但A和B之間沒有資料依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。

是以最終,程式可能會有兩種執行順序:

四分鐘快速入門Java線程的六種狀态與流轉

21.volatile實作原理了解嗎?

volatile有兩個作用,保證可見性和有序性。

可見性:當一個變量被聲明為 volatile 時,它會告訴編譯器和CPU将該變量存儲在主記憶體中,而不是線程的本地記憶體中。即每個線程讀取的都是主記憶體中最新的值,避免了多線程并發下的資料不一緻問題。

四分鐘快速入門Java線程的六種狀态與流轉

有序性:重排序可以分為編譯器重排序和處理器重排序,valatile保證有序性,就是通過分别限制這兩種類型的重排序。

四分鐘快速入門Java線程的六種狀态與流轉

為了實作volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。

  1. 在每個volatile寫操作的前面插入一個StoreStore屏障
  2. 在每個volatile寫操作的後面插入一個StoreLoad屏障
  3. 在每個volatile讀操作的後面插入一個LoadLoad屏障
  4. 在每個volatile讀操作的後面插入一個LoadStore屏障
四分鐘快速入門Java線程的六種狀态與流轉

22.synchronized用過嗎?怎麼使用?

synchronized經常用的,用來保證代碼的原子性。

synchronized主要有三種用法:

  • 修飾執行個體方法: 作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖。
四分鐘快速入門Java線程的六種狀态與流轉
  • 修飾靜态方法:給目前類加鎖,在同一時間内,隻能有一個線程持有該類對應的 Class 對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該靜态方法。
四分鐘快速入門Java線程的六種狀态與流轉
  • 修飾代碼塊 :指定一個同步鎖對象,這個對象可以是具體的Object或者是類.class。在同一時間内,隻能有一個線程持有該同步鎖對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該代碼塊。
四分鐘快速入門Java線程的六種狀态與流轉

注意事項:

  • 修飾執行個體方法:不同的對象執行個體之間并不會互相影響,它們的鎖是互相獨立的。是以,如果不同的線程在不同的對象執行個體上執行同一個同步方法,它們之間并不會因為共享變量而産生互斥的效果。
  • 修飾靜态方法:應該盡量避免持有鎖的時間過長,否則可能會導緻其他線程長時間等待,進而影響系統性能。同時,也要注意避免死鎖的情況。
  • 修飾代碼塊:同步鎖并不是對整個代碼塊進行加鎖,而是對同步鎖對象進行加鎖。是以,如果在不同的代碼塊中使用了相同的同步鎖對象,它們之間也會産生互斥的效果。而如果使用不同的同步鎖對象,則它們之間并不會産生互斥的效果。

23.synchronized的實作原理?

我們使用synchronized的時候,發現不用自己去lock和unlock,是因為JVM幫我們把這個事情做了。

  1. synchronized修飾代碼塊時,JVM采用monitorenter、monitorexit兩個指令來實作同步,monitorenter 指令指向同步代碼塊的開始位置, monitorexit 指令則指向同步代碼塊的結束位置。
四分鐘快速入門Java線程的六種狀态與流轉
/**
 * @author 百裡
 */
public class BaiLiSyncDemo {
    public void main(String[] args) {
        synchronized (this) {
            int a = 1;
        }
    }
}           
public void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: iconst_1
         5: istore_3
         6: aload_2
         7: monitorexit
         8: goto          18
        11: astore        4
        13: aload_2
        14: monitorexit
        15: aload         4
        17: athrow
        18: return           
  1. synchronized修飾同步方法時,JVM采用ACC_SYNCHRONIZED标記符來實作同步,這個辨別指明了該方法是一個同步方法。同樣可以寫段代碼反編譯看一下。
四分鐘快速入門Java線程的六種狀态與流轉
/**
 * @author 百裡
 */
public class BaiLiSyncDemo {
    public synchronized void main(String[] args) {
        int a = 1;
    }
}           

synchronized鎖住的是什麼呢?

執行個體對象結構裡有對象頭,對象頭裡面有一塊結構叫Mark Word,Mark Word指針指向了monitor。

所謂的Monitor其實是一種同步機制,我們可以稱為内部鎖或者Monitor鎖。

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor實作的。

反編譯class檔案方法:

反編譯一段synchronized修飾代碼塊代碼,javap -c -s -v -l ***.class,可以看到相應的位元組碼指令。

24.synchronized的可見性,有序性,可重入性是怎麼實作的?

synchronized怎麼保證可見性?

  • 線程加鎖前,将清空工作記憶體中共享變量的值,進而使用共享變量時需要從主記憶體中重新讀取最新的值。
  • 線程加鎖後,其它線程無法擷取主記憶體中的共享變量。
  • 線程解鎖前,必須把共享變量的最新值重新整理到主記憶體中。

synchronized怎麼保證有序性?

synchronized同步的代碼塊,具有排他性,一次隻能被一個線程擁有,是以synchronized保證同一時刻,代碼是單線程執行的。

因為as-if-serial語義的存在,單線程的程式能保證最終結果是有序的,但是不保證不會指令重排。

是以synchronized保證的有序是執行結果的有序性,而不是防止指令重排的有序性。

synchronized怎麼實作可重入的?

synchronized 是可重入鎖,也就是說,允許一個線程二次請求自己持有對象鎖的臨界資源,這種情況稱為可重入鎖。

之是以是可重入的。是因為 synchronized 鎖對象有個計數器,當一個線程請求成功後,JVM會記下持有鎖的線程,并将計數器計為1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。

當線程執行完畢後,計數器會遞減,直到計數器為0才釋放該鎖。

25.說說synchronized和ReentrantLock的差別

可以從鎖的實作、性能、功能特點等幾個次元去回答這個問題:

  • 鎖的實作: synchronized是Java語言的關鍵字,基于JVM實作。而ReentrantLock是基于JDK的API層面實作的(一般是lock()和unlock()方法配合try/finally語句塊來完成。)
  • 性能: 在JDK1.6鎖優化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6開始,增加了适應性自旋、鎖消除等,兩者性能就差不多了。
  • 功能特點: ReentrantLock 比 synchronized 多了一些進階功能,如等待可中斷、可實作公平鎖、可實作選擇性通知。
      • ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實作這個機制
      • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized隻能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
      • synchronized與wait()和notify()/notifyAll()方法結合實作等待/通知機制;ReentrantLock類借助Condition接口與newCondition()方法實作。
      • ReentrantLock需要手工聲明來加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動釋放鎖。

下面的表格列出了兩種鎖之間的差別:

四分鐘快速入門Java線程的六種狀态與流轉

26.ReentrantLock實作原理?

ReentrantLock是一種可重入的排它鎖,主要用來解決多線程對共享資源競争的問題;它提供了比synchronized關鍵字更加靈活的鎖機制。其實作原理主要涉及以下三個方面:

  • 基本結構

ReentrantLock内部維護了一個Sync對象(AbstractQueuedSynchronizer類的子類),Sync持有鎖、等待隊列等狀态資訊,實際上 ReentrantLock的大部分功能都是由Sync來實作的。

  • 加鎖過程

當一個線程調用ReentrantLock的lock()方法時,會先嘗試CAS操作擷取鎖,如果成功則傳回;否則,線程會被放入等待隊列中,等待喚醒重新嘗試擷取鎖。

如果一個線程已經持有了鎖,那麼它可以重入這個鎖,即繼續擷取該鎖而不會被阻塞。ReentrantLock通過維護一個計數器來實作重入鎖功能,每次重入計數器加1,每次釋放鎖計數器減1,當計數器為0時,鎖被釋放。

  • 解鎖過程

當一個線程調用ReentrantLock的unlock()方法時,會将計數器減1,如果計數器變為了0,則鎖被完全釋放。如果計數器還大于0,則表示有其他線程正在等待該鎖,此時會喚醒等待隊列中的一個線程來擷取鎖。

總結:

ReentrantLock的實作原理主要是基于CAS操作和等待隊列來實作。它通過Sync對象來維護鎖的狀态,支援重入鎖和公平鎖等特性,提供了比synchronized更加靈活的鎖機制,是Java并發程式設計中常用的同步工具之一。

27.ReentrantLock怎麼實作公平鎖的?

ReentrantLock可以通過構造函數的參數來控制鎖的公平性,如果傳入 true,就表示該鎖是公平的;如果傳入 false,就表示該鎖是不公平的。

new ReentrantLock()構造函數預設建立的是非公平鎖 NonfairSync

四分鐘快速入門Java線程的六種狀态與流轉

同時也可以在建立鎖構造函數中傳入具體參數建立公平鎖 FairSync

四分鐘快速入門Java線程的六種狀态與流轉

FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜态内部類,隻不過實作不同鎖語義。

非公平鎖和公平鎖的差別:

  • 非公平鎖在調用 lock 後,首先就會調用 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被占用,那麼直接就擷取到鎖傳回了。
  • 非公平鎖在 CAS 失敗後,和公平鎖一樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,如果發現鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,但是公平鎖會判斷等待隊列是否有線程處于等待狀态,如果有則不去搶鎖,乖乖排到後面。
四分鐘快速入門Java線程的六種狀态與流轉

28.什麼是CAS?

CAS叫做CompareAndSwap,比較并交換,主要是通過處理器的指令來保證操作的原子性的。

CAS 操作包含三個參數:共享變量的記憶體位址(V)、預期原值(A)和新值(B),當且僅當記憶體位址 V 的值等于 A 時,才将 V 的值修改為 B;否則,不會執行任何操作。

在多線程場景下,使用 CAS 操作可以確定多個線程同時修改某個變量時,隻有一個線程能夠成功修改。其他線程需要重試或者等待。這樣就避免了傳統鎖機制中的鎖競争和死鎖等問題,提高了系統的并發性能。

29.CAS存在什麼問題?如何解決?

CAS的經典三大問題:

四分鐘快速入門Java線程的六種狀态與流轉

ABA問題

ABA 問題是指一個變量從A變成B,再從B變成A,這樣的操作序列可能會被CAS操作誤判為未被其他線程修改過。例如線程A讀取了某個變量的值為 A,然後被挂起,線程B修改了這個變量的值為B,然後又修改回了A,此時線程A恢複執行,進行CAS操作,此時仍然可以成功,因為此時變量的值還是A。

怎麼解決ABA問題?

  • 加版本号

每次修改變量,都在這個變量的版本号上加1,這樣,剛剛A->B->A,雖然A的值沒變,但是它的版本号已經變了,再判斷版本号就會發現此時的A已經被改過了。

比如使用JDK5中的AtomicStampedReference類或JDK8中的LongAdder類。這些原子類型不僅包含資料本身,還包含一個版本号,每次進行操作時都會更新版本号,隻有當版本号和期望值都相等時才能執行更新,這樣可以避免 ABA 問題的影響。

循環性能開銷

自旋CAS,如果一直循環執行,一直不成功,會給CPU帶來非常大的執行開銷。

怎麼解決循環性能開銷問題?

可以使用自适應自旋鎖,即在多次操作失敗後逐漸加長自旋時間或者放棄自旋鎖轉為阻塞鎖;

隻能保證一個變量的原子操作

CAS 保證的是對一個變量執行操作的原子性,如果需要對多個變量進行複合操作,CAS 操作就無法保證整個操作的原子性。

怎麼解決隻能保證一個變量的原子操作問題?

  • 可以使用鎖機制來保證整個複合操作的原子性。例如,使用 synchronized 關鍵字或 ReentrantLock 類來保證多個變量的複合操作的原子性。在鎖的作用下,隻有一個線程可以執行複合操作,其他線程需要等待該線程釋放鎖後才能繼續執行。這樣就可以保證整個操作的原子性了。
  • 可以使用類似于事務的方式來保證多個變量的複合操作的原子性。例如,使用AtomicReference 類,可以将多個變量封裝到一個對象中,通過 CAS 操作更新整個對象,進而保證多個變量的複合操作的原子性。隻有當整個對象的值和期望值都相等時才會執行更新操作。

30.Java多線程中如何保證i++的結果正确

四分鐘快速入門Java線程的六種狀态與流轉
  • 使用 Atomic變量,Java 并發包内提供了一些原子類型,如AtomicInteger、AtomicLong等,這些類提供了一些原子操作方法,可以保證相應的操作的原子性。
四分鐘快速入門Java線程的六種狀态與流轉

這裡使用 AtomicInteger 類來保證 i++ 操作的原子性。

  • 使用synchronized,對i++操作加鎖。
四分鐘快速入門Java線程的六種狀态與流轉

這裡使用 synchronized 方法來保證 increment() 方法的原子性,進而保證 i++ 操作的結果正确。

  • 使用鎖機制,除了使用 synchronized 關鍵字外,還可以使用 ReentrantLock 類來保護臨界區域。
四分鐘快速入門Java線程的六種狀态與流轉

這裡使用 ReentrantLock 類的 lock() 和 unlock() 方法來保護 i++操作的原子性。

31.AtomicInteger的原理是什麼?

一句話概括:使用CAS實作。

在AtomicInteger中,CAS操作的流程如下:

  1. 調用 incrementAndGet()方法,該方法會通過調用unsafe.getAndAddInt()方法,擷取目前 AtomicInteger對象的值val
  2. 将 val + 1 得到新的值 next
四分鐘快速入門Java線程的六種狀态與流轉
  1. 使用 unsafe.compareAndSwapInt() 方法進行 CAS 操作,将對象中目前值與預期值(步驟1中擷取的val)進行比較,如果相等,則将 next指派給val;否則傳回 false
  2. 如果CAS操作傳回false,說明有其他線程已經修改了AtomicInteger對象的值,需要重新執行步驟 1
四分鐘快速入門Java線程的六種狀态與流轉

總結:

在 CAS 操作中,由于隻有一個線程可以成功修改共享變量的值,是以可以保證操作的原子性,即多線程同時修改AtomicInteger變量時也不會出現競态條件。這樣就可以在多線程環境下安全地對AtomicInteger進行整型變量操作。其它的原子操作類基本都是大同小異。

32.什麼是線程死鎖?我們該如何避免線程死鎖?

死鎖是指兩個或兩個以上的線程在執行過程中,因争奪資源而造成的互相等待的現象,在無外力作用的情況下,這些線程會一直互相等待而無法繼續運作下去。

四分鐘快速入門Java線程的六種狀态與流轉

那麼為什麼會産生死鎖呢?死鎖的産生必須具備以下四個條件:

四分鐘快速入門Java線程的六種狀态與流轉
  • 互斥條件:指線程在占用某個資源時,會把該資源标記為已占用,并且鎖住該資源,以保證同一時間内隻有一個線程可以通路該資源。其他需要通路該資源的線程就隻能等待該線程釋放該資源,在資源被釋放之後才能進行通路。
  • 請求并持有條件:指一個線程己經持有了至少一個資源,但又提出了新的資源請求,而新資源己被其它線程占有,是以目前線程會被阻塞,但阻塞的同時并不釋放自己已經擷取的資源。
  • 不可剝奪條件:指線程擷取到的資源在自己使用完之前不能被其它線程搶占,隻有在自己使用完畢後才由自己釋放該資源。
  • 環路等待條件:指在發生死鎖時,若幹線程形成頭尾相接的循環等待資源關系。

該如何避免死鎖呢?答案是至少破壞死鎖發生的一個條件。

  • 互斥:我們沒有辦法破壞,因為用鎖為的就是互斥。不過其他三個條件都是有辦法打破的,到底如何做呢?
  • 請求并持有:可以一次性請求所有的資源。
  • 不可剝奪:設定逾時時間。已經占用資源的線程進一步申請其他資源時,如果長時間申請不到,逾時釋放已經持有的資源。
  • 環路等待:注意加鎖順序,保證每個線程按同樣的順序進行加鎖。

33.如何排查死鎖問題

可以使用jdk自帶的指令行工具排查:

  1. 使用jps查找運作的Java程序:jps -l
  2. 使用jstack檢視線程堆棧資訊:jstack -l 程序id

基本就可以看到死鎖的資訊。

還可以利用圖形化工具,比如JConsole(JConsole工具在JDK的bin目錄中)。出現線程死鎖以後,點選JConsole線程面闆的檢測到死鎖按鈕,将會看到線程的死鎖資訊。

示範樣例如下:

package lock;

/**
 * @author 百裡
 */
public class BaiLiDeadLock {

    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread-1擷取了鎖1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread-1嘗試擷取鎖2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread-2擷取了鎖2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread-2嘗試擷取鎖1");
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}           

1.建立連接配接,找到相應的線程,點選連接配接

2.選擇線程标簽,點選檢測死鎖。檢視死鎖線程資訊

四分鐘快速入門Java線程的六種狀态與流轉
四分鐘快速入門Java線程的六種狀态與流轉
四分鐘快速入門Java線程的六種狀态與流轉
四分鐘快速入門Java線程的六種狀态與流轉

34.什麼是線程池?

線程池是一種用于管理和複用線程的機制,它提供了一種執行大量異步任務的方式,并且可以在多個任務之間合理地配置設定和管理系統資源。

線程池的主要優點包括:

  • 改善了資源使用率,降低了線程建立和銷毀的開銷。
  • 提高了系統響應速度,因為線程池已經預先建立好了一些線程,可以更加快速地配置設定資源以響應使用者請求。
  • 提高了代碼可讀性和可維護性,因為線程池将線程管理和任務執行進行了分離,可以更加友善地對其進行調整和優化。
  • 可以設定線程數目上限,避免了缺乏控制的線程建立造成的系統無法承受的負載壓力。

35.簡單說一下線程池的工作流程

用一個通俗的比喻:

有一個銀行營業廳,總共有六個視窗,現在有三個視窗坐着三個營業員小姐姐在營業。小天去辦業務,可能會遇到什麼情況呢?

  1. 小天發現有空閑的視窗,直接去找小姐姐辦理業務。
四分鐘快速入門Java線程的六種狀态與流轉
  1. 小天發現沒有空閑的視窗,就在排隊區排隊等。
四分鐘快速入門Java線程的六種狀态與流轉
  1. 小天發現沒有空閑的視窗,等待區也滿了,經理一看,就讓休息的營業員小姐姐趕緊回來上班,等待區号靠前的趕緊去新視窗辦,小天去排隊區排隊。小姐姐比較辛苦,假如一段時間發現他們可以不用接着營業,經理就讓她們接着休息。
四分鐘快速入門Java線程的六種狀态與流轉
  1. 小天一看,六個視窗都滿了,等待區也沒位置了。小天就開始投訴急了,要鬧,經理趕緊出來了,經理該怎麼辦呢?
四分鐘快速入門Java線程的六種狀态與流轉
  1. 我們銀行系統已經癱瘓
  2. 誰叫你來辦的你找誰去
  3. 看你比較急,去隊裡加個塞
  4. 今天沒辦法,不行你看改一天

上面的這個流程幾乎就跟JDK線程池的大緻流程類似。

  1. 營業中的 3個視窗對應核心線程池數:corePoolSize
  2. 總的營業視窗數6對應:maximumPoolSize
  3. 打開的臨時視窗在多少時間内無人辦理則關閉對應:unit
  4. 排隊區就是等待隊列:workQueue
  5. 無法辦理的時候銀行給出的解決方法對應:RejectedExecutionHandler
  6. threadFactory 該參數在JDK中是線程工廠,用來建立線程對象,一般不會動。

是以我們線程池的工作流程也比較好了解了:

四分鐘快速入門Java線程的六種狀态與流轉
  1. 線程池剛建立時,裡面沒有一個線程。任務隊列是作為參數傳進來的。不過,就算隊列裡面有任務,線程池也不會馬上執行它們。
  2. 當調用 execute() 方法添加一個任務時,線程池會做如下判斷:
  • 如果正在運作的線程數量小于 corePoolSize,那麼馬上建立線程運作這個任務;
  • 如果正在運作的線程數量大于或等于 corePoolSize,那麼将這個任務放入隊列;
  • 如果這時候隊列滿了,而且正在運作的線程數量小于 maximumPoolSize,那麼還是要建立非核心線程立刻運作這個任務;
  • 如果隊列滿了,而且正在運作的線程數量大于或等于 maximumPoolSize,那麼線程池會根據拒絕政策來對應處理。
  1. 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
  2. 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果目前運作的線程數大于 corePoolSize,那麼這個線程就被停掉。是以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。

36.線程池主要參數有哪些?

四分鐘快速入門Java線程的六種狀态與流轉
package pool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author 百裡
 */
public class BaiLiThreadPoolDemo {
    public static void main(String[] args) {
        //基于Executor架構實作線程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5,  //corePoolSize
                12,  //maximumPoolSize
                5,  //keepAliveTime
                TimeUnit.SECONDS,   //unit
                new ArrayBlockingQueue<>(5),  //workQueue
                Executors.defaultThreadFactory(),  //threadFactory
                new ThreadPoolExecutor.DiscardPolicy()  //handler
        );
        threadPoolExecutor.execute(() -> {
            System.out.println(
                    Thread.currentThread().getName() + ":點贊評論加分享"
            );
        });
    }
}           

線程池有七大參數,我們重點關注corePoolSize、maximumPoolSize、workQueue、handler 可以幫助我們更好地了解和優化線程池的性能

  1. corePoolSize

此值是用來初始化線程池中核心線程數,當線程池中線程數< corePoolSize時,系統預設是添加一個任務才建立一個線程池。當線程數 = corePoolSize時,新任務會追加到workQueue中。

  1. maximumPoolSize

maximumPoolSize表示允許的最大線程數 = (非核心線程數+核心線程數),當BlockingQueue也滿了,但線程池中總線程數 < maximumPoolSize時候就會再次建立新的線程。

  1. keepAliveTime

非核心線程 =(maximumPoolSize - corePoolSize ) ,非核心線程閑置下來不幹活最多存活時間。

  1. unit

線程池中非核心線程保持存活的時間的機關

  • TimeUnit.DAYS;天
  • TimeUnit.HOURS;小時
  • TimeUnit.MINUTES;分鐘
  • TimeUnit.SECONDS;秒
  • TimeUnit.MILLISECONDS; 毫秒
  • TimeUnit.MICROSECONDS; 微秒
  • TimeUnit.NANOSECONDS; 納秒
  1. workQueue

線程池等待隊列,維護着等待執行的Runnable對象。當運作當線程數= corePoolSize時,新的任務會被添加到workQueue中,如果workQueue也滿了則嘗試用非核心線程執行任務,等待隊列應該盡量用有界的。

  1. threadFactory

建立一個新線程時使用的工廠,可以用來設定線程名、是否為daemon線程等等。

  1. handler

corePoolSize、workQueue、maximumPoolSize都不可用的時候執行的飽和政策。

37.線程池的拒絕政策有哪些?

線上程池中,當送出的任務數量超過了線程池的最大容量,線程池就需要使用拒絕政策來處理無法處理的新任務。Java 中提供了 4 種預設的拒絕政策:

  • AbortPolicy(預設政策):直接抛出 runtime 異常,阻止系統正常運作。
  • CallerRunsPolicy:由送出該任務的線程來執行這個任務。
  • DiscardPolicy:直接丢棄任務,不給予任何處理。
  • DiscardOldestPolicy:丢棄隊列中最老的一個請求,嘗試再次送出目前任務。

除了這些預設的政策之外,我們也可以自定義自己的拒絕政策,實作RejectedExecutionHandler接口即可。

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定義的拒絕政策處理邏輯
    }
}           

38.線程池有哪幾種工作隊列

四分鐘快速入門Java線程的六種狀态與流轉
  • 有界隊列(ArrayBlockingQueue):是一個用數組實作的有界阻塞隊列,按FIFO排序。
  • 無界隊列(LinkedBlockingQueue):是基于連結清單結構的阻塞隊列,按FIFO排序,容量可以選擇進行設定,不設定的話,将是一個無邊界的阻塞隊列,是以在任務數量很大且任務執行時間較長時,無界隊列可以保證任務不會被丢棄,但同時也會導緻線程池中線程數量不斷增加,可能會造成記憶體溢出等問題。
  • 延遲隊列(DelayQueue):是一個任務定時周期的延遲執行的隊列。根據指定的執行時間從小到大排序,否則根據插入到隊列的先後排序。
  • 優先級隊列(PriorityBlockingQueue):是具有優先級的無界阻塞隊列。與無界隊列類似,優先級隊列可以保證所有任務都會被執行,但不同的是優先級隊列可以對任務進行管理和排序,確定高優先級的任務優先執行。
  • 同步隊列(SynchronousQueue):是一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀态,吞吐量通常要高于無界隊列。

39.線程池送出execute和submit有什麼差別?

在Java中,線程池中一般有兩種方法來送出任務:execute() 和 submit()

  1. execute() 用于送出不需要傳回值的任務
四分鐘快速入門Java線程的六種狀态與流轉
  1. submit() 用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個 future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值
四分鐘快速入門Java線程的六種狀态與流轉

40.怎麼關閉線程池?

可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是周遊線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,是以無法響應中斷的任務可能永遠無法終止。

shutdown:将線程池狀态置為shutdown,并不會立即停止:

  1. 停止接收外部submit的任務
  2. 内部正在跑的任務和隊列裡等待的任務,會執行完
  3. 等到第二步完成後,才真正停止

shutdownNow:将線程池狀态置為stop。一般會立即停止,事實上不一定:

  1. 和shutdown()一樣,先停止接收外部送出的任務
  2. 忽略隊列裡等待的任務
  3. 嘗試将正在跑的任務interrupt中斷
  4. 傳回未執行的任務清單

shutdown 和shutdownnow差別如下:

  • shutdownNow:能立即停止線程池,正在跑的和正在等待的任務都停下了。這樣做立即生效,但是風險也比較大。
  • shutdown:隻是關閉了送出通道,用submit()是無效的;而内部的任務該怎麼跑還是怎麼跑,跑完再徹底停止線程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 百裡
 */
public class BaiLiShutdownDemo {
    public static void main(String[] args) {
        // 建立一個線程池,包含兩個線程
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 送出任務到線程池
        executor.submit(() -> {
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Task 1 finished");
        });

        executor.submit(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Task 2 finished");
        });

        // 關閉線程池
        executor.shutdown();

        while (!executor.isTerminated()) {
            System.out.println("Waiting for all tasks to finish...");
            try {
                // 每500毫秒檢查一次
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        System.out.println("All tasks finished");
    }
}           
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author 百裡
 */
public class BaiLiShutdownNowDemo {
    public static void main(String[] args) {
        // 建立一個線程池,包含兩個線程
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 送出任務到線程池
        executor.submit(() -> {
            while (!Thread.interrupted()) {
                System.out.println("Task 1 running...");
            }
            System.out.println("Task 1 interrupted");
        });

        executor.submit(() -> {
            while (!Thread.interrupted()) {
                System.out.println("Task 2 running...");
            }
            System.out.println("Task 2 interrupted");
        });

        // 關閉線程池
        List<Runnable> unfinishedTasks = null;
        executor.shutdownNow();

        try {
            // 等待直到所有任務完成或逾時60秒
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                // 如果等待逾時,則記錄未完成的任務清單
                unfinishedTasks = executor.shutdownNow();
                System.out.println("Some tasks were not finished");
            }
        } catch (InterruptedException e) {
            // 如果等待過程中發生異常,則記錄未完成的任務清單
            unfinishedTasks = executor.shutdownNow();
            Thread.currentThread().interrupt();
        }

        if (unfinishedTasks != null && !unfinishedTasks.isEmpty()) {
            System.out.println("Unfinished tasks: " + unfinishedTasks);
        } else {
            System.out.println("All tasks finished");
        }
    }
}           

41.有哪幾種常見的線程池

在Java中,常見的線程池類型主要有四種,都是通過工具類Excutors建立出來的。

四分鐘快速入門Java線程的六種狀态與流轉
  • newFixedThreadPool (固定數目線程):該線程池具有固定的線程數,當送出的任務超過線程池大小時,會将任務放入隊列等待執行
  • newCachedThreadPool (可緩存線程):該線程池的線程數不定,當線程池中有空閑線程時,會直接使用空閑線程,否則會建立新的線程執行任務。适用于執行大量短生命周期的異步任務。
  • newSingleThreadExecutor (單線程):該線程池隻有一個線程,在該線程執行任務的過程中,其他任務都會在隊列中等待執行。
  • newScheduledThreadPool (定時及周期執行):該線程池可以執行定時任務和周期性任務,也可以送出普通的異步任務。

需要注意阿裡巴巴《Java開發手冊》裡禁止使用這種方式來建立線程池。

42.說一說newSingleThreadExecutor工作原理

四分鐘快速入門Java線程的六種狀态與流轉

線程池特點:

  • 核心線程數為1
  • 最大線程數也為1
  • 阻塞隊列是無界隊列LinkedBlockingQueue,可能會導緻OOM
  • keepAliveTime為0
四分鐘快速入門Java線程的六種狀态與流轉

工作流程:

  • 送出任務
  • 線程池是否有一個線程正在運作,如果沒有,建立線程執行任務
  • 如果有并且非空閑狀态,将任務加到阻塞隊列
  • 目前的唯一線程,從隊列取任務,執行完一個,再繼續取,一個線程執行任務。

使用場景:

适用于串行執行任務的場景,一個任務一個任務地執行。

43.說一說newFixedThreadPool工作原理

四分鐘快速入門Java線程的六種狀态與流轉

線程池特點:

  • 核心線程數和最大線程數大小一樣
  • 沒有所謂的非空閑時間,即keepAliveTime為0
  • 阻塞隊列為無界隊列LinkedBlockingQueue,可能會導緻OOM
四分鐘快速入門Java線程的六種狀态與流轉

工作流程:

  • 送出任務
  • 如果線程數少于核心線程,建立核心線程執行任務
  • 如果線程數等于核心線程,把任務添加到LinkedBlockingQueue阻塞隊列
  • 如果線程執行完任務,去阻塞隊列取任務,繼續執行。

使用場景:

FixedThreadPool 适用于處理CPU密集型的任務,確定CPU在長期被工作線程使用的情況下,盡可能的少的配置設定線程,即适用執行長期的任務。

44.說一說newCachedThreadPool工作原理

四分鐘快速入門Java線程的六種狀态與流轉

線程池特點:

  • 核心線程數為0
  • 最大線程數為Integer.MAX_VALUE,即無限大,可能會因為無限建立線程,導緻OOM
  • 阻塞隊列是SynchronousQueue
  • 非核心線程空閑存活時間為60秒

當送出任務的速度大于處理任務的速度時,每次送出一個任務,就必然會建立一個線程。極端情況下會建立過多的線程,耗盡 CPU 和記憶體資源。由于空閑 60 秒的線程會被終止,長時間保持空閑的 CachedThreadPool 不會占用任何資源。

四分鐘快速入門Java線程的六種狀态與流轉

工作流程:

  • 送出任務
  • 因為沒有核心線程,是以任務直接加到SynchronousQueue隊列。
  • 判斷是否有空閑線程,如果有,就去取出任務執行。
  • 如果沒有空閑線程,就建立一個線程執行。
  • 執行完任務的線程,還可以存活60秒,如果在這期間,接到任務,可以繼續活下去;否則,被銷毀。

使用場景:

用于并發執行大量短期的小任務。

45.說一說newScheduledThreadPool工作原理

四分鐘快速入門Java線程的六種狀态與流轉

線程池特點:

  • 最大線程數為Integer.MAX_VALUE,也有OOM的風險
  • 阻塞隊列是DelayedWorkQueue
  • keepAliveTime為0
  • scheduleAtFixedRate() :按某種速率周期執行
  • scheduleWithFixedDelay():在某個延遲後執行
四分鐘快速入門Java線程的六種狀态與流轉
四分鐘快速入門Java線程的六種狀态與流轉

工作流程:

  • 線程從DelayQueue中擷取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大于等于目前時間。
  • 線程執行這個ScheduledFutureTask。
  • 線程修改ScheduledFutureTask的time變量為下次将要被執行的時間。
  • 線程把這個修改time之後的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

使用場景:

周期性執行任務的場景,需要限制線程數量的場景。

import java.util.concurrent.*;

/**
 * @author 百裡
 */
public class BaiLiScheduledThreadPoolDemo {
    public static void main(String[] args) throws Exception {
        // 建立一個可以執行定時任務的線程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 排程一個定時任務,每隔 2 秒鐘輸出一次目前時間
        ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(() -> {
            System.out.println("Current time: " + System.currentTimeMillis());
        }, 0, 2, TimeUnit.SECONDS);

        // 主線程休眠 10 秒鐘後取消任務
        Thread.sleep(10000);
        scheduledFuture.cancel(true);

        // 關閉線程池
        executorService.shutdown();
    }
}           
import java.util.concurrent.*;

/**
 * @author 百裡
 */
public class BaiLiScheduleWithFixedDelayDemo {
    public static void main(String[] args) throws Exception {
        // 建立一個可以執行定時任務的線程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 排程一個周期性任務,每次任務執行完畢後等待 2 秒鐘再執行下一個任務
        executorService.scheduleWithFixedDelay(() -> {
            System.out.println("Current time: " + System.currentTimeMillis());
        }, 0, 2, TimeUnit.SECONDS);

        // 主線程休眠 10 秒鐘後關閉線程池
        Thread.sleep(10000);
        executorService.shutdown();
    }
}           

46.線程池異常怎麼處理知道嗎?

在使用線程池處理任務的時候,任務代碼可能抛出RuntimeException,抛出異常後,線程池可能捕獲它,也可能建立一個新的線程來代替異常的線程,我們可能無法感覺任務出現了異常,是以我們需要考慮線程池異常情況。

常見的異常處理方式:

四分鐘快速入門Java線程的六種狀态與流轉

1.try-catch捕獲異常

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 百裡
 */
public class BaiLiHandlerException implements Runnable {

    @Override
    public void run() {
        try {
            // 任務代碼
            int a = 10 / 0;
        } catch (Exception e) {
            System.err.println("任務執行異常:" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        BaiLiHandlerException task = new BaiLiHandlerException();
        executor.execute(task);
    }
}           

2.使用Thread.UncaughtExceptionHandler處理異常

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

/**
 * @author 百裡
 */
public class BaiLiHandlerException implements Runnable {


    @Override
    public void run() {
        // 任務代碼
        int a = 10 / 0;
    }

    public static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.err.println("任務執行異常:" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        BaiLiHandlerException task = new BaiLiHandlerException();
        Thread thread = new Thread(task);
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        thread.start();
    }
}           

3.重寫ThreadPoolExecutor.afterExcute處理異常

package exception;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

/**
 * @author 百裡
 */
public class BaiLiHandlerException implements Runnable {


    @Override
    public void run() {
        // 任務代碼
        int a = 10 / 0;
    }

    public static class MyThreadPoolExecutor extends ThreadPoolExecutor {
        public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        }

        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            if (t != null) {
                System.err.println("任務執行異常:" + t.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat("MyThread-%d").build());
        BaiLiHandlerException task = new BaiLiHandlerException();
        executor.execute(task);

    }
}           

4.使用future.get處理異常

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

/**
 * @author 百裡
 */
public class BaiLiHandlerException {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(() -> {
            throw new RuntimeException("任務執行失敗");
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            Throwable ex = e.getCause();
            System.out.println("捕獲到異常: " + ex.getMessage());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            executor.shutdownNow();
            System.out.println("線程被中斷,已執行相應處理");
        }
        executor.shutdown();
    }
}           

47.能說一下線程池有幾種狀态嗎?

線程池有這幾個狀态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED

//線程池狀态
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;           

線程池各個狀态切換圖:

四分鐘快速入門Java線程的六種狀态與流轉

RUNNING

  • 該狀态的線程池會接收新任務,并處理阻塞隊列中的任務;
  • 調用線程池的shutdown()方法,可以切換到SHUTDOWN狀态;
  • 調用線程池的shutdownNow()方法,可以切換到STOP狀态;

SHUTDOWN

  • 該狀态的線程池不會接收新任務,但會處理阻塞隊列中的任務;
  • 隊列為空,并且線程池中執行的任務也為空,進入TIDYING狀态;

STOP

  • 該狀态的線程不會接收新任務,也不會處理阻塞隊列中的任務,而且會中斷正在運作的任務;
  • 線程池中執行的任務為空,進入TIDYING狀态;

TIDYING

  • 該狀态表明所有的任務已經運作終止,記錄的任務數量為0。
  • terminated()執行完畢,進入TERMINATED狀态

TERMINATED

  • 該狀态表示線程池徹底終止

48.單機線程池執行斷電了應該怎麼處理?

單機線程池是一種常見的多線程程式設計方式,它可以用于異步執行任務,提高應用程式的性能和并發能力。在單機線程池中,所有任務都由同一個線程處理,是以如果該線程在執行任務時突然斷電,則會出現以下問題:

  • 任務不完整
  • 資料丢失
  • 系統資源洩漏

如果單機線程池在執行任務時突然遇到斷電等異常情況,應該盡快采取以下措施:

  • 恢複中斷的任務:當系統重新啟動後,需要檢查之前被中斷的任務,并将其重新送出到單機線程池中進行處理,以確定所有任務都能夠被正确執行。
  • 資料備份及恢複:對于需要處理的資料,應該事先進行備份,以避免資料丢失或損壞。在系統重新開機後,需要将備份資料恢複到系統中,以確定資料的完整性和正确性。
  • 系統資源清理:在單機線程池執行過程中,可能會産生未釋放的系統資源,如檔案句柄、鎖等。在系統重新開機後,需要清理這些未釋放的系統資源,以避免資源洩漏和系統運作不穩定。

49.NIO的原理,包括哪幾個元件?

NIO(Java Non-blocking I/O)是一種 I/O 技術,其核心原理是基于事件驅動的方式進行操作。

NIO 的工作原理:基于緩沖區、通道和選擇器的組合,通過高效地利用系統資源,以支援高并發和高吞吐量的資料處理。相比傳統的 I/O 程式設計方式,Java NIO 提供了更為靈活和高效的程式設計方式。

NIO三大核心元件: Channel(通道)、Buffer(緩沖區)、Selector(選擇器)。

Selector、Channel 和 Buffer 的關系圖如下:

四分鐘快速入門Java線程的六種狀态與流轉
  1. Channel(通道):類似于傳統 I/O 中的 Stream,是用于實際資料傳輸的元件。在 NIO 中,有多種類型的 Channel 可以使用,例如 FileChannel、SocketChannel、DatagramChannel 等,可用于檔案操作、網絡傳輸等不同場景。
  2. Buffer(緩沖區):用于存儲資料的容器,可以了解為暫存需要傳輸的資料的地方。在 NIO 中,存在多種類型的緩沖區,如 ByteBuffer、CharBuffer、IntBuffer等。
  3. Selector(選擇器):用于注冊 Channel 并監聽其 I/O 事件。當 Channel 準備好讀或寫資料時,會得到通知。Selector 可以高效地輪詢多個 Channel,并且避免了使用多線程或多程序對多個 Channel 進行輪詢的情況,進而減少了系統資源開銷。

通俗了解NIO原理:

NIO 是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以配置設定 50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得配置設定 10000 個。

50.什麼是零拷貝?

零拷貝(Zero-Copy)是一種 I/O 操作優化技術,可以快速高效地将資料從檔案系統移動到網絡接口,而不需要将其從核心空間複制到使用者空間。

傳統I/O操作過程:

傳統 I/O 的工作方式是,資料讀取和寫入是從使用者空間到核心空間來回複制,而核心空間的資料是通過作業系統層面的 I/O 接口從磁盤讀取或寫入。代碼通常如下,一般會需要兩個系統調用:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);           

代碼很簡單,雖然就兩行代碼,但是這裡面發生了不少的事情:

四分鐘快速入門Java線程的六種狀态與流轉
  • 使用者應用程序調用read函數,向作業系統發起IO調用,上下文從使用者态轉為核心态(切換1)
  • DMA控制器把資料從磁盤中,讀取到核心緩沖區。
  • CPU把核心緩沖區資料,拷貝到使用者應用緩沖區,上下文從核心态轉為使用者态(切換2),read函數傳回
  • 使用者應用程序通過write函數,發起IO調用,上下文從使用者态轉為核心态(切換3)
  • CPU将應用緩沖區中的資料,拷貝到socket緩沖區
  • DMA控制器把資料從socket緩沖區,拷貝到網卡裝置,上下文從核心态切換回使用者态(切換4),write函數傳回

從流程圖可以看出,傳統IO的讀寫流程,包括了4次上下文切換(4次使用者态和核心态的切換),4次資料拷貝(兩次CPU拷貝以及兩次的DMA拷貝)

這種簡單又傳統的檔案傳輸方式,存在備援的上文切換和資料拷貝,在高并發系統裡是非常糟糕的,多了很多不必要的開銷,會嚴重影響系統性能。

是以,要想提高檔案傳輸的性能,就需要減少「使用者态與核心态的上下文切換」和「記憶體拷貝」的次數。

零拷貝主要是用來解決作業系統在處理 I/O 操作時,頻繁複制資料的問題。關于零拷貝主要技術有MMap+Write、SendFile等幾種方式。

Mmap+Wirte實作零拷貝:

四分鐘快速入門Java線程的六種狀态與流轉
  • 使用者程序通過mmap方法向作業系統核心發起IO調用,上下文從使用者态切換為核心态。
  • CPU利用DMA控制器,把資料從硬碟中拷貝到核心緩沖區。
  • 上下文從核心态切換回使用者态,mmap方法傳回。
  • 使用者程序通過write方法向作業系統核心發起IO調用,上下文從使用者态切換為核心态。
  • CPU将核心緩沖區的資料拷貝到的socket緩沖區。
  • CPU利用DMA控制器,把資料從socket緩沖區拷貝到網卡,上下文從核心态切換回使用者态,write調用傳回。

可以發現,mmap+write實作的零拷貝,I/O發生了4次使用者空間與核心空間的上下文切換,以及3次資料拷貝。其中3次資料拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

SendFile實作零拷貝:

四分鐘快速入門Java線程的六種狀态與流轉
  1. 使用者程序發起sendfile系統調用,上下文(切換1)從使用者态轉向核心态
  2. DMA控制器,把資料從硬碟中拷貝到核心緩沖區。
  3. CPU将讀緩沖區中資料拷貝到socket緩沖區
  4. DMA控制器,異步把資料從socket緩沖區拷貝到網卡,
  5. 上下文(切換2)從核心态切換回使用者态,sendfile調用傳回。

可以發現,sendfile實作的零拷貝,I/O發生了2次使用者空間與核心空間的上下文切換,以及3次資料拷貝。其中3次資料拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

繼續閱讀