ThreadLocal详解,源码级的详解,快来学

简介: 从源码入手,20分钟快速掌握ThreadLocal的原理。

@[toc]

一、引入

在多线程环境中,我们都知道多个线程共享变量进行修改操作时,容易发生线程不安全的问题,如下代码所示

public class ThreadLocalTest {
   
   
    // 共享变量
    private static Integer num = 1;

    public static void main(String[] args) throws InterruptedException {
   
   
        Thread thread1 = new Thread(() -> {
   
   
            if (num == 1) {
   
   
                // 修改操作
                num++;
            }
        });

        Thread thread2 = new Thread(() -> {
   
   
            if (num == 1) {
   
   
                // 修改操作
                num++;
            }
        });

        thread1.start();
        thread2.start();
    }
}

当线程1thread1在执行num++时被阻塞,此时由于num=1,线程2thread2num==1成立,并执行num++操作,此时num=2,紧接着线程1thread1得到运行时间片并执行num++,得到的结果是num=3,这个结果与其预期结果是不一致的。

使用共享变量导致线程不安全.png

那怎么办呢?我们索性不用共享变量了,把该变量声明在每一个线程内部作为局部变量,如下代码所示

public class ThreadLocalTest {
   
   

    public static void main(String[] args) throws InterruptedException {
   
   
        Thread thread1 = new Thread(() -> {
   
   
            // 将共享变量声明到线程内部
            Integer num = 1;
            if (num == 1) {
   
   
                num++;
            }
        });

        Thread thread2 = new Thread(() -> {
   
   
            // 将共享变量声明到线程内部
            Integer num = 1;
            if (num == 1) {
   
   
                num++;
            }
        });

        thread1.start();
        thread2.start();
    }
}

此时运行代码,两个线程的运行结果均可以符合预期,但是我们就无法使用共享变量了,这将极大增加我们代码的复杂度,比如我们有100个线程,如果在每个线程内部都设置一个相同的局部变量,那就太low了。

使用局部变量导致难以维护.png


那么如何既使用共享变量,又能避免线程不安全的问题呢?

下面引出本文主角:ThreadLocal

二、介绍

先看一下ThreadLocal的官方解释。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻译成人话就是:我们可以通过ThreadLocal实例隐式地给线程设置局部变量,即该实例可以在每一个线程内部创建一个变量的副本。而该ThreadLocal实例我们就可以通过全局共享变量的方式创建。如下图所示

使用ThreadLocal隐式创建局部变量.png

很多文章中将ThreadLocal解释为多线程环境中保证线程安全的一个类,但这种说法其实是十分片面的。

  • 从官方解释上来看,ThreadLocal的功能仅仅是为了给多个线程设置局部变量,这和线程安全是没有关系的,线程安全针对的是全局的共享变量
  • 从类路径来看,ThreadLocal位于java.lang包,而非java.util.concurrent包。

  • 从其使用方法来看,无论是set()方法、get()方法、remove()方法,都与线程同步或加锁没有任何关系

那么为什么还要说他是线程安全的呢?还是因为它为每一个线程设置一个变量副本,每个线程访问或修改的都是其副本,因此可以认为它提供了一种线程安全的实现。

三、实例化

ThreadLocal提供了两种实例化的方式,第一种是使用静态方法withInitial实例化,另一种是使用new实例化

1. 使用静态方法withInitial实例化

ThreadLocal提供了一个静态方法withInitial(Supplier<? extends S> supplier)实例化ThreadLocal对象,如下所示

private ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);

我们看一下withInitial()方法的源码如下

public class ThreadLocal<T> {
   
   

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
   
   
        return new 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();
        }
    }
}

从源码中可知,我们实例化的ThreadLocal对象的实际类型为SuppliedThreadLocal,它继承于ThreadLocal,其内部十分简单

  • 以函数式接口Supplier为参数的构造方法

    接收Supplier对象为参数,并赋值给supplier属性,可理解为一个工厂方法,与spring中的FactoryBean作用相同。

  • 重写ThreadLocal类的initalValue()方法

    该方法用于从supplier工厂中获取实例对象。

2. 使用new实例化

new的方式十分简单

private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();


以上两种实例化ThreadLocal对象方式的不同之处为:

使用静态方法withInitial()实例化的ThreadLocal对象中存在一个对象工厂supplier,因此当我们首次调用get()方法时,ThreadLocal内部直接从该对象工厂supplier中获取对象作为当前线程中的变量副本。

使用new实例化的ThreadLocal对象中不存在对象工厂supplier,因此我们在首次调用get()方法之前,需要调用set()方法对当前线程设置变量值。

四、Thread与静态内部类ThreadLocalMap

ThreadLocal中有一个十分重要的静态内部类ThreadLocalMap,它实质上是一个<key, value>为键值对的entry数组,其中key为当前ThreadLocal实例(即this),value为前面从Supplier工厂中获取的值或通过set()方法保存的值。如下图所示

ThreadLocalMap结构.png

ThreadLocalMap结构很简单,但它有一个不太容易理解的地方是对它的引用。

我们把目光转移到Thread类中,在Thread类中有一个ThreadLocalMap类型的成员属性threadLocals,源码如下所示

public class Thread implements Runnable {
   
   

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

关联上面ThreadLocalMap结构图,我们可以对ThreadLocalThreadLocalMapThread三者之间的关系做出如下阐述:

每一个线程Thread中维护着一个ThreadLocalMap实例,在ThreadLocalMap实例中保存着一个map集合,该map集合以ThreadLocal实例为key,以ThreadLocal实例中Supplier工厂中提供的值或通过set()方法保存的值为value。

简言之,每一个线程Thread中维护着一组以ThreadLocal实例为key的map集合。

我们使用下面代码结合图片的方式对其进行理解。

public class ThreadLocalTest {
   
   

    // 使用withInitail()静态方法实例化ThreadLocal对象
    private static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);
    // 使用new的方式实例化ThreadLocal对象
    private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
   
   
        Thread thread1 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread1
            stringThreadLocal.set("threa1");
            // 获取当前线程局部变量thread1
            String s = stringThreadLocal.get();
            System.out.println("线程1中stringThreadLocal的值:" + s);

            // 获取当前线程局部变量integer
            Integer integer = integerThreadLocal.get();
            System.out.println("线程1中integerThreadLocal的值:" + integer);
        });

        Thread thread2 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread2
            stringThreadLocal.set("threa2");
            // 获取当前线程局部变量thread1
            String s = stringThreadLocal.get();
            System.out.println("线程2中stringThreadLocal的值:" + s);

            // 获取当前线程局部变量integer
            Integer integer = integerThreadLocal.get();
            System.out.println("线程2中integerThreadLocal的值:" + integer);
        });

        thread1.start();
        thread2.start();
    }
}

输出如下:

线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1中integerThreadLocal的值:1
线程2中integerThreadLocal的值:1

在上面的代码中,线程1thread1和线程2thread2使用两个共享变量integerThreadLocalstringThreadLocal,其中integerThreadLocal使用静态方法withInitial()定义了一个Supplier对象工厂,线程1和线程2调用get()方法时从该对象工厂中获取值,另外,在线程1thread1和线程2thread2中,分别使用set()方法对stringThreadLocal设置字符串值thread1thread2

ThreadLocalThreadLocalMapThread三者之间的关系通过图示的方式将其表现出来如下所示

ThreadLocal、ThreadLocalMap、Thread三者关系.png

五、initialValue()方法

ThreadLocal中该方法源码如下:

protected T initialValue() {
   
   
    return null;
}

该方法用于返回当前线程的这个线程局部变量的“初始值”。该方法将在线程第一次使用get()方法访问变量时调用,除非该线程先前调用了set()方法,在这种情况下,将不会为该线程调用initialValue()方法。通常,每个线程最多调用此方法一次,但在随后调用remove()get()的情况下,可能会再次调用该方法。

这个实现简单地返回null;如果程序员希望线程局部变量具有非null的初始值,则必须将ThreadLocal子类化,并重写此方法。通常,将使用匿名内部类。

刚巧不巧,ThreadLocal内部实现了其子类SuppliedThreadLocal,并对initialValue()方法进行了重写。

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();
    }
}

重写后的initialValue()方法返回的是从Supllier工厂中获取的实例。

六、set()方法

set()方法用于设置当前线程ThreadLocalMap属性中的当前ThreadLocal实例(即this)对应的值。源码如下:

public void set(T value) {
   
   
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
   
   
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

源码也是非常简单的,无非就是获取当前线程的ThreadLocalMap属性,然后将当前ThreadLocal实例(即this)作为key保存到ThreadLocalMap属性中。

七、get()方法

get()方法用于返回当前线程ThreadLocalMap属性中的当前ThreadLocal实例(即this)对应的值。如果变量对当前线程没有值,则通过setInitialValue()方法从initialValue()方法中获取值,将获取到的值保存到当前线程ThreadLocalMap属性中后再返回该值。

public T get() {
   
   
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
   
   
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
   
   
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

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);
    return value;
}

setInitialValue()方法中,我们看到先通过initialValue()方法从Supplier工厂中获取到值,剩下的逻辑和set()方法相同,最后将该值返回。

八、remove()方法

该方法逻辑就是将当前线程ThreadLocalMap属性中的以当前ThreadLocal实例(即this)为key的键值对移除。源码如下

public void remove() {
   
   
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

九、本地变量的继承性

现在我们探讨一个问题,ThreadLocal在父线程中创建的局部变量副本能被其子线程获取吗?如果你对上面ThreadLocalThreadLocalMapThread三者之间的关系有很好的理解的话,我们可以换个方式提问:子线程中的ThreadLocalMap集合能够继承父线程中的ThreadLocalMap集合吗?

我们使用以下代码示例说明

public class ThreadLocalTest {
   
   

    // 使用withInitail()静态方法实例化ThreadLocal对象
    private static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 1);
    // 使用new的方式实例化ThreadLocal对象
    private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
   
   
        Thread thread1 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread1
            stringThreadLocal.set("threa1");
            // 获取当前线程局部变量thread1
            String s = stringThreadLocal.get();
            System.out.println("线程1中stringThreadLocal的值:" + s);

            // 获取当前线程局部变量integer
            Integer integer = integerThreadLocal.get();
            System.out.println("线程1中integerThreadLocal的值:" + integer);

            new Thread(() -> {
   
   
                // 获取从父线程中继承的string变量
                System.out.println("线程1_1中stringThreadLocal的值:" + stringThreadLocal.get());
                // 获取从父线程中继承的integer变量
                System.out.println("线程1_1中integerThreadLocal的值:" + integerThreadLocal.get());
            }).start();
        });

        Thread thread2 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread2
            stringThreadLocal.set("threa2");
            // 获取当前线程局部变量thread1
            String s = stringThreadLocal.get();
            System.out.println("线程2中stringThreadLocal的值:" + s);

            // 获取当前线程局部变量integer
            Integer integer = integerThreadLocal.get();
            System.out.println("线程2中integerThreadLocal的值:" + integer);

            new Thread(() -> {
   
   
                // 获取从父线程中继承的string变量
                System.out.println("线程2_1中stringThreadLocal的值:" + stringThreadLocal.get());
                // 获取从父线程中继承的integer变量
                System.out.println("线程2_1中integerThreadLocal的值:" + integerThreadLocal.get());
            }).start();
        });

        thread1.start();
        thread2.start();
    }
}

在该示例代码中,我们通过withInitial()new两种方式创建两个全局ThreadLocal实例:integerThreadLocalstringThreadLocal

  • integerThreadLocal 通过Supplier对象工厂自动在当前线程中创建局部变量副本
  • stringThreadLocal通过我们在线程中手动调用set()方法创建局部变量副本

线程1thread1 和 线程2thread2中又分别创建其子线程,并在子线程中获取integerThreadLocalstringThreadLocal创建的局部变量副本。

运行代码后输出如下:

线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1中integerThreadLocal的值:1
线程2中integerThreadLocal的值:1

线程1_1中stringThreadLocal的值:null
线程1_1中integerThreadLocal的值:1
线程2_1中stringThreadLocal的值:null
线程2_1中integerThreadLocal的值:1

从输出中我们看到,如果ThreadLocal实例是通过withInitial()方法创建的,那么无论是子线程还是孙线程,我们都是可以获取到局部变量副本的。但是如果ThreadLocal实例是通过new的方法创建的,由于在子线程中没有调用set()方法设置变量副本,所以也就无法获取到当前子线程中的局部变量副本。

那么如何使局部变量副本在父子关系的线程中具有继承性呢?请看lnheritableThreadLocal

1. lnheritableThreadLocal可继承的ThreadLocal

lnheritableThreadLocalThreadLocal的子类,从其命名可以看出,它是个具有继承功能ThreadLocal,官方解释如下:

这个类扩展了ThreadLocal,以提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程有值的所有可继承的线程局部变量的初始值。

我们看一下它的源码,没几行

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

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

    ThreadLocalMap getMap(Thread t) {
   
   
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
   
   
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

这么短短的几行代码就能实现线程局部变量的继承,关键之处在t.inheritableThreadLocals,无论是获取还是实例化ThreadLocalMap,都是和线程ThreadinheritableThreadLocals相关。

其实,Thread类中存在两个ThreadLocalMap类型的属性:threadLocalsinheritableThreadLocals

public class Thread implements Runnable {
   
   

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
  • 如果当前ThreadLocal实例的实际类型为ThreadLocal,则线程局部变量副本保存在threadLocals
  • 如果当前ThreadLocal实例的实际类型为InheritableThreadLocal,则线程局部变量副本保存在inheritableThreadLocals

我们通过以下代码进行演示

public class ThreadLocalTest {
   
   

    // 使用new的方式实例化ThreadLocal对象
    private static ThreadLocal<String> stringInheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
   
   
        Thread thread1 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread1
            stringInheritableThreadLocal.set("threa1");
            // 获取当前线程局部变量thread1
            String s = stringInheritableThreadLocal.get();
            System.out.println("线程1中stringThreadLocal的值:" + s);

            new Thread(() -> {
   
   
                // 获取从父线程中继承的string变量
                System.out.println("线程1_1中stringThreadLocal的值:" + stringInheritableThreadLocal.get());
            }).start();
        });

        Thread thread2 = new Thread(() -> {
   
   
            // 设置当前线程局部变量thread2
            stringInheritableThreadLocal.set("threa2");
            // 获取当前线程局部变量thread1
            String s = stringInheritableThreadLocal.get();
            System.out.println("线程2中stringThreadLocal的值:" + s);

            new Thread(() -> {
   
   
                // 获取从父线程中继承的string变量
                System.out.println("线程2_1中stringThreadLocal的值:" + stringInheritableThreadLocal.get());
            }).start();
        });

        thread1.start();
        thread2.start();
    }
}

运行代码后输出如下:

线程1中stringThreadLocal的值:threa1
线程2中stringThreadLocal的值:threa2
线程1_1中stringThreadLocal的值:threa1
线程2_1中stringThreadLocal的值:threa2

由此可见,使用InheritableThreadLocal时,在父线程中通过set()方法设置的局部变量副本,可以继承到其子线程的局部变量副本中。




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
6月前
|
存储 Java 数据安全/隐私保护
【JUC】ThreadLocal 如何实现数据的线程隔离?
【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
|
3月前
|
存储 设计模式 安全
深入理解ThreadLocal原理
本文深入探讨了Java中的ThreadLocal及其内部数据结构ThreadLocalMap的工作原理和特性,帮助读者理解如何利用ThreadLocal实现线程局部变量的隔离和线程安全。
深入理解ThreadLocal原理
|
5月前
|
存储 安全 Java
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
深入理解Java中的ThreadLocal机制:原理、方法与使用场景解析
87 2
ThreadLocal优化,使用ThreadLocal可以提高代码的复用性,感觉比较丢人,ThreadLocal的作用主要是复用代码
ThreadLocal优化,使用ThreadLocal可以提高代码的复用性,感觉比较丢人,ThreadLocal的作用主要是复用代码
|
6月前
|
存储 安全 Java
ThreadLocal原理讲解
ThreadLocal原理讲解
56 0
|
存储 Java
大厂是怎么用ThreadLocal?ThreadLocal核心原理分析
ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。
119 1
|
Java 数据库连接
ThreadLocal原理和实践
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
162 0
ThreadLocal原理和实践
|
存储 Java 数据安全/隐私保护
ThreadLocal的实现原理&源码解析
ThreadLocal是Java中的一个线程封闭机制,它提供了一种线程局部变量的解决方案,可以使每个线程都拥有自己独立的变量副本,互不干扰。
93 0
|
安全 Java
分析ThreadLocal如何做到单个线程独享
分析ThreadLocal如何做到单个线程独享
80 0
|
存储 算法 安全
ThreadLocal原理剖析
ThreadLocal原理剖析
222 0