【源码分析】ThreadLocal详解

一、前置知识

1.1 ThreadLocal的作用

ThreadLocal主要起到线程隔离的作用,每个线程可以通过ThreadLocal存放自己私有的数据。线程1存放的数据,只有线程1能拿到,其它线程都不能获取到该数据。应用场景主要有:

  • Spring框架中的事务功能,@transactional注解的处理类将数据库连接对象放在了ThreadLocal中
  • Mybatis框架中分页功能也使用了ThreadLocal

1.2 Java中的四种引用类型

  • 强引用:JVM宁可报OOM,也不会回收强引用所指向的对象。
  • 软引用:当堆空间不足时,JVM就会回收软引用所指向的对象。应用场景:缓存。
  • 弱引用:只要进行垃圾回收,就会回收弱引用所指向的对象。应用场景:ThreadLocal防止内存泄漏。
  • 虚引用:只有一个作用——管理直接内存。直接内存是指操作系统所管理的内存,一个Java程序一般都是运行在JVM所管理的内存中,而JVM中会有一个指向直接内存地址的引用。当虚引用指向的对象回收时会分为两步:(1)先回收JVM中指向直接内存地址的引用,并将这个引用放置到一个队列中。(2)将队列中引用指向的直接内存上的数据回收掉。我们可以监控存放指向直接内存地址的队列,从而知道虚引用指向的对象什么时候被回收了,也就是起到了一个通知作用

二、ThreadLocal源码分析

2.1 ThreadLocal的使用

public class ThreadLocalDemo {
    private static final ThreadLocal<Person> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //线程1在threadLocal中存数据
        new Thread(() -> {
            threadLocal.set(new Person());
        }).start();
        //线程2从threadLocal中取数据
        new Thread(() -> {
            System.out.println(threadLocal.get());
        }).start();
    }

    static class Person{
        String name = "zhangsan";
    }
}

执行结果:
执行结果
从结果中可以看到,ThreadLocal是具有线程隔离效果的。线程1在ThreadLocal中存入Person对象,线程2是无法取出这个Person对象的。

2.2 ThreadLocal源码结构

我们主要关注红框标注的几个最重要的方法与内部类。

  • ThreadLocalMap静态内部类
  • get()
  • set(T)
  • remove()
  • getMap(Thread)
  • createMap(Thread, T)
    ThreadLocal源码结构

2.2.1 get()方法

    public T get() {
    	//获取当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	/**
        	* 从当前线程的ThreadLocalMap对象中获取key=this的value,这里this表示的是
        	* ThreadLocal对象,而key所对应的value就是我们存放在ThreadLocal的值
        	**/
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

get()方法我们可以知道,我们往ThreadLocal对象存值时,值其实是被放在了当前线程所对应的ThreadLocalMap中,那么这个ThreadLocalMap又是何物?

2.2.2 ThreadLocalMap静态内部类

既然是一个类,那么我们首先看一下这个类的结构是怎样的。
ThreadLocalMap
首先我们看一下它的构造函数,看看它底层到底是存储的是什么数据

//private static final int INITIAL_CAPACITY = 16;
//private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
	//ThreadLocalMap底层原来是一个Entry数组,每一个Entry对象表示的就是一对key,value数据
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

从这个构造函数,我们可以初步画出ThreadLocalMap底层的存储结构。ThreadLocalMap底层是一个数组,数组中每个元素存放的是指向Entry对象的引用。
ThreadLocalMap
在ThreadLocalMap的源码结构图中,我们可以看到Entry类是ThreadLocalMap的静态内部类,我们来分析一下它的源码:

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

    Entry(ThreadLocal<?> k, Object v) {
    	//前面分析过,Entry中的key其实就是ThreadLocal对象,这里super(k)表示,指向ThreadLocal对象的引用是一个弱引用
        super(k);
        value = v;
    }
}

既然我们知道了Entry类中的key是一个弱引用,那么ThreadLocalMap的底层图,我们可以画的更具体了。
ThreadLocalMap
到这里我们把ThreadLocalMap的结构就分析清楚了,那么我们还未解释为什么ThreadLocal可以实现线程隔离,不要急,等我分析完接下来的set()方法,然后再解释。

2.2.3 ThreadLocal的set()方法

public void set(T value) {
	//获取当前线程对象
    Thread t = Thread.currentThread();
    //通过当前线程获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//若map存在,则将key=ThreadLocal,value=value的键值对存进map
        map.set(this, value);
    else
    	//若map不存在,则创建map,并将key,value存进map
        createMap(t, value);
}

2.2.4 ThreadLocal的getMap()方法

ThreadLocalMap getMap(Thread t) {
	//返回当前线程对象的threadLocals属性,这是实现线程隔离的关键
    return t.threadLocals;
}

我们发现threadLocals是Thread类中的一个属性,那么也就是说,每个线程都有一个threadLocals,这个属性是每个线程所私有的。在这里插入图片描述

2.2.5 ThreadLocal的createMap()方法

void createMap(Thread t, T firstValue) {
	//给每个线程都创建了一个ThreadLocalMap对象,并用每个线程的threadLcoals指向该对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

看到这里,你应该明白为什么ThreadLocal是线程隔离的了吧。为了让大家更清楚,我画一个图来解释。
ThreadLocal
虽然每个线程都使用了公共的ThreadLocal对象,但是我们只是将这个对象的引用作为Entry中的key而已,实际存储则是在每个线程私有的ThreadLocalMap对象中存储的。既然每个线程都有自己的ThreadLocalMap对象,线程1将数据存在线程1的ThreadLocalMap中,线程2必然访问不到。

三、ThreadLocal为什么要使用弱引用

前面我们清楚了ThreadLocal能实现线程隔离,那么为什么Entry中的key要通过弱引用指向公共的ThreadLocal对象呢?这是为了防止内存泄漏。
试想一个情景,当一个线程取出ThreadLocal中的数据,使用完后,不再需要ThreadLocal存储数据了,那我们希望JVM下次垃圾回收时,能把ThreadLocal对象回收掉。由于弱引用的存在,那么我们只需将ThreadLocal对象的引用置为null即可。看以下代码:

public class ThreadLocalDemo {
    private static ThreadLocal<Person> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //向threadLocal中存放数据
        threadLocal.set(new Person());
        //从threadLocal中取出数据并使用
        System.out.println(threadLocal.get());
        //使用完数据后,我不再需要ThreadLocal中的数据了,回收ThreadLocal对象资源
        threadLocal = null;
        //手动调用gc
        System.gc();
        //防止垃圾还没回收,线程就已经结束了
        new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        //查看ThreadLocal对象是否被回收了
        System.out.println(threadLocal.get());
    }

    static class Person{
        String name = "zhangsan";
    }
}

执行结果如下:
结果
我们可以看到,当我们把指向ThreadLocal的引用threadLocal置为null后,JVM垃圾回收就会回收ThreadLocal对象。那么这和弱引用又有什么关系呢?
我们假设Entry中的key是一个强引用指向ThreadLocal对象,如下图所示:
在这里插入图片描述
那么当我们想回收ThreadLocal对象时,我们将指向ThreadLocal对象的引用tl置为空,此时key依然是强引用指向了我们想回收的ThreadLocal对象。文章开头我们介绍过强引用,JVM宁可报OOM都不会回收强引用指向的对象,所以ThreadLocal对象永远不会被JVM回收,该对象所占用的堆内存永远都不会得到释放。

四、使用完ThreadLocal后为什么总是要调用remove()方法

调用remove()方法和前面使用弱引用一样,也是为了防止内存溢出,只不过它们针对的对象不同。我们首先看一下ThreadLocalremove()方法的源码:

 public void remove() {
 	//获取当前线程所对应的ThreadLocalMap对象
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
     	//若map不为空,则删除key=ThreadLocal所对应的value。这里调用了ThreadLocalMap中的remove()方法
         m.remove(this);
 }

我们 再看一下ThreadLocalMap中的remove()方法的源码:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    //这里是计算key在table数组中的位置索引。计算方法与hashmap中的一样
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
        	//找到key所对应的Entry后,清除整个Entry对象
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

也就是说,调用ThreadLocal中的remove()方法,会清除ThreadLocalMap中key=ThreadLocal对象引用的Entry,这样做是为了保证value中所指向的对象能够被回收。
如果使用完ThreadLocal,我们没有调用remove()方法,那么ThreadLocalMap中的数据会一直存在,直到线程结束。即使我们回收了ThreadLocal对象,也会如此,如下图:
在这里插入图片描述
Entry中的value应该是Person对象的强引用,而此时我们已经不需要Person对象了,JVM并不能回收强引用所指向的对象。

五、 总结

总结一下:

  • ThreadLocalMap:实现了ThreadLocal的线程隔离。
  • 弱引用:避免了ThreadLocal对象导致的内存泄漏
  • 调用remove()方法:避免了value强引用导致不需要的数据无法被回收,从而导致内存泄漏。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值