ThreadLocal深入解析

ThreadLocal是什么

ThreadLocal 提供了线程私有的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。当一个线程结束时,所有 ThreadLocal 中标记为该线程所拥有的实例副本都会被回收。

 

应用场景

ThreadLocal 适用于每个线程需要自己独立的实例,且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

  • 线程间隔离

  • 方法间共享
     

使用示例

一个最简单的使用示例如下:

public class ThreadLocalTest {

     private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();

    public void setThreadLocal(String value) {
        threadLocalString.set(value);
    }

    public String getThreadLocal() {
        return threadLocalString.get();
    }


    public static void main(String[] args) {

        final ThreadLocalTest test = new ThreadLocalTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String s1 = "s1";
                test.setThreadLocal(s1);
                try {
                    Thread.sleep(1000);//加一秒睡眠是为了让下面那个线程先把赋值操作做完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(test.getThreadLocal());
            }
        },"t1").start();
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                String s2 = "s2";
                test.setThreadLocal(s2);
                System.out.println(test.getThreadLocal());
            }
        },"t2").start();
    }

}

打印结果:

s2
s1

 

ThreadLocal在JDK8中的实现

静态内部类Entry

ThreadLocal 类,ThreadLocalMap类,Entry类之间的关系:

public class ThreadLocal<T> {
	static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
        }
   }
}

ThreadLocal 类的静态内部类 ThreadLocalMap的实例会维护某个 ThreadLocal 与具体实例的映射。

与我们平常理解的map不同的是,ThreadLocalMap 的每个 Entry 都是一个对键(就是下文代码中的ThreadLocal<?> k)的弱引用
另外,每个 Entry 都包含对值(Object v)的强引用。

什么叫弱引用、强引用?传送门:一文读懂Java四种引用类型

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

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

使用弱引用的原因在于,当没有强引用指向ThreadLocal<?> k时,它可被回收,从而避免它不能被回收而造成的内存泄漏的问题。

但是,这里又可能出现另外一种内存泄漏的问题。

ThreadLocalMap 维护ThreadLocal 变量与具体实例(ThreadLocal<?> kObject v)的映射关系,当 ThreadLocal<?> k被回收后,这个映射关系的键变为 null,但 Entry 无法被移除。

Entiry没有被移除意味着什么?就是实例Object v被该 Entry 引用而无法被回收造成内存泄漏。(上文代码中可以看到Object value是Entiry类下的一个属性,所以自然有Entry引用Object value

那么这个问题是如何解决的?别着急,我们一步一步看。
 

读取实例

public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      T result = (T)e.value;
      return result;
    }
  }
  return setInitialValue();
}

读取实例时,线程首先通过getMap(t)方法获取自己的 ThreadLocalMap。

从下文中getMap(Thread t)方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段threadLocals

也就是由 Thread 维护 ThreadLocal 对象与具体实例的映射。

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

获取到 ThreadLocalMap 后,通过上文代码中的map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。

该方法中的 this 即当前访问的 ThreadLocal 对象。

如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。

如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。
 

设置初始值

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

首先,通过initialValue()方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。
然后通过getMap(t)拿到该线程对应的 ThreadLocalMap 对象。
若该对象不为 null,则直接将该 ThreadLocal 对象this与对应实例初始值value的映射添加进该线程的 ThreadLocalMap中。
若为 null,则先创建该 ThreadLocalMap 对象再将映射添加到其中。
 

设置实例

除了通过initialValue()方法设置初始值,还可通过 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);
}

通过getMap(t)获取该线程的 ThreadLocalMap 对象后,直接将该 ThreadLocal 对象this与对应实例value的映射添加进该线程的 ThreadLocalMap中。后面的if-else与上文“设置初始值”中同理。
 

防止内存泄漏

针对上文提到的:“实例Object v被该 Entry 引用而无法被回收造成内存泄漏”,是怎么解决的呢?
其实很简单。在ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。

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();
    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();
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值