首頁  >  文章  >  Java  >  如何使用Java中的ThreadLocal類別?

如何使用Java中的ThreadLocal類別?

王林
王林轉載
2023-05-08 23:49:061172瀏覽

    Threadlocal有什麼用:

    簡單的說就是,一個ThreadLocal在一個執行緒中是共享的,在不同執行緒之間又是隔離的(每個線程都只能看到自己線程的值)。如下圖:

    如何使用Java中的ThreadLocal類別?

    ThreadLocal使用實例

    API介紹

    在使用Threadlocal之前我們先看以下它的API:

    如何使用Java中的ThreadLocal類別?

    ThreadLocal類別的API非常的簡單,在這裡比較重要的就是get()、set()、remove(),set用於賦值操作,get用於取得變數的值,remove就是刪除目前變數的值.需要注意的是initialValue方法會在第一次呼叫時被觸發,用於初始化當前變數值,預設是initialValue回傳的是null。

    ThreadLocal的使用

    說完了ThreadLocal類別的API了,那我們就來動手實踐一下了,來理解前面的那句話:一個ThreadLocal在一個線程中是共享的,在不同線程之間又是隔離的(每個線程都只能看到自己線程的值)

    public class ThreadLocalTest {
    
        private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
    	// 重写这个方法,可以修改“线程变量”的初始值,默认是null
            @Override
            protected Integer initialValue() {
                return 0;
            }
        };
    
        public static void main(String[] args) throws InterruptedException {
    
            //一号线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("一号线程set前:" + threadLocal.get());
                    threadLocal.set(1);
                    System.out.println("一号线程set后:" + threadLocal.get());
                }
            }).start();
    
            //二号线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("二号线程set前:" + threadLocal.get());
                    threadLocal.set(2);
                    System.out.println("二号线程set后:" + threadLocal.get());
    
                }
            }).start();
    
            //主线程睡1s
            Thread.sleep(1000);
    
            //主线程
            System.out.println("主线程的threadlocal值:" + threadLocal.get());
    
        }
    
    }

    稍微解釋一下上面的程式碼:

    每一個ThreadLocal實例就類似於一個變數名,不同的ThreadLocal實例就是不同的變數名,它們內部會存有一個值(暫時如此理解)在後面的描述中所說的「ThreadLocal變數或是執行緒變數」代表的就是ThreadLocal類別的實例。

    在類別中建立了一個靜態的 “ThreadLocal變數”,在主線程中建立兩個線程,在這兩個線程中分別設定ThreadLocal變數為1和2。然後等待一號和二號線程執行完畢後,在主線程中查看ThreadLocal變數的值。

    程式結果及分析⌛

    如何使用Java中的ThreadLocal類別?

    程式結果重點看的是主執行緒輸出的是0,如果是一個普通變量,在一號執行緒和二號線程中將普通變量設為1和2,那麼在一二號線程執行完畢後在打印這個變量,輸出的值肯定是1或者2(到底輸出哪一個由操作系統的線程調度邏輯有關)。但使用ThreadLocal變數經由兩個執行緒賦值後,在主執行緒程中輸出的卻是初始值0。在這也就是為什麼“一個ThreadLocal在一個線程中是共享的,在不同線程之間又是隔離的”,每個線程都只能看到自己線程的值,這也就是ThreadLocal的核心作用:實現線程範圍的局部變數。

    Threadlocal 的原始碼分析

    原理

    每個Thread物件都有一個ThreadLocalMap,當建立一個ThreadLocal的時候,就會將該ThreadLocal物件加入到該Map中,其中鍵就是ThreadLocal,值可以是任意型別。這句話剛看可能不是很懂,下面我們一起看完原始碼就懂了。

    前面我們的理解是所有的常數值或是引用型別的引用都是保存在ThreadLocal實例中的,但實際上不是的,這種說法只是讓我們更好的理解ThreadLocal變數這個概念。在ThreadLocal存入一個值,實際上是存入一個值線程物件中的ThreadLocalMap,ThreadLocalMap我們可以簡單的理解成一個Map,而儲存到這個Map值的key就是ThreadLocal實例本身。

    原始碼

    如何使用Java中的ThreadLocal類別?

    ????也就是說,想要存入的ThreadLocal中的資料其實並沒有存到ThreadLocal物件中去,而是以這個ThreadLocal實例作為key存到了當前線程中的一個Map中去了,而取得ThreadLocal的值時同樣也是這個道理。這就是為什麼ThreadLocal可以實現線程之間隔離的原因了。

    內部類別ThreadLocalMap

    ThreadLocalMap是ThreadLocal的內部類,實作了一套自己的Map結構✨

    ThreadLocalMap屬性:

    static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
            //初始容量16
            private static final int INITIAL_CAPACITY = 16;
            //散列表
            private Entry[] table;
            //entry 有效数量 
            private int size = 0;
            //负载因子
            private int threshold;

    ThreadLocalMap設定ThreadLocal變數

    private void set(ThreadLocal<?> key, Object value) {
                Entry[] tab = table;
                int len = tab.length;
                
                //与运算  & (len-1) 这就是为什么 要求数组len 要求2的n次幂 
                //因为len减一后最后一个bit是1 与运算计算出来的数值下标 能保证全覆盖 
                //否者数组有效位会减半 
                //如果是hashmap 计算完下标后 会增加链表 或红黑树的查找计算量 
                int i = key.threadLocalHashCode & (len-1);
                
                // 从下标位置开始向后循环搜索  不会死循环  有扩容因子 必定有空余槽点
                for (Entry e = tab[i];   e != null;  e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
                    //一种情况 是当前引用 返回值
                    if (k == key) {
                        e.value = value;
                        return;
                    }
                    //槽点被GC掉 重设状态 
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    			//槽点为空 设置value
                tab[i] = new Entry(key, value);
                //设置ThreadLocal数量
                int sz = ++size;
    			
    			//没有可清理的槽点 并且数量大于负载因子 rehash
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }

    ThreadLocalMap屬性介紹????:

    • #和普通Hashmap類似儲存在一個陣列內,但與hashmap使用的拉鍊法解決散列衝突不同的是ThreadLocalMap使用開放位址法

    • 陣列初始容量16,負載因子2/3

    • node節點的key封裝了WeakReference 用於回收

    ThreadLocalMap儲存位置

    儲存在Thread中,有兩個ThreadLocalMap變數

    如何使用Java中的ThreadLocal類別?

    #threadLocals 在ThreadLocal在物件方法set中去創建也由ThreadLocal來維護

    public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    inheritableThreadLocals 和ThreadLocal類似InheritableThreadLocal重寫了createMap方法

    void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }

    inheritableThreadLocals 作用是将ThreadLocalMap传递给子线程

    如何使用Java中的ThreadLocal類別?

    init方法中 条件满足后直接为子线程创建ThreadLocalMap

    如何使用Java中的ThreadLocal類別?

    注意:

    • 仅在初始化子线程的时候会传递 中途改变副线程的inheritableThreadLocals 变量 不会将影响结果传递到子线程 。

    • 使用线程池要注意 线程不回收 尽量避免使用父线程的inheritableThreadLocals 导致错误

    Key的弱引用问题

    为什么要用弱引用,官方是这样回答的

    To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

    为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

    生命周期长的线程可以理解为:线程池的核心线程

    ThreadLocal在没有外部对象强引用时如Thread,发生GC时弱引用Key会被回收,而Value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

    • key 使用强引用????: 引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

    • key 使用弱引用????: 引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

    Java8中已经做了一些优化如,在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

    java中的四种引用

    • 强引用????: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象

    • 软引用????: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)

    • 弱引用????: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象

    • 虚引用????: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)

    通常ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长,如果没有手动删除对应key(线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏),一定会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除,Java8已经做了上面的代码优化。

    以上是如何使用Java中的ThreadLocal類別?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除