《JAVA多线程》之深入理解ThreadLocal

本文深入探讨了ThreadLocal的工作原理,包括其与synchronized的区别,如何为每个线程提供独立的变量副本,以及可能引起的内存溢出问题。通过源码分析,详细解释了ThreadLocalMap的结构和作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal简介

ThreadLocal不是Thread,是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

ThreadLocal与synchronized的比较

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
ThreadLocal是线程局部变量,是一种多线程间并发访问变量的解决方案。和synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的方式,为每个线程提供变量的独立副本,以保证线程的安全。

举个例子

public class ThreadLocalTestDemo {

    public static void main(String[] args){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("==第一个线程==");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                System.out.println(threadLocal.get());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("==第二个线程==");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                System.out.println(threadLocal.get());
            }
        }).start();
    }
}

运行结果:
在这里插入图片描述
通过测试类我们可以看出,在不同的线程中,访问的是同一个ThreadLocal对象,但是获取的值却是不一样的。

分析ThreadLocal原理

万事撸源码。说多了都是废话,直接看源码
ThreadLocal中全部的方法和内部类结构如下:
在这里插入图片描述
我们要弄清楚ThreadLocal是如何做到每一个线程维护一个变量的,那就必须先弄清楚
ThreadLocal.ThreadLocalMap这个内部类

 /**
    *ThreadLocalMap是自定义的哈希映射,仅适用于维护线程局部值。 
    *没有在ThreadLocal类之外导出任何操作。该类是包私有的,以允许在Thread类中声明
    *字段。为了帮助处理非常大且长期存在的用法,哈希表条目使用WeakReferences作为键。
    *但是,由于未使用引用队列,因此仅在表开始空间不足时,才保证删除过时的条目
     */
    static class ThreadLocalMap {

        /**
         *此哈希映射中的条目使用其主要引用字段作为键(始终是ThreadLocal对象)
         *扩展了WeakReference。请注意,空键(即entry.get()== null)
         *意味着不再引用该键,因此可以从表中删除条目。
         *在下面的代码中,此类条目被称为“陈旧条目”。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

Entry是一个包含key和value的一个对象,ThreadLocal<?>为key,要保存的线程局部变量的值为value,通过super(k);调用 WeakReference的构造函数, 将ThreadLocal对象变成一个弱引用的对象,这样做是为了在线程销毁的时候,对应的实体会被回收,不会出现内存泄漏

Thread和ThreadLocalMap的关系

看下图,不难看出Thread中的threadLocals对应的就是ThreadLocal中的ThreadLocalMap:
在这里插入图片描述
简单来说:每一个Thread中都保存着自己的一个ThreadLocalMap,一个ThreadLoaclMap中可以有多个ThreadLocal对象,也可以说同一个线程下不同的ThreadLocal对象共用一个ThreadLocalMap ,其中一个ThreadLocal对象对应着map中的一个Entry(即ThreadLocalMap的key是ThreadLocal的对象,value是独享数据)
下图可以更好的理解:
在这里插入图片描述
思考:为什么用ThreadLocalMap来保存线程的局部对象(即ThreadLocal)?
因为一个线程所拥有的局部对象可能会很多,会有很多个ThreadLocal,这样的话,不管一个线程拥有多少个局部变量,都用一个ThreadLocalMap来保存,我们来看map.set(this,value)。并且会自动扩容;

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

可以看出ThreadLocal 所操作的是当前线程的 ThreadLocalMap 对象中的 table 数组,并把操作的 ThreadLocal 作为键存储

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,所以set的时候先获取当前线程的map对象,如果map是null,说明第一次添加,会进行创建createMap(t,value)

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


    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

初始化TreadLocalMap中的table的初始值是16,超过容量2/3的时候下一次再像map中set数据的时候会扩容

ThreadLocal的get方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        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;
    }
    
    protected T initialValue() {
        return null;
    }

在调用get拿数据的时候,首先拿到当前线程中的map,判断是否为null。如果是空的,会调用setInitialValue(),这里的value一定是null。如果map不为null,调用getEntry(this)方法,传入当前的ThreadLocal。

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

   private int expungeStaleEntry(int staleSlot) {
       Entry[] tab = table;
       int len = tab.length;

       // expunge entry at staleSlot
       tab[staleSlot].value = null;
       tab[staleSlot] = null;
       size--;

       // Rehash until we encounter null
       Entry e;
       int i;
       for (i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
           ThreadLocal<?> k = e.get();
           if (k == null) {
               e.value = null;
               tab[i] = null;
               size--;
           } else {
               int h = k.threadLocalHashCode & (len - 1);
               if (h != i) {
                   tab[i] = null;.
                   while (tab[h] != null)
                       h = nextIndex(h, len);
                   tab[h] = e;
               }
           }
       }
       return i;
   }

通过哈希计算得出下标,然后比较当前参数key(TreadLocal)和map中存储的TreadLocal是不是一个,如果不是的话就调用getEntryAfterMiss()开始线性查找,
一旦碰到空对象就停止返回,这里expungeStaleEntry()方法也对内存泄漏做了优化操作。即对key=null 的key所对应的value进行赋空, 释放了空间避免内存泄漏。同时它遍历下一个key为空的entry, 并将value赋值为null, 等待下次GC释放掉其空间
我们发现,set()和get()方法都会调用该方法。

ThreadLocal可能引起的OOM内存溢出问题简要分析

刚刚我们也说了,ThreadLocal可能导致内存泄漏,那么具体的原因是为什么呢?
因为ThreadLocal的原理是操作Thread内部的一个ThreadLocalMap,这个Map的Entry继承了WeakReference,即Entry中的key是弱引用。java中的弱引用会在下次GC的时候会被回收掉,所以key会被回收,但是value并不会呗回收掉。这样导致key为null,value有值。线程如果销毁,value也会被回收,但是如果在线程池中,线程执行完之后是返回线程池中,并不是销毁,同时GC的时候把key清除了,那么这个value永远不会被清除,久而久之就会内存溢出。所以jdk开发者针对这一情况也做了优化。也就是我们上面说的expungeStaleEntry()这个方法。但是这样做也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。所以最能解决问题的办法就是用完ThreadLocal后手动调用remove()。

思考:弱引用如果有内存泄漏危险,那为什么key不设置为强引用
强引用更不行,因为如果key是强引用,当TreadLocal对象要被回收时。但是TreadLocalMap中依然保持这个ThreadLocal对象的强引用,而ThreadLocalMap又被当前线程Thread强引用,也就是说当线程不销毁的时候,ThreadLocalMap就不会被回收,从而导致ThreadLocal也不会被回收,除非手动删除key
弱引用会在下一次GC的时候强制回收。虽然也会导致内存溢出,但是最起码也有set、get、removede方法操作对null key进行擦除的补救措施, 方案上略胜一筹。

总结

(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;

(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;

(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;

(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;

(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);

(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;

(7)线程死亡时,线程局部变量会自动回收内存;

(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;

(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值