admin管理员组

文章数量:1599279

目录

一、ThreadLocal的介绍

1.1 ThreadLocal作用

1.2 API介绍

二、ThreadLocal源码分析 

2.1 源码解读

 1.Thread类

2.ThreadLocalMap

3. ThreadLocal源码

4.InheritableThreadLocal

2.三者关系

三、ThreadLocal 内存泄露问题

3.1 阿里规范

3.2 强引用、软引用、弱引用、虚引用分别是什么?

1. 强引用(默认支持模式)

2. 软引用

3. 弱引用

4.虚引用

5. 引用总结

3.3 内存泄露原因

1.原因

四、源码分析

4.1 为什么要用弱引用?不用如何?

4.2 为什么源代码用弱引用?

4.3 清理脏Entry

1. key为null的entry原理分析

3. 结论 

五、总结

5.1 最佳实践

5.2 总结  

六、扩展


一、ThreadLocal的介绍

   ThreadLocal为每个线程提供独立的局部变量。

官方解释:

   ThreadLocal提供线程局部变量。这些变量与政策的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其getset方法)都有自己的、独立初始化的变量副本ThreadLocal实例通常是类中的私有静态字段,使用它的目的是系统将状态(例如,用户ID事务ID)与线程关联起来。

         ThreadLocal可以看做是一个线程安全的map,但是这个map的key的值不需要我们关心,而是内部根据不同线程而自动创建key,默认key的value初始值都是一样的。

        对的应用和ThreadLocal的使用案例,进行应用,他们都是解决线程安全问题,但是对应不同需求,ThreadLocal替代不了

        并且阿里巴巴开发手册中强制规范中要求,,每次线程执行完都要remove清理掉当前线程的操作值,避免出现内存泄露·或影响后续逻辑,尤其是在线程池中,因为ThreadLocal是以线程为key线程池有时复用线程,如果不及时清理就会导致每次执行时ThreadLocal的值无法重置,导致数据越来越多,最终内存泄露·

   ThreadLocal可以每个Thread都用自己独立的实例副本并且该副本只有当前线程使用,其他线程无法访问到从而,避免了多线程访问共享的问题。

1.1 ThreadLocal作用

   ThreadLocal:实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,每个线程独立的一份,可以参考本地内存)。

        主要解决了让每个线程绑定自己的值,通过使用get()set()方法,获取默认值或将其更改为当前线程所存的副本的值从而避免了线程安全问题。

比如我们之前使用过 8锁的案例,资源类是一个,会出现多线程竞争的问题,从而产生线程安全问题。

        使用TreadLocal可以通过线程之间进行资源隔离实现数据独立从而变向的解决了资源安全问题。因为不存在竞争,每个线程都有一份数据,从而不存在竞态条件

        在公用共享数据,被多个请求同时访问时,就会存在线程安全问题,解决方法synchronizedlock cas 都是通过加锁的方式。

        使用ThreadLocal是将共享数据根据线程的不同创建出独立的副本,互不干扰。

1.2 API介绍

        

        可以看到ThreadLocalapi很干净,提供了get获取值,set修改值,remove删除值,以上都没有需要传递key,因为默认当前线程就是参数key

   initialVlaue()withInitial方法都是ThreadLocal初始化的方法,因为默认情况下ThreadLocal每个keyvalue都是null,在使用时会报错空指针异常,所以需要给定一个默认初始化的值。

   initialVlaue是通过匿名内部类的方式,withInitial可以使用lamok表达式来实现。

二、ThreadLocal源码分析 

        ThreadLocal 和 Thread 、TheadLocalMap 是密不可分的。

2.1 源码解读

 1.Thread类

public class Thread implements Runnable {
        /* Make sure registerNatives is the first thing <clinit> does. */
        private static native void registerNatives();
        static {
            registerNatives();
        }
        // 线程的名称 
        private volatile String name;
        
  	 	// 线程的优先级 
        private int            priority;
	
        // 等待队列中的下一个线程
        private Thread         threadQ;
        // 用于内部调试,用于计算线程的大小
        private long           eetop;

        /* 是否为该线程进行单步执行 */
        private boolean     single_step;

        /* 是否是守护线程 */
        private boolean     daemon = false;

        /* JVM 的状态 */
        private boolean     stillborn = false; // 表示线程是否被启动

        /* 要执行的任务 */
        private Runnable target;

        /* 线程归属的线程组 */
        private ThreadGroup group;

        /* 这个线程的类加载器 */
        private ClassLoader contextClassLoader;

        /* The inherited AccessControlContext of this thread */
        private AccessControlContext inheritedAccessControlContext;

        /* 用于自动编号匿名线程 */
        private static int threadInitNumber;
        // 自动编号下一个线程 
        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        }

        /* 与线程相关的 ThreadLocal值,此Map由 ThreadLocal 类维护 */
        ThreadLocal.ThreadLocalMap threadLocals = null;

        /*
    	与线程相关的InheritableThreadLocal 值。此Map 由 InheritableThreadLocal 类维护 
*/
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

        /*
 线程请求的堆栈大小。如果创建者未指定堆栈大小,则为 0。它由 VM 处理,有些 VM 会忽略它
*/
        private long stackSize;

        /*
* JVM-private state that persists after native thread termination.
*/
        private long nativeParkEventPointer;

        /*
* Thread ID
*/
        private long tid;

        /* For generating thread ID */
        private static long threadSeqNumber;

        /* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/

        private volatile int threadStatus = 0;


        private static synchronized long nextThreadID() {
            return ++threadSeqNumber;
        }

        /**
* The argument supplied to the current call to
* java.util.concurrent.locks.LockSupport.park.
* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
* Accessed using java.util.concurrent.locks.LockSupport.getBlocker
*/
        volatile Object parkBlocker;

        /* The object in which this thread is blocked in an interruptible I/O
* operation, if any.  The blocker's interrupt method should be invoked
* after setting this thread's interrupt status.
*/
        private volatile Interruptible blocker;
        private final Object blockerLock = new Object();

        /* Set the blocker field; invoked via sun.misc.SharedSecrets from java.nio code
*/
        void blockedOn(Interruptible b) {
            synchronized (blockerLock) {
                blocker = b;
            }
        }

        /**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
}

        从Thread的属性中可以看到了ThreadLocal.ThreadLocalMap threadLocals = nullThreadLocal.ThreadLocalMap inheritableThreadLocals = null这两值都是ThreadLocal的内部类ThreadLocalMap

        每个 Thread 对象都有一个 ThreadLocalMap 属性,用于存储该线程的所有线程本地变量。当一个线程创建了一个线程本地变量后,这个线程本地变量会被存储在该线程的 ThreadLocalMap 中。

2.ThreadLocalMap

   ThreadLocalMapThreadLocal的内部,它是相当于是一个map,用于存独立线程的副本的,ThreadLocalMap内部还有一个内部类Entry实现了WeakReference(弱引用)。

        ThreadLocal 是一个用于创建线程本地变量的类,它提供了 get()set() 方法用于获取和设置线程本地变量的值。每个线程拥有自己的一个 ThreadLocalMap 对象,其中存储了线程本地变量的值。一个 ThreadLocal 对象对应了一个线程本地变量。多个线程可以拥有同一个 ThreadLocal 对象,但是它们在自己的 ThreadLocalMap 中存储的是不同的值。

        ThreadLocalMapThread 的一个属性,用于存储该线程的所有线程本地变量。它是一个自定义的 hash 表实现,其中的每个元素都是一个 Entry 对象,每个 Entry 对象包含了一个 ThreadLocal 对象和一个 Object 对象,ThreadLocal 对象用于表示线程本地变量,Object 对象用于存储线程本地变量的值。

3. ThreadLocal源码

        通过Thread的源码可以发现每个线程内部都有一个独立的ThreadLocalMap用来存放独立的值,ThreadLocalMap相当于那个Map容器,那么两者之间是如何关联起来的呢?

        是通过ThreadLocal进行关联,可以通过源码看出来:

public class ThreadLocal<T> {
    /**
    	每个线程的ThreadLocalMap的哈希码
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
       定义一个原型的哈希码,初始值为0
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
连续生成的哈希代码之间的差异 - 将隐式顺序线程本地 ID 转换为两大小表的近乎最佳分布的乘法哈希值。
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 返回下一个哈希码
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
    	返回当前线程的局部变量的初始化,初始值为null,所以在初始ThreadLocal时需要进行定义初始化
    	值,以免出现程序运行问题。
     */
    protected T initialValue() {
        return null;
    }

    /**
        通过lambda表达式进行设置初始值
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
	 无参构造
     */
    public ThreadLocal() {
    }

    /**
    	返回当前线程的局部变量的副本值,如果变量没有当前线程的值,则会返回initaValue 值
     */
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 从 ThreadLocalMap 中以,当前线程为 key 去尝试获取 Map 
        ThreadLocalMap map = getMap(t);
        // 判断map 是否存在 
        if (map != null) {
            // 根据当前线程获取存在Map的 Entry实体对象,
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 判断实体对象是否存在 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 将value 转成指定类型 
                T result = (T)e.value;
                // 返回结果 
                return result;
            }
        }
        // 如果不存在 则以当前 thread为key 创建map,value 就是initaValue 的初始值。
        return setInitialValue();
    }

    /**
         当map不存在时则初始化ThreadLocalMap 
     */
    private T setInitialValue() {
        // 获取初始化值
        T value = initialValue();
        // 获取当前线程 
        Thread t = Thread.currentThread();
        // 尝试从Map中获取ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        // 判断map 是否存在 
        if (map != null)
            // map 存在则将新的值进行替换操作 
            map.set(this, value);
        else
            // 不存在则创建Map 
            createMap(t, value);
        // 返回初始值 
        return value;
    }

    /**
    将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写此方法,仅依靠该方法 initialValue 来设置线程局部变量的值。
    参数:
    value – 要存储在此线程本地的当前线程副本中的值
     */
    public void set(T value) {
        // 获取当前线程 
        Thread t = Thread.currentThread();
        // 尝试从Map中获取当前线程的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在 
        if (map != null)
            // 存在则进行新值的替换操作 
            map.set(this, value);
        else
            // 不存在的创建当前线程的Map
            createMap(t, value);
    }

    /**
     删除此线程局部变量的当前线程值。如果此线程局部变量随后由当前线程 读取 ,
    则其值将通过调用其方法重新初始化,除非其 initialValue 值在此期间由当前线程 设置 。
    这可能会导致在当前线程中多次调用 initialValue 该方法。
     */
     public void remove() {
         // 获取当前线程map 
         ThreadLocalMap m = getMap(Thread.currentThread());
         // 判断是否存在 
         if (m != null)
             // 存在则调用Map方法进行移除 
             m.remove(this);
     }

    /**
        获取线程的threadLocals 也就是 ThreadLocalMap的值 默认情况是null,只有在用到的时候
    	才会初始化,也就是ThreadLocal中进行初化的。
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
        创建线程和 ThreadLocal进行关联的ThreadLocalMap,也就是初始化
    	Thread的threadLocals 属性
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    /**
  	 用于创建继承线程局部变量映射的工厂方法。设计为仅从线程构造函数调用。
    参数:
    父映射 – 与父线程关联的映射
    返回:
    包含父级的可继承绑定的映射
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    /**
    方法childValue在子类InheritableThreadLocal中明显定义,
    但为了提供createInheritedMap工厂方法而不需要在InheritableThreadLocal中对map类进行子类化,
    因此在这里内部定义。这种技术比在方法中嵌入测试实例的替代方法更可取
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

    /**
         ThreadLocal的一个初始化扩展,支持使用lambda表达式进行初始化查找 
     */
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;
    	// 函数式初始化构造
        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }
    	// 重写了 initiaValue也就是初始化值 为suppler的返回值。
        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

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

        /**
此哈希映射中的条目扩展了 WeakReference,使用其主 ref 字段作为键(始终是 ThreadLocal 对象)。
请注意,空键(即 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;
            }
        }

        /**
         * Map的初始化容量,存在2的幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 表,根据需要调整大小。table.length 必须始终是 2 的幂。
         */
        private Entry[] table;

        /**
         * map中初始化容器大小
         */
        private int size = 0;

        /**
         * 要调整容器大小的下一个大小也就是 
         */
        private int threshold; // Default to 0

        /**
         * 设置调整大小阈值以在最坏情况下保持 2/3 的负载系数。
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
        构造一个最初包含(firstKey,firstValue)的新映射。
		ThreadLocalMaps是懒惰构造的,因此我们仅在至少有一个条目要放入时才创建一个。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 创建一个默认值16的table 
            table = new Entry[INITIAL_CAPACITY];
            // 通过计划hash下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 将值放在指的table中
            table[i] = new Entry(firstKey, firstValue);
            // size 变成1 
            size = 1;
            // 
            setThreshold(INITIAL_CAPACITY);
        }

        /**
    构造一个新映射,包括来自给定父映射的所有可继承线程局部变量。仅由createInheritedMap调用。
    参数:
    父映射 – 与父线程关联的映射
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            // 获取父table 
            Entry[] parentTable = parentMap.table;
            // 获取长度 
            int len = parentTable.length;
            setThreshold(len);
            // 创建一个指定长度的table 
            table = new Entry[len];
            // 遍历进行数据拷贝
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        /**
     获取与密钥关联的条目。此方法本身仅处理快速路径:直接命中现有键。否则,
        它会中继到 getEntryAfterMiss。
        这旨在最大限度地提高直接命中的性能,部分原因是使此方法易于内联。
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 计算出hash值 
            int i = key.threadLocalHashCode & (table.length - 1);
            // 从table中进行获取
            Entry e = table[i];
            // 判断是否不等于null 并 entry 等于 key 类型 
            if (e != null && e.get() == key)
                return e;
            else
                // 在其直接哈希槽中找不到密钥时使用的 getEntry 方法的版本。
                return getEntryAfterMiss(key, i, e);
        }

        /**
 			遍历寻找  在其直接哈希槽中找不到密钥时使用的 getEntry 方法的版本。
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            // 获取当前的table 
            Entry[] tab = table;
            // 获取当前table 的长度 
            int len = tab.length;

            // 判断 e是否不为null 
            while (e != null) {
                // 获取实体的中key 
                ThreadLocal<?> k = e.get();
                // 判断是否是要找到的 
                if (k == key)
                    return e;
                // 判断key 是否为null 如果key 为null 则表表明需要进行清理 
                if (k == null)
                    // 用于清除 ThreadLocalMap 中已经没有引用的 key-value 对,
                    // 即清除 Entry 中的弱引用为 null 的 key-value 对
                    expungeStaleEntry(i);
                else
                    // 计算下一个hash值 
                    i = nextIndex(i, len);
                // 将e变成下一个 
                e = tab[i];
            }
            return null;
        }

        /**
            设置 map 中 key 的值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;
            // 计算出hash值 
            int i = key.threadLocalHashCode & (len-1);
        	// 循环遍历 tab 
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 获取 key 
                ThreadLocal<?> k = e.get();
            	// 判断key 是否要修改的key 
                if (k == key) {
                    // 重新设置value 
                    e.value = value;
                    return;
                }
            	// 如果 key 为null 则 说明该 Entry 已经无效,可以用新的 Entry 替换
                if (k == null) {
                    //用于在 key 被垃圾回收后删除对应的 Entry 并在相同位置插入一个新的 Entry。
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 此时表示没有找到则会新建map 并在链表尾部添加
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 用于在添加新的值时,回收一些出现的entry,避免了内存泄露
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        /**
         * 移除指定key Map 
         */
        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();
                    // 用于清除 ThreadLocalMap 中已经没有引用的 key-value 对,
                    // 即清除 Entry 中的弱引用为 null 的 key-value 对
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

        /**
      当需要添加一个新的键值对时,如果当前 ThreadLocalMap 中没有该键对应的值,但是存在一个已经失效的键值对,
	那么会用新的键值对替换掉失效的键值对。同时,为了避免垃圾收集器释放内存空间而导
	致的频繁增量式哈希(即每次添加元素都需要重新哈希),该方法会把所有失效键值对所
	在的“run”(即在两个 null 插槽之间的一串条目)中的所有失效键值对都删除掉。
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        /**
        清理已经被移除的entry
       通过重新散列位于 staleSlot 和下一个空插槽之间的任何可能冲突的条目来清除过时的条目。
		这还会删除在尾随 null 之前遇到的任何其他过时条目。
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 清除指定entry的 valu 为null 
            tab[staleSlot].value = null;
            // 指定位置的为null 
            tab[staleSlot] = null;
            // table 长度建议 
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 遍历整个table 进行清理操作 
            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 {
                    // 获取key的hash值
                    int h = k.threadLocalHashCode & (len - 1);
                    // 判断key 的hash码变动
                    // 变动可能存在其他key 过期 或者扩容 则需要重新放到指的位置上
                    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;
        }

        /**
             清除被移除的entry的方法
         */
        private boolean cleanSomeSlots(int i, int n) {
            // 设置清除标记 
            boolean removed = false;
            // 获取当前的table 
            Entry[] tab = table;
            // 当前的长度 
            int len = tab.length;
            do {
                // 获取到下一个table下标
                i = nextIndex(i, len);
                // 获取到这个值 
                Entry e = tab[i];
                // 判断是否 e 是否是被清除了,但是没有被完全清理
                if (e != null && e.get() == null) {
                    // 
                    n = len;
                    removed = true;
                    // 调用 expungeStaleEntry 进行清理
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        /**
          进行容器扩容,会先尝试清理过期的entry如果还是不行则会进行扩容操作
         */
            private void rehash() {
            // 清理操作 
            expungeStaleEntries();

            // 判断是否达到需要扩容的值 
            if (size >= threshold - threshold / 4)
                // 扩容
                resize();
        }

        /**
         * 原始容量的2倍进行扩容 
         */
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

        /**
         * 遍历整个 table 如何 通过 expungeStaleEntry 进行清除过期的entry 
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
    }
}

        通过对ThreadLocal的源码可以发现,Thread中的ThreadLocalMap的是懒加载的,是在每次使用时,才会创建,并且是在ThreadLocal中获取时才会创建,也就是说只用线程使用ThreadLocal才会给ThreadLocalMap赋值,并不要的资源浪费。

        并且在ThreadLocal去获取值的时候都通过Thread为可以去ThreadLocalMap中获取获取值,并且不管是在get还是set都会判断key ==null如果等于则表示entry需要被清理因为Entry使用的弱引用在每次垃圾回收时都会断开引用,但是Entry没有被Thread直接引用所需要需要使用清理的方式变内存泄露

        一个线程拥有一个独立的ThreadLocalMapThreadLocalMap中存储的是ThreadLocal,它并没有设计成ThreadLocal中有个 ThreadLocalMap 用来存储不同线程为Entry的原因是因为一个线程可能会有一个ThreadLocal,一个ThreadLocal就是一个Entry所需要需要每个线程都有独立ThreadLocalMap来存放不同的数据副本。

4.InheritableThreadLocal

        从Thread的源码中可以看到它是预留这个的。ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

        InheritableThreadLocal是Java中的一个线程本地变量类,它可以让子线程继承父线程中的线程本地变量。在Java中,每个线程都有自己的线程本地变量,它们是线程私有的,不同线程之间互不干扰。但是,如果一个线程创建了另一个子线程,子线程无法访问父线程中的线程本地变量。这时,就可以使用InheritableThreadLocal来解决这个问题。

        ThreadLocal并不支持线程之间的值传递,因为它没有实现InheritableThreadLocal接口。InheritableThreadLocal是ThreadLocal的一个子类,它可以让子线程继承父线程中的线程本地变量。但是,ThreadLocal并没有实现InheritableThreadLocal接口,所以它不支持线程之间的值传递。

2.三者关系

    ThreadThreadLocal还有ThreadLocalMap之间关系:

 ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为valueEntry对象。

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

        当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为valueEntry往这个ThreadLocalMap中存放。

近似的可以理解为:

   ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocalkey),不过是经过了两层包装的ThreadLocal对象:

         JVM内部维护了线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己的当做key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前线程去Map里面获取,通过这样让每个线程都拥有独立的自己的变量,这样每个线程都有独立的ThreadLocalMap存放着独立的值,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

        ThreadLocal是-个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类,每Thread对象维护着一个ThreadLocalMap的引用ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。

        1)调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLoca对象,值Value是传递进来的对象.

        2) 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为一个key来让线程从ThreadLocalMap获取value.正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

三、ThreadLocal 内存泄露问题

        通过源码可以看出每次不管是get还是set都是做了对内存泄露进行了处理,那么为什么出现内存泄露呢?

        ThreadLocalMap 中的 entry 在使用时会绑定到某个线程上,因此当线程终止后,由于对应的 ThreadLocalMap 被垃圾回收,其中的 entry 对象也变得无法访问,但是却仍占据着内存空间,成为一种内存泄漏。因此,ThreadLocalMap 设计了一个自我清理的机制,使用WeakReference,并且会在每次调用 set() 或 get() 方法时清理过期的 entry,以避免内存泄漏的问题。

        虽然Entry使用的弱引用,但是弱引用只能清理所执行对象,也就是map.entry == key 清理调用了map.key引用,但是没有办法清理引用它的对象,就会导致value一直存在。

        也就是弱引用可以让entry进行回收,但是如果entryvalue是强引用,则value不会被回收。

        清理过期 entry 的机制是这样的:当调用 set() 或 get() 方法时,都会顺便调用 ThreadLocalMap 的 expungeStaleEntry() 方法,该方法会遍历整个表,清理掉所有 key 为 null 的 entry,以及与当前线程不匹配的 entry。这样可以避免线程结束后导致的内存泄漏。

3.1 阿里规范

        阿里强制要求使用remove方法进行回收,避免内存泄露。

还有为什么要使用弱引用?不使用会怎么样?

        ThreadLocalMap 中的 key 使用了弱引用,是为了防止内存泄漏。如果 ThreadLocalMap 的 key 使用的是强引用,当 ThreadLocal 无法访问时,ThreadLocalMap 中的 entry 却仍然存在,entry 中的对象也无法被垃圾回收,这就会导致内存泄漏。使用弱引用作为 key,当 ThreadLocal 对象不再被引用时,entry 中的 key 就会被回收,这时 ThreadLocalMap 中的 entry 也就变得无效了,对应的 value 对象也可以被垃圾回收。

        如果不使用弱引用,而是使用强引用,当 ThreadLocal 不再被使用时,ThreadLocalMap 中的 entry 仍然会存在,这会导致 entry 中的对象无法被回收,从而导致内存泄漏。

3.2 强引用、软引用、弱引用、虚引用分别是什么?

        默认创建对象引用就是Reference也就强引用。

        Java 技术允许使用 finalize()Object中 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法一般是有JVM虚拟机的人员编写,目的是在对象被不可撤销地丢弃之前执行清理操作。

1. 强引用(默认支持模式)

        当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收

        强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

        对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强) 引用赋值为 null,般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。        

2. 软引用

        软引用是一种相对强引用弱化了一些的引用,需要用iava.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。对于只有软引用的对象来说,

        当系统内存充足时它不会被回收,当系统内存不足时它被回收

        软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

3. 弱引用

        弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的在存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存

软引用和弱引用的应用场景:

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中又可能造成内存溢出。

此时使用软引用可以解决这个问题。

        设计思路是: 用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

  Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>():

4.虚引用

 

 1. 虚引用必须和引用队列(ReferenceQueue)联合使用

虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用

2.PhantomReference的get方法总是返回null

虚引用的主要作用是跟踪对象被垃圾回收的状态仅仅是提供了一种确保对象被 finalize以后,做某些事情的通知机制PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

3.处理监控通知使用

换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作

5. 引用总结

3.3 内存泄露原因

1.原因

        ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

(1) 第一层包装是使用 WeakReference<ThreadLoca?>>将ThreadLocal对象变成一个弱引用的对象;

(2) 第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLoca<?>>

        ThreadLocalMap 中的 entry 在使用时会绑定到某个线程上,因此当线程终止后,由于对应的 ThreadLocalMap 被垃圾回收,其中的 entry 对象也变得无法访问,但是却仍占据着内存空间,成为一种内存泄漏

        弱引用可以让entry回收,但是value没有被回收。在ThreadLocalMap中,Entry的key是ThreadLocal对象,而value则是存储在ThreadLocal对象的一个变量中。由于ThreadLocal对象是强引用,所以只有在ThreadLocal对象被回收时,才会使ThreadLocalMap中对应的Entry变得不可达。但是,由于value是被ThreadLocal对象持有的,所以即使Entry变得不可达,value也不会被回收,可能会导致内存泄漏。因此,需要在合适的时机手动清理无用的Entry,以及对应的value,防止内存泄漏。

四、源码分析

4.1 为什么要用弱引用?不用如何?

public void function1 (){
    ThreadLocal<String> tl = new ThreadLocal();  // line1 
    tl.set("xxx");// line2 
    tl.get();// line3 
}

  • line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
  • line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象.

4.2 为什么源代码用弱引用?

        当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷,第2个坑后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null

         在使用set、get、remove,会尝试删除key为的etry,可以释放对象所占用的内存。

4.3 清理脏Entry

        可以通过源码发现 在使用set、get、remove,会尝试删除key为的etry,可以释放对象所占用的内存。

        1.当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当treadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threacLocal实例就没有任何一条路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Ertry,就没有办法访问这些key为null的Entry的value.如果当前线程再迟迟不结束的话,这些key为nul的Enty的value就会一直存在一条强引用链: Thread Ref-> Thread -> ThreaLocalMap -> Entiy->value永远无法回收,造成内存泄漏

        2.当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收;

        3.但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool0时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心。

1. key为null的entry原理分析

        ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链

        虽然弱引用,保证了key指向的ThreadLoca对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现kev为nul时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露我们要在不使用某ThreadLocal对象后,手动调Hremoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

3. 结论 

        从前面的set;getEntry,remove方法看出,针对threadLocal存在的内存泄漏的问题在threadLocal的生命周期里都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

五、总结

        ThreadLocal是可以使用web环境下的,但是存在一些问题需要处理。

        因为在web的环境下,每一个请求都是一个线程处理,所以存在无法获取上次请求的ThreadLocal,解决方式可以通过借助其他工具如redis,将需要使用数据放入到redis中,每次请求都取出来放入到ThreadLocal中使用,来方便本次请求的调用,需要使用使用完成之后需要清理,避免内存溢出。

5.1 最佳实践

  • 1.ThreadLocal.withIntial(() ->初始化值);
  • 2.建议把ThreadLocal修饰为static
    • ThreadLocal能实现了线程的数据隔离所以,ThreadLocal可以只初始化一次,不在于它自己本身,而在于Thread的ThreadLocalMap只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。
  • 3.用完记得手动remove。

5.2 总结  

  1. ThreadLocal 并不解决线程间共享数据的问题。
  2. ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景。
  3. ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。
  4. 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
  5. ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题。
  6. 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry对象的值(即为具体实例) 以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法。

六、扩展

    TransmittableThreadLocal 是阿里提供的,可以更好的对ThreadLocal进行升级,本章简单介绍下web环境下使用流程。

本文标签: ThreadLocal