ThreadLocal 作用

在并发编程中时常有这样一种需求:每条线程都需要存取一个同名变量,但每条线程中该变量的值均不相同。

如果是你,该如何实现上述功能?常规的思路如下:
使用一个线程共享的Map,Map中的key为线程对象,value即为需要存储的值。那么,我们只需要通过map.get(Thread.currentThread())即可获取本线程中该变量的值。

这种方式确实可以实现我们的需求,但它有何缺点呢?——答案就是:需要同步,效率低!

由于这个map对象需要被所有线程共享,因此需要加锁来保证线程安全性。当然我们可以使用java.util.concurrent.*包下的ConcurrentHashMap提高并发效率,但这种方法只能降低锁的粒度,不能从根本上避免同步锁。而JDK提供的ThreadLocal就能很好地解决这一问题。下面来看看ThreadLocal是如何高效地实现这一需求的。

ThreadLocal 是什么

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

ThreadLocal 解析

ThreadLocal的内部结构图

threadLocal
threadLocal

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

每个Thread线程内部都有一个Map。

Map里面存储线程本地对象(key)和线程的变量副本(value)

但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

Thread线程内部的Map在类中描述如下:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal类提供如下几个核心方法:

  • get()方法用于获取当前线程的副本变量值。
  • set()方法用于保存当前线程的副本变量值。
  • initialValue()为当前线程初始副本变量值。
  • remove()方法移除当前前程的副本变量值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* 返回当前线程的副本变量
* 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() {
Thread t = Thread.currentThread();
// 返回Thread 对象中的ThreadLocalMap
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;
}
}
// 如果没有值的话调用默认setInitialValue()方法
return setInitialValue();
}
/**
* 存放在Thread的 threadLocals
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 设置初始值
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial 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;
}
// map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
protected T initialValue() {
return null;
}
/**
* 赋值
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

ThreadLocalMap

  • ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static class ThreadLocalMap {
    /**
    * 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象
    *
    * 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) {
    // Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
    super(k);
    value = v;
    }
    }
    }

ThreadLocalMap的问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

Java的四种引用方式

  • Java的数据类型分为两类:基本数据类型、引用数据类型。

    • 基本数据类型的值存储在栈内存中
    • 引用数据类型需要开辟两块存储空间,一块在堆内存中,用于存储该类型的对象;另一块在栈内存中,用于存储堆内存中该对象的引用。
  • Java对象的引用类型包括: 强引用,软引用,弱引用,虚引用

  • Java中提供这四种引用类型主要有两个目的:

    • 第一是可以让程序员通过代码的方式决定某些对象的生命周期,随着 java.lang.ref这个包下的类的引进,程序员拥有了一点点控制你创建的对象何时释放,销毁的权利
    • 第二是有利于JVM进行垃圾回收

      强引用

  • 强引用,就是我们最常见的普通对象引用,我们 new 出来的对象就是强引用,只要尚且存在强引用指向某一个对象,那就能表明该对象还存活,GC 不能去回收这种对象。需要回收强引用指向的对象,可以等待超出引用区域,或者是显式设置对象为 null,就可以通知让 GC 回收,当然实际的回收时间要看 GC 策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
// 创建一个对象,new出来的对象都是分配在java堆中的
Sample sample = new Sample(); // sample这个引用就是强引用

sample = null; // 将这个引用指向空指针,
// 那么上面那个刚new来的对象就没用任何其它有效的引用指向它了
// 也就说该对象对于垃圾收集器是符合条件的
// 因此在接下来某个时间点 GC进行收集动作的时候, 该对象将会被销毁,内存被释放
}
}
class Sample {
}
  • 也可以画个简单的图理解一下:

软引用

  • 当内存资源充足的时候,垃圾回收器不会回收软引用对应的对象的内存空间;但当内存资源紧张时,软引用所对应的对象就会被垃圾回收器回收。

  • 软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。 SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

  • 也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
// 创建一个对象,new出来的对象都是分配在java堆中的
Sample sample = new Sample(); //sample这个引用就是强引用
// 创建一个软引用指向这个对象 那么此时就有两个引用指向Sample对象
SoftReference<Sample> softRef = new SoftReference<Sample>(sample);

// 将强引用指向空指针 那么此时只有一个软引用指向Sample对象
// 注意:softRef这个引用也是强引用,它是指向SoftReference这个对象的
// 那么这个软引用在哪呢? 可以跟一下java.lang.Reference的源码
// private T referent; 这个才是软引用, 只被jvm使用
sample = null;

// 可以重新获得Sample对象,并用一个强引用指向它
sample = softRef.get();
}
}
class Sample {
}
  • 利用软引用解决OOM问题
    • 下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

      public void addBitmapToCache(String path) {
      // 强引用的Bitmap对象
      Bitmap bitmap = BitmapFactory.decodeFile(path);

      // 软引用的Bitmap对象
      SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);

      // 添加该对象到Map中使其缓存
      imageCache.put(path, softBitmap);
      }
      public Bitmap getBitmapByPath(String path) {
      // 从缓存中取软引用的Bitmap对象
      SoftReference<Bitmap> softBitmap = imageCache.get(path);

      // 判断是否存在软引用
      if (softBitmap == null) {
      return null;
      }
      // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
      Bitmap bitmap = softBitmap.get();
      return bitmap;
      }

弱引用

  • 弱引用会被Jvm忽略,也就说在GC进行垃圾收集的时候,如果一个对象只有弱引用指向它,那么和没有引用指向它是一样的效果,jvm都会对它就行果断的销毁,释放内存。
  • 那么,ThreadLocalMap中的key使用弱引用的原因也是如此。当一条线程中的ThreadLocal对象使用完毕,没有强引用指向它的时候,垃圾收集器就会自动回收这个Key,从而达到节约内存的目的。
    1
    WeakReference<Person> wr = new WeakReference<Person>(new Person());

虚引用

1
2
3
虚引用等于没有引用,无法通过虚引用访问其对应的对象。

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

如何避免泄漏

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal 应用场景

Hibernate的session获取场景:每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();

// 获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
// 判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session == null && !session.isOpen()){
if(sessionFactory == null){
// 创建Hibernate的SessionFactory
rbuildSessionFactory();
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}

return session;
}

总结:

  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
  • 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。

参考