日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

sendfile實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。?

1.并行與并發(fā)有什么區(qū)別?

并行和并發(fā)都是指多個(gè)任務(wù)同時(shí)執(zhí)行的概念,但是它們之間有著明顯的區(qū)別。

  • 并行:多個(gè)任務(wù)在同一時(shí)刻同時(shí)運(yùn)行,通常需要使用多個(gè)處理器或者多核處理器來(lái)實(shí)現(xiàn)。例如,一個(gè)計(jì)算機(jī)同時(shí)執(zhí)行多個(gè)程序、多個(gè)線程或者多個(gè)進(jìn)程時(shí),就是采用并行的方式來(lái)處理任務(wù),這樣能夠提高計(jì)算機(jī)的處理效率。
  • 并發(fā):多個(gè)任務(wù)同時(shí)進(jìn)行,但是這些任務(wù)的執(zhí)行是交替進(jìn)行的,即一個(gè)任務(wù)執(zhí)行一段時(shí)間后,再執(zhí)行另外一個(gè)任務(wù)。它是通過(guò)操作系統(tǒng)的協(xié)作調(diào)度來(lái)實(shí)現(xiàn)各個(gè)任務(wù)的切換,達(dá)到看上去同時(shí)進(jìn)行的效果。例如,一個(gè)多線程程序中的多個(gè)線程就是同時(shí)運(yùn)行的,但是因?yàn)?CPU 只能處理一個(gè)線程,所以在任意時(shí)刻只有一個(gè)線程在執(zhí)行,線程之間會(huì)通過(guò)競(jìng)爭(zhēng)的方式來(lái)獲取 CPU 的時(shí)間片。

總的來(lái)說(shuō),雖然并行和并發(fā)都是多任務(wù)處理的方式,但是并行是采用多核處理器等硬件實(shí)現(xiàn)任務(wù)同步執(zhí)行,而并發(fā)則是通過(guò)操作系統(tǒng)的調(diào)度算法來(lái)合理地分配系統(tǒng)資源,使得多個(gè)任務(wù)看上去同時(shí)執(zhí)行。

2.說(shuō)說(shuō)什么是進(jìn)程和線程?

進(jìn)程和線程是操作系統(tǒng)中的概念,用于描述程序運(yùn)行時(shí)的執(zhí)行實(shí)體。

進(jìn)程:一個(gè)程序在執(zhí)行過(guò)程中的一個(gè)實(shí)例,每個(gè)進(jìn)程都有自己獨(dú)立的地址空間,也就是說(shuō)它們不能直接共享內(nèi)存。進(jìn)程的特點(diǎn)包括:

  • 需要占用獨(dú)立的內(nèi)存空間;
  • 可以并發(fā)地執(zhí)行多個(gè)任務(wù);
  • 進(jìn)程之間需要通過(guò)進(jìn)程間通信(IPC)來(lái)交換數(shù)據(jù);

線程:進(jìn)程中的一個(gè)執(zhí)行單元,一個(gè)進(jìn)程中可以包含多個(gè)線程,這些線程共享進(jìn)程的內(nèi)存空間。線程的特點(diǎn)包括:

  • 線程共享進(jìn)程內(nèi)存空間,可以方便、高效地訪問(wèn)變量;
  • 同一個(gè)進(jìn)程中的多個(gè)線程可以并發(fā)地執(zhí)行多個(gè)任務(wù);
  • 線程之間切換開(kāi)銷小,可以實(shí)現(xiàn)更細(xì)粒度的控制,例如 UI 線程控制界面刷新,工作線程進(jìn)行耗時(shí)的計(jì)算等。

線程相比于進(jìn)程,線程的創(chuàng)建和銷毀開(kāi)銷較小,上下文切換開(kāi)銷也較小,因此線程是實(shí)現(xiàn)多任務(wù)并發(fā)的一種更加輕量級(jí)的方式。

3.說(shuō)說(shuō)線程有幾種創(chuàng)建方式?

JAVA中創(chuàng)建線程主要有三種方式:

  • 定義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接口的實(shí)現(xiàn)類,并重寫該接口的run()方法
 
/**
 * 實(shí)現(xiàn)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接口的實(shí)現(xiàn)類,并重寫該接口的call()方法,一般配合FutureTask使用
 
/**
 * 實(shí)現(xiàn)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.為什么調(diào)用start()方法時(shí)會(huì)執(zhí)行run()方法,那怎么不直接調(diào)用run()方法?

JVM執(zhí)行start方法,會(huì)先創(chuàng)建一個(gè)線程,由創(chuàng)建出來(lái)的新線程去執(zhí)行thread的run方法,這才起到多線程的效果。

start()和run()的主要區(qū)別如下:

  • start方法可以啟動(dòng)一個(gè)新線程,run方法只是類的一個(gè)普通方法而已,如果直接調(diào)用run方法,程序中依然只有主線程這一個(gè)線程。
  • start方法實(shí)現(xiàn)了多線程,而run方法沒(méi)有實(shí)現(xiàn)多線程。
  • start不能被重復(fù)調(diào)用,而run方法可以。
  • start方法中的run代碼可以不執(zhí)行完,就繼續(xù)執(zhí)行下面的代碼,也就是說(shuō)進(jìn)行了線程切換。然而,如果直接調(diào)用run方法,就必須等待其代碼全部執(zhí)行完才能繼續(xù)執(zhí)行下面的代碼。
/**
 * 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.線程有哪些常用的調(diào)度方法

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("線程進(jìn)入永久等待"+ 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("線程進(jìn)入超時(shí)等待"+ LocalTime.now());
                    lock.wait(5000);
                    System.out.println("線程超時(shí)等待喚醒"+ 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(); // 調(diào)用 yield 方法,讓出 CPU 執(zhí)行時(shí)間
        }
    }

    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()+":當(dāng)前線程中斷狀態(tài)_"+isInterrupted());
        if(isInterrupted()){
            if(!interrupted()){
                System.out.println(Thread.currentThread().getName()+":當(dāng)前線程中斷狀態(tài)_"+isInterrupted());
            }
        }
    }

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

        interruptedDemo.interrupt();
        System.out.println(Thread.currentThread().getName()+":當(dāng)前線程中斷狀態(tài)_"+Thread.interrupted());
    }
}

6.線程有幾種狀態(tài)?

線程在自身的生命周期中, 并不是固定地處于某個(gè)狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換,如下圖:

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

線程上下文切換指的是在多線程運(yùn)行時(shí),操作系統(tǒng)從當(dāng)前正在執(zhí)行的線程中保存其上下文(包括當(dāng)前線程的寄存器、程序指針、棧指針等狀態(tài)信息),并將這些信息恢復(fù)到另一個(gè)等待執(zhí)行的線程中,從而實(shí)現(xiàn)線程之間的切換。

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

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

  • 共享變量:共享變量是指多個(gè)線程都可以訪問(wèn)和修改的變量,它們通常是在主線程中創(chuàng)建的。多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行讀寫操作時(shí),可能會(huì)出現(xiàn)競(jìng)態(tài)條件導(dǎo)致數(shù)據(jù)錯(cuò)誤或程序異常。因此需要使用同步機(jī)制比如synchronized、Lock等來(lái)保證線程安全
  • 管道通信:管道是一種基于文件描述符的通信機(jī)制,形成一個(gè)單向通信的數(shù)據(jù)流管道。它通常用于只有兩個(gè)進(jìn)程或線程之間的通信。其中一個(gè)進(jìn)程將數(shù)據(jù)寫入到管道(管道的輸出端口),而另一個(gè)進(jìn)程從管道的輸入端口讀取數(shù)據(jù)
  • 信號(hào)量:信號(hào)量是一種計(jì)數(shù)器,用于控制多個(gè)線程對(duì)資源的訪問(wèn)。當(dāng)一個(gè)線程需要訪問(wèn)資源時(shí),它需要申請(qǐng)獲取信號(hào)量,如果信號(hào)量的計(jì)數(shù)器值大于 0,則可以訪問(wèn)資源,否則該線程就會(huì)等待。當(dāng)線程結(jié)束訪問(wèn)資源后,需要釋放信號(hào)量,并將計(jì)數(shù)器加1
  • 條件變量:條件變量是一種通知機(jī)制,用于在多個(gè)線程之間傳遞狀態(tài)信息和控制信息。當(dāng)某個(gè)線程需要等待某個(gè)條件變量發(fā)生改變時(shí),它可以調(diào)用 wait() 方法掛起,并且釋放所占用的鎖。當(dāng)某個(gè)線程滿足條件后,可以調(diào)用 notify() 或者 signal() 方法來(lái)通知等待該條件變量的線程繼續(xù)執(zhí)行
 
/**
 * 共享變量
 * 創(chuàng)建人:百里
 */
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();
    }
}
 
/**
 * 管道通信模式
 * 創(chuàng)建人:百里
 */
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();
    }
}
 
/**
 * 信號(hào)量
 * 創(chuàng)建人:百里
 */
public class BaiLiSemaphoreDemo {
    public static void main(String[] args) {
        // 實(shí)例化一個(gè)信號(hào)量對(duì)象,初始值為 0
        Semaphore semaphore = new Semaphore(0);

        // 創(chuàng)建生產(chǎn)者線程
        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "_Producer:" + i);
                    semaphore.release(); // 把信號(hào)量的計(jì)數(shù)器加 1
                    Thread.sleep(1000); //模擬停頓
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "ProducerThread");

        // 創(chuàng)建消費(fèi)者線程
        Thread consumeThread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    semaphore.acquire(); // 請(qǐng)求占有信號(hào)量,如果計(jì)數(shù)器不為 0,計(jì)數(shù)器減 1,否則線程阻塞等待
                    System.out.println(Thread.currentThread().getName() + "_Consume:" + i);
                    Thread.sleep(1000); //模擬停頓
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "ConsumeThread");

        producerThread.start();
        consumeThread.start();
    }
}
 
/**
 * 條件變量|可重入鎖
 * 創(chuàng)建人:百里
 */
public class BaiLIConditionDemo {
    public static void main(String[] args) {
        // 實(shí)例化一個(gè)可重入鎖對(duì)象
        ReentrantLock lock = new ReentrantLock();
        // 獲取該鎖對(duì)象的條件變量
        Condition condition = lock.newCondition();

        // 創(chuàng)建生產(chǎn)者線程
        Thread producerThread = new Thread(() -> {
            try {
                lock.lock(); // 獲取鎖對(duì)象
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " produce: " + i);
                    condition.signal(); // 喚醒處于等待狀態(tài)下的消費(fèi)者線程
                    condition.await(); // 使當(dāng)前線程處于等待狀態(tài),并釋放鎖對(duì)象
                    Thread.sleep(1000);
                }
                condition.signal(); // 避免消費(fèi)者線程一直等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 釋放鎖對(duì)象
            }
        }, "producer");

        // 創(chuàng)建消費(fèi)者線程
        Thread consumerThread = new Thread(() -> {
            try {
                lock.lock(); // 獲取鎖對(duì)象
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " consume: " + i);
                    condition.signal(); // 喚醒處于等待狀態(tài)下的生產(chǎn)者線程
                    condition.await(); // 使當(dāng)前線程處于等待狀態(tài),并釋放鎖對(duì)象
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 釋放鎖對(duì)象
            }
        }, "consumer");

        // 啟動(dòng)生產(chǎn)者和消費(fèi)者線程
        producerThread.start();
        consumerThread.start();
    }
}

9.ThreadLocal是什么?

ThreadLocal也就是線程本地變量。如果你創(chuàng)建了一個(gè)ThreadLocal變量,那么訪問(wèn)這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的一個(gè)本地拷貝,多個(gè)線程操作這個(gè)變量的時(shí)候,實(shí)際是操作自己本地內(nèi)存里面的變量,從而起到線程隔離的作用,避免了線程安全問(wèn)題。

ThreadLocal是整個(gè)線程的全局變量,不是整個(gè)程序的全局變量。

/**
 * ThreadLocal
 * 創(chuàng)建人:百里
 */
public class BaiLiThreadLocalDemo {
    //創(chuàng)建一個(gè)靜態(tài)的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怎么實(shí)現(xiàn)?

  • ThreadLocal是Java中所提供的線程本地存儲(chǔ)機(jī)制,可以利用該機(jī)制將數(shù)據(jù)緩存在某個(gè)線程內(nèi)部,該線程可以在任意時(shí)刻、任意方法中獲取緩存的數(shù)據(jù)
  • ThreadLocal底層是通過(guò)ThreadLocalMap來(lái)實(shí)現(xiàn)的,每個(gè)Thread對(duì)象(注意不是ThreadLocal對(duì)象)中都存在一個(gè)ThreadLocalMap,Map的key為ThreadLocal對(duì)象,Map的value為需要緩存的值

實(shí)現(xiàn)方式觀察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內(nèi)存泄露是怎么回事?

如果在線程池中使用ThreadLocal會(huì)造成內(nèi)存泄漏,因?yàn)楫?dāng)ThreadLocal對(duì)象使用完之后,應(yīng)該要把設(shè)置的key,value,也就是Entry對(duì)象進(jìn)行回收,但線程池中的線程不會(huì)回收,而線程對(duì)象是通過(guò)強(qiáng)引用指向ThreadLocalMap,ThreadLocalMap也是通過(guò)強(qiáng)引用指向Entry對(duì)象,線程不被回收,Entry對(duì)象也就不會(huì)被回收,從而出現(xiàn)內(nèi)存泄漏。

解決辦法是在使用了ThreadLocal對(duì)象之后,手動(dòng)調(diào)用ThreadLocal的remove方法,手動(dòng)清除Entry對(duì)象。

package communication;

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

/**
 * 創(chuàng)建人:百里
 */
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);
                // 不調(diào)用 remove 方法,會(huì)導(dǎo)致內(nèi)存泄漏
                //threadLocal.remove();
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
    }
}

12.ThreadLocalMap的結(jié)構(gòu)

ThreadLocalMap雖然被稱為Map,但是其實(shí)它是沒(méi)有實(shí)現(xiàn)Map接口的,不過(guò)結(jié)構(gòu)還是和HashMap比較類似的,主要關(guān)注的是兩個(gè)要素:元素?cái)?shù)組和散列方法。

  • 元素?cái)?shù)組一個(gè)table數(shù)組,存儲(chǔ)Entry類型的元素,Entry是ThreadLocal弱引用作為key,Object作為value的結(jié)構(gòu)。
private Entry[] table;
  • 散列方法就是怎么把對(duì)應(yīng)的key映射到table數(shù)組的相應(yīng)下標(biāo),ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table數(shù)組長(zhǎng)度減一&運(yùn)算(相當(dāng)于取余)
int i = key.threadLocalHashCode & (table.length - 1);

補(bǔ)充一點(diǎn)每創(chuàng)建一個(gè)ThreadLocal對(duì)象,它就會(huì)新增0x61c88647,這個(gè)值很特殊,它是斐波那契數(shù)也叫黃金分割數(shù)。這樣帶來(lái)的好處就是hash分布非常均勻。

private static final int HASH_INCREMENT = 0x61c88647;

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

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

我們可能都知道HashMap使用了鏈表來(lái)解決沖突,也就是所謂的鏈地址法。

ThreadLocalMap內(nèi)部使用的是開(kāi)放地址法來(lái)解決 Hash沖突的問(wèn)題。具體來(lái)說(shuō),當(dāng)發(fā)生Hash沖突時(shí),ThreadLocalMap會(huì)將當(dāng)前插入的元素從沖突位置開(kāi)始依次往后遍歷,直到找到一個(gè)空閑的位置,然后把該元素放在這個(gè)空閑位置。這樣即使出現(xiàn)了Hash沖突,不會(huì)影響到已經(jīng)插入的元素,而只是會(huì)影響到新的插入操作。

查找的時(shí)候,先根據(jù)ThreadLocal對(duì)象的hash值找到對(duì)應(yīng)的位置,然后比較該槽位Entry對(duì)象中的key是否和get的key一致,如果不一致就依次往后查找。

14.ThreadLocalMap擴(kuò)容機(jī)制

ThreadLocalMap 的擴(kuò)容機(jī)制和 HashMap 類似,也是在元素?cái)?shù)量達(dá)到閾值(默認(rèn)為數(shù)組長(zhǎng)度的 2/3)時(shí)進(jìn)行擴(kuò)容。具體來(lái)說(shuō),在 set() 方法中,如果當(dāng)前元素?cái)?shù)量已經(jīng)達(dá)到了閾值,就會(huì)調(diào)用 rehash() 方法,rehash()會(huì)先去清理過(guò)期的Entry,然后還要根據(jù)條件判斷size >= threshold - threshold / 4 也就是size >= threshold * 3/4來(lái)決定是否需要擴(kuò)容:

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

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

//清理過(guò)期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);
    }
}

發(fā)現(xiàn)需要擴(kuò)容時(shí)調(diào)用resize()方法,resize()方法首先將數(shù)組長(zhǎng)度翻倍,然后創(chuàng)建一個(gè)新的數(shù)組newTab。接著遍歷舊數(shù)組oldTab中的所有元素,散列方法重新計(jì)算位置,開(kāi)放地址解決沖突,然后放到新的newTab,遍歷完成之后,oldTab中所有的entry數(shù)據(jù)都已經(jīng)放入到newTab中了,然后table引用指向newTab.

需要注意的是,新數(shù)組的長(zhǎng)度始終是2的整數(shù)次冪,并且擴(kuò)容后新數(shù)組的長(zhǎng)度始終大于舊數(shù)組的長(zhǎng)度。這是為了保證哈希函數(shù)計(jì)算出的位置在新數(shù)組中仍然有效。

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怎么進(jìn)行父子線程通信

在Java多線程編程中,父子線程之間的數(shù)據(jù)傳遞和共享問(wèn)題一直是一個(gè)非常重要的議題。如果不處理好數(shù)據(jù)的傳遞和共享,會(huì)導(dǎo)致多線程程序的性能下降或者出現(xiàn)線程安全問(wèn)題。ThreadLocal是Java提供的一種解決方案,可以非常好地解決父子線程數(shù)據(jù)共享和傳遞的問(wèn)題。

那么它是如何實(shí)現(xiàn)通信的了?在Thread類中存在InheritableThreadLocal變量,簡(jiǎn)單的說(shuō)就是使用InheritableThreadLocal來(lái)進(jìn)行傳遞,當(dāng)父線程的InheritableThreadLocal不為空時(shí),就會(huì)將這個(gè)值傳到當(dāng)前子線程的InheritableThreadLocal。

/**
 * ThreadLocal父子線程通信
 * 創(chuàng)建人:百里
 */
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.說(shuō)一下你對(duì)Java內(nèi)存模型(JMM)的理解?

Java 內(nèi)存模型(Java Memory Model)是一種規(guī)范,用于描述 Java 虛擬機(jī)(JVM)中多線程情況下,線程之間如何協(xié)同工作,如何共享數(shù)據(jù),并保證多線程的操作在各個(gè)線程之間的可見(jiàn)性、有序性和原子性。

具體定義如下:

  • 所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。
  • 每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的拷貝副本。
  • 線程對(duì)變量的所有操作都必須在本地內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。
  • 不同的線程之間無(wú)法直接訪問(wèn)對(duì)方本地內(nèi)存中的變量;線程間共享變量時(shí),通過(guò)主內(nèi)存來(lái)實(shí)現(xiàn)通信、協(xié)作和傳遞信息。

Java內(nèi)存模型的抽象圖:

在這個(gè)抽象的內(nèi)存模型中,在兩個(gè)線程之間的通信(共享變量狀態(tài)變更)時(shí),會(huì)進(jìn)行如下兩個(gè)步驟:

  • 線程A把在本地內(nèi)存更新后的共享變量副本的值,刷新到主內(nèi)存中。
  • 線程B在使用到該共享變量時(shí),到主內(nèi)存中去讀取線程A更新后的共享變量的值,并更新線程B本地內(nèi)存的值。

17.說(shuō)說(shuō)你對(duì)原子性、可見(jiàn)性、有序性的理解?

原子性、有序性、可見(jiàn)性是并發(fā)編程中非常重要的基礎(chǔ)概念,JMM的很多技術(shù)都是圍繞著這三大特性展開(kāi)。

  • 原子性:指一個(gè)操作是不可分割、不可中斷的,在執(zhí)行過(guò)程中不會(huì)受到其他線程的干擾,要么全部執(zhí)行,要么就全不執(zhí)行。即使是在多線程的環(huán)境下,一個(gè)操作也是原子性的執(zhí)行完成。

線程切換會(huì)帶來(lái)原子性問(wèn)題,示例:

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

上面展示語(yǔ)句中,除了語(yǔ)句1是原子操作,其它兩個(gè)語(yǔ)句都不是原子性操作,下面我們來(lái)分析一下語(yǔ)句2

其實(shí)語(yǔ)句2在執(zhí)行的時(shí)候,包含三個(gè)指令操作

  • 指令 1:首先,把變量 count 從內(nèi)存加載到 CPU 的寄存器
  • 指令 2:然后,在寄存器中執(zhí)行 +1 操作
  • 指令 3:最終,將結(jié)果寫入內(nèi)存

對(duì)于上面的三條指令來(lái)說(shuō),如果線程 A 在指令 1 執(zhí)行完后做線程切換,線程 A 和線程 B 按照下圖的序列執(zhí)行,那么我們會(huì)發(fā)現(xiàn)兩個(gè)線程都執(zhí)行了 count+=1 的操作,但是得到的結(jié)果不是我們期望的 2,而是 1。

  • 可見(jiàn)性:指一個(gè)線程對(duì)共享變量的修改,對(duì)于其他線程應(yīng)該是立即可見(jiàn)的,確保了各個(gè)線程之間對(duì)內(nèi)存狀態(tài)的正確觀察。
  • 有序性:指程序執(zhí)行的順序按照代碼的順序執(zhí)行。在單線程的情況下,代碼執(zhí)行順序與編寫的順序一致。但在多線程環(huán)境中,由于時(shí)間片輪換,不同的線程可能會(huì)交替執(zhí)行不同的代碼。

原子性、可見(jiàn)性、有序性都應(yīng)該怎么保證呢?

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

18.說(shuō)說(shuō)什么是指令重排?

在不影響單線程程序執(zhí)行結(jié)果的前提下,計(jì)算機(jī)為了最大限度的發(fā)揮機(jī)器性能,對(duì)機(jī)器指令進(jìn)行重排序優(yōu)化。

從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面3種重排序:

  • 編譯器重排序:編譯器在不改變單線程程序語(yǔ)義的前提下重新安排語(yǔ)句的執(zhí)行順序。例如把變量緩存到寄存器中、提取公共子表達(dá)式等。
  • 指令級(jí)重排序:如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。例如亂序執(zhí)行的 Load 和 Store 指令、分支預(yù)測(cè)以及指令突發(fā)等。
  • 內(nèi)存系統(tǒng)重排序:由于數(shù)據(jù)讀寫過(guò)程中涉及到多個(gè)緩沖區(qū),這使得加載和存儲(chǔ)的操作看上去可能是亂序執(zhí)行,于是需要內(nèi)存系統(tǒng)的重排序。例如寫入緩存中的操作順序,對(duì)于其他CPU的 Cache 來(lái)說(shuō)是不可見(jiàn)的。

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

JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)禁止特定類型的編譯器重排序和指令級(jí)重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。

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

指令重排也是有一些限制的,有兩個(gè)規(guī)則happens-before和as-if-serial來(lái)約束。

happens-before的定義:

  • 如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見(jiàn),而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
  • 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照 happens-before關(guān)系指定的順序來(lái)執(zhí)行。只要沒(méi)有改變程序的執(zhí)行結(jié)果,編譯器和處理器怎么優(yōu)化都可以。

happens-before的六大規(guī)則:

  • 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。

/**
 * 順序性規(guī)則
 * 順序執(zhí)行是針對(duì)代碼邏輯而言的,在實(shí)際執(zhí)行的時(shí)候發(fā)生指令重排序但是并沒(méi)有改變?cè)创a的邏輯。
 * @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);
    }
}
  • 監(jiān)視器鎖規(guī)則:一個(gè)unlock操作之前對(duì)某個(gè)鎖的lock操作必須發(fā)生在該unlock操作之前

import java.util.concurrent.locks.ReentrantLock;

/**
 * 重排鎖的話,會(huì)導(dǎo)致邏輯改變。
 * @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變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作必須發(fā)生在該變量的讀操作之前。
  • 傳遞性規(guī)則:如果A happens-before B,且B happens-before C,那么A happens-before C。

從圖中,我們可以看到:

  • “x=42”Happens-Before 寫變量“v=true”,這是規(guī)則1的內(nèi)容;
  • 寫變量“v=true”Happens-Before 讀變量“v=true”,這是規(guī)則3的內(nèi)容;
  • 再根據(jù)這個(gè)傳遞性規(guī)則,我們得到結(jié)果:“x=42”Happens-Before 讀變量“v=true”;

這意味著什么呢?如果線程 B 讀到了“v=true”,那么線程A設(shè)置的“x=42”對(duì)線程B是可見(jiàn)的。也就是說(shuō),線程B能看到“x == 42“。

/**
 * 傳遞性規(guī)則
 * @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);
        }
    }
}
  • 線程啟動(dòng)規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。

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

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

在Java語(yǔ)言里面,Happens-Before的語(yǔ)義本質(zhì)上是一種可見(jiàn)性,A Happens-Before B 意味著A事件對(duì)B事件來(lái)說(shuō)是可見(jiàn)的,并且無(wú)論A事件和B事件是否發(fā)生在同一個(gè)線程里。

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

as-if-serial是指無(wú)論如何重排序都不會(huì)影響單線程程序的執(zhí)行結(jié)果。這個(gè)原則的核心思想是編譯器和處理器等各個(gè)層面的優(yōu)化,不能改變程序執(zhí)行的意義。

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

所以最終,程序可能會(huì)有兩種執(zhí)行順序:

21.volatile實(shí)現(xiàn)原理了解嗎?

volatile有兩個(gè)作用,保證可見(jiàn)性和有序性。

可見(jiàn)性:當(dāng)一個(gè)變量被聲明為 volatile 時(shí),它會(huì)告訴編譯器和CPU將該變量存儲(chǔ)在主內(nèi)存中,而不是線程的本地內(nèi)存中。即每個(gè)線程讀取的都是主內(nèi)存中最新的值,避免了多線程并發(fā)下的數(shù)據(jù)不一致問(wèn)題。

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

為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序。

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

22.synchronized用過(guò)嗎?怎么使用?

synchronized經(jīng)常用的,用來(lái)保證代碼的原子性。

synchronized主要有三種用法:

  • 修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖。

  • 修飾靜態(tài)方法:給當(dāng)前類加鎖,在同一時(shí)間內(nèi),只能有一個(gè)線程持有該類對(duì)應(yīng)的 Class 對(duì)象的鎖,其他線程需要等待鎖的釋放才能繼續(xù)執(zhí)行該靜態(tài)方法。

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

注意事項(xiàng):

  • 修飾實(shí)例方法:不同的對(duì)象實(shí)例之間并不會(huì)互相影響,它們的鎖是相互獨(dú)立的。因此,如果不同的線程在不同的對(duì)象實(shí)例上執(zhí)行同一個(gè)同步方法,它們之間并不會(huì)因?yàn)楣蚕碜兞慷a(chǎn)生互斥的效果。
  • 修飾靜態(tài)方法:應(yīng)該盡量避免持有鎖的時(shí)間過(guò)長(zhǎng),否則可能會(huì)導(dǎo)致其他線程長(zhǎng)時(shí)間等待,從而影響系統(tǒng)性能。同時(shí),也要注意避免死鎖的情況。
  • 修飾代碼塊:同步鎖并不是對(duì)整個(gè)代碼塊進(jìn)行加鎖,而是對(duì)同步鎖對(duì)象進(jìn)行加鎖。因此,如果在不同的代碼塊中使用了相同的同步鎖對(duì)象,它們之間也會(huì)產(chǎn)生互斥的效果。而如果使用不同的同步鎖對(duì)象,則它們之間并不會(huì)產(chǎn)生互斥的效果。

23.synchronized的實(shí)現(xiàn)原理?

我們使用synchronized的時(shí)候,發(fā)現(xiàn)不用自己去lock和unlock,是因?yàn)镴VM幫我們把這個(gè)事情做了。

  1. synchronized修飾代碼塊時(shí),JVM采用monitorenter、monitorexit兩個(gè)指令來(lái)實(shí)現(xiàn)同步,monitorenter 指令指向同步代碼塊的開(kāi)始位置, monitorexit 指令則指向同步代碼塊的結(jié)束位置。

/**
 * @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修飾同步方法時(shí),JVM采用ACC_SYNCHRONIZED標(biāo)記符來(lái)實(shí)現(xiàn)同步,這個(gè)標(biāo)識(shí)指明了該方法是一個(gè)同步方法。同樣可以寫段代碼反編譯看一下。

/**
 * @author 百里
 */
public class BaiLiSyncDemo {
    public synchronized void main(String[] args) {
        int a = 1;
    }
}

synchronized鎖住的是什么呢?

實(shí)例對(duì)象結(jié)構(gòu)里有對(duì)象頭,對(duì)象頭里面有一塊結(jié)構(gòu)叫Mark word,Mark Word指針指向了monitor。

所謂的Monitor其實(shí)是一種同步機(jī)制,我們可以稱為內(nèi)部鎖或者M(jìn)onitor鎖。

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor實(shí)現(xiàn)的。

反編譯class文件方法:

反編譯一段synchronized修飾代碼塊代碼,javap -c -s -v -l ***.class,可以看到相應(yīng)的字節(jié)碼指令。

24.synchronized的可見(jiàn)性,有序性,可重入性是怎么實(shí)現(xiàn)的?

synchronized怎么保證可見(jiàn)性?

  • 線程加鎖前,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值。
  • 線程加鎖后,其它線程無(wú)法獲取主內(nèi)存中的共享變量。
  • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。

synchronized怎么保證有序性?

synchronized同步的代碼塊,具有排他性,一次只能被一個(gè)線程擁有,所以synchronized保證同一時(shí)刻,代碼是單線程執(zhí)行的。

因?yàn)閍s-if-serial語(yǔ)義的存在,單線程的程序能保證最終結(jié)果是有序的,但是不保證不會(huì)指令重排。

所以synchronized保證的有序是執(zhí)行結(jié)果的有序性,而不是防止指令重排的有序性。

synchronized怎么實(shí)現(xiàn)可重入的?

synchronized 是可重入鎖,也就是說(shuō),允許一個(gè)線程二次請(qǐng)求自己持有對(duì)象鎖的臨界資源,這種情況稱為可重入鎖。

之所以是可重入的。是因?yàn)?synchronized 鎖對(duì)象有個(gè)計(jì)數(shù)器,當(dāng)一個(gè)線程請(qǐng)求成功后,JVM會(huì)記下持有鎖的線程,并將計(jì)數(shù)器計(jì)為1。此時(shí)其他線程請(qǐng)求該鎖,則必須等待。而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖,就可以再次拿到這個(gè)鎖,同時(shí)計(jì)數(shù)器會(huì)遞增。

當(dāng)線程執(zhí)行完畢后,計(jì)數(shù)器會(huì)遞減,直到計(jì)數(shù)器為0才釋放該鎖。

25.說(shuō)說(shuō)synchronized和ReentrantLock的區(qū)別

可以從鎖的實(shí)現(xiàn)、性能、功能特點(diǎn)等幾個(gè)維度去回答這個(gè)問(wèn)題:

  • 鎖的實(shí)現(xiàn): synchronized是Java語(yǔ)言的關(guān)鍵字,基于JVM實(shí)現(xiàn)。而ReentrantLock是基于JDK的API層面實(shí)現(xiàn)的(一般是lock()和unlock()方法配合try/finally語(yǔ)句塊來(lái)完成。)
  • 性能: 在JDK1.6鎖優(yōu)化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6開(kāi)始,增加了適應(yīng)性自旋、鎖消除等,兩者性能就差不多了。
  • 功能特點(diǎn): ReentrantLock 比 synchronized 多了一些高級(jí)功能,如等待可中斷、可實(shí)現(xiàn)公平鎖、可實(shí)現(xiàn)選擇性通知。
  • ReentrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過(guò)lock.lockInterruptibly()來(lái)實(shí)現(xiàn)這個(gè)機(jī)制
  • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
  • synchronized與wait()和notify()/notifyAll()方法結(jié)合實(shí)現(xiàn)等待/通知機(jī)制;ReentrantLock類借助Condition接口與newCondition()方法實(shí)現(xiàn)。
  • ReentrantLock需要手工聲明來(lái)加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動(dòng)釋放鎖。

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

26.ReentrantLock實(shí)現(xiàn)原理?

ReentrantLock是一種可重入的排它鎖,主要用來(lái)解決多線程對(duì)共享資源競(jìng)爭(zhēng)的問(wèn)題;它提供了比synchronized關(guān)鍵字更加靈活的鎖機(jī)制。其實(shí)現(xiàn)原理主要涉及以下三個(gè)方面:

  • 基本結(jié)構(gòu)

ReentrantLock內(nèi)部維護(hù)了一個(gè)Sync對(duì)象(AbstractQueuedSynchronizer類的子類),Sync持有鎖、等待隊(duì)列等狀態(tài)信息,實(shí)際上 ReentrantLock的大部分功能都是由Sync來(lái)實(shí)現(xiàn)的。

  • 加鎖過(guò)程

當(dāng)一個(gè)線程調(diào)用ReentrantLock的lock()方法時(shí),會(huì)先嘗試CAS操作獲取鎖,如果成功則返回;否則,線程會(huì)被放入等待隊(duì)列中,等待喚醒重新嘗試獲取鎖。

如果一個(gè)線程已經(jīng)持有了鎖,那么它可以重入這個(gè)鎖,即繼續(xù)獲取該鎖而不會(huì)被阻塞。ReentrantLock通過(guò)維護(hù)一個(gè)計(jì)數(shù)器來(lái)實(shí)現(xiàn)重入鎖功能,每次重入計(jì)數(shù)器加1,每次釋放鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí),鎖被釋放。

  • 解鎖過(guò)程

當(dāng)一個(gè)線程調(diào)用ReentrantLock的unlock()方法時(shí),會(huì)將計(jì)數(shù)器減1,如果計(jì)數(shù)器變?yōu)榱?,則鎖被完全釋放。如果計(jì)數(shù)器還大于0,則表示有其他線程正在等待該鎖,此時(shí)會(huì)喚醒等待隊(duì)列中的一個(gè)線程來(lái)獲取鎖。

總結(jié):

ReentrantLock的實(shí)現(xiàn)原理主要是基于CAS操作和等待隊(duì)列來(lái)實(shí)現(xiàn)。它通過(guò)Sync對(duì)象來(lái)維護(hù)鎖的狀態(tài),支持重入鎖和公平鎖等特性,提供了比synchronized更加靈活的鎖機(jī)制,是Java并發(fā)編程中常用的同步工具之一。

27.ReentrantLock怎么實(shí)現(xiàn)公平鎖的?

ReentrantLock可以通過(guò)構(gòu)造函數(shù)的參數(shù)來(lái)控制鎖的公平性,如果傳入 true,就表示該鎖是公平的;如果傳入 false,就表示該鎖是不公平的。

new ReentrantLock()構(gòu)造函數(shù)默認(rèn)創(chuàng)建的是非公平鎖 NonfairSync

同時(shí)也可以在創(chuàng)建鎖構(gòu)造函數(shù)中傳入具體參數(shù)創(chuàng)建公平鎖 FairSync

FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜態(tài)內(nèi)部類,只不過(guò)實(shí)現(xiàn)不同鎖語(yǔ)義。

非公平鎖和公平鎖的區(qū)別:

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

28.什么是CAS?

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

CAS 操作包含三個(gè)參數(shù):共享變量的內(nèi)存地址(V)、預(yù)期原值(A)和新值(B),當(dāng)且僅當(dāng)內(nèi)存地址 V 的值等于 A 時(shí),才將 V 的值修改為 B;否則,不會(huì)執(zhí)行任何操作。

在多線程場(chǎng)景下,使用 CAS 操作可以確保多個(gè)線程同時(shí)修改某個(gè)變量時(shí),只有一個(gè)線程能夠成功修改。其他線程需要重試或者等待。這樣就避免了傳統(tǒng)鎖機(jī)制中的鎖競(jìng)爭(zhēng)和死鎖等問(wèn)題,提高了系統(tǒng)的并發(fā)性能。

29.CAS存在什么問(wèn)題?如何解決?

CAS的經(jīng)典三大問(wèn)題:

ABA問(wèn)題

ABA 問(wèn)題是指一個(gè)變量從A變成B,再?gòu)腂變成A,這樣的操作序列可能會(huì)被CAS操作誤判為未被其他線程修改過(guò)。例如線程A讀取了某個(gè)變量的值為 A,然后被掛起,線程B修改了這個(gè)變量的值為B,然后又修改回了A,此時(shí)線程A恢復(fù)執(zhí)行,進(jìn)行CAS操作,此時(shí)仍然可以成功,因?yàn)榇藭r(shí)變量的值還是A。

怎么解決ABA問(wèn)題?

  • 加版本號(hào)

每次修改變量,都在這個(gè)變量的版本號(hào)上加1,這樣,剛剛A->B->A,雖然A的值沒(méi)變,但是它的版本號(hào)已經(jīng)變了,再判斷版本號(hào)就會(huì)發(fā)現(xiàn)此時(shí)的A已經(jīng)被改過(guò)了。

比如使用JDK5中的AtomicStampedReference類或JDK8中的LongAdder類。這些原子類型不僅包含數(shù)據(jù)本身,還包含一個(gè)版本號(hào),每次進(jìn)行操作時(shí)都會(huì)更新版本號(hào),只有當(dāng)版本號(hào)和期望值都相等時(shí)才能執(zhí)行更新,這樣可以避免 ABA 問(wèn)題的影響。

循環(huán)性能開(kāi)銷

自旋CAS,如果一直循環(huán)執(zhí)行,一直不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷。

怎么解決循環(huán)性能開(kāi)銷問(wèn)題?

可以使用自適應(yīng)自旋鎖,即在多次操作失敗后逐漸加長(zhǎng)自旋時(shí)間或者放棄自旋鎖轉(zhuǎn)為阻塞鎖;

只能保證一個(gè)變量的原子操作

CAS 保證的是對(duì)一個(gè)變量執(zhí)行操作的原子性,如果需要對(duì)多個(gè)變量進(jìn)行復(fù)合操作,CAS 操作就無(wú)法保證整個(gè)操作的原子性。

怎么解決只能保證一個(gè)變量的原子操作問(wèn)題?

  • 可以使用鎖機(jī)制來(lái)保證整個(gè)復(fù)合操作的原子性。例如,使用 synchronized 關(guān)鍵字或 ReentrantLock 類來(lái)保證多個(gè)變量的復(fù)合操作的原子性。在鎖的作用下,只有一個(gè)線程可以執(zhí)行復(fù)合操作,其他線程需要等待該線程釋放鎖后才能繼續(xù)執(zhí)行。這樣就可以保證整個(gè)操作的原子性了。
  • 可以使用類似于事務(wù)的方式來(lái)保證多個(gè)變量的復(fù)合操作的原子性。例如,使用AtomicReference 類,可以將多個(gè)變量封裝到一個(gè)對(duì)象中,通過(guò) CAS 操作更新整個(gè)對(duì)象,從而保證多個(gè)變量的復(fù)合操作的原子性。只有當(dāng)整個(gè)對(duì)象的值和期望值都相等時(shí)才會(huì)執(zhí)行更新操作。

30.Java多線程中如何保證i++的結(jié)果正確

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

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

  • 使用synchronized,對(duì)i++操作加鎖。

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

  • 使用鎖機(jī)制,除了使用 synchronized 關(guān)鍵字外,還可以使用 ReentrantLock 類來(lái)保護(hù)臨界區(qū)域。

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

31.AtomicInteger的原理是什么?

一句話概括:使用CAS實(shí)現(xiàn)。

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

  1. 調(diào)用 incrementAndGet()方法,該方法會(huì)通過(guò)調(diào)用unsafe.getAndAddInt()方法,獲取當(dāng)前 AtomicInteger對(duì)象的值val
  2. 將 val + 1 得到新的值 next

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

總結(jié):

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

32.什么是線程死鎖?我們?cè)撊绾伪苊饩€程死鎖?

死鎖是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的互相等待的現(xiàn)象,在無(wú)外力作用的情況下,這些線程會(huì)一直相互等待而無(wú)法繼續(xù)運(yùn)行下去。

那么為什么會(huì)產(chǎn)生死鎖呢?死鎖的產(chǎn)生必須具備以下四個(gè)條件:

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

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

  • 互斥:我們沒(méi)有辦法破壞,因?yàn)橛面i為的就是互斥。不過(guò)其他三個(gè)條件都是有辦法打破的,到底如何做呢?
  • 請(qǐng)求并持有:可以一次性請(qǐng)求所有的資源。
  • 不可剝奪:設(shè)置超時(shí)時(shí)間。已經(jīng)占用資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果長(zhǎng)時(shí)間申請(qǐng)不到,超時(shí)釋放已經(jīng)持有的資源。
  • 環(huán)路等待:注意加鎖順序,保證每個(gè)線程按同樣的順序進(jìn)行加鎖。

33.如何排查死鎖問(wèn)題

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

  1. 使用jps查找運(yùn)行的Java進(jìn)程:jps -l
  2. 使用jstack查看線程堆棧信息:jstack -l 進(jìn)程id

基本就可以看到死鎖的信息。

還可以利用圖形化工具,比如JConsole(JConsole工具在JDK的bin目錄中)。出現(xiàn)線程死鎖以后,點(diǎn)擊JConsole線程面板的檢測(cè)到死鎖按鈕,將會(huì)看到線程的死鎖信息。

演示樣例如下:

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.新建連接,找到相應(yīng)的線程,點(diǎn)擊連接

2.選擇線程標(biāo)簽,點(diǎn)擊檢測(cè)死鎖。查看死鎖線程信息

34.什么是線程池?

線程池是一種用于管理和復(fù)用線程的機(jī)制,它提供了一種執(zhí)行大量異步任務(wù)的方式,并且可以在多個(gè)任務(wù)之間合理地分配和管理系統(tǒng)資源。

線程池的主要優(yōu)點(diǎn)包括:

  • 改善了資源利用率,降低了線程創(chuàng)建和銷毀的開(kāi)銷。
  • 提高了系統(tǒng)響應(yīng)速度,因?yàn)榫€程池已經(jīng)預(yù)先創(chuàng)建好了一些線程,可以更加快速地分配資源以響應(yīng)用戶請(qǐng)求。
  • 提高了代碼可讀性和可維護(hù)性,因?yàn)榫€程池將線程管理和任務(wù)執(zhí)行進(jìn)行了分離,可以更加方便地對(duì)其進(jìn)行調(diào)整和優(yōu)化。
  • 可以設(shè)置線程數(shù)目上限,避免了缺乏控制的線程創(chuàng)建造成的系統(tǒng)無(wú)法承受的負(fù)載壓力。

35.簡(jiǎn)單說(shuō)一下線程池的工作流程

用一個(gè)通俗的比喻:

有一個(gè)銀行營(yíng)業(yè)廳,總共有六個(gè)窗口,現(xiàn)在有三個(gè)窗口坐著三個(gè)營(yíng)業(yè)員小姐姐在營(yíng)業(yè)。小天去辦業(yè)務(wù),可能會(huì)遇到什么情況呢?

  1. 小天發(fā)現(xiàn)有空閑的窗口,直接去找小姐姐辦理業(yè)務(wù)。

  1. 小天發(fā)現(xiàn)沒(méi)有空閑的窗口,就在排隊(duì)區(qū)排隊(duì)等。

  1. 小天發(fā)現(xiàn)沒(méi)有空閑的窗口,等待區(qū)也滿了,經(jīng)理一看,就讓休息的營(yíng)業(yè)員小姐姐趕緊回來(lái)上班,等待區(qū)號(hào)靠前的趕緊去新窗口辦,小天去排隊(duì)區(qū)排隊(duì)。小姐姐比較辛苦,假如一段時(shí)間發(fā)現(xiàn)他們可以不用接著營(yíng)業(yè),經(jīng)理就讓她們接著休息。

  1. 小天一看,六個(gè)窗口都滿了,等待區(qū)也沒(méi)位置了。小天就開(kāi)始投訴急了,要鬧,經(jīng)理趕緊出來(lái)了,經(jīng)理該怎么辦呢?

  1. 我們銀行系統(tǒng)已經(jīng)癱瘓
  2. 誰(shuí)叫你來(lái)辦的你找誰(shuí)去
  3. 看你比較急,去隊(duì)里加個(gè)塞
  4. 今天沒(méi)辦法,不行你看改一天

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

  1. 營(yíng)業(yè)中的 3個(gè)窗口對(duì)應(yīng)核心線程池?cái)?shù):corePoolSize
  2. 總的營(yíng)業(yè)窗口數(shù)6對(duì)應(yīng):maximumPoolSize
  3. 打開(kāi)的臨時(shí)窗口在多少時(shí)間內(nèi)無(wú)人辦理則關(guān)閉對(duì)應(yīng):unit
  4. 排隊(duì)區(qū)就是等待隊(duì)列:workQueue
  5. 無(wú)法辦理的時(shí)候銀行給出的解決方法對(duì)應(yīng):RejectedExecutionHandler
  6. threadFactory 該參數(shù)在JDK中是線程工廠,用來(lái)創(chuàng)建線程對(duì)象,一般不會(huì)動(dòng)。

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

  1. 線程池剛創(chuàng)建時(shí),里面沒(méi)有一個(gè)線程。任務(wù)隊(duì)列是作為參數(shù)傳進(jìn)來(lái)的。不過(guò),就算隊(duì)列里面有任務(wù),線程池也不會(huì)馬上執(zhí)行它們。
  2. 當(dāng)調(diào)用 execute() 方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判斷:
  • 如果正在運(yùn)行的線程數(shù)量小于 corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);
  • 如果正在運(yùn)行的線程數(shù)量大于或等于 corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列;
  • 如果這時(shí)候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于 maximumPoolSize,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行這個(gè)任務(wù);
  • 如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于 maximumPoolSize,那么線程池會(huì)根據(jù)拒絕策略來(lái)對(duì)應(yīng)處理。
  1. 當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來(lái)執(zhí)行。
  2. 當(dāng)一個(gè)線程無(wú)事可做,超過(guò)一定的時(shí)間(keepAliveTime)時(shí),線程池會(huì)判斷,如果當(dāng)前運(yùn)行的線程數(shù)大于 corePoolSize,那么這個(gè)線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會(huì)收縮到 corePoolSize 的大小。

36.線程池主要參數(shù)有哪些?

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框架實(shí)現(xiàn)線程池
        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() + ":點(diǎn)贊評(píng)論加分享"
            );
        });
    }
}

線程池有七大參數(shù),我們重點(diǎn)關(guān)注corePoolSize、maximumPoolSize、workQueue、handler 可以幫助我們更好地理解和優(yōu)化線程池的性能

  1. corePoolSize

此值是用來(lái)初始化線程池中核心線程數(shù),當(dāng)線程池中線程數(shù)< corePoolSize時(shí),系統(tǒng)默認(rèn)是添加一個(gè)任務(wù)才創(chuàng)建一個(gè)線程池。當(dāng)線程數(shù) = corePoolSize時(shí),新任務(wù)會(huì)追加到workQueue中。

  1. maximumPoolSize

maximumPoolSize表示允許的最大線程數(shù) = (非核心線程數(shù)+核心線程數(shù)),當(dāng)BlockingQueue也滿了,但線程池中總線程數(shù) < maximumPoolSize時(shí)候就會(huì)再次創(chuàng)建新的線程。

  1. keepAliveTime

非核心線程 =(maximumPoolSize - corePoolSize ) ,非核心線程閑置下來(lái)不干活最多存活時(shí)間。

  1. unit

線程池中非核心線程保持存活的時(shí)間的單位

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

線程池等待隊(duì)列,維護(hù)著等待執(zhí)行的Runnable對(duì)象。當(dāng)運(yùn)行當(dāng)線程數(shù)= corePoolSize時(shí),新的任務(wù)會(huì)被添加到workQueue中,如果workQueue也滿了則嘗試用非核心線程執(zhí)行任務(wù),等待隊(duì)列應(yīng)該盡量用有界的。

  1. threadFactory

創(chuàng)建一個(gè)新線程時(shí)使用的工廠,可以用來(lái)設(shè)定線程名、是否為daemon線程等等。

  1. handler

corePoolSize、workQueue、maximumPoolSize都不可用的時(shí)候執(zhí)行的飽和策略。

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

在線程池中,當(dāng)提交的任務(wù)數(shù)量超過(guò)了線程池的最大容量,線程池就需要使用拒絕策略來(lái)處理無(wú)法處理的新任務(wù)。Java 中提供了 4 種默認(rèn)的拒絕策略:

  • AbortPolicy(默認(rèn)策略):直接拋出 runtime 異常,阻止系統(tǒng)正常運(yùn)行。
  • CallerRunsPolicy:由提交該任務(wù)的線程來(lái)執(zhí)行這個(gè)任務(wù)。
  • DiscardPolicy:直接丟棄任務(wù),不給予任何處理。
  • DiscardOldestPolicy:丟棄隊(duì)列中最老的一個(gè)請(qǐng)求,嘗試再次提交當(dāng)前任務(wù)。

除了這些默認(rèn)的策略之外,我們也可以自定義自己的拒絕策略,實(shí)現(xiàn)RejectedExecutionHandler接口即可。

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

38.線程池有哪幾種工作隊(duì)列

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

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

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

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

  1. submit() 用于提交需要返回值的任務(wù)。線程池會(huì)返回一個(gè)future類型的對(duì)象,通過(guò)這個(gè) future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過(guò)future的get()方法來(lái)獲取返回值

40.怎么關(guān)閉線程池?

可以通過(guò)調(diào)用線程池的shutdown或shutdownNow方法來(lái)關(guān)閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的interrupt方法來(lái)中斷線程,所以無(wú)法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無(wú)法終止。

shutdown:將線程池狀態(tài)置為shutdown,并不會(huì)立即停止:

  1. 停止接收外部submit的任務(wù)
  2. 內(nèi)部正在跑的任務(wù)和隊(duì)列里等待的任務(wù),會(huì)執(zhí)行完
  3. 等到第二步完成后,才真正停止

shutdownNow:將線程池狀態(tài)置為stop。一般會(huì)立即停止,事實(shí)上不一定:

  1. 和shutdown()一樣,先停止接收外部提交的任務(wù)
  2. 忽略隊(duì)列里等待的任務(wù)
  3. 嘗試將正在跑的任務(wù)interrupt中斷
  4. 返回未執(zhí)行的任務(wù)列表

shutdown 和shutdownnow區(qū)別如下:

  • shutdownNow:能立即停止線程池,正在跑的和正在等待的任務(wù)都停下了。這樣做立即生效,但是風(fēng)險(xiǎn)也比較大。
  • shutdown:只是關(guān)閉了提交通道,用submit()是無(wú)效的;而內(nèi)部的任務(wù)該怎么跑還是怎么跑,跑完再?gòu)氐淄V咕€程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

        // 提交任務(wù)到線程池
        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");
        });

        // 關(guān)閉線程池
        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) {
        // 創(chuàng)建一個(gè)線程池,包含兩個(gè)線程
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任務(wù)到線程池
        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");
        });

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

        try {
            // 等待直到所有任務(wù)完成或超時(shí)60秒
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                // 如果等待超時(shí),則記錄未完成的任務(wù)列表
                unfinishedTasks = executor.shutdownNow();
                System.out.println("Some tasks were not finished");
            }
        } catch (InterruptedException e) {
            // 如果等待過(guò)程中發(fā)生異常,則記錄未完成的任務(wù)列表
            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.有哪幾種常見(jiàn)的線程池

在Java中,常見(jiàn)的線程池類型主要有四種,都是通過(guò)工具類Excutors創(chuàng)建出來(lái)的。

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

需要注意阿里巴巴《Java開(kāi)發(fā)手冊(cè)》里禁止使用這種方式來(lái)創(chuàng)建線程池。

42.說(shuō)一說(shuō)newSingleThreadExecutor工作原理

線程池特點(diǎn):

  • 核心線程數(shù)為1
  • 最大線程數(shù)也為1
  • 阻塞隊(duì)列是無(wú)界隊(duì)列LinkedBlockingQueue,可能會(huì)導(dǎo)致OOM
  • keepAliveTime為0

工作流程:

  • 提交任務(wù)
  • 線程池是否有一個(gè)線程正在運(yùn)行,如果沒(méi)有,新建線程執(zhí)行任務(wù)
  • 如果有并且非空閑狀態(tài),將任務(wù)加到阻塞隊(duì)列
  • 當(dāng)前的唯一線程,從隊(duì)列取任務(wù),執(zhí)行完一個(gè),再繼續(xù)取,一個(gè)線程執(zhí)行任務(wù)。

使用場(chǎng)景:

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

43.說(shuō)一說(shuō)newFixedThreadPool工作原理

線程池特點(diǎn):

  • 核心線程數(shù)和最大線程數(shù)大小一樣
  • 沒(méi)有所謂的非空閑時(shí)間,即keepAliveTime為0
  • 阻塞隊(duì)列為無(wú)界隊(duì)列LinkedBlockingQueue,可能會(huì)導(dǎo)致OOM

工作流程:

  • 提交任務(wù)
  • 如果線程數(shù)少于核心線程,創(chuàng)建核心線程執(zhí)行任務(wù)
  • 如果線程數(shù)等于核心線程,把任務(wù)添加到LinkedBlockingQueue阻塞隊(duì)列
  • 如果線程執(zhí)行完任務(wù),去阻塞隊(duì)列取任務(wù),繼續(xù)執(zhí)行。

使用場(chǎng)景:

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

44.說(shuō)一說(shuō)newCachedThreadPool工作原理

線程池特點(diǎn):

  • 核心線程數(shù)為0
  • 最大線程數(shù)為Integer.MAX_VALUE,即無(wú)限大,可能會(huì)因?yàn)闊o(wú)限創(chuàng)建線程,導(dǎo)致OOM
  • 阻塞隊(duì)列是SynchronousQueue
  • 非核心線程空閑存活時(shí)間為60秒

當(dāng)提交任務(wù)的速度大于處理任務(wù)的速度時(shí),每次提交一個(gè)任務(wù),就必然會(huì)創(chuàng)建一個(gè)線程。極端情況下會(huì)創(chuàng)建過(guò)多的線程,耗盡 CPU 和內(nèi)存資源。由于空閑 60 秒的線程會(huì)被終止,長(zhǎng)時(shí)間保持空閑的 CachedThreadPool 不會(huì)占用任何資源。

工作流程:

  • 提交任務(wù)
  • 因?yàn)闆](méi)有核心線程,所以任務(wù)直接加到SynchronousQueue隊(duì)列。
  • 判斷是否有空閑線程,如果有,就去取出任務(wù)執(zhí)行。
  • 如果沒(méi)有空閑線程,就新建一個(gè)線程執(zhí)行。
  • 執(zhí)行完任務(wù)的線程,還可以存活60秒,如果在這期間,接到任務(wù),可以繼續(xù)活下去;否則,被銷毀。

使用場(chǎng)景:

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

45.說(shuō)一說(shuō)newScheduledThreadPool工作原理

線程池特點(diǎn):

  • 最大線程數(shù)為Integer.MAX_VALUE,也有OOM的風(fēng)險(xiǎn)
  • 阻塞隊(duì)列是DelayedWorkQueue
  • keepAliveTime為0
  • scheduleAtFixedRate() :按某種速率周期執(zhí)行
  • scheduleWithFixedDelay():在某個(gè)延遲后執(zhí)行

工作流程:

  • 線程從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務(wù)是指ScheduledFutureTask的time大于等于當(dāng)前時(shí)間。
  • 線程執(zhí)行這個(gè)ScheduledFutureTask。
  • 線程修改ScheduledFutureTask的time變量為下次將要被執(zhí)行的時(shí)間。
  • 線程把這個(gè)修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

使用場(chǎng)景:

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

import java.util.concurrent.*;

/**
 * @author 百里
 */
public class BaiLiScheduledThreadPoolDemo {
    public static void main(String[] args) throws Exception {
        // 創(chuàng)建一個(gè)可以執(zhí)行定時(shí)任務(wù)的線程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 調(diào)度一個(gè)定時(shí)任務(wù),每隔 2 秒鐘輸出一次當(dāng)前時(shí)間
        ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(() -> {
            System.out.println("Current time: " + System.currentTimeMillis());
        }, 0, 2, TimeUnit.SECONDS);

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

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

/**
 * @author 百里
 */
public class BaiLiScheduleWithFixedDelayDemo {
    public static void main(String[] args) throws Exception {
        // 創(chuàng)建一個(gè)可以執(zhí)行定時(shí)任務(wù)的線程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 調(diào)度一個(gè)周期性任務(wù),每次任務(wù)執(zhí)行完畢后等待 2 秒鐘再執(zhí)行下一個(gè)任務(wù)
        executorService.scheduleWithFixedDelay(() -> {
            System.out.println("Current time: " + System.currentTimeMillis());
        }, 0, 2, TimeUnit.SECONDS);

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

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

在使用線程池處理任務(wù)的時(shí)候,任務(wù)代碼可能拋出RuntimeException,拋出異常后,線程池可能捕獲它,也可能創(chuàng)建一個(gè)新的線程來(lái)代替異常的線程,我們可能無(wú)法感知任務(wù)出現(xiàn)了異常,因此我們需要考慮線程池異常情況。

常見(jiàn)的異常處理方式:

1.try-catch捕獲異常

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

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

    @Override
    public void run() {
        try {
            // 任務(wù)代碼
            int a = 10 / 0;
        } catch (Exception e) {
            System.err.println("任務(wù)執(zhí)行異常:" + 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() {
        // 任務(wù)代碼
        int a = 10 / 0;
    }

    public static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.err.println("任務(wù)執(zhí)行異常:" + 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() {
        // 任務(wù)代碼
        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("任務(wù)執(zhí)行異常:" + 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("任務(wù)執(zhí)行失敗");
        });
        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("線程被中斷,已執(zhí)行相應(yīng)處理");
        }
        executor.shutdown();
    }
}

47.能說(shuō)一下線程池有幾種狀態(tài)嗎?

線程池有這幾個(gè)狀態(tài):RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED

//線程池狀態(tài)
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;

線程池各個(gè)狀態(tài)切換圖:

RUNNING

  • 該狀態(tài)的線程池會(huì)接收新任務(wù),并處理阻塞隊(duì)列中的任務(wù);
  • 調(diào)用線程池的shutdown()方法,可以切換到SHUTDOWN狀態(tài);
  • 調(diào)用線程池的shutdownNow()方法,可以切換到STOP狀態(tài);

SHUTDOWN

  • 該狀態(tài)的線程池不會(huì)接收新任務(wù),但會(huì)處理阻塞隊(duì)列中的任務(wù);
  • 隊(duì)列為空,并且線程池中執(zhí)行的任務(wù)也為空,進(jìn)入TIDYING狀態(tài);

STOP

  • 該狀態(tài)的線程不會(huì)接收新任務(wù),也不會(huì)處理阻塞隊(duì)列中的任務(wù),而且會(huì)中斷正在運(yùn)行的任務(wù);
  • 線程池中執(zhí)行的任務(wù)為空,進(jìn)入TIDYING狀態(tài);

TIDYING

  • 該狀態(tài)表明所有的任務(wù)已經(jīng)運(yùn)行終止,記錄的任務(wù)數(shù)量為0。
  • terminated()執(zhí)行完畢,進(jìn)入TERMINATED狀態(tài)

TERMINATED

  • 該狀態(tài)表示線程池徹底終止

48.單機(jī)線程池執(zhí)行斷電了應(yīng)該怎么處理?

單機(jī)線程池是一種常見(jiàn)的多線程編程方式,它可以用于異步執(zhí)行任務(wù),提高應(yīng)用程序的性能和并發(fā)能力。在單機(jī)線程池中,所有任務(wù)都由同一個(gè)線程處理,因此如果該線程在執(zhí)行任務(wù)時(shí)突然斷電,則會(huì)出現(xiàn)以下問(wèn)題:

  • 任務(wù)不完整
  • 數(shù)據(jù)丟失
  • 系統(tǒng)資源泄漏

如果單機(jī)線程池在執(zhí)行任務(wù)時(shí)突然遇到斷電等異常情況,應(yīng)該盡快采取以下措施:

  • 恢復(fù)中斷的任務(wù):當(dāng)系統(tǒng)重新啟動(dòng)后,需要檢查之前被中斷的任務(wù),并將其重新提交到單機(jī)線程池中進(jìn)行處理,以確保所有任務(wù)都能夠被正確執(zhí)行。
  • 數(shù)據(jù)備份及恢復(fù):對(duì)于需要處理的數(shù)據(jù),應(yīng)該事先進(jìn)行備份,以避免數(shù)據(jù)丟失或損壞。在系統(tǒng)重啟后,需要將備份數(shù)據(jù)恢復(fù)到系統(tǒng)中,以確保數(shù)據(jù)的完整性和正確性。
  • 系統(tǒng)資源清理:在單機(jī)線程池執(zhí)行過(guò)程中,可能會(huì)產(chǎn)生未釋放的系統(tǒng)資源,如文件句柄、鎖等。在系統(tǒng)重啟后,需要清理這些未釋放的系統(tǒng)資源,以避免資源泄漏和系統(tǒng)運(yùn)行不穩(wěn)定。

49.NIO的原理,包括哪幾個(gè)組件?

NIO(Java Non-blocking I/O)是一種 I/O 技術(shù),其核心原理是基于事件驅(qū)動(dòng)的方式進(jìn)行操作。

NIO 的工作原理:基于緩沖區(qū)、通道和選擇器的組合,通過(guò)高效地利用系統(tǒng)資源,以支持高并發(fā)和高吞吐量的數(shù)據(jù)處理。相比傳統(tǒng)的 I/O 編程方式,Java NIO 提供了更為靈活和高效的編程方式。

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

Selector、Channel 和 Buffer 的關(guān)系圖如下:

  1. Channel(通道):類似于傳統(tǒng) I/O 中的 Stream,是用于實(shí)際數(shù)據(jù)傳輸?shù)慕M件。在 NIO 中,有多種類型的 Channel 可以使用,例如 FileChannel、SocketChannel、DatagramChannel 等,可用于文件操作、網(wǎng)絡(luò)傳輸?shù)炔煌瑘?chǎng)景。
  2. Buffer(緩沖區(qū)):用于存儲(chǔ)數(shù)據(jù)的容器,可以理解為暫存需要傳輸?shù)臄?shù)據(jù)的地方。在 NIO 中,存在多種類型的緩沖區(qū),如 ByteBuffer、CharBuffer、IntBuffer等。
  3. Selector(選擇器):用于注冊(cè) Channel 并監(jiān)聽(tīng)其 I/O 事件。當(dāng) Channel 準(zhǔn)備好讀或?qū)憯?shù)據(jù)時(shí),會(huì)得到通知。Selector 可以高效地輪詢多個(gè) Channel,并且避免了使用多線程或多進(jìn)程對(duì)多個(gè) Channel 進(jìn)行輪詢的情況,從而減少了系統(tǒng)資源開(kāi)銷。

通俗理解NIO原理:

NIO 是可以做到用一個(gè)線程來(lái)處理多個(gè)操作的。假設(shè)有 10000 個(gè)請(qǐng)求過(guò)來(lái),根據(jù)實(shí)際情況,可以分配 50 或者 100 個(gè)線程來(lái)處理。不像之前的阻塞 IO 那樣,非得分配 10000 個(gè)。

50.什么是零拷貝?

零拷貝(Zero-Copy)是一種 I/O 操作優(yōu)化技術(shù),可以快速高效地將數(shù)據(jù)從文件系統(tǒng)移動(dòng)到網(wǎng)絡(luò)接口,而不需要將其從內(nèi)核空間復(fù)制到用戶空間。

傳統(tǒng)I/O操作過(guò)程:

傳統(tǒng) I/O 的工作方式是,數(shù)據(jù)讀取和寫入是從用戶空間到內(nèi)核空間來(lái)回復(fù)制,而內(nèi)核空間的數(shù)據(jù)是通過(guò)操作系統(tǒng)層面的 I/O 接口從磁盤讀取或?qū)懭搿4a通常如下,一般會(huì)需要兩個(gè)系統(tǒng)調(diào)用:

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

代碼很簡(jiǎn)單,雖然就兩行代碼,但是這里面發(fā)生了不少的事情:

  • 用戶應(yīng)用進(jìn)程調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換1)
  • DMA控制器把數(shù)據(jù)從磁盤中,讀取到內(nèi)核緩沖區(qū)。
  • CPU把內(nèi)核緩沖區(qū)數(shù)據(jù),拷貝到用戶應(yīng)用緩沖區(qū),上下文從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)(切換2),read函數(shù)返回
  • 用戶應(yīng)用進(jìn)程通過(guò)write函數(shù),發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換3)
  • CPU將應(yīng)用緩沖區(qū)中的數(shù)據(jù),拷貝到socket緩沖區(qū)
  • DMA控制器把數(shù)據(jù)從socket緩沖區(qū),拷貝到網(wǎng)卡設(shè)備,上下文從內(nèi)核態(tài)切換回用戶態(tài)(切換4),write函數(shù)返回

從流程圖可以看出,傳統(tǒng)IO的讀寫流程,包括了4次上下文切換(4次用戶態(tài)和內(nèi)核態(tài)的切換),4次數(shù)據(jù)拷貝(兩次CPU拷貝以及兩次的DMA拷貝)

這種簡(jiǎn)單又傳統(tǒng)的文件傳輸方式,存在冗余的上文切換和數(shù)據(jù)拷貝,在高并發(fā)系統(tǒng)里是非常糟糕的,多了很多不必要的開(kāi)銷,會(huì)嚴(yán)重影響系統(tǒng)性能。

所以,要想提高文件傳輸?shù)男阅埽托枰獪p少「用戶態(tài)與內(nèi)核態(tài)的上下文切換」和「內(nèi)存拷貝」的次數(shù)。

零拷貝主要是用來(lái)解決操作系統(tǒng)在處理 I/O 操作時(shí),頻繁復(fù)制數(shù)據(jù)的問(wèn)題。關(guān)于零拷貝主要技術(shù)有MMap+Write、SendFile等幾種方式。

Mmap+Wirte實(shí)現(xiàn)零拷貝:

  • 用戶進(jìn)程通過(guò)mmap方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。
  • CPU利用DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
  • 上下文從內(nèi)核態(tài)切換回用戶態(tài),mmap方法返回。
  • 用戶進(jìn)程通過(guò)write方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。
  • CPU將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到的socket緩沖區(qū)。
  • CPU利用DMA控制器,把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,上下文從內(nèi)核態(tài)切換回用戶態(tài),write調(diào)用返回。

可以發(fā)現(xiàn),mmap+write實(shí)現(xiàn)的零拷貝,I/O發(fā)生了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

SendFile實(shí)現(xiàn)零拷貝:

  1. 用戶進(jìn)程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài)
  2. DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
  3. CPU將讀緩沖區(qū)中數(shù)據(jù)拷貝到socket緩沖區(qū)
  4. DMA控制器,異步把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,
  5. 上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。

可以發(fā)現(xiàn),sendfile實(shí)現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。

分享到:
標(biāo)簽:線程 Java
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定