详解ThreadLocal

为什么出现ThreadLocal ?

在多线程环境下,如果多个线程同时修改一个公共变量,可能会出现线程安全问题,即该变量的最终结果可能出现异常。为了解决线程安全问题,JDK提供了很多技术手段,比如使用synchronized或Lock来给访问公共资源的代码上锁,保证了代码的原子性。但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

ThreadLocal是什么?

ThreadLocal是一个与线程相关的类,但它本身并不是一个Thread。

这个类可以提供线程局部变量,与普通变量有所不同。虽然你可以实例化一个ThreadLocal对象,但当每个线程访问或设置它时,它们实际上是在操作本线程内的该对象的副本。这也意味着,这个对象在不同的线程中,副本的值是不一样的

ThreadLocal是Java提供的一个线程本地变量的实现机制,其核心目的是让每个线程拥有自己独立的变量副本。这样,在多线程环境下,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本,从而实现了线程数据隔离,避免了线程安全问题。

如何使用?

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocalVar = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程1中的本地变量值
                threadLocalVar.set("Hello");
                // 打印本地变量
                System.out.println("Thread 1 value: " + threadLocalVar.get());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程2中的本地变量值
                threadLocalVar.set("World");
                // 打印本地变量
                System.out.println("Thread 2 value: " + threadLocalVar.get());
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

我们创建了两个线程(t1和t2),每个线程都设置了不同的本地变量值,并打印出来。通过使用ThreadLocal类,每个线程都有自己独立的本地变量副本,因此它们之间不会相互干扰。

输出结果

Thread 1 value: Hello
Thread 2 value: World

 注意:

  • ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
  • ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法

 ThreadLocal如何实现?

public class ThreadLocal<T> {
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 根据当前ThreadLocal对象获取对应的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 返回Entry对象的值作为结果
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map为空或者Entry对象为空,则设置初始值并返回
        return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
        // 返回当前线程的threadLocals成员变量
        return t.threadLocals;
    }
    
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 将给定的ThreadLocal对象和对应的值存储到table中
            map.set(this, value);
        } else {
            // 如果map为空,则创建一个新的ThreadLocalMap对象并存储给定的值
            createMap(t, value);
        }
    }
    ThreadLocalMap getMap(Thread t) {
        // 返回当前线程的threadLocals成员变量
        return t.threadLocals;
    }
}

public class Thread implements Runnable {
    //...
    // 定义一个ThreadLocalMap类型的成员变量threadLocals,用于存储当前线程的ThreadLocal对象及其对应的值
    ThreadLocal.ThreadLocalMap threadLocals = null;   
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 
}

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;  // 存储键值对的Entry数组
        static class Entry extends WeakReference<ThreadLocal<?>> {
          Object value;
          Entry(ThreadLocal<?> k, Object v) {
              super(k);  // 使用super()方法创建弱引用,key值为ThreadLocal对象
              value = v;
          }
        }
        //...
        private Entry getEntry(ThreadLocal<?> key) {
          int i = key.threadLocalHashCode & (table.length - 1);  // 计算索引位置
          Entry e = table[i];
          if (e != null && e.get() == key)
              return e;
          else
              return getEntryAfterMiss(key, i, e);  // 如果未找到,则进行查找操作
        }
        private void set(ThreadLocal<?> key, Object value) {
            // 将给定的ThreadLocal对象和对应的值存储到table中
        }
         private void remove(ThreadLocal<?> key) {
      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);
      for (Entry e = tab[i];
            e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
            }
          }
        }
    }
}

ThreadLocalMap 本质是一个<key,value>形式的节点组成的数组,

ThreadLocalMap 的节点是一个静态内部类 Entry,它继承自 WeakReference<ThreadLocal<?>>。这意味着每个 Entry 对象都是一个弱引用,其引用的是 ThreadLocal 类型的对象作为键(key)。值为代码放入的值

      弱引用

  • 不阻断垃圾回收:与强引用不同,弱引用不会增加对象的引用计数,因此即使有弱引用指向一个对象,该对象也可能在任何时间被垃圾回收器回收。
  • 生命周期短暂:一旦垃圾回收器发现某个对象只被弱引用指向,不管当前内存是否足够,这个对象都会被回收。

图源 https://javabetter.cn/sidebar/sanfene/javase.html 

看张经典的图帮助我们理解

 

图源来自阿里文档 

ThreadLocal的实现原理可以分为两个关键部分:

  1. 每个线程(Thread)内部都有一个独立的Map对象,用于存储数据。由于每个线程都有自己的Map,因此不同线程之间的数据是隔离的,不会互相干扰。

  2. 当我们创建一个ThreadLocal对象时,这个对象将作为键(key)来帮助我们在线程内部的Map中定位数据。

下面是对ThreadLocal的核心机制的简要概括:

  • 我们创建的ThreadLocal对象仅仅是一个键(key)。
  • 每个线程(Thread)内部都有一个Map,这个Map中存储了ThreadLocal对象(作为键)和该线程为这个ThreadLocal对象设置的值(作为值)。
  • 对于不同的线程来说,每次获取自己的副本值时,其他线程无法访问到当前线程的副本值,从而实现了副本之间的隔离,互不干扰。

 ThreadLocal的内存泄漏问题

ThreadLocal可能会引起内存泄漏问题

首先,了解内存泄漏的基础知识是必要的。内存泄漏是指一块被分配的内存既不能被访问,也无法被垃圾回收器回收的情况。在Java中,强引用与弱引用对垃圾回收的影响不同,强引用会阻止对象被垃圾回收,而弱引用则不会。

ThreadLocal对象(作为ThreadLocalMap中的键)被垃圾回收器回收,而与之关联的ThreadLocalMap仍然存活时(由于其生命周期通常与线程相同),如果对应的值没有被适当地清理,就会出现以下情况:

  • ThreadLocalMap中仍然存在键(ThreadLocal对象)为null的条目。
  • 这些条目的值(即ThreadLocal对象所关联的数据)依然占用内存。
  • 由于ThreadLocal对象已被回收,这些值无法通过正常的引用路径被访问或清理。

这种情况就会导致内存泄漏问题,因为即使ThreadLocal对象不再需要,其对应的值也无法被垃圾回收,从而占用不必要的内存资源。

为了避免这种内存泄漏,可以采取以下措施:

  1. 显式清理:在使用完ThreadLocal后,调用remove()方法来显式地从ThreadLocalMap中移除对应的键值对。

  2. 合理设计:在设计上,确保ThreadLocal对象的生命周期与使用它的线程相匹配,避免长时间持有不必要的ThreadLocal对象。

  3. 线程池管理:在使用线程池时,注意线程的复用,及时清理线程本地变量,特别是在长生命周期的线程池中。

  4. 资源监控:定期监控和分析应用的内存使用情况,以便及时发现潜在的内存泄漏问题。

那为什么还要设计为弱引用?

在现代程序运行中,线程池模式被广泛采用,这导致线程的生命周期变得相对较长。由于ThreadLocal可以为每个线程提供独立的数据存储,如果这些数据没有得到及时回收,就可能导致内存泄漏和最终的内存溢出(OOM)。

为了减轻程序员手动管理资源的负担,并防止内存溢出的风险,Java引入了一套基于弱引用的自动回收机制。这个机制的核心是使用WeakReference来包装ThreadLocal对象,从而允许垃圾回收器在需要时回收它们。

下面是对这种机制的简要概括:

  • 弱引用包装:ThreadLocal对象被封装在一个弱引用中,这样即使它被用作ThreadLocalMap的键,也不会阻止垃圾回收器回收它。
  • 自动清理:当ThreadLocal对象不再强引用时,垃圾回收器可以自动回收它,从而避免内存泄漏。
  • 资源管理:通过这种方式,程序员不需要显式地调用remove方法来清理ThreadLocal对象,减少了手动管理资源的复杂性。
  • 防止OOM:这种弱引用机制有助于减少长时间运行的线程池中ThreadLocal对象的累积,防止内存溢出。

参考文献:

图解,深入浅出带你理解ThreadLocal-阿里云开发者社区 https://javabetter.cn/sidebar/sanfene/javase.html 

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值