专栏首页JavaEE谈谈你对ThreadLocal的理解

谈谈你对ThreadLocal的理解

1. 你知道ThreadLocal是什么吗?

简单地说,就是用来隔离数据的。用ThreadLocal来保存的数据,只对当前线程生效,当前线程对该数据做的任何操作,对别的线程是不生效的。举个栗子一看便知:

public class TestThreadLocal {

    private static User user = new User("jerry", "123"));

    public void fun(User user){
        System.out.println(Thread.currentThread().getName() + "开始执行,修改前的username[" + user.getUsername() + "]");
        user.setUsername("tom");
        System.out.println(Thread.currentThread().getName() + "执行完毕,修改后的username[" + user.getUsername() + "]");
    }

    public static void main(String[] args) {
        TestThreadLocal testThreadLocal = new TestThreadLocal();
        new Thread(() -> {
            testThreadLocal.fun(user);
        }, "线程A").start();

        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            testThreadLocal.fun(user);
        }, "线程B").start();
    }
}

这段代码很简单,就是有个user,初始的username为jerry,然后线程A将其改名为tom,3秒钟后,线程B再去读取user的username。因为线程A将起改为tom了,所以线程B开始执行时读取到的应该是tom。看执行结果:

执行结果

分析得没错,线程B开始执行时读取到的确实是tom,说明线程A的修改对线程B生效了。假如用ThreadLocal,结果就不是这样了。

怎么用呢?首先将上述代码中的:

private static User user = new User("jerry", "123"));

改成:

private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));

然后将testThreadLocal.fun(user);改成testThreadLocal.fun(user.get());,这样就可以了,再次执行,结果如下:

执行结果

2. 说说ThreadLocal的常用方法

  • set(T t):将值绑定到当前线程中。注意是当前线程,比如你在main线程中set值,再另起两个线程去get,是取不到的;
  • get():当前线程从ThreadLocal中取出自己的副本;

假如你定义了一个静态变量:

private static ThreadLocal<User> threadLocal = new ThreadLocal<>();

那么你必须在线程A和线程B中分别进行set,而不能在主线程中进行set,如下:

new Thread(() -> {
    threadLocal.set(new User("jerry", "123"));
    testThreadLocal.fun(threadLocal.get());
}, "线程A").start();

new Thread(() -> {
    threadLocal.set(new User("jerry", "123"));
    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    testThreadLocal.fun(threadLocal.get());
}, "线程B").start();

jdk8提供了新的构造方式,即开篇中用到的那种方式:

private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));

这样就不用在每个线程中进行set了,在这里初始化一次,每个线程get时都会取出属于自己的副本。

3. 对ThreadLocal的应用场景有了解吗?

  • 很多框架中用到了ThreadLocal,比如Spring中,用ThreadLocal来保存数据库连接,这样可以保证单个线程的操作使用的是同一个数据库连接;
  • 可以用ThreadLocal来做session、cookie的隔离;
  • 最经典的一个,SimpleDataFormat调用parse格式化时间的时候,parse方法会先调用Calendar.clear(),再调用Calendar.add(),如果一个线程调用了调用完add,在准备继续parse的时候,另一个线程clear掉了,这就出问题了,所以可以用ThreadLocal;
  • 还有一个就是可以用来传参数,比如你多个方法都要对user对象进行操作,并且有些方法是第三方jar的,不能把user当成参数传过去,那么就可以将user装到ThreadLocal中,要用的时候get出来即可。

4. ThreadLocal的底层原理你造不?

来看看ThreadLocal的set和get方法就秒懂了:

public void set(T value) {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value);
}

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

看到这里是不是就一目了然呢,其实ThreadLocal里面有个ThreadLocalMap,把当前线程作为key,就可以拿到这个线程专属的ThreadLocalMap,所以说每个线程都有一个ThreadLocalMap,拿到了这个ThreadLocalMap后(如果拿到的不为空,就用这个,为空就创建一个,保证每个线程只有一个),将当前ThreadLocal绑定的值设置到map中;get的时候就将map的entry.value返回,就是我们存入的对象啦。

5. 那你对ThreadLocalMap了解过吗?

上面说了,每个线程只对应一个ThreadLocalMap,但是一个线程可以有很多个ThreadLocal来保存不同的对象,那么ThreadLocalMap怎么来保存这多个ThreadLocal呢?那就是用你数组!源码中有这么一个数组:

private Entry[] table;

这个就是用来保存ThreadLocal的。怎么判断当前新加入的ThreadLocal放在数组的哪个位置呢?索引怎么计算出来?ThreadLocalMap会利用usafe类计算出一个threadLocalHashCode,然后再根据:

int i = key.threadLocalHashCode & (len-1)

计算出来的i就是存放当前ThreadLocal的索引。

6. 你用ThreadLocal遇到过什么问题吗?

首先来看看ThreadLocalMap中的这段代码:

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

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

ThreadLocalMap中的key,也就是ThreadLocal对象,被设计成弱引用了,所以在外部没有强引用key的时候,key会被垃圾回收清理掉,ThreadLocalMap中就会出现key为null的Entry,永远无法被GC回收,从而造成内存泄漏。为了避免这个问题,用完ThreadLocal最好调用一下remove方法,手动清除掉。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ​day019: 谈谈你对BigInt的理解。

    这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-900719925474099...

    用户3806669
  • 面试杂谈 - 谈谈你对GC的理解

    接下来就对GC做一个全方位的总结,希望下次可以自信地回答面试官:我是可以被贵公司回收的那种。

    acupt
  • day016: 谈谈你对闭包的理解

    首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————全局作用域和函数作用域, 当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有...

    用户3806669
  • 来谈一谈你对安全的理解

    在不同的学习阶段以及不同安全岗位对于安全的理解是不一样的,知识星球新推出一个作业功能,我在知识星球提出了一个作业也就是一个问题,问题如下:

    信安之路
  • day017: 谈谈你对原型链的理解

    在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。

    用户3806669
  • day034: 谈谈你对JS中this的理解。

    onclick和addEventerListener中 this 默认指向绑定事件的元素。

    用户3806669
  • 你好,谈谈你对前端路由的理解

    作者:尼克陈 https://juejin.cn/post/6917523941435113486

    用户4456933
  • 谈谈java的ThreadLocal

    简单介绍 ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过Thread...

    Spark学习技巧
  • 谈谈你对堆栈理解(初稿)

    为了理解堆栈区别, 我对比 c++,java,APP,javascipt(vue,v8) ,node.js, solidity, 都提到一个共同概念-虚拟机....

    程序员小王

扫码关注云+社区

领取腾讯云代金券