天天看点

ThreadLocal原理、源码、内存泄露分析

ThreadLocal

ThreadLocal类和Thread类都位于

java.lang

包下面,关系紧密。Thread类里面有一个成员变量

也就是说,每个线程都有自己独立的

ThreadLocalMap

(容器)。

ThreadLocal

就是通过与线程的这个

ThreadLocalMap threadLocals

变量发生交互来实现线程级变量私有和隔离的。

ThreadLocal.set()

的执行逻辑是:

首先拿到当前的Thread对象,然后往当前线程自己的

ThreadLocalMap

容器中塞一个值进去。

ThreadLocal.get()

也是从自己线程的容器中取。

这样就做到了线程隔离。每个线程通过

ThreadLocal.get()

得到的值,是自己这个线程先前塞到容器中的值(或者得到

null

,如果当前线程之前没有

set

过值的话)。

测试代码

public class TestThreadLocal {
    static ThreadLocal<Node> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            //当前线程还未设置过,此处get返回null
            Node person = threadLocal.get();
            System.out.println("T1->" + person);
            threadLocal.set(new Node());
            //此处get返回Node{value=0}
            person = threadLocal.get();
            System.out.println("T1 after->" + person);
        },"T1");

        Thread t2 = new Thread(()->{
            //当前线程还未设置过,此处get返回null
            Node person = threadLocal.get();
            System.out.println("T2 before->" + person);
            TimeUnit.SECONDS.sleep(5);

            //T1已经设置过了,但当前线程还未设置过,此处get仍然返回null,不同线程间的访问是相互隔离的
            Node person2 = threadLocal.get();
            System.out.println("T2 after->" + person2);
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
    private static class Node {
        public int value = 0;
    }
}
           

ThreadLocalMap

ThreadLocalMap

可以看成一个存储键值对(

key-value

)的容器,容器里面的每一条记录是一个

Entry

:

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

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

其中key就是

ThreadLocal

类型的实例变量,

Object

类型的value就是每个线程希望独占一份的目标变量。

每个线程中所持有的这个线程私有的

ThreadLocalMap

容器就是

ThreadLocal

实现线程隔离的关键。

有几个点值得详细讨论一下:

  1. 同一个线程内部,所有ThreadLocal变量所持有的线程私有变量都是放在这一个ThreadLocalMap容器中的。
  2. 不同于java.util.HashMap采用链表法(链表+红黑树)处理哈希冲突,ThreadLocalMap实现上采用开放寻址法。

处理hash冲突常见的几种方式:

a 开放寻址法,包括线性寻址(+1,+1,+1,…)、平方寻址(+1,+4,+9,…)

b 链表法,将相同hash值的对象通过一个链条串在该hash值所对应的槽位上。

c 再哈希

d 建立公共溢出区,凡是发生hash冲突的对象都存放到溢出区。

  1. Entry为什么做成

    WeakReference

    (弱引用)呢?
我们知道,只被弱引用所持有的对象,每次GC一旦执行,该对象就会被回收。

其实道理也很简单,弱引用不会延长目标变量的生命周期。目标变量生命周期内(有强引用指着),多一个弱引用无所谓;反过来只要变量超出生命周期,不再有强引用,那么GC在每回合执行时,面对只有弱引用的对象,执行一次回收一次,不会造成

Memory Leak

那么,ThreadLocal的使用过程中就真的没有内存泄露的风险了吗?

回顾一下强软弱虚4种引用关系:

强引用(Strong Reference)对象有强引用,对象不会被回收。

软引用(Soft Reference)内存不够时,会对仅有软引用的对象进行回收,通常用作缓存服务器。

弱引用(Weak Reference)内存回收线程一旦周期性的开始工作,每次执行都会将只有软引用的对象进行回收。也就是说,仅被弱引用关联的对象只能存活到下一次垃圾回收发生之前。

虚引用(Phantom Reference)完全不影响对象的回收。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。

ThreadLocal内存泄露分析

通过前面的分析,ThreadLocalMap中存放的每一条Entry,都是以弱引用的方式对ThreadLocal<?>类型的key进行了包装,看似不会造成内存泄露。但是别忘了,Entry中除了key,还存在对value的强引用。只要线程还在,线程中的ThreadLocalMap对象,以及其中加入的Entry就会一直存在,不管Entry中引用的key是否已经被回收,value会一直存在。value这个强引用,如果不置为null,其引用的对象就不会被回收,造成内存泄露。

总之,需要记住一点:ThreadLocal.set(x)包装一个对象x,会将这个对象x的存活期延长到一个线程的整个生命周期(该对象被丢进了线程特有的ThreadLocalMap的,一条Entry的Value中,而Entry对Value是强引用。因此必然会有这个结论)。

ThreadLocal有两种使用方式:

1、跟随线程生命周期的ThreadLocal,被包装的对象在整个线程的生命周期内都是被需要的,这种情况不会造成内存泄露。

2、如果只是临时使用ThreadLocal,即ThreadLocal变量的生命周期不需要伴随整个线程的话,应该回收自定义的 ThreadLocal 变量,否则可能会影响后续业务逻辑和造成内存泄露。

// shareObj只需要在特定一段时间内被线程内共享,varThreadLocal是临时变量,不跟随整个线程的生命周期
// 这种情况需要在使用完毕之后remove掉,否则,当varThreadLocal作为临时变量被销毁后,尽管Entry中的key所引用的对象变成了null,
// 但是Value依然是强引用指向shareObj,即shareObj未被正确回收。造成内存泄露。
varThreadLocal.set(shareObj);
try {
	// ...
} finally {
    // remove让相应Entry中的value指向null,解除对shareObj的强引用
	varThreadLocal.remove();
}
           

总结:

1、每个线程内部调用

ThreadLocal.set()

方法,可以将变量存放到当前Thread对象内一个私有的容器(

ThreadLocalMap

)中。

get()

也是从当前线程私有的容器中拿取。

2、一个

ThreadLocal

变量虽然看起来是全局变量,但它内部包装的值(Object)却做到了每个线程私藏一份。每个线程都只能读写自己线程的独立副本,互不干扰。

3、

ThreadLocal

一个常用的举例是,可以用来存放当前线程持有的跟数据库的连接(

Connection

)对象。这样能保证:**一个事务的开始、提交、回滚必须在同一个连接中完成。**一个线程一开始先从连接池拿到一个

Connection

对象,放进自己的

ThreadLocal<Connection>

中;在执行事务过程中,任何时刻从

ThreadLocal.get()

拿到的都是同一个

Connection

对象,这样才能保证这个事务的开始、提交都是经由同一个

Connection

完成。

最后想说一点是,了解了

ThreadLocal

的原理之后,要实现线程私有的变量完全可以通过一个

HashMap

来实现(需要加锁)。Map的key就是线程的Id,value就是要在线程内部独占一份的变量。