ThreadLocal 简介

ThreadLocal是Java中用于解决线程安全问题的工具,它为每个线程提供独立的变量副本,避免并发冲突。本文从ThreadLocal的作用、源码解析到面试相关问题,详细介绍了ThreadLocal的工作原理和可能的内存泄露问题,以及如何通过正确使用remove方法来避免内存泄露。
摘要由CSDN通过智能技术生成

ThreadLocal 是什么

在百度百科上的注释是:

ThreadLocal是Thread的局部变量,用于编多线程程序,对解决多线程程序的并发问题有一定的启示作用。

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

在这里插入图片描述

ThreadLocal 作用

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

从ThreadLocal 源码看 Thread实现原理

ThreadLocal 类

将 ThreadLocal 类整体的构成如下:
在这里插入图片描述
略微简化下:

public class ThreadLocal<T> {
	/*
	* 用来获取ThreadLocal在当前线程中保存的变量副本
	*/
    public T get() {
	...
	}
	/*
	* 用来设置当前线程中变量的副本
	*/
	public void set() {
	...
	}

	/*
	* 用来移除当前线程中变量的副本
	*/
	public void remove() {
	...
	}
	/*
	* 用来初始化当前线程中变量的副本,默认返回为null,当线程首次调用get()
	* 方法前会执行,除非该线程已经执行过了set()方法,那么initialValue()
	* 方法就不会执行。
	* 可以通过重写该方法来实现ThreradLocal的默认初始化。
	*/
	protected T initialValue() {
		return null;
	}
}

set()源码

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
     /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

从源码中可以看到set()方法首先是去获取了线程,然后从当前线程中获取了类型为ThreadLocalMap的threadLocals变量。如果不存在则创建改变量,如果存在就将该变量TreadLocal 设置成 map 的key。
这样就实现了变量对线程的绑定,并且通过map可以实现多个ThreadLocal变量。
值得注意的是ThreadLocalMap中Entry的继承了WeakReference,并将key值设置成了弱引用。(回顾一下,java有强软弱虚四种引用)
这里其实可以引申出来很多相关问题,此处暂且不表,统一在文末讲述。


    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

get() 源码

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

经过set()方法的解释,再看get()方法就相对较为简单。需要注意的是在get()方法之前,需要先进行set(),否则会执行setInitialValue()方法进行初始化,可以通过initialValue()方法来定制所需要的初始值。

remove() 源码

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

使用remove方法可以把ThreadLocal进行移除

ThreadLocal 面试相关问题

ThreadLocal 为什么要使用弱引用

在这里插入图片描述
如图所示:
ThreadLocalMap 的引用被当前线程所持有,当线程不结束的情况下,Map永远不会被释放,例如使用了线程池中的线程。因此,为了避免生成内存泄露的情况,把Entry的key的引用设置成了弱引用,如虚线2所示。当ThreadLocal在被用户正常使用时,ThreadLocal被实线1强引用,因此不会被GC回收,当ThreadLocal被显式置空,此时当发生GC时,ThreadLocal就会被GC回收,此时Entry的key指向null,value 不变,为了f防止内存泄露,ThreadLocalMap就可以根据key == null 这一条件对现有的Map内容进行回收。
在对ThreadLocalMap进行get、set方法时,都会执行ExpungeStaleEntry方法进行资源回收,其源码如下:

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        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;

                        // 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;
        }

ThreadLocal 为什么可能出现的内存泄露

既然ThreadLocalMap 已经在set()、get()方法中已经做了对内存的回收,那为什么还会说ThreadLcoal 还是可能会出现内存泄露呢。
原因在于如果线程池中的一个线程在使用过ThreadLocal一次之后线程一直没有在被使用,或者分配了之后不再调用set和get方法,那么这个期间就会发生真正的内存泄露。

怎么避免ThreadLocal

在使用完毕之后,尽量使用remove方法把ThreadLocal进行移除。

参考资料:
百度百科
https://www.jianshu.com/p/6fc3bba12f38
http://www.threadlocal.cn/#threadlocal1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值