ThreadLocal详解

前言

ThreadLocal工作中会偶有用到,是解决thread间的数据隔离问题的(并不是为解决并发和共享问题的),也是面试常见问题,比如:ThreadLocal知道吗?说说你自己的理解?或者这样问:在多线程环境下,如何防止自己的变量被其它线程篡改?无论基于哪种原因都是很有必要学习的。

ThreadLocal是什么

贴一段源码中的介绍:这个类提供线程的局部变量,可以通过get()和set()方法来获取和设置自己的局部变量;ThreadLocal实例通常是pricate static fields的,希望将信息关联到一个线程中,例如:user ID、Transaction ID

概括的说:存储各个线程互不相同的信息,实现线程间的数据隔离

/**
 * 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).
 */

ThreadLocal能做什么

  1. 实例1:参数传递(用户信息传递)

    当前用户信息需要被线程内的所有方法共享

    • 方案1:传递参数

      将user作为参数在每个方法中进行传递,缺点:会产生代码冗余问题,并且可维护性差

    • 方案2:使用Map

      缺点:如果在单线程环境下可以保证安全,但是在多线程环境下是不可以的。如果使用加锁或是ConcurrentHashMap都会产生性能问题。

    • 方案3:使用ThreadLocal,实现不同方法间的资源共享

public class ThreadLocalNormalUsage02 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("张三");
        //将User对象存储到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名: " + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名: " + user.name);
    }
}


class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

结果:

总结:实际项目中也会用到这个实例,在请求的拦截器中添加用户信息到ThreadLocal< User >的ThreadLocalMap中,这样同一线程中的各个方法或组建就可以获取到对应的用户信息。

  1. 实例2:典型工具类(SimpleDateFormat和Random)
class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        //创建一份 SimpleDateFormat 对象
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

ThreadLocal为每个线程创建独立的SimpleDateFormat对象(实际应用中,针对不通的format形式,会创建不同的方法,方法中会是一个新SimpleDateFormat对象),当然也可以使用LocalDateTime。
注:可参考这篇博客——地址

ThreadLocal原理

  • 首先看set()方法:
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取ThreadLocalMap,这个map是什么那?下面有介绍
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //如果map存在,则以当前线程t为key,数据为value放到map
        map.set(this, value);
    else
        //否则创建新的map再存放数据
        createMap(t, value);
}

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

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

再来看一下ThreadLocalMap

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;
    }
    //map的其他定义
}

ThreadLocalMap实际上是ThreadLocal的一个静态内部类,数据就存放在Entry中;

那这个map如何从Thread中获取的那?看下面getMap()和Thread类中的代码

//ThreadLocal中的getMap方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * Thread的threadLocals
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

实际上Thread类中维护了一个ThreadLocalMap变量。

如果map不存在,则创建一个,源码如下:

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 大致了解set的过程,再来看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() {
    //获取线程对象t
    Thread t = Thread.currentThread();
    //在对象t中获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //从Entry中获取数据
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

如果没有获取map(可能没有set值),则初始化value值

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

实际上value是null。

  • 删除数据:
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;
        }
    }
}
  • 总结:ThreadLocal实现数据隔离主要是依赖ThreadLocalMap,而每一个线程中都有一个ThreadLocal.ThreadLocalMap变量,这样在set和get数据时,都是获取对应线程中的ThreadLocalMap,数据则存在map里面的Entry中,key为Threalocal对象。

关于ThreadLocal内存泄漏

  • 内存泄漏的原因:

    • 个人认为ThreadLocal内存泄漏只是有可能,是个小概率事件
    • 内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回
     static class ThreadLocalMap {
         static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
     
             Entry(ThreadLocal<?> k, Object v) {
                 //调用父类,父类是一个弱引用
                 super(k);
                 //强引用
                 value = v;
             }
     }
    
    • 强引用:当内存不足时触发GC,宁愿抛出OOM也不会回收强引用的内存

    • 弱引用:触发GC后便会回收弱引用的内存

    • 分析:

      • 正常情况:当Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了
      • 非正常情况:当Thread一直在运行始终不结束,强引用就不会被回收,存在以下调用链——Thread–>ThreadLocalMap–>Entry(key为null)–>value;因为调用链中的 value 和 Thread,存在强引用,所以value无法被回收,就有可能出现OOM。

      JDK的设计已经考虑到了这个问题,所以在set()、remove()、resize()方法中会扫描到key为null的Entry,并且把对应的value设置为null,这样value对象就可以被回收。

  • 如何避免内存泄漏:

    调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。

NPE问题

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public Long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        //如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
        System.out.println("out:" + threadLocalNPE.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println("inner:" + threadLocalNPE.get());
            }
        }).start();
    }
}

上面例子出现NPE问题,主要是get方法返回值类型,我们知道在未对ThreadLocalMap进行set值时,如果直接获取,代码会给我们初始化一个null保存到map中;如果上面get方法返回值是基本数据类型,则会出现装箱和拆箱,导致NPE。

共享数据问题

如果在每个线程中ThreadLocal.set()进去的东西本来就是多个线程共享的同一对象,比如static对象,那么多个线程调用ThreadLocal.get()获取的内容还是同一个对象,还是会发生线程安全问题

总结

可以不使用ThreadLocal就不要强行使用:如果在任务数很少的时候,在局部方法中创建对象就可以解决问题,这样就不需要使用ThreadLocal。

优先使用框架的支持,而不是自己创造:例如在Spring框架中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。

参考博文:

https://juejin.im/post/5e0d8765f265da5d332cde44#heading-5
https://juejin.im/post/5ac2eb52518825555e5e06ee#heading-4
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-13
https://www.jianshu.com/p/6bf1adb775e0
https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/lufeng20/article/details/24314381?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/377bb840802f

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值