自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

服了,一個(gè)ThreadLocal被問出了花

開發(fā)
ThreadLocal英文翻譯過來就是:線程本地量,它其實(shí)是一種線程的隔離機(jī)制,保障了多線程環(huán)境下對(duì)于共享變量訪問的安全性。

一、故事

地鐵上,小帥無力地倚靠著桿子,腦子里盡是剛才面試官的奪命連環(huán)問,“用過TheadLocal么?ThreadLocal是如何解決共享變量訪問的安全性的呢?你覺得啥場(chǎng)景下會(huì)用到TheadLocal? 我們?cè)谌粘S肨hreadLocal的時(shí)候需要注意什么?ThreadLocal在高并發(fā)場(chǎng)景下會(huì)造成內(nèi)存泄漏嗎?為什么?如何避免?......”

這些問題,如同陰影一般,在小帥的腦海里揮之不去。

是的,他萬萬沒想到,自詡“多線程小能手”的他栽在了ThreadLocal上。

這是小帥苦投了半個(gè)月簡(jiǎn)歷之后才拿到的面試機(jī)會(huì),然而又喪失了。當(dāng)下行情實(shí)在是卷到了極點(diǎn)。

都兩個(gè)月了,面試機(jī)會(huì)少,居然還每次都被問翻,這樣下去真要回老家另謀出路了,小帥內(nèi)心五味成雜......

小伙伴們,試問一下,如果是你,面對(duì)上述的問題,你能否對(duì)答如流呢?

二、概要

既然被問到了,那么作為事后諸葛的老貓就和大家一起來接面試官的招吧。

我們將從以下點(diǎn)來全面剖析一下ThreadLocal。

概覽

三、基本篇

1.什么是ThreadLocal?

ThreadLocal英文翻譯過來就是:線程本地量,它其實(shí)是一種線程的隔離機(jī)制,保障了多線程環(huán)境下對(duì)于共享變量訪問的安全性。

看到上面的定義之后,那么問題就來了,ThreadLocal是如何解決共享變量訪問的安全性的呢?

其實(shí)ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問自己內(nèi)部的副本變量。由于副本都?xì)w屬于各自的線程,所以就不存在多線程共享的問題了。

便于理解,我們看一下下圖。

結(jié)構(gòu)圖

至于上述圖中提及的threadLocals(ThreadLocalMap),我們后文看源代碼的時(shí)候再繼續(xù)來看。大家心中暫時(shí)有個(gè)概念。

既然都是保證線程訪問的安全性,那么和Synchronized區(qū)別是什么呢?

在上面聊到共享變量訪問安全性的問題上,其實(shí)大家還會(huì)很容易想起另外一個(gè)關(guān)鍵字Synchronized。聊聊區(qū)別吧,整理了一張圖,看起來可能會(huì)更加直觀一些,如下。

對(duì)比

通過上圖,我們發(fā)現(xiàn)ThreadLocal其實(shí)是一種線程隔離機(jī)制。Synchronized則是一種基于Happens-Before規(guī)則里的監(jiān)視器鎖規(guī)則從而保證同一個(gè)時(shí)刻只有一個(gè)線程能夠?qū)蚕碜兞窟M(jìn)行更新。

Synchronized加鎖會(huì)帶來性能上的下降。ThreadLocal采用了空間換時(shí)間的設(shè)計(jì)思想,也就是說每個(gè)線程里面都有一個(gè)專門的容器來存儲(chǔ)共享變量的副本信息,然后每個(gè)線程只對(duì)自己的變量副本做相對(duì)應(yīng)的更新操作,這樣避免了多線程鎖競(jìng)爭(zhēng)的開銷。

2.ThreadLocal的使用

上面說了這么多,咱們來使用一下。就拿SimpleDateFormat來做個(gè)例子。當(dāng)然也會(huì)有一道這樣的面試題,SimpleDateFormat是否是線程安全的?在阿里Java開發(fā)規(guī)約中,有強(qiáng)制性地提到SimpleDateFormat 是線程不安全的類。其實(shí)主要的原因是由于多線程操作SimpleDateFormat中的Calendar對(duì)象引用,然后出現(xiàn)臟讀導(dǎo)致的。

踩坑代碼:

/**
 * @author 公眾號(hào):程序員老貓
 * @date 2024/2/1 22:58
 */
public class DateFormatTest {
    private static final SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

上述咱們通過線程池的方式針對(duì)SimpleDateFormat進(jìn)行了測(cè)試。其輸出結(jié)果如下。

我們可以看到剛開始好好的,后面就異常了。

我們通過ThreadLocal的方式將其優(yōu)化一下。代碼如下:

/**
 * @author 公眾號(hào):程序員老貓
 * @date 2024/2/1 22:58
 */
public class DateFormatTest {

    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = dateFormatThreadLocal.get().parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

運(yùn)行了一下,完全正常了。

Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024

3.TheadLocal使用場(chǎng)景

那么我們什么時(shí)候會(huì)用到ThreadLocal呢?

上面針對(duì)SimpleDateFormat的封裝也算是一個(gè)吧。

  • 用來替代參數(shù)鏈傳遞:在編寫API接口時(shí),可以將需要傳遞的參數(shù)放入ThreadLocal中,從而不需要在每個(gè)調(diào)用的方法上都顯式地傳遞這些參數(shù)。這種方法雖然不如將參數(shù)封裝為對(duì)象傳遞來得常見,但在某些情況下可以簡(jiǎn)化代碼結(jié)構(gòu)。
  • 數(shù)據(jù)庫連接和會(huì)話管理:在某些應(yīng)用中,如Web應(yīng)用程序,ThreadLocal可以用來保持對(duì)數(shù)據(jù)庫連接或會(huì)話的管理,以簡(jiǎn)化并發(fā)控制并提高性能。例如,可以使用ThreadLocal來維護(hù)一個(gè)連接池,使得每個(gè)請(qǐng)求都能共享相同的連接,而不是每次都需要重新建立連接。
  • 全局存儲(chǔ)信息:例如在前后端分離的應(yīng)用中,ThreadLocal可以用來在服務(wù)端維護(hù)用戶的上下文信息或者一些配置信息,而不需要通過HTTP請(qǐng)求攜帶大量的用戶信息。這樣做可以在不改變?cè)屑軜?gòu)的情況下,提供更好的用戶體驗(yàn)。

如果大家還能想到其他使用的場(chǎng)景也歡迎留言。

四、升華篇

1.ThreadLocal原理

上述其實(shí)咱們聊得相對(duì)而言還是比較淺的。那么接下來,咱們豐富一下之前提到的結(jié)構(gòu)圖,從源代碼側(cè)深度剖一下ThreadLocal吧。

深度結(jié)構(gòu)圖

對(duì)應(yīng)上述圖中,解釋一下。

  • 圖中有兩個(gè)線程Thread1以及Thread2。
  • Thread類中有一個(gè)叫做threadLocals的成員變量,它是ThreadLocal.ThreadLocalMap類型的。
  • ThreadLocalMap內(nèi)部維護(hù)了Entry數(shù)組,每個(gè)Entry代表一個(gè)完整的對(duì)象,key是ThreadLocal本身,value是ThreadLocal的泛型對(duì)象值。

對(duì)應(yīng)的我們看一下Thread的源代碼,如下:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

在源碼中threadLocals的初始值為Null。

抽絲剝繭,咱們繼續(xù)看一下ThreadLocalMap在調(diào)用構(gòu)造函數(shù)進(jìn)行初始化的源代碼:

static class ThreadLocalMap {
        
        private static final int INITIAL_CAPACITY = 16; //初始化容量
        private Entry[] table; //ThreadLocalMap數(shù)據(jù)真正存儲(chǔ)在table中
        private int size = 0; //ThreadLocalMap條數(shù)
        private int threshold; // 默認(rèn)為0,達(dá)到這個(gè)大小,則擴(kuò)容
        //類Entry的實(shí)現(xiàn)
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //構(gòu)造函數(shù)
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY]; //初始化table數(shù)組,INITIAL_CAPACITY默認(rèn)值為16
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值
            table[i] = new Entry(firstKey, firstValue);//創(chuàng)建節(jié)點(diǎn),設(shè)置key-value
            size = 1;
            setThreshold(INITIAL_CAPACITY); //設(shè)置擴(kuò)容閾值
        }
    }

在源碼中涉及比較核心的還有set,get以及remove方法。我們依次來看一下:

set方法如下:

public void set(T value) {
        Thread t = Thread.currentThread(); //獲取當(dāng)前線程t
        ThreadLocalMap map = getMap(t);  //根據(jù)當(dāng)前線程獲取到ThreadLocalMap
        if (map != null)  //如果獲取的ThreadLocalMap對(duì)象不為空
            map.set(this, value); //K,V設(shè)置到ThreadLocalMap中
        else
            createMap(t, value); //創(chuàng)建一個(gè)新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread對(duì)象的ThreadLocalMap屬性
    }

    void createMap(Thread t, T firstValue) { //調(diào)用ThreadLocalMap的構(gòu)造函數(shù)
        t.threadLocals = new ThreadLocalMap(this, firstValue); //this表示當(dāng)前類ThreadLocal
    }

get方法如下:

public T get() {
        //1、獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //2、獲取當(dāng)前線程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map數(shù)據(jù)不為空,
        if (map != null) {
            //3.1、獲取threalLocalMap中存儲(chǔ)的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是數(shù)據(jù)為null,則初始化,初始化的結(jié)果,TheralLocalMap中存放key值為threadLocal,值為null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

remove方法:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

那么為什么需要remove方法呢?其實(shí)這里會(huì)涉及到內(nèi)存泄漏的問題了。后面咱們細(xì)看。

對(duì)照著上述的結(jié)構(gòu)圖以及源碼,如果面試官問ThreadLocal原理的時(shí)候,相信大家應(yīng)該可以說出個(gè)所以然來。

  • Thread線程類有一個(gè)類型為ThreadLocal.ThreadLocalMap的變量threadLocals,即每個(gè)線程都有一個(gè)屬于自己的ThreadLocalMap。
  • ThreadLocalMap方法內(nèi)部維護(hù)著Entry數(shù)組,其中key是ThreadLocal本身,而value則為其泛型值。
  • 并發(fā)場(chǎng)景下,每個(gè)線程都會(huì)存儲(chǔ)當(dāng)前變量副本到自己的ThreadLocalMap中,后續(xù)這個(gè)線程對(duì)于共享變量的操作,都是從TheadLocalMap里進(jìn)行變更,不會(huì)影響全局共享變量的值。

2.高并發(fā)場(chǎng)景下ThreadLocal會(huì)造成內(nèi)存泄漏嗎?什么原因?qū)е拢咳绾伪苊猓?/h4>

(1) 造成內(nèi)存泄漏的原因

這個(gè)問題其實(shí)還是得從ThreadLocal底層源碼的實(shí)現(xiàn)去看。高并發(fā)場(chǎng)景下,如果對(duì)ThreadLocal處理得當(dāng)?shù)脑捚鋵?shí)就不會(huì)造成內(nèi)存泄漏。我們看下面這樣一組源代碼片段:

static class ThreadLocalMap {
        ...
        //類Entry的實(shí)現(xiàn)
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
       ...
    }

上文中其實(shí)我們已經(jīng)知道Entry中以key和value的形式存儲(chǔ),key是ThreadLocal本身,上面代碼中我們看到entry進(jìn)行key設(shè)置的時(shí)候用的是super(k)。那就意味著調(diào)用的父類的方法去設(shè)置了key,我們?cè)倏匆幌赂割愂鞘裁?,父類其?shí)是WeakReference。關(guān)于WeakReference底層的實(shí)現(xiàn),大家有興趣可以展開去看看源代碼,老貓?jiān)谶@里直接說結(jié)果。

WeakReference 如字面意思,弱引用,當(dāng)一個(gè)對(duì)象僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強(qiáng)引用)指向的時(shí)候, 如果這時(shí)GC運(yùn)行, 那么這個(gè)對(duì)象就會(huì)被回收,不論當(dāng)前的內(nèi)存空間是否足夠,這個(gè)對(duì)象都會(huì)被回收。

關(guān)于這些引用的強(qiáng)弱,稍微聊一下,這里其實(shí)涉及到j(luò)vm的回收機(jī)制。在JDK1.2之后,java對(duì)引用的概念其實(shí)做了擴(kuò)充的,分為強(qiáng)引用,軟引用,弱引用,虛引用。

  • 強(qiáng)引用:其實(shí)就是咱們一般用“=”的賦值行為,如 Student s = new Student(),只要強(qiáng)引用還在,對(duì)象就不會(huì)被回收。
  • 軟引用:不是必須存活的對(duì)象,jvm在內(nèi)存不夠的情況下即將內(nèi)存溢出前會(huì)對(duì)其進(jìn)行回收。例如緩存。
  • 弱引用:非必須存活的對(duì)象,引用關(guān)系比軟引用還弱,無論內(nèi)存夠還是不夠,下次的GC一定會(huì)被回收。
  • 虛引用:別名幽靈引用或者幻影引用。等同于沒有引用,唯一的目的是對(duì)象被回收的時(shí)候會(huì)受到系統(tǒng)通知。

明白這些概念之后,咱們?cè)倏纯瓷厦娴脑创a,我們就會(huì)發(fā)現(xiàn),原來Key其實(shí)是弱引用,而里面的value因?yàn)槭侵苯淤x值行為所以是強(qiáng)引用。

如下圖:

jvm存儲(chǔ)

圖中我們可以看到由于threadLocal對(duì)象是弱引用,如果外部沒有強(qiáng)引用指向的話,它就會(huì)被GC回收,那么這個(gè)時(shí)候?qū)е翬ntry的key就為NULL,如果此時(shí)value外部也沒有強(qiáng)引用指向的話,那么這個(gè)value就永遠(yuǎn)無法訪問了,按道理也該被回收。但是由于entry還在強(qiáng)引用value(看源代碼)。那么此時(shí)value就無法被回收,此時(shí)內(nèi)存泄漏就出現(xiàn)了。本質(zhì)原因是因?yàn)関alue成為了一個(gè)永遠(yuǎn)無法被訪問也無法被回收的對(duì)象。

那肯定有小伙伴會(huì)有疑問了,線程本身生命周期不是很短么,如果短時(shí)間內(nèi)被銷毀,就不會(huì)內(nèi)存泄漏了,因?yàn)橹灰€程銷毀,那么value也會(huì)被回收。這話是沒錯(cuò)。但是咱們的線程是計(jì)算機(jī)珍貴資源,為了避免重復(fù)創(chuàng)建線程帶來開銷,系統(tǒng)中我們往往會(huì)使用線程池(線程池傳送門),如果使用線程池的話,那么線程的生命周期就被拉長(zhǎng)了,那么就可想而知了。

(2) 如何避免

解法如下:

  • 每次使用完畢之后記得調(diào)用一下remove()方法清除數(shù)據(jù)。
  • ThreadLocal變量盡量定義成static final類型,避免頻繁創(chuàng)建ThreadLocal實(shí)例。這樣可以保證程序中一直存在ThreadLocal強(qiáng)引用,也能保證任何時(shí)候都能通過ThreadLocal的弱引用訪問Entry的value值,從而進(jìn)行清除。

不過話說回來,其實(shí)ThreadLocal內(nèi)部也做了優(yōu)化的。在set()的時(shí)候也會(huì)采樣清理,擴(kuò)容的時(shí)候也會(huì)檢查(這里希望大家自己深入看一下源代碼),在get()的時(shí)候,如果沒有直接命中或者向后環(huán)形查找的時(shí)候也會(huì)進(jìn)行清理。但是為了系統(tǒng)的穩(wěn)健萬無一失,所以大家盡量還是將上面的兩個(gè)注意點(diǎn)在寫代碼的時(shí)候注意下。

總結(jié)

面試的時(shí)候大家總會(huì)去背一些八股文,但是這種也只是臨時(shí)應(yīng)付面試官而已,真正的懂其中的原理才是硬道理。無論咋問,萬變不離核心原理。當(dāng)然這些核心原理在我們的日常編碼中也會(huì)給我們帶來很大的幫助,用法很簡(jiǎn)單,翻車了如何處理,那還不是得知其所以然么,伙伴們,你們覺得呢?

責(zé)任編輯:趙寧寧 來源: 程序員老貓
相關(guān)推薦

2020-05-28 10:23:57

5G網(wǎng)絡(luò)技術(shù)

2025-02-11 09:17:57

2021-11-30 08:26:22

ThreadLocal內(nèi)存飆升存儲(chǔ)模型

2021-05-07 18:12:32

ThreadLocal面試項(xiàng)目

2020-06-09 08:06:31

RocketMQ消息耗時(shí)

2022-11-13 10:07:22

SpringSpringBoot

2021-07-26 17:18:03

Linux進(jìn)程通信

2021-04-15 09:18:22

單例餓漢式枚舉

2022-01-25 12:14:39

面試try-catch代碼

2023-11-09 09:02:26

TypeScriptas const

2024-05-29 08:46:19

2013-06-28 17:28:04

推送

2022-04-08 08:48:16

線上事故日志訂閱者

2021-05-21 07:26:15

DataSource接口數(shù)據(jù)庫

2023-03-28 16:37:38

論文視頻

2021-05-27 07:54:21

JavaStateAQS

2024-08-14 08:35:38

sql數(shù)據(jù)庫OOM 異常

2009-09-02 18:36:46

LinuxLinux操作系統(tǒng)Linux開發(fā)

2021-09-13 08:41:52

職場(chǎng)互聯(lián)網(wǎng)自閉

2024-12-13 08:02:10

PythonGenerator懶加載
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)