浅谈ThreadLocal的理解

512 阅读6分钟

前言

在说到ThreadLocal之前,首先需要了解一下java中集中引用类型,下面就简单的说明一下四种引用类型


引用类型介绍

说到ThreadLocal的话,就不得不提及到几种引用类型: 强、软、弱、虚

强引用
// 强引用,就算发生gc,当对象还被引用这就不会被回收,当发生内存溢出直接爆出异常
// 新增启动参数
// -Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError    会出现一下异常
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
public static void main(String[] args) {
    byte[] bytes = new byte[1024 * 10214];
    System.gc();
    System.out.println(bytes);
}
软引用
// 软引用:当内存空间不足时,会被回收到空间足够则就不回收,

// 配置启动参数:-Xms3m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError
// ①调整启动参数:到5M是,会存在部分
// 当去掉启动参数,则输出全部有值

// 声明集合类存储软应用类
private static List<SoftReference> list = new ArrayList<>();

public static void softReference() {
    for (int i = 0; i < 10; i++) {
        byte[] bytes = new byte[1024 * 1024];
        // 对于softReference是强引用,而softReference的bytes则为软引用
        SoftReference<byte[]> softReference的bytes则为软引用 = new SoftReference<byte[]>(bytes);
        list.add(softReference);
    }
    // 主动触发gc
    System.gc();
    list.stream().forEach(e -> System.out.println(e.get()));
}
运行结果:内存为3M时
    null
    null
    null
    null
    null
    null
    null
    null
    null
    [B@2dda6444
    
 运行结果:内存为5M时   
    null
    null
    null
    null
    null
    null
    [B@79fc0f2f
    [B@50040f0c
    [B@2dda6444
    [B@5e9f23b4
   

弱引用:

// 启动参数:-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
// 执行结果可以看出在添加到list的过程中实际上出现发生了gc,
// 手动执行gc之后list中所有的WeakReference引用都被释放
private static List<WeakReference> list = new ArrayList<>();

public static void weakReference() {
    for (int i = 0; i < 5; i++) {
        byte[] bytes = new byte[1024 * 1024];
        WeakReference<byte[]> ref = new WeakReference<>(bytes);
        list.add(ref);
    }
    list.stream().forEach(e -> System.out.println(e.get()));
    System.gc();
    System.out.println("=============gc之后===============");
    list.stream().forEach(e -> System.out.println(e.get()));
}

运行结果:
null
null
null
[B@79fc0f2f
[B@50040f0c
=============gc之后===============
null
null
null
null
null
   

虚引用:

// 虚引用通常和ReferenceQueue一起连用,所以引入ReferenceQueue类
// 虚引用无论gc是否发生,都不能获取到对象数据
// 原因①:PhantomReference在继承Reference类是只有一个get方法的实现,返回的是null
// 原因①:因为虚引用是使用的计算机的直接内存,jvm不能跨空间获取
// 涉及用户态和内核态的切换问题,感兴趣的可以自己去学习一下
public static void phantomReference() {
    byte[] bytes = new byte[1024 * 1024];
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    PhantomReference sf = new PhantomReference<>(bytes, referenceQueue);
    System.out.println("gc前:" + sf.get());
    System.gc();
    System.out.println("gc后:" + sf.get());
}

运行结果:
gc前:null
gc后:null
   

ThreadLocal如何实现线程的资源隔离

基本的引用类型介绍完毕,下面步入正题:

众所周知,ThreadLocal在通常的用法是为了实现线程之间的一个隔离性,保证只有一个线程去持有对应的资源,那么ThreadLocal是怎么实现这个原理的呢?那么话不多少,咱们直接看源码!

// 在类的中声明一个全局的静态ThreadLocal
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 在类的某个方法上调用set方法,保存线程的资源
threadLocal.set("");

ThreadLocal源码:


public void set(T value) {
    // 首先调用set方法的时候,要获取到当前正在执行的线程
    Thread t = Thread.currentThread();
    // 这个getMap方法很关键
    ThreadLocalMap map = getMap(t);
    
    // 如果当前线程的ThreadLocalMap不为null,说明当前线程已经在线程内部有了自己的一份map
    if (map != null)
        // 注意这一步相当于对资源的一个覆盖成新的value
        map.set(this, value);
    else
        // 关键步骤创建线程Thread的私有threadLocals属性并赋值
        createMap(t, value);
}

首先阅读上面的代码,首先咱们先看createMap,这个方法才是操作线程和放入资源数据的方法:

// 可以看到次方法就是简单的几行,但实际上创建了ThreadLocal的静态内部类对象ThreadLocalMap
// 传入this对象,this则是当前ThreadLocal对象,上文定义的threadLocal,firstValue就是传入的数据
// 那么此时当前线程的threadLocals属性就指向了ThreadLocalMap对象,并保存在线程私有变量里
// 这样就用线程的方式实现了隔离,保证每个线程保存资源仅对自己可见
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

getMap方法介绍:

// 可以看到getMap方法是根据当前的执行线程获取线程内部的threadLocals属性,
// 那么threadLocals是什么呢?
// 其实threadLocals是保存在Thread类的ThreadLocal.ThreadLocalMap类型的变量是线程私有的
// 显而易见ThreadLocalMap是ThreadLocal类里的一个静态内部类,会存在每个线程Thread内部一份
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


ThreadLocal.ThreadLocalMap threadLocals = null;

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

ThreadLocal的内存溢出问题

如图所示: image.png

  1. ThreadLocalRef是ThreadLocal的一个强引用,而key则是ThreadLocal的弱引用,当ThreadLocalRef所在的栈空间被垃圾回收时,那么ThreadLocal也会被回收,对应的弱引用key也会置为null,由于value是强引用所有value不会被回收。 2.可以通过当前线程的引用获取线程的threadLocals属性,获取对应的ThreadLocalMap,那么只要当前线程一直存在(如线程池),那么它所引用的ThreadLocalMap就不会被回收,当value占用的内存空间比较大的情况下,很容易发生内存溢出的问题。

那么我们如何避免发生内存溢出这样的问题呢? ThreadLocal提供了两个方法:

  1. remove方法:当我们线程执行完成任务时显式的调用remove方法来避免内存溢出问题
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

2.set方法 :

// 上文中的set方法实际上内部有类似的检查机制,内部的replaceStaleEntry方法
map.set(this, value);
 
 
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    // 通过使用ThreadLocal的hash值,获取在Entry数组的位置来获取对应的key,
    // 当key为空,说明entry对象的key被回收了,所以使用replaceStaleEntry方法清理回收key的entry
    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();
}

待完善···敬请期待!!!

ThreadLocal父子线程实现资源你共享

如何实现父子线程实现资源共享呢?

这时就需要了解另一个类InheritableThreadLocal,此类继承自ThreadLocal,并且内部只提供了三个方法:

childValue、getMap、createMap,实际上是重写了ThreadLocal的getMap、createMap和两个方法,而childValue则是创建子线程时和父线程进行一个绑定关系,详细代码如下:

// 这个是一段重要的代码,次方法创建线程的时候,对线程进行初始化调用的,new Thread会调用init方法,下面会讲到
protected T childValue(T parentValue) {
    return parentValue;
}

// 重写ThreadLocal的getMap方法,获取的是线程的inheritableThreadLocals属性
// inheritableThreadLocals这个属性可以让父子线程进行一个资源共享,保证父子线程的可见性
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

// 重写ThreadLocal的createMap方法,添加线程的资源到inheritableThreadLocals属性,为了保证父子线程的可见
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

线程的init方法:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {

// 删除一些代码。。。。。


// 可以看到此方法获取当前父线程,并且下面的判断是存在父线程
// 则会把父线程资源存入放入子线程的inheritableThreadLocals属性中
Thread parent = currentThread();



    SecurityManager security = System.getSecurityManager();
 
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
 
    this.stackSize = stackSize;

    tid = nextThreadID();
}

看到了如上代码相信大家应该可以理解了吧: 父子线程资源共享代码示例:

    public static void main(String[] args) {
        threadLocal.set("我是主线程");

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "<====>" + threadLocal.get());
            }, "thread-name-" + i);
            t.start();
        }
    }
运行结果:
thread-name-0<====>我是主线程
thread-name-3<====>我是主线程
thread-name-1<====>我是主线程
thread-name-5<====>我是主线程
thread-name-2<====>我是主线程
thread-name-7<====>我是主线程
thread-name-8<====>我是主线程
thread-name-6<====>我是主线程
thread-name-4<====>我是主线程
thread-name-9<====>我是主线程 

总结:

  1. 创建InheritableThreadLocal并调用set方法时,实际上调用InheritableThreadLocal的createMap方法,将资源最终放入父线程的inheritableThreadLocals属性
  2. 创建子线程时,调用线程的init方法,判父线程的inheritableThreadLocals属性不为null,则将父线程的inheritableThreadLocals属性赋值给子线程的inheritableThreadLocals属性
  3. 调用get方法时,最终会调用的InheritableThreadLocal的getMap,最终获取线程的inheritableThreadLocals属性的数据,达到父子线程的共享

ThreadLocal常用的业务场景和注意点

ThreadLocal通常的使用场景是存储当前线程的全局token(登录信息),可以在当前执行线程在整个声周期中获取到当前线程的一个私有数据对象, 但使用ThreadLocal需要注意几点:

  1. 在线程的开始和结束要使用remove方法,避免内存溢出问题(可以用拦截器或自己实现aop方式)
  2. Thread和线程池的配合使用,如果不及时的嗲用remove方法,很可能会出现B用户可以查看A用户的数据
  3. 通常使用ThreadLocal定义为全局的,使用static修饰

以上就是笔者的一些经验和总结,希望对大家开发有所帮助,如有希望了解的或者想要分享的可以给笔者留言!!~~ 笔芯^_^