ThreadLocal原理

204 阅读7分钟

目录

  • ThreadLocal简介
  • ThreadLocal的使用
  • ThreadLocal的原理
  • ThreadLocal内存泄漏问题
  • ThreadLocal hash冲突问题

ThreadLocal简介

ThreadLocal是Java中一个很酷的工具类,可以用于多线程并发编程。它提供了线程局部变量,这意味着每个线程所访问的变量都是独立的,互不干扰,不会出现线程安全问题,还能提高并发性能。

在Java中,每个线程都有一个自己的线程栈,线程栈中的变量称为线程本地变量(Thread Local Variable)。不同线程之间的线程本地变量互不干扰。ThreadLocal就是利用了这种机制来实现线程局部变量。通过ThreadLocal创建的变量,每个线程都有自己独立的拷贝,互不干扰。

ThreadLocal并不是用来解决共享资源访问问题的。虽然它也能实现多线程间的数据隔离,但与锁相比,它显得非常轻量级,没有任何锁的粒度和开销。因此,ThreadLocal一般用来解决线程安全问题,提高并发性能,而不是用来解决共享资源访问问题。

ThreadLocal的使用

ThreadLocal的使用非常简单,它只提供了三个方法:

  • public T get():获取当前线程的变量副本。
  • public void set(T value):设置当前线程的变量副本。
  • public void remove():删除当前线程的变量副本。

下面是一个使用ThreadLocal的例子:

public class ThreadLocalTest {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(1);
        System.out.println("主线程中的变量值:" + threadLocal.get());

        new Thread(() -> {
            threadLocal.set(2);
            System.out.println("子线程中的变量值:" + threadLocal.get());
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程中的变量值:" + threadLocal.get());
    }
}

上述代码中,主线程和子线程都有一个独立的ThreadLocal变量副本,它们互不干扰。

需要注意的是,每个线程都应该负责清理自己的ThreadLocal变量,否则可能会导致内存泄漏。可以在使用完ThreadLocal变量之后,调用remove()方法进行清理。另外,线程池中的线程复用可能会导致ThreadLocal变量的值错乱,因此在使用线程池时需要特别小心。

ThreadLocal的原理

image.png

1:每个Thread里面都有一个ThreadLocalMap对象

Thread.java:

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

这里可以理解成map一样的hash表接口:key为ThreadLocal,value为线程本地变量具体的值

因为一个线程请求,里面可以使用多个ThreadLocal,所以要使用Hash结构,代码如下:

@SneakyThrows
  public static void main(String[] args) {

    //定义2个ThreadLocal ,每个线程都使用者两个ThreadLocal存储不同的线程本地变量
    ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

    /*
    线程1
     */
    new Thread(() -> {
      threadLocal1.set(10);
      threadLocal2.set("张三");

      //模拟 线程其他业务逻辑,花费5秒
      try {
        TimeUnit.SECONDS.sleep(5);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      Integer integer = threadLocal1.get();
      String string = threadLocal2.get();
      System.out.println(Thread.currentThread().getName() + " 拿到的值为:" + integer + " 和 " + string);

      threadLocal1.remove();
      threadLocal2.remove();

    }).start();

    /*
    线程2
     */
    new Thread(() -> {
      threadLocal1.set(20);
      threadLocal2.set("李四");

      //模拟 线程其他业务逻辑,花费5秒
      try {
        TimeUnit.SECONDS.sleep(5);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      Integer integer = threadLocal1.get();
      String string = threadLocal2.get();
      System.out.println(Thread.currentThread().getName() + " 拿到的值为:" + integer + " 和 " + string);

      threadLocal1.remove();
      threadLocal2.remove();

    }).start();

    System.in.read();
  }

————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————
//执行结果:
Thread-1 拿到的值为:20 和 李四
Thread-0 拿到的值为:10 和 张三

2:ThreadLocalMap实现原理

ThreadLocalMap和HashMap的实现原理非常相似。它们都是哈希结构,将key和value组合成对象,然后使用数组存储对象。

ThreadLocalMap使用ThreadLocal作为key,以存储线程本地变量的值作为value,以确保线程安全。

这里定义成对象格式:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
...其他代码...
}

ThreadLocal中的set()、get()和remove()方法本质上就是在数组中添加、查找和删除数组节点。(有关详细信息,请参阅源代码,此处不再赘述。)

ThreadLocal内存泄露问题

代码介绍:

请看上面的图片,Entry对象是从WeakReference<ThreadLocal<?>>继承而来的。Entry的构造方法接受一个参数ThreadLocal<?> k,然后将其放入了super(k)中。

此时,这里传入的k(即ThreadLocal)实际上是一个弱引用对象。为了代码简洁,才使用Entry继承了WeakReference

实际上,也可以不用Entry对象继承WeakReference。在构造Entry对象时,可以传入WeakReference<ThreadLocal<?>> k。这样做的主要目的是为了代码简洁,以及在后面获取threadLocal时显得简洁。

强弱软虚:

上面的WeakReference 就是弱引用,我们稍微回顾以下java里面的强弱软虚引用说明:

  • 强引用(Strong Reference):普通的对象引用,只要强引用还存在,垃圾收集器就不会回收被引用的对象。

  • 弱引用(Weak Reference) :通过 WeakReference 类实现,被弱引用指向的对象在垃圾收集器运行时,如果没有强引用关联,无论内存是否足够,都会被回收。

  • 软引用(Soft Reference) :通过 SoftReference 类实现,被软引用指向的对象在内存不足时才会被垃圾收集器回收,通常用于实现内存敏感的缓存。

  • 虚引用(Phantom Reference) :通过 PhantomReference 类实现,被虚引用关联的对象在被垃圾收集器回收时可以收到一个系统通知,主要用于跟踪对象被垃圾收集器回收的活动,不能单独使用它们来获取对象。

问题:

1:上面说到ThreadLocal其实是个弱引用对象,只要jvm发生了gc,并且没有强引用关联(即线程任务的下文不会用到该threadLocal的引用),那么这个threadlocal会被清理。假设现在一个threadlocal正好满足被清理条件,此时已经被清理了

2:回到线程部分,这个时候线程还得继续执行,线程任务并没有销毁,线程没有dead,所以Thread里面的ThrealocalMap就不会销毁,因为ThrealocalMap被Thread强引用。

3:因为ThrealocalMap没有被销毁,所以Entry没有被销毁,因为Entry被ThrealocalMap强引用。

4:因为Entry没有被销毁,所以Entry里面的value就没有被销毁,因为value被entry强引用。

但是问题来了。entry里面的ThreadLocal已经被销毁了,已经为空了。但是里面的value依然存在,这里就造成了内存泄露问题。

解决方案:

手动调用ThreadLocal的remove方法,使得Entry里面的value被销毁(这里注意,remove只会销毁value,并不会销毁Threadlocal,详情请自行查看源码), 那么等到下次gc时候,value对应的ThreadLocal也会被销毁, 那么整个entry对象就被销毁了。

所以在每次使用完ThreadLocal之后,请一定手动调用remove方法

ThreadLocal hash冲突问题

虽然ThreadLocal的使用简单,但是它也有一些问题,其中一个是Hash冲突问题。

在ThreadLocalMap中,ThreadLocal对象是作为key来使用的。ThreadLocalMap使用ThreadLocal对象的hashCode作为散列值,然后根据散列值存储该对象的值。如果两个ThreadLocal对象的hashCode相同,那么它们会被存储在同一个散列桶中,这就会引起冲突。

当发生冲突时,ThreadLocalMap会采用线性探测法(Linear Probing)。线性探测法指的是当一个散列值被占用时,就顺序往下一个位置探测,直到找到一个空位。具体来说,就是线性探测法会不断查找下一个位置,直到找到一个空闲的位置为止,然后将该键值对存储到该位置上,这就是线性探测法的核心思想。

如果线性探测法查找到一个空闲位置时,发现该位置之前的位置都已经被其他ThreadLocal对象占用了,那么它就会继续往下探测,直到找到一个空闲位置为止。由于线性探测法只查找一个位置,因此它可能会导致Hash冲突问题。当ThreadLocalMap中的元素数量较多时,这种情况会变得更加严重。

为了避免Hash冲突问题,可以采用以下方法:

  • 尽量避免使用相同的hashCode,可以通过重写ThreadLocal的hashCode方法来实现。
  • 将ThreadLocal作为静态变量使用,这样可以避免创建多个ThreadLocal实例。

另外,需要注意的是,由于线性探测法的存在,ThreadLocalMap的容量不能太大。如果容量过大,那么线性探测法会花费较长的时间来查找空闲位置,这会影响并发性能。