ThreadLocal原理详解

1.概述

我们知道,想要实现变量共享,可以采取定义static修饰的类变量(静态变量)的形式,所有的线程共享这个变量。但是现在,我们想要实现每个线程独享这个变量,应该怎么实现呢?

2.什么是ThreadLocal

于是乎,ThreadLocal出现了。它实现了线程与线程之间的数据的隔离,互不干扰。那么ThreadLocal到底是何方神圣呢?

ThreadLocal是用来管理线程的私有数据,使得线程对该数据的操作对其他线程不可见,达到数据隔离的效果。

那么,Java中是怎么实现这种机制的呢?接下来一一揭晓。

2.1 Thread、ThreadLocal、ThreadLocalMap三剑客

ThreadLocal中定义了内部类ThreadLocalMapThreadLocalMap类似于HashMap,其中keyThreadLocalvalue为想要存储的值。然后呢,每个Thread对象中维护了一个ThreadLocalMap,因此,每个线程都可以很方便的访问只属于自己的数据。

我们通过源代码来跟踪一下Thread、ThreadLocal、ThreadLocalMap三者之间的联系。

ThreadLocal.java

// ThreadLocal.java
public class ThreadLocal {
    // 静态内部类
    static class ThreadLocalMap {
    }
}

Thread.java

// Thread.java
public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

那么,它们三者的关系呢就像下面这张图一样:

  • 人(Thread):当前线程对象。
  • 公文包(ThreadLocalMap):属于当前线程对象,用于存放只能由当前线程访问的数据。
  • 文件(ThreadLocal):文件的key为ThreadLocal对象

ThreadLocal原理详解_第1张图片
再来看看它们的数据结构:

ThreadLocal原理详解_第2张图片

3.ThreadLocal的作用

先来体验一下ThreadLocal的API:

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

    public static void main(String[] args) {
        System.out.println(threadLocal.get());
        threadLocal.set("one and only");
        System.out.println(threadLocal.get());
    }
}

ThreadLocal原理详解_第3张图片

接下来我们来验证一下数据隔离的效果。

import lombok.SneakyThrows;

import java.util.concurrent.TimeUnit;

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

    @SneakyThrows
    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            threadLocal.set(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName() + "完成了签名");
            try {
                // 睡眠2秒,以保证李四线程先执行完成
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "查看了自己的数据,签名:" + threadLocal.get());
        }, "张三");
        Thread threadB = new Thread(() -> {
            threadLocal.set(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName() + "完成了签名");
            System.out.println(Thread.currentThread().getName() + "查看了自己的数据,签名:" + threadLocal.get());
        }, "李四");
        threadA.start();
        // 先启动张三线程,再启动李四线程
        TimeUnit.SECONDS.sleep(1);
        threadB.start();
        // 释放内存,防止造成内存泄漏
        threadLocal.remove();
    }
}

执行结果:

ThreadLocal原理详解_第4张图片

从结果中可知,虽然我们营造了一个李四在张三之后进行签名的场景,但是李四签名之后并没有覆盖掉张三的签名,说明,张三和李四之间的行为是互不影响的,也就是所谓的数据隔离。

4.ThreadLocal实战场景

我们先来看一个多线程安全的问题,来自于日期格式化类SimpleDateFormat,代码如下:

public class ThreadLocalTest {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static String[] dateArray = {"2023-01-01 09:00:00", "2023-02-01 10:00:00", "2023-03-01 11:00:00"};

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    Date date = simpleDateFormat.parse(dateArray[finalI]);
                    System.out.println(simpleDateFormat.format(date));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

执行结果:

我们看到,并没有按照预期打印出正确的结果,相反还报错了,仅打印出了一个时间,还是个错误的时间,这说明在多线程环境下出现了线程安全的问题。

ThreadLocal原理详解_第5张图片

那么为什么会出现线程安全问题呢?我们来看看SimpleDateFormat的源码。

ThreadLocal原理详解_第6张图片

来看这段官方文档,表明SimpleDateFormat是不同步的,建议为每个线程创建一个单独的SimpleDateFormat实例,这不正符合ThreadLocal的使用场景吗,或者在多线程环境下使用线程同步。

我们这里可以采用ThreadLocal来为每个线程创建SimpleDateFormat实例的方式来解决线程安全的问题。

public class ThreadLocalTest {

    private static String[] dateArray = {"2023-01-01 09:00:00", "2023-02-01 10:00:00", "2023-03-01 11:00:00"};

    // 前面我们知道直接调用get(),返回的值为null,因此我们需要提前设置初始化值
    private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                SimpleDateFormat simpleDateFormat = threadLocal.get();
                try {
                    Date date = simpleDateFormat.parse(dateArray[finalI]);
                    System.out.println(simpleDateFormat.format(date));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

执行结果:

这次结果打印正常了,由于线程执行的随机性,所以打印顺序也是随机的。

ThreadLocal原理详解_第7张图片

5.源码分析

前面我们在初体验ThreadLocal的API时,就已得知,第一次调用ThreadLocalget()方法时,返回值为null,因为它并没有一个初始化值,因此我们需要先为每个线程设置一个初始化值,这些都需要我们去跟踪源码进行一一分析。

源码分析环节,我们从以下几个方面进行:

  • Thread.get()
  • 设置初始化值
  • Thread.set()
  • ThreadLocalMap

5.1 ThreadLocal.get()

先来看看get()的源码:

// ThreadLocal.java

public T get() {
    // 1.获取当前线程
    Thread t = Thread.currentThread();
    // 2.获取当前线程对象独有的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 3.如果ThreadLocalMap存在,且存在key,则返回值,否则设置初始化值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 4.如果无法从如果ThreadLocalMap获取值,则设置初始化值。
    return setInitialValue();
}

因为我们是直接调用get()方法,所以此时ThreadLocalMap中是没有值的,因此走到setInitialValue()

// ThreadLocal.java

private T setInitialValue() {
    // 初始化值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

这点重点关注initialValue()方法。

protected T initialValue() {
    return null;
}

出乎意料,这里竟然直接返回null,这就涉及到我们的第二点为线程的独立变量设置初始化值。

5.2 设置初始化值

在分析get()方法时,我们得知initialValue()的返回值为null,要设置初始化值,我们可以继承ThreadLocal并覆写initialValue()即可。

public class CustomThreadLocal extends ThreadLocal<SimpleDateFormat> {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
}

还有一种写法,我们在实战场景部分已经使用过了,代码如下:

private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

主要借助了ThreadLocal的一个内部类SuppliedThreadLocal

先来看withInitial()方法,参数是Supplier接口,这也是很常见的函数式接口。方法的内部实际上也是返回了ThreadLocal的子类SuppliedThreadLocal。这种写法与我们的第一种写法的原理是一样的,免去我们再手动创建一个子类。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

再来看SuppliedThreadLocal,得知,我们只需要在提供的Supplier中定义好初始化独立变量的逻辑即可。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

5.3 ThreadLocal.set()

接下来我们来看看set()方法。

// ThreadLocal.java

public void set(T value) {
    // 1.获取当前线程
    Thread t = Thread.currentThread();
    // 2.获取当前线程对象独有的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 3.如果ThreadLocalMap存在就设置新值,否则就创建一个新值并存储到线程中
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

// 返回线程对象的threadLocals变量
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建一个新的ThreadLocalMap并设置值
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

这里我们重点关注的就是ThreadLocalMap了,它是用来存储数据的地方。

5.4 ThreadLocalMap

这里我们贴出关键的代码。

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

不难发现,它的结构类似于HashMap,但它的Entry是继承自WeakReference>,因此,它的key是一个弱引用。

我们来回顾一下四大引用引用的特征:

强引用(StrongLyReference)

传统意义上的引用,类似于Object obj = new Object();

只要强引用关系存在,垃圾收集器就不会回收这些对象。

软引用(SoftReference)

描述一些还有用,但非必需的对象。

只被软引用关联着的对象,垃圾收集时,如果内存空间足够,则不会收集这些对象;一旦内存空间不足,才会收集这些对象。

弱引用(WeakReference)

描述一些还有用,但非必需的对象,垃圾收集时,不管内存空间是否足够,都会收集这些对象。

虚引用(PhantomReference)

形同虚设,并不会影响对象的生命周期。

垃圾收集时,会将虚引用放入队列,就可以得知改对象被收集了。

这里我们再探讨一个问题,那就是ThreadLocal的内存泄漏问题,那么为什么会出现内存泄漏呢?

// Thread.java
public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap对象与Thread线程对象的生命周期是一样的,因此即使ThreadLocalMap的key是弱引用,因此在垃圾回收时是可以被收集的,那么可能的原因就是value值无法被回收。这就是我们需要注意的点,在使用ThreadLocal时,使用完毕后,配合调用remove(),就会清除掉ThreadLocalMap中key为null的value。

6.总结

综上所述,ThreadLocal出现的原因就是为了能够为每个线程提供属于自己的变量,来达到数据隔离的效果。

你可能感兴趣的:(java,java)