并发编程之ThreadLocal源码解析

目录

  • ThreadLocal 简介
  • ThreadLocal 使用场景
  • ThreadLocal 使用示例
  • ThreadLocal 源码解析
  • ThreadLocal 内存泄漏问题及解决方案
  • ThreadLocal 子线程问题及源码解析

ThreadLocal 简介

ThreadLocal 可以存储一个变量,该变量只有当前线程可以取出,对其它线程不可见,具有线程隔离性,在并发编程中经常会用到。

使用场景

  • 场景一、存储当前登录用户信息

一个请示到达后端系统,系统会根据 session 或者 token 获取当前用户信息,并将该信息存储至 ThreadLocal,在整个请示处理过程中,随时都可以从 ThreadLocal 中取出用户信息,并且不会与其它请求的用户信息混淆。

SpringSecurity 就是将用户信息构造成 SecurityContext 对象默认存储在 ThreadLocalSecurityContextHolderStrategy 类中的 ThreadLocal<SecurityContext> contextHolder 属性中。

  • 场景二、存储数据库连接

后端处理需要做数据库操作,向数据库连接池申请一个连接,一个处理过程不止一次数据库操作,不用每次操作都申请一个新连接。在第一次申请数据库连接时,将连接存储至 ThreadLocal,在后续处理过程中,可以随时取出。

Mybatis 通过 SqlSessionManager 类管理数据库连接,通过 startManagedSession 方法申请数据库连接时,就会将该连接存储在 ThreadLocal<SqlSession> localSqlSession 属性中。

使用示例

ThreadLocal 有几个重要的方法

public class ThreadLocal<T> {
    protected T initialValue();
    public T get();
    public void set(T value);
    public void remove();
}
  • initialValue() 方法用于在创建 ThreadLocal 时初始化一个值,默认为 null,可以通过继承 ThreadLocal 类重写该方法,实现初始化。
  • get() 获取暂存在 ThreadLocal 中的对象
  • set() 将一个对象暂存在 ThreadLocal 中
  • remove() 将 ThreadLocal 中的对象清除

一个 ThreadLocal 对象只能为一个线程暂存一个对象,通过 set() 方法进行存储,之后可以通过 get() 方法将该对象取出,如果 get() 时尚未进行 set() 操作,则会调用 initialValue() 方法进行初始化,然后返回初始化的对象。

public class ThreadLocalDemo {

    public static void main(String[] args) {
        ThreadLocal<Integer> local = new ThreadLocal<>();
        Random random = new Random();
        IntStream.range(0, 5).forEach(v -> new Thread(() -> {
            int i = random.nextInt(10);
            System.out.println("处理前 -- 线程:" + v + ",设值:" + i);
            local.set(i);
            try {
                // 模拟请求处理过程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("处理后 -- 线程:" + v + ",取值:" + local.get());
        }).start());
    }
}

打印结果如下

处理前 -- 线程:0,设值:6
处理前 -- 线程:1,设值:7
处理前 -- 线程:3,设值:9
处理前 -- 线程:2,设值:7
处理前 -- 线程:4,设值:9
处理后 -- 线程:4,取值:9
处理后 -- 线程:0,取值:6
处理后 -- 线程:3,取值:9
处理后 -- 线程:2,取值:7
处理后 -- 线程:1,取值:7

可以看到,每个线程都可以正确取出本线程存储的值,这就是 ThreadLocal

源码解析

ThreadLocal 通过 set() 方法进行数据存储,但是数据并不是存储在 ThreadLocal 对象中,而是存储在当前线程的 Thread 类对象的 threadLocals 属性中,数据类型为 ThreadLocalMapThreadLocalMap 类定义在 ThreadLocal 类中,是一个静态内部类,可以将 ThreadLocalMap 看成一个 HashMap,该 mapkeyThreadLocal 对象,值就是 set() 方法需要存储的值。

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

所以,整体的存储结构就是,当前线程 Thread 对象的 threadLocals 属性中存储了一个 ThreadLocalMap,当有 ThreadLocal 实例需要存储值时,就向该 map 添加一个键值对。

// 如果当前线程的 threadLocals 属性已赋值,则向该 map 中设值,
// 否则创建一个 ThreadLocalMap 并赋值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 直接从 Thread 对象中获取 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建一个 ThreadLocalMap 对象,并赋值到 Thread 对象
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在获取值时,通过 Thread.currentThread() 获取到当前线程对象,从而实现了线程隔离。

public T get() {
    Thread t = Thread.currentThread();
    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;
        }
    }
    return setInitialValue();
}

ThreadLocal 的内存泄漏问题

ThreadLocal 可能会产生内存泄漏,但是只要正确使用是可以避免这个问题的。

内存泄漏原因解析

首先看一下 ThreadLocalMap 的定义,ThreadLocalMap 的结构跟 HashMap 差不多,都是使用内部的 Entry 对象进行实际数据存储,在上文中我们说 ThreadLocalMap 可以看做是 key 为 ThreadLocal 对象的 HashMap 其实并不准确。ThreadLocalMap 真实的 key 是使用 WeakReference 包裹的 ThreadLocal 对象,表示 key 其实是 ThreadLocal 对象的弱引用。

弱引用的用处是,如果指向一个对象的引用只有弱引用,则在 gc 时,该对象会被回收。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
为什么弱引用可能导致内存泄漏?
public class ThreadLocalDemo {

    public static void main(String... args) {
        ThreadLocal<Integer> intLocal = new ThreadLocal<>();
        ThreadLocal<String> stringLocal = new ThreadLocal<>();
        
        intLocal.set(1);
        System.out.println(intLocal.get());
        stringLocal.set("hello");
        System.out.println(stringLocal.get());
    }
}

通过上面的代码,分析一下对象的引用,以 intLocal 为例

  • intLocal 本身是一个强引用,ThreadLocal 实例拥有一个强引用,
  • 当调用 intLocal.set(1) 时,intLocal 会被存储至当前线程的 threadLocals 属性中,并以弱引用形式存储,ThreadLocal 实例此又拥有一个弱引用,
  • 当方法执行完成,intLocal 作为一个局部变量会被回收,ThreadLocal 实例失去一个强引用,则只剩下一个存储在线程对象中的弱引用。

为什么使用弱引用?

因为只是用于分析,本例只是使用主线程,当 main 方法执行完成时,主线程也会被回收。但是在实际应用中,一般会使用线程池技术,线程在请求完成之后并不会消亡,而是被回收至线程池中,后续请求到时,还可以拿出来继续使用。如果使用强引用,那么此时,ThreadLocal 实例就会一直存在于 Thread 对象中,只要线程不消亡,ThreadLocal 实例就不会被回收,可能导致内存泄漏。

使用了弱引用就可以避免上面的问题?

通过上面的分析知道,ThreadLocal 实例在请求完成后,就只剩下一个存储在线程对象中的弱引用,所以只要发生 gc,该对象就会自动回收。

那么问题来了,线程对象中存储的是一个键值对,现在 key 被自动回收了,value 呢?key 对应的 value 并没有相应的回收机制,内存泄漏依然存在。

如何解决内存泄漏问题

当某个 ThreadLocal 实例不再使用时,调用 remove() 方法,从 ThreadLocalMap 中清除键值对

子线程问题

以前文提到的 场景一、存储当前登录用户信息 为例,当前用户信息已存储在主线程的 ThreadLocal 中,
此时,有操作需要另外开启线程进行操作,即开启子线程进行操作,那么在子线程中,能否拿到主线程中 ThreadLocal 存储的值?

public class Test {
    public static void main(String[] args) {
        ThreadLocal<Integer> local = new ThreadLocal<>();
        local.set(100);
        
        new Thread(() -> {
            System.out.println("子线程,取值:" + local.get());
        }).start();
    }
}

打印结果如下,说明子线程无法继承父线程存储在 ThreadLocal 的值。

子线程,取值:null

JDK 也考虑到这个问题,所以提供了可以继承的 InheritableThreadLocal 类,它是 ThreadLocal 的一个子类,用法跟 ThreadLocal 一样。

public class Test {

    public static void main(String[] args) {
        ThreadLocal<Integer> local = new InheritableThreadLocal<>();
        local.set(100);
        Random random = new Random();
        new Thread(() -> {
            int i = random.nextInt(10);
            System.out.println("子线程,取值:" + local.get());
            local.set(i);
            System.out.println("子线程,重新设值:" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程,重新取值:" + local.get());
        }).start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("父线程,取值:" + local.get());
    }
}

打印结果如下,说明子线程继承了父线程 ThreadLocal 的内容,但是子线程再次修改时,对父线程是不可见的

子线程,取值:100
子线程,重新设值:4
子线程,重新取值:4
父线程,取值:100
源码分析

InheritableThreadLocal 与 ThreadLocal 不同的地方就在于数据存储的位置不同,
ThreadLocal 将数据存储在 Thread 的 threadLocals 属性,而 InheritableThreadLocal 将属性存储在 Thread 的 inheritableThreadLocals 属性,

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Thread 类的 init 方法中对 inheritableThreadLocals 属性进行了特殊处理,该方法比较长,我们只截取了中间与 inheritableThreadLocals 属性相关的部分代码。
可以看到,线程在启动时,会将父线程的 inheritableThreadLocals 复制一份至子线程中,从而实现子线程继承了父线程的 ThreadLocal 数据。

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    // ... 省略
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = 
        		ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // ... 省略
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值