谈谈你对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方法,手动清除掉。

推荐阅读更多精彩内容