深入理解ThreadLocal

56 阅读2分钟

整体设计

ThreadLocal 主要作用是解决线程间数据共享问题带来锁的竞争问题. 如果一个变量的作用范围和线程紧紧绑定在一起, 就可以使用ThreadLocal. 每个线程都保存一个ThreadLocalMap变量副本来实现线程间资源隔离,避免竞争

Demo

    public class ThreadLocalDemo {
	private static ThreadLocal threadLocal1 = new ThreadLocal();
	public static void main(String[] args) throws InterruptedException {
		ThreadLocal threadLocal1 = new ThreadLocal();
		threadLocal1.set("haha");

		Thread t1 = new Thread(()->{
			threadLocal1.set("a");
			try {
				Thread.sleep(2);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
			System.out.println(threadLocal1.get());
		});

		t1.start();
		t1.join();
		System.gc();
		Thread.sleep(1000);
		System.out.println(threadLocal1.get());
	}
}

结果输出:

a
haha

可以看出, main和t1线程调用同一个Threadlocal的set方法, 但是get出来值都是当初 set进去的值. 这就是ThreadLocal的特性, 线程隔离.

类图

1697010145409

ThreadLocalMap 是位于ThreadLocal静态内部类, 每个Thread 内部都有一个ThreadLocalMap, 它是一个Map, 用于存储K-V结构的数据, ThreadLocalMap.Entry 是位于ThreadLocalMap的一个静态内部类,实现了WeakReference类. ThreadLocal 是Thread 和 ThreadLocalMap之间连接的一个桥梁

源码

直接看jdk中的源码吧

应用场景

  • 存储用户会话信息
  • 上下文信息保存
  • 数据库连接 (spring 中 TransactionSynchronizedManager 会将连接保存到当前线程的ThreadLocal中, 从而单线程内能够形成事务的必要条件)

内存泄露

  • “为何ThreadLocalMap的key是弱引用” 这是一个关于ThreadLocal的高频面试题 , 通常的答案是“防止内存泄露”, 因为ThreadLocalMap的key如果是强引用,如果某个时刻ThreadLocal对象对回收了, 但ThreadLocalMap还保持该ThreadLocal对象的引用,就会造成内存泄露. 例1:
    public void test(){
        new Thread(()->{
            while(true){
                ThreadLocal t1 = new ThreadLocal();
                t1.set("test");
            }
        })
    }

在上面的极端例子中, ThreadLocal的实例变量的生命范围按理应该出了while就可以被GC回收了, 但如果ThreadLocalMap中的key是强引用, 而ThreadLocalMap被Thread线程实例持有着, 就造成每一轮ThreadLocal的实例都是可达的, 但是在业务代码中有无法使用,形成内存泄露的局面.

例子2:

  public static ThreadLocal t1 = new ThreadLocal();
    public void test(){
        new Thread(()->{
                t1.set("test");
                t1 = null;
                while(true){

                }
        })
    }

在例2的情况中, t1 被置为null, 但由于ThreadLocalMap的key是强引用, 导致ThreadLocalMap中依然保存着t1的引用, 造成内存泄露.

例子1中,我们可以通过显示执行remove操作来避免内存泄露; 但例子2中,我们必须使用弱引用才能达到目的.

ThreadLocal 的优势与劣势

  • 优势 通过使用变量副本将资源与线程关联起来, 避免了在调用函数时将参数传递给函数的麻烦.同时也避免的公共资源的争抢
  • 劣势
    1. 可以在任何地方修改,不知何时可以remove
    2. 昂贵的继承成本, 每新建一个子线程,都会从父线程继承过来ThreadLocal相关数据 替换方案: scoped value

参考