【JAVA多线程】ThreadLocal源码分析

在这里插入图片描述

ThreadLocal

此类提供线程局部变量。这些变量不同于它们的正常对应变量,因为每个访问一个(通过它的 getset方法)的线程都有它自己的、独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程的类中的私有静态字段(例如,用户 ID)相关联。

场景:一般在连接池优化上会使用到ThreadLocal,避免使用同步,提高性能。

用法

public class Test {
    public  static ThreadLocal<Integer> local = new ThreadLocal<>();

    public static void main(String[] args) {
        for(int i=0; i<2;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Double d = Math.random() * 10;
                    local.set(d.intValue());
                    new A().get();
                    new B().get();
                }
            }).start();

        }


    }
    static class A{
        public void get(){
            System.out.println(local.get());
        }
    }
    static class B{
        public void get(){
            System.out.println(local.get());
        }
    }

}

set

set是用来设置想要在线程本地的数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。

 public void set(T value) {
        Thread t = Thread.currentThread();//先拿到当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);//设置本地变量值
        else
            createMap(t, value);//不存在先创建map
    }

那ThreadLocalMap又是什么?其实是线程自身的一个成员属性

跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中。

Entry继承自WeakReference

 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。

get

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//获取Entry
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//获取保存的值
                return result;
            }
        }
        return setInitialValue();
    }

remove

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
		/**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

我看看ThreadLocalMap的expungeStaleEntry这个方法,这个方法在ThreadLocalMap get、set、remove、rehash等方法都会调用到。

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

            // 将entry赋空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

           
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) { //找到已经被GC的ThreadLocal
                    e.value = null;//清理掉key为空的值
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

如果数据初始化好之后,一直不调用get、set等方法,这样Entry就一直不能回收,导致内存泄漏。所以一旦数据不使用最好主动remove。

如果线程资源回收了,还会泄漏吗?

当然不会,线程回收后,存在线程相关的ThreadLocalMap也会被回收。大部分场景中,线程都是一直存活或者长时间存活。

问题

内存泄露问题
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

脏数据问题
由于项目基本上都是使用线程池,因为Thread对象是复用的,那么与Thread对象绑定的ThreadLocal变量也可能被复用。

父子线程共享线程变量
很多场景下通过ThreadLocal来透传全局上下文,会发现子线程的value和主线程不一致。

猜你喜欢

转载自blog.csdn.net/qq_15604349/article/details/124498503