一、ThreadLocal概述

  • 定义:threadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。

  • 结构:

  • threadlocal是解决线程安全的一种有效的方式,但是从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争

二、ThreadLocal 实现原理 ⭐

通过实例代码来简单演示下ThreadLocal的使用。

复制代码
/**
 * @BelongsProject: threadlocal
 * @Author: xc
 * @CreateTime: 2023-10-27  13:39
 * @Description: TODO
 * @Version: 1.0
 */
public class TestThreadLocal {

    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();


    public static void main(String[] args) {
        new Thread(() -> {
            try {
                threadLocal.set("aaa");
                threadLocal1.set("aaa1");
                threadLocal2.set("aaa2");

                System.out.println("threadA-threadLocal:" + threadLocal.get());
                System.out.println("threadA-threadLocal1:" + threadLocal1.get());
                System.out.println("threadA-threadLocal2:" + threadLocal2.get());

                threadLocal.remove();

                System.out.println("threadA-threadLocal:" + threadLocal.get());
                System.out.println("threadA-threadLocal1:" + threadLocal1.get());
                System.out.println("threadA-threadLocal2:" + threadLocal2.get());

                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            threadLocal.set("bbb");
            System.out.println("threadB:" + threadLocal.get());
            threadLocal.remove();
            System.out.println("threadB:" + threadLocal.get());
        }).start();
    }

}
threadA-threadLocal:aaa
threadA-threadLocal1:aaa1
threadA-threadLocal2:aaa2
threadA-threadLocal:null
threadA-threadLocal1:aaa1
threadA-threadLocal2:aaa2
threadB:bbb
threadB:null

在日常开发中简单的使用一般使用threadlocal的get set remove,最常用的方法这里就不分析了,主要分析threadlocal的设计理念和对应的一些不常用的方法解析以及threadlocal使用的安全隐患

InheritableThreadLocal

在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:

public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    IntStream.range(0,10).forEach(i -> {
        //每个线程的序列号,希望在子线程中能够拿到
        threadLocal.set(i);
        //这里来了一个子线程,我们希望可以访问上面的threadLocal
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行上述代码,你会看到:

Thread-0:null
Thread-1:null
Thread-2:null
Thread-3:null

因为在子线程中,是没有threadLocal的。如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal。顾名思义,这就是一个支持线程间父子继承的ThreadLocal,将上述代码中的threadLocal使用InheritableThreadLocal:

InheritableThreadLocal threadLocal = new InheritableThreadLocal();

再执行,就能看到:

Thread-0:0
Thread-1:1
Thread-2:2
Thread-3:3
Thread-4:4

可以看到,每个线程都可以访问到从父进程传递过来的一个数据。

原理

package java.lang;
import java.lang.ref.*;



public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可以看到,InheritableThreadLocal 继承了ThreadLocal,并且重写了三个方法,看来实现的门道就在这三个方法里面。

InheritableThreadLocal 重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是threadLocals。当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。

可以这么说,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。

如何让子线程可以访问父线程的本地变量。

这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。

private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
		// ... 省略无关部分
        Thread parent = currentThread();
       // ... 省略无关部分
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		// ... 省略无关部分
    }


再来看看里面是如何执行createInheritedMap 的。

  private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap 对象中,threadLocal 实现线程内部变量共享,InheritableThreadLocal 实现了父线程与子线程的变量继承。

虽然InheritableThreadLocal看起来挺方便的,但是依然要注意以下几点:

  1. 变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况

    针对上述情况,阿里开源了一个TTL库,即Transmittable ThreadLocal来解决这个问题,有兴趣的朋友们可以去看看。

  2. 变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题

threadLocals

原理

我们先看下ThreadLocal 与 Thread 的类图,了解他们的主要方法和相互之间的关系。


图中几个类我们标注一下:

  • Thread

  • ThreadLocal

  • SuppliedThreadLocal

  • ThreadLocalMap

  • ThreadLocalMap.Entry

接下去,我们首先先开始了解这几个类的相互关系:

  • Thread 类中有一个 threadLocals 成员变量(实际上还有一个inheritableThreadLocals上面所示),它的类型是ThreadLocal 的内部静态类ThreadLocalMap

  • ThreadLocalMap 是一个定制化的Hashmap,为什么是个HashMap?很好理解,每个线程可以关联多个ThreadLocal变量。

  • ThreadLocalMap 初始化时会创建一个大小为16的Entry 数组,Entry 对象也是用来保存 key- value 键值对(这个Key固定是ThreadLocal 类型)。值得注意的是,这个Entry 继承了 WeakReference(这个设计是为了防止内存泄漏,后面会讲)

  • 从源码中可以看出来,自始至终,这些本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面

ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起 来, 当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。

withInitial解析

由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值,案例如下:

public class InnerClass {
    public static void main(String[] args) {
         ThreadLocal<Integer> localInt = ThreadLocal.withInitial(new Supplier<Integer>() {
             @Override
             public Integer get() {
                 System.out.println("执行了初始化方法");
                 return 6;
             }
         });

        new Thread(() -> {
            try {
                String name = localInt.getClass().getName();
                Integer integer = localInt.get();
                System.out.println(integer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

    }
}

当threadlocal获取初始化值的时候调用链路为

threadlocal中get()->setInitialValue()->setInitialValue方法中initialValue()(注意:此时threadlocal类型为SuppliedThreadLocal为ThreadLocal中的内部类也为它的子类,覆盖重写了ThreadLocal的初始化方法initialValue())

在调用supplier.get()方法时真正实现就是自定义的初始化的withInitial声明定义中的get值,方便各个线程统一初始化

ThreadLocalMap解析

如上图所示,threadlocalmap的引用结构图已经很详细的展示详细可参考:

https://www.zhihu.com/question/477411483?ssr_src=heifetz&utm_id=0

https://blog.csdn.net/cj_eryue/article/details/112857999

知识点

  1. ThreadLocal 的主要问题是会产生脏数据内存泄露https://juejin.cn/post/6844904102015713293

  2. this的巧妙利用:https://juejin.cn/post/6844904197528256520