高并發(fā)多線程總結(jié)

1655369765444.png

1.多線程基本概念

首先,我們要理解多線程編程,必須清楚幾個(gè)基本概念:
進(jìn)程——進(jìn)程是操作系統(tǒng)層面的概念,它是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位
線程——線程是進(jìn)程內(nèi)部的程序流,每個(gè)進(jìn)程內(nèi)部都會(huì)有多個(gè)線程,所有線程共享進(jìn)程的內(nèi)部資源,所以,一個(gè)進(jìn)程可以有多個(gè)線程,多個(gè)線程采用時(shí)間片輪轉(zhuǎn)的方式并發(fā)執(zhí)行,
并發(fā)——所謂并發(fā),就是指宏觀上并行微觀上串行機(jī)制,一個(gè)CPU執(zhí)行多個(gè)任務(wù)
并行——多個(gè)CPU執(zhí)行多個(gè)任務(wù)

2.線程實(shí)現(xiàn)方式

//繼承Thread類(lèi)
public class Multithreading extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

//實(shí)現(xiàn)Runnable接口
public class Multithreading implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

//使用Future類(lèi)

public class Multithreading{
    public static void main(String[] args) {
        new CompletableFuture<String>().thenRunAsync(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 200; i++) {
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }).join();
    }
}

三種方法看情況使用,這里推薦使用CompletableFuture類(lèi),內(nèi)置了很多使用方法,基本可以滿足多線程要求,有關(guān)線程池部分,后面會(huì)講到!

3.鎖機(jī)制

3.1并發(fā)三大特性——原子性、有序性、可見(jiàn)性

可見(jiàn)性——一個(gè)線程修改了共享變量的值,另一個(gè)線程可以立刻感知到,這是由CPU高速緩存造成的,高速緩存與內(nèi)存數(shù)據(jù)不一致
原子性——一個(gè)操作或多次操作,要么全部執(zhí)行、要么全部不執(zhí)行
有序性——正常編譯代碼是按順序執(zhí)行的,不過(guò)有時(shí)候,在代碼順序?qū)Τ绦蚪Y(jié)果無(wú)影響時(shí),會(huì)對(duì)代碼進(jìn)行重排序執(zhí)行

3.2解決方式

volatile——解決可見(jiàn)性、有序性
那么volatile是如何保證可見(jiàn)性和有序性的呢?
這就涉及volatile的原理:
第一,基于happens-before規(guī)則保證有序性,凡是volatile修飾的變量的寫(xiě)操作都是在多操作之前,第二,設(shè)置內(nèi)存屏障,禁止指令重排保證有序性;
基于lock前綴指令和MESI緩存一致性協(xié)議保證可見(jiàn)性(不用深究,操作系統(tǒng)的指令)
synhonzied,加鎖Lock——解決原子性
核心思想:對(duì)需要修改的共享資源進(jìn)行上鎖,讓所有線程進(jìn)行串行化修改變量
Synchronzied的底層原理:對(duì)所有關(guān)鍵字修飾的代碼段(或者說(shuō)線程),都會(huì)封裝成ObjectMoniter對(duì)象進(jìn)行處理;在jvm中,每一個(gè)對(duì)象都會(huì)相應(yīng)的對(duì)象頭信息,這其中就包括了指向monior的指針,線程會(huì)通過(guò)這個(gè)monitor指針去對(duì)象對(duì)應(yīng)的monitor對(duì)象上進(jìn)行加鎖,這是該對(duì)象就是加鎖狀態(tài);
ObjectMonitor對(duì)象四大核心組件:
EntryList——想獲取該對(duì)象的阻塞線程、
waitSet——進(jìn)入等待資源的線程、
owner——當(dāng)前加鎖線程、
count——0 當(dāng)前對(duì)象無(wú)人加鎖 1 當(dāng)前對(duì)象已經(jīng)加鎖
ReentrantLock鎖
與Synchronzied相比,Synchronzied屬于重量級(jí)鎖,而ReentrantLock是通過(guò)對(duì)象加鎖,二者都是可重入鎖,但是ReentrantLock需要認(rèn)為加鎖
底層原理:AQS(抽象隊(duì)列同步器)+CAS(CompareAndSet)
AQS組件介紹:
state:與count類(lèi)似,加鎖標(biāo)記 0 無(wú)鎖狀態(tài) 1 有鎖狀態(tài)
任務(wù)隊(duì)列:沒(méi)有獲取鎖的阻塞線程隊(duì)列——雙向鏈表
當(dāng)前加鎖線程:記錄當(dāng)前加鎖的線程
實(shí)現(xiàn)思路:首先通過(guò)CAS判斷state是否上鎖,只有一個(gè)線程能夠上鎖成功,其他線程則進(jìn)入任務(wù)隊(duì)列,需要注意的是,因?yàn)镽eenrantLock四可重入鎖,所以state可以無(wú)線增大,所以只有一層一層解鎖直至state=0時(shí)才會(huì)解鎖

3.3鎖的級(jí)別

偏向鎖
輕量級(jí)鎖
重量級(jí)鎖
自旋鎖

4.線程池

概念:使用緩存的思想將線程放入線程池中,避免重復(fù)創(chuàng)建線程減少系統(tǒng)資源消耗,使用線程池也可以更加方便的管理線程
實(shí)現(xiàn)方式:
![PXGNUG5WSQ`ZZ5NZ{O~3DI.png
核心參數(shù)介紹:
corePoolSize——核心線程數(shù)量
maximumPoolSize——最大線程數(shù)量
workQueue——工作隊(duì)列
handle——處理策略
keepAliveTime——非核心線程的存活時(shí)間
實(shí)現(xiàn)思想:當(dāng)線程進(jìn)入線程池,首先判斷核心線程數(shù)是否空余,如果核心線程池已滿,則進(jìn)入工作隊(duì)列。當(dāng)工作隊(duì)列也填滿時(shí),這時(shí)如果最大線程數(shù)未滿,則創(chuàng)建新線程來(lái)執(zhí)行任務(wù),如果已經(jīng)滿了,則執(zhí)行飽和策略(常見(jiàn)的策略有中斷拋出異常、丟棄任務(wù)、丟棄隊(duì)列中存在時(shí)間最久的任務(wù)、讓提交任務(wù)的線程去執(zhí)行任務(wù))
線程池實(shí)現(xiàn)復(fù)用的原理
線程池中執(zhí)行的是一個(gè)一個(gè)的隊(duì)列,
核心邏輯是ThreadPoolExecutor類(lèi)中的execute方法,其本身維護(hù)了一個(gè)HashSet<Worker> workers;Worker對(duì)象實(shí)現(xiàn)了Runnable,本質(zhì)上也是任務(wù),核心在run方法里面,話不多說(shuō),上代碼~

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    // 該worker正在運(yùn)行的線程
    final Thread thread;
    
    // 將要運(yùn)行的初始任務(wù)
    Runnable firstTask;
    
    // 每個(gè)線程的任務(wù)計(jì)數(shù)器
    volatile long completedTasks;
 
    // 構(gòu)造方法   
    Worker(Runnable firstTask) {
        setState(-1); // 調(diào)用runWorker()前禁止中斷
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this); // 通過(guò)ThreadFactory創(chuàng)建一個(gè)線程
    }
 
    // 實(shí)現(xiàn)了Runnable接口的run方法
    public void run() {
        runWorker(this);
    }
    
    ... // 此處省略了其他方法
private boolean addWorker(Runnable firstTask, boolean core) {
    retry: // 循環(huán)退出標(biāo)志位
    for (;;) { // 無(wú)限循環(huán)
        int c = ctl.get();
        int rs = runStateOf(c); // 線程池狀態(tài)
 
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && 
            ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) // 換成更直觀的條件語(yǔ)句
            // (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty())
           )
           // 返回false的條件就可以分解為:
           //(1)線程池狀態(tài)為STOP,TIDYING,TERMINATED
           //(2)線程池狀態(tài)為SHUTDOWN,且要執(zhí)行的任務(wù)不為空
           //(3)線程池狀態(tài)為SHUTDOWN,且任務(wù)隊(duì)列為空
            return false;
 
        // cas自旋增加線程個(gè)數(shù)
        for (;;) {
            int wc = workerCountOf(c); // 當(dāng)前工作線程數(shù)
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize)) // 工作線程數(shù)>=線程池容量 || 工作線程數(shù)>=(核心線程數(shù)||最大線程數(shù))
                return false;
            if (compareAndIncrementWorkerCount(c)) // 執(zhí)行cas操作,添加線程個(gè)數(shù)
                break retry; // 添加成功,退出外層循環(huán)
            // 通過(guò)cas添加失敗
            c = ctl.get();  
            // 線程池狀態(tài)是否變化,變化則跳到外層循環(huán)重試重新獲取線程池狀態(tài),否者內(nèi)層循環(huán)重新cas
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    // 簡(jiǎn)單總結(jié)上面的CAS過(guò)程:
    //(1)內(nèi)層循環(huán)作用是使用cas增加線程個(gè)數(shù),如果線程個(gè)數(shù)超限則返回false,否者進(jìn)行cas
    //(2)cas成功則退出雙循環(huán),否者cas失敗了,要看當(dāng)前線程池的狀態(tài)是否變化了
    //(3)如果變了,則重新進(jìn)入外層循環(huán)重新獲取線程池狀態(tài),否者重新進(jìn)入內(nèi)層循環(huán)繼續(xù)進(jìn)行cas
 
    // 走到這里說(shuō)明cas成功,線程數(shù)+1,但并未被執(zhí)行
    boolean workerStarted = false; // 工作線程調(diào)用start()方法標(biāo)志
    boolean workerAdded = false; // 工作線程被添加標(biāo)志
    Worker w = null;
    try {
        w = new Worker(firstTask); // 創(chuàng)建工作線程實(shí)例
        final Thread t = w.thread; // 獲取工作線程持有的線程實(shí)例
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock; // 使用全局可重入鎖
            mainLock.lock(); // 加鎖,控制并發(fā)
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get()); // 獲取當(dāng)前線程池狀態(tài)
 
                // 線程池狀態(tài)為RUNNING或者(線程池狀態(tài)為SHUTDOWN并且沒(méi)有新任務(wù)時(shí))
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // 檢查線程是否處于活躍狀態(tài)
                        throw new IllegalThreadStateException();
                    workers.add(w); // 線程加入到存放工作線程的HashSet容器,workers全局唯一并被mainLock持有
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock(); // finally塊中釋放鎖
            }
            if (workerAdded) { // 線程添加成功
                t.start(); // 調(diào)用線程的start()方法
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted) // 如果線程啟動(dòng)失敗,則執(zhí)行addWorkerFailed方法
            addWorkerFailed(w);
    }
    return workerStarted;
  }
}

從上面可以看到,在addwork()中,進(jìn)行了一系列校驗(yàn),代碼邏輯就是上文提到的實(shí)現(xiàn)思想。

5.ThreadLocal詳解

1.理解:可以把ThreadLocal看做是存在于Thread類(lèi)的一個(gè)屬性字段,提供一個(gè)只有Thread才能訪問(wèn)的局部變量。
2.使用方式(見(jiàn)下方代碼)

public class GCTest {
    public ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
    public ThreadLocal<String> stringLocal = new ThreadLocal<String>();

    public void set(int i){
        intLocal.set(i);
        stringLocal.set(Thread.currentThread().getName());
    }

    public int getInt(){
        return intLocal.get();
    }

    public String getString(){
        return stringLocal.get();
    }
    public static void main(String[] args) {
        GCTest gcTest = new GCTest();
        gcTest.set(1);
        System.out.println(gcTest.getString());
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    gcTest.set(1);
                    System.out.println(gcTest.getString()+":"+ gcTest.getInt()+i);
                }
            }
        }).start();
//注意新線程的打印與第一個(gè)線程,看數(shù)據(jù)是否隔離
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    gcTest.set(1);
                    System.out.println(gcTest.getString()+":"+ gcTest.getInt()+i);
                    String name = Thread.currentThread().getName();
                    System.out.println(name);
                }
            }
        }).start();
    }
}

3.內(nèi)存泄漏
首先解釋一下什么是內(nèi)存泄漏,在jvm中,存在四種對(duì)象引用方式——強(qiáng)引用、軟引用、弱引用、虛引用
強(qiáng)引用:把對(duì)象賦給一個(gè)引用變量,只要對(duì)象不為null,在GC時(shí)就不會(huì)被回收
軟引用:需要繼承softReference,會(huì)在內(nèi)存不足是進(jìn)行回收
弱引用:需要繼承WeakReference,只要發(fā)生GC就會(huì)被回收
虛引用:需要繼承PhantomReference,監(jiān)控通知時(shí)使用,跟蹤對(duì)象垃圾回收的狀態(tài)
簡(jiǎn)而言之,內(nèi)存泄漏就是對(duì)象不被程序調(diào)用,但是GC又回收不了時(shí)產(chǎn)生的。
接下來(lái),再說(shuō)說(shuō)ThreadLocal與內(nèi)存泄漏的關(guān)系,我們知道ThreadLocal的設(shè)計(jì)里面,其實(shí)內(nèi)部的一個(gè)Map對(duì)象的包裝,key為T(mén)hreadLocal對(duì)象本身,value為存入的值,所以這就存在一個(gè)問(wèn)題了,當(dāng)ThreadLoacl對(duì)象被回收之后,但是線程還是存在一個(gè)弱引用通過(guò)ThreadLocalMap指向ThreeadLocal對(duì)象的,這時(shí)ThreadLocalMap的value一直無(wú)法回收。
解釋?zhuān)哼@種概率非常低,我們知道只要ThreadLocal沒(méi)有被回收,那就沒(méi)有內(nèi)存泄漏的風(fēng)險(xiǎn),在這,我們也知道其實(shí)ThreadLocalMap是依附在Thread上的,只要Thread銷(xiāo)毀,那么ThreadLocaMap也會(huì)銷(xiāo)毀,所以在非線程池的環(huán)境下,也不會(huì)有內(nèi)存泄漏的風(fēng)險(xiǎn),而且ThreadLocal本身也做了一些保護(hù)措施,在線程池的環(huán)境下,如果發(fā)現(xiàn)ThreadLocalMap的key為null時(shí),則會(huì)將其清除。
綜上:要存在長(zhǎng)期內(nèi)存泄漏,要滿足三個(gè)條件——ThreadLocal被回收、線程被復(fù)用、線程復(fù)用后不再調(diào)用set/get/remove方法

面試總結(jié)系列第一面——請(qǐng)大家多多關(guān)照

有關(guān)AQS與ObjectMonitor的底層原理,這塊內(nèi)容有些枯燥,光講解的話有點(diǎn)難理解,如果大家有興趣,歡迎留言評(píng)論區(qū)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容