亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.430618.com 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

sendfile實現的零拷貝,I/O發生了2次用戶空間與內核空間的上下文切換,以及3次數據拷貝。其中3次數據拷貝中,包括了2次DMA拷貝和1次CPU拷貝。?

1.并行與并發有什么區別?

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

  • 并行:多個任務在同一時刻同時運行,通常需要使用多個處理器或者多核處理器來實現。例如,一個計算機同時執行多個程序、多個線程或者多個進程時,就是采用并行的方式來處理任務,這樣能夠提高計算機的處理效率。
  • 并發:多個任務同時進行,但是這些任務的執行是交替進行的,即一個任務執行一段時間后,再執行另外一個任務。它是通過操作系統的協作調度來實現各個任務的切換,達到看上去同時進行的效果。例如,一個多線程程序中的多個線程就是同時運行的,但是因為 CPU 只能處理一個線程,所以在任意時刻只有一個線程在執行,線程之間會通過競爭的方式來獲取 CPU 的時間片。

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

2.說說什么是進程和線程?

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

進程:一個程序在執行過程中的一個實例,每個進程都有自己獨立的地址空間,也就是說它們不能直接共享內存。進程的特點包括:

  • 需要占用獨立的內存空間;
  • 可以并發地執行多個任務;
  • 進程之間需要通過進程間通信(IPC)來交換數據;

線程:進程中的一個執行單元,一個進程中可以包含多個線程,這些線程共享進程的內存空間。線程的特點包括:

  • 線程共享進程內存空間,可以方便、高效地訪問變量;
  • 同一個進程中的多個線程可以并發地執行多個任務;
  • 線程之間切換開銷小,可以實現更細粒度的控制,例如 UI 線程控制界面刷新,工作線程進行耗時的計算等。

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

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

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.線程有哪些常用的調度方法

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.線程有幾種狀態?

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

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

線程上下文切換指的是在多線程運行時,操作系統從當前正在執行的線程中保存其上下文(包括當前線程的寄存器、程序指針、棧指針等狀態信息),并將這些信息恢復到另一個等待執行的線程中,從而實現線程之間的切換。

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

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

  • 共享變量:共享變量是指多個線程都可以訪問和修改的變量,它們通常是在主線程中創建的。多個線程對同一個共享變量進行讀寫操作時,可能會出現競態條件導致數據錯誤或程序異常。因此需要使用同步機制比如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變量,那么訪問這個變量的每個線程都會有這個變量的一個本地拷貝,多個線程操作這個變量的時候,實際是操作自己本地內存里面的變量,從而起到線程隔離的作用,避免了線程安全問題。

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為需要緩存的值

實現方式觀察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對象。

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比較類似的,主要關注的是兩個要素:元素數組和散列方法。

  • 元素數組一個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沖突,不會影響到已經插入的元素,而只是會影響到新的插入操作。

查找的時候,先根據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.

需要注意的是,新數組的長度始終是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內存模型的抽象圖:

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

  • 線程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。

  • 可見性:指一個線程對共享變量的修改,對于其他線程應該是立即可見的,確保了各個線程之間對內存狀態的正確觀察。
  • 有序性:指程序執行的順序按照代碼的順序執行。在單線程的情況下,代碼執行順序與編寫的順序一致。但在多線程環境中,由于時間片輪換,不同的線程可能會交替執行不同的代碼。

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

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

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

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

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

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

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

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

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

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

happens-before的定義:

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

happens-before的六大規則:

  • 程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作。

/**
 * 順序性規則
 * 順序執行是針對代碼邏輯而言的,在實際執行的時候發生指令重排序但是并沒有改變源代碼的邏輯。
 * @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操作之前

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。

從圖中,我們可以看到:

  • “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中的任意操作。

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

  • 線程結束規則:如果線程A執行操作ThreadB.join()并成功返回,那么線程B中的任意操作 happens-before于 ThreadB.join()操作成功返回后的線程A操作。

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

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

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

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

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

21.volatile實現原理了解嗎?

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

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

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

為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

  1. 在每個volatile寫操作的前面插入一個StoreStore屏障
  2. 在每個volatile寫操作的后面插入一個StoreLoad屏障
  3. 在每個volatile讀操作的后面插入一個LoadLoad屏障
  4. 在每個volatile讀操作的后面插入一個LoadStore屏障

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

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

synchronized主要有三種用法:

  • 修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖。

  • 修飾靜態方法:給當前類加鎖,在同一時間內,只能有一個線程持有該類對應的 Class 對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該靜態方法。

  • 修飾代碼塊 :指定一個同步鎖對象,這個對象可以是具體的Object或者是類.class。在同一時間內,只能有一個線程持有該同步鎖對象的鎖,其他線程需要等待鎖的釋放才能繼續執行該代碼塊。

注意事項:

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

23.synchronized的實現原理?

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

  1. synchronized修飾代碼塊時,JVM采用monitorenter、monitorexit兩個指令來實現同步,monitorenter 指令指向同步代碼塊的開始位置, monitorexit 指令則指向同步代碼塊的結束位置。

/**
 * @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標記符來實現同步,這個標識指明了該方法是一個同步方法。同樣可以寫段代碼反編譯看一下。

/**
 * @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不用手動釋放鎖。

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

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

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

FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜態內部類,只不過實現不同鎖語義。

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

  • 非公平鎖在調用 lock 后,首先就會調用 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被占用,那么直接就獲取到鎖返回了。
  • 非公平鎖在 CAS 失敗后,和公平鎖一樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,如果發現鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,但是公平鎖會判斷等待隊列是否有線程處于等待狀態,如果有則不去搶鎖,乖乖排到后面。

28.什么是CAS?

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

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

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

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

CAS的經典三大問題:

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++的結果正確

  • 使用 Atomic變量,Java 并發包內提供了一些原子類型,如AtomicInteger、AtomicLong等,這些類提供了一些原子操作方法,可以保證相應的操作的原子性。

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

  • 使用synchronized,對i++操作加鎖。

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

  • 使用鎖機制,除了使用 synchronized 關鍵字外,還可以使用 ReentrantLock 類來保護臨界區域。

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

31.AtomicInteger的原理是什么?

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

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

  1. 調用 incrementAndGet()方法,該方法會通過調用unsafe.getAndAddInt()方法,獲取當前 AtomicInteger對象的值val
  2. 將 val + 1 得到新的值 next

  1. 使用 unsafe.compareAndSwapInt() 方法進行 CAS 操作,將對象中當前值與預期值(步驟1中獲取的val)進行比較,如果相等,則將 next賦值給val;否則返回 false
  2. 如果CAS操作返回false,說明有其他線程已經修改了AtomicInteger對象的值,需要重新執行步驟 1

總結:

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

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

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

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

  • 互斥條件:指線程在占用某個資源時,會把該資源標記為已占用,并且鎖住該資源,以保證同一時間內只有一個線程可以訪問該資源。其他需要訪問該資源的線程就只能等待該線程釋放該資源,在資源被釋放之后才能進行訪問。
  • 請求并持有條件:指一個線程己經持有了至少一個資源,但又提出了新的資源請求,而新資源己被其它線程占有,所以當前線程會被阻塞,但阻塞的同時并不釋放自己已經獲取的資源。
  • 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其它線程搶占,只有在自己使用完畢后才由自己釋放該資源。
  • 環路等待條件:指在發生死鎖時,若干線程形成頭尾相接的循環等待資源關系。

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

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

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.選擇線程標簽,點擊檢測死鎖。查看死鎖線程信息

34.什么是線程池?

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

線程池的主要優點包括:

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

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

用一個通俗的比喻:

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

  1. 小天發現有空閑的窗口,直接去找小姐姐辦理業務。

  1. 小天發現沒有空閑的窗口,就在排隊區排隊等。

  1. 小天發現沒有空閑的窗口,等待區也滿了,經理一看,就讓休息的營業員小姐姐趕緊回來上班,等待區號靠前的趕緊去新窗口辦,小天去排隊區排隊。小姐姐比較辛苦,假如一段時間發現他們可以不用接著營業,經理就讓她們接著休息。

  1. 小天一看,六個窗口都滿了,等待區也沒位置了。小天就開始投訴急了,要鬧,經理趕緊出來了,經理該怎么辦呢?

  1. 我們銀行系統已經癱瘓
  2. 誰叫你來辦的你找誰去
  3. 看你比較急,去隊里加個塞
  4. 今天沒辦法,不行你看改一天

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

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

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

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

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

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.線程池有哪幾種工作隊列

  • 有界隊列(ArrayBlockingQueue):是一個用數組實現的有界阻塞隊列,按FIFO排序。
  • 無界隊列(LinkedBlockingQueue):是基于鏈表結構的阻塞隊列,按FIFO排序,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,因此在任務數量很大且任務執行時間較長時,無界隊列可以保證任務不會被丟棄,但同時也會導致線程池中線程數量不斷增加,可能會造成內存溢出等問題。
  • 延遲隊列(DelayQueue):是一個任務定時周期的延遲執行的隊列。根據指定的執行時間從小到大排序,否則根據插入到隊列的先后排序。
  • 優先級隊列(PriorityBlockingQueue):是具有優先級的無界阻塞隊列。與無界隊列類似,優先級隊列可以保證所有任務都會被執行,但不同的是優先級隊列可以對任務進行管理和排序,確保高優先級的任務優先執行。
  • 同步隊列(SynchronousQueue):是一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀態,吞吐量通常要高于無界隊列。

39.線程池提交execute和submit有什么區別?

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

  1. execute() 用于提交不需要返回值的任務

  1. submit() 用于提交需要返回值的任務。線程池會返回一個future類型的對象,通過這個 future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來獲取返回值

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創建出來的。

  • newFixedThreadPool (固定數目線程):該線程池具有固定的線程數,當提交的任務超過線程池大小時,會將任務放入隊列等待執行
  • newCachedThreadPool (可緩存線程):該線程池的線程數不定,當線程池中有空閑線程時,會直接使用空閑線程,否則會創建新的線程執行任務。適用于執行大量短生命周期的異步任務。
  • newSingleThreadExecutor (單線程):該線程池只有一個線程,在該線程執行任務的過程中,其他任務都會在隊列中等待執行。
  • newScheduledThreadPool (定時及周期執行):該線程池可以執行定時任務和周期性任務,也可以提交普通的異步任務。

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

42.說一說newSingleThreadExecutor工作原理

線程池特點:

  • 核心線程數為1
  • 最大線程數也為1
  • 阻塞隊列是無界隊列LinkedBlockingQueue,可能會導致OOM
  • keepAliveTime為0

工作流程:

  • 提交任務
  • 線程池是否有一個線程正在運行,如果沒有,新建線程執行任務
  • 如果有并且非空閑狀態,將任務加到阻塞隊列
  • 當前的唯一線程,從隊列取任務,執行完一個,再繼續取,一個線程執行任務。

使用場景:

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

43.說一說newFixedThreadPool工作原理

線程池特點:

  • 核心線程數和最大線程數大小一樣
  • 沒有所謂的非空閑時間,即keepAliveTime為0
  • 阻塞隊列為無界隊列LinkedBlockingQueue,可能會導致OOM

工作流程:

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

使用場景:

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

44.說一說newCachedThreadPool工作原理

線程池特點:

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

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

工作流程:

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

使用場景:

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

45.說一說newScheduledThreadPool工作原理

線程池特點:

  • 最大線程數為Integer.MAX_VALUE,也有OOM的風險
  • 阻塞隊列是DelayedWorkQueue
  • keepAliveTime為0
  • scheduleAtFixedRate() :按某種速率周期執行
  • scheduleWithFixedDelay():在某個延遲后執行

工作流程:

  • 線程從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,拋出異常后,線程池可能捕獲它,也可能創建一個新的線程來代替異常的線程,我們可能無法感知任務出現了異常,因此我們需要考慮線程池異常情況。

常見的異常處理方式:

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;

線程池各個狀態切換圖:

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 的關系圖如下:

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

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

  • 用戶應用進程調用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實現零拷貝:

  • 用戶進程通過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實現零拷貝:

  1. 用戶進程發起sendfile系統調用,上下文(切換1)從用戶態轉向內核態
  2. DMA控制器,把數據從硬盤中拷貝到內核緩沖區。
  3. CPU將讀緩沖區中數據拷貝到socket緩沖區
  4. DMA控制器,異步把數據從socket緩沖區拷貝到網卡,
  5. 上下文(切換2)從內核態切換回用戶態,sendfile調用返回。

可以發現,sendfile實現的零拷貝,I/O發生了2次用戶空間與內核空間的上下文切換,以及3次數據拷貝。其中3次數據拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

分享到:
標簽:線程 Java
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定