ThreadLocal及其所带来的安全问题

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言


一、初步认识ThreadLocal

官方解释如下:

This class provides thread-local variables.  These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable.  {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).

顾名思义:这个类提供线程局部变量,即每个线程访问一个(通过其* {@code get}或{@code set}方法)有自己的独立初始化变量的副本.

了解TeadLocal的概念是不够的,我们应该站在设计者的角度去探寻他的构造,才能更清晰的去去认识ThreadLocal.

  1. ThreadLocalMap
//从其实现来看,是类似HashMap容器的一种基于Key,value的容器
//特殊的是,它继承了弱引用,这也是引发内存泄漏的原因所在
//区别强引用,软引用,弱引用,虚引用的关键在于,他们面对gc时的态度,(弱引用,在gc发现弱引用后,会立即回收,生命周期更短)
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(ThreadLocal<?> firstKey, Object firstValue) {
			//声明一个初始长度为16的table
            table = new Entry[INITIAL_CAPACITY];
            //threadLocalMap的hashCode与(16-1)取模,得到存放位置的容器下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置扩容阈值
            setThreshold(INITIAL_CAPACITY);
        }
  1. Thread
public
class Thread implements Runnable {
	//隶属于该线程的thradLcoal
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  1. ThreadLocal类在这里插入图片描述

总而言之:每一个Thread内部都维护了ThreadLocal.ThreadLocalMap这么一个对象,ThreadLocal则使用TreadLocalMap来存放当前线程的变量副本.

二、ThreadLocal使用

既然对ThreadLocal的概念有了初步的了解,那我们来看看他重要的API和实现.
日常开发中,ThreadLocal的使用其实很简单:

ThreadLocal<T> threadLocal = new ThreadLocal<>();
        	   threadLocal.set();
               threadLocal.get();
               //remove方法很重要
               threadLocal.remove();

再看下其方法实现:

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

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

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 getMap(Thread t) {
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
private void set(ThreadLocal<?> key, Object value) {

            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();
				//key值相同,覆盖原value
                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();
        }

remove方法

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     
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) {
                /就是reference指向null,也就是将该entry的key指向null。方便后面对该entry进行清理
                //Entry extends WeakReference  extends  Reference
                    e.clear();
                   // 清理staleSlot位置上的entry,继而staleSlot之后的key为null的entry都清理了,顺带将一些有哈希冲突的entry给填充回可用的index中。
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

#思考引发的安全问题
那么问题来了,ThreadLocal的使用非常简单,但是它作为并发工具类中一员,那么必然会存在并发安全的风险.

  1. 内存泄漏
    ThreadLocal自身并不储存值,而是作为一个key来让线程从ThreadLocal获取value.Entry是中的key是弱引用,所以当gc时弱引用对象必然被回收,ThreadLocal必然会被回收.但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了ThreadLocal对象)使用了又不再调用get/set方法,就可能引发内存泄漏.

    这就需要我们在使用完ThreadLocal后,需要显式调用remove方法去删除.

  2. 池化技术的线程复用导致数据错乱
    在之前我们也看到,ThreadLocal是真对线程自身的变量副本,那么问题来了,如果线程重复使用,那是不是有并发安全的风险呢?
    线程复用,顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值.
    此时我们假设这么一种场景,我们把tomcat线程池参数设置为1,在使用ThreadLocal,连续发送两个HTTP请求,此时我们从ThreadLocal中获取的值,肯定是相等的.所以,我们在使用时,应该在finally中显示的清除ThreadLocal中数据.

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值