threadlocal

ThreadLocal是什么?

ThreadLocal类提供线程局部变量。这些变量与普通的对应变量的不同之处在于:每个访问一个变量的线程(通过它的get或set方法)都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,它们希望将状态与线程相关联(例如,用户ID或事务ID)。

只要线程是存活的,并且ThreadLocal实例是可访问的,每个线程都持有对其线程局部变量副本的隐式引用;在一个线程消失后,它的所有线程本地实例副本都要服从垃圾收集(除非存在对这些副本的其他引用)。

ThreadLocal.set()方法整体流程如下:

  1. 每次进入set()方法,都会调用Thread.currentThread()方法获取到当前的线程对象;
  2. 然后调用 getMap(t)获取当前线程中的ThreadLocalMap对象,通过源码调试,我们知道ThreadLocalMap类型的threadLocals引用位于java.lang.Thread类中,这样对可以断定,每个Thread都持有一个ThreadLocalMap;
  3. getMap(t)后会判断结果是否为空,非空则调用ThreadLocalMap.set()方法,空则调用createMap()用线程和值创建新的ThreadLocalMap对象;
  4. ThreadLocalMap是ThreadLocal的一个静态内部类,ThreadLocalMap内部再封装一个静态的内部Entry类来存放值,Entry继承WeakReference弱引用,所以最后数据存入一个table数组,数组的元素对象有两个字段,refrent==threadLocal,value==的object;
  5. ThreadLocalMap.set()方法,先从Entry[]数组中查询当前key对应的引用,如果存在则返回,如果不存在,则清除replaceStaleEntry(key, value, i),处理内存泄露问题;
  6. createMap()底层维护的是一个Entry[],来存放ThreadLocal和value;
  7. 从set()中也可以看出ThreadLocalMap是惰性构造的,所以我们只有在至少有一个条目要放进去时才创建一个ThreadLocalMaps。

1.ThreadLocal底层数据结构

Thread 类的内部定义了两个 ThreadLocalMap,ThreadLocalMap 是存储 ThreadLocal 的容器,一个是此线程私有的 threadLocals ,一个是可继承父类 ThreadLocal 的inheritableThreadLocals;

其定义了一个静态内部类 ThreadLocalMap,我们可以把 ThreadLocalMap 理解为线程存储私有数据的容器。存取数据时调用的是 ThreadLocalMap 类的 get、set 方法。

ThreadLocalMap 中定义了一个 Entry 数组,该数组存放一个个 Entry 对象;Entry 对象是以 ThreadLocal 作为 key,任意类型作为 value 的一种键值对。

一个 Thread 类可以有多个 ThreadLocal 类,一个 Thread 类拥有一个 ThreadLocalMap,该集合并没有实现 map 接口,底层数据结构是数组;键值对Entry(ThreadLocal<?> k, Object v)(多个 ThreadLocal)都存储在 table 数组中,就像Map [{id:1},{name:”张三”},{age:20}]。

该数据结构是为了让每个线程可以关联多个 ThreadLocal 变量。这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。

2.如何保证线程安全

set方法:

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

get方法:

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

可以看到ThreadLocal操作值的时候是取得当前线程的ThreadLocalMap对象,然后把值设置到了这个对象中,这样对于不同的线程得到的就是不同的ThreadLocalMap,那么向其中保存值或者修改值都只是会影响到当前线程,这样就保证了线程安全。

3.ThreadLocal在什么情况下会造成内存泄漏

ThreadLocalMap内部维护了一个Entry[] table来存储键值对的映射关系,内存泄漏和Entry类有非常大的关系,下面是Entry的源码:

1
2
3
4
5
6
7
8
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。

由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。

Entry对象属于ThreadLocalMap,ThreadLocalMap属于Thread,如果线程本身的生命周期很短,短时间内就会被销毁,那么「内存泄漏」立刻就会得到解决,只要线程被销毁,value也会随之被回收。问题是,线程本身是非常珍贵的计算机资源,很少会去频繁的创建和销毁,一般都是通过线程池来使用,这就将线程的生命周期大大拉长,「内存泄漏」的影响也会越来越大。

4.如何解决内存泄漏问题

每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。

如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法仍然有可能key的弱引用被回收后,引用没有被回收,此时该仍然可能会导致内存泄漏。这个问题没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

既然弱引用会导致内存泄漏,那ThreadLocalMap为什么对ThreadLocal的引用要设置成弱引用?

为了尽快回收这个线程变量,因为这个线程变量可能使用场景不是特别多,所以希望使用完后能尽快被释放掉。因为线程拥有的资源越多,就越臃肿,线程切换的开销就越大,所以希望尽量降低线程拥有的资源量。

5.java中的引用方式:强引用,弱引用,软引用,虚引用

强引用  >  软引用  >  弱引用  >  虚引用

1、强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object o=new Object(); // 强引用

2、软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

String str=new String("abc"); // 强引用 SoftReference<String> softRef=new SoftReference<String>(str); // 软引用

3、弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

String str=new String("abc"); WeakReference<String> abcWeakRef = new WeakReference<String>(str); str=null;

4、虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。