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

29 篇文章 2 订阅
1 篇文章 0 订阅

一、引入

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

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,这个结果与其预期结果是不一致的。

在这里插入图片描述

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

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了。

在这里插入图片描述


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

下面引出本文主角: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解释为多线程环境中保证线程安全的一个类,但这种说法其实是十分片面的。

  • 从官方解释上来看,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结构很简单,但它有一个不太容易理解的地方是对它的引用。

我们把目光转移到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三者之间的关系通过图示的方式将其表现出来如下所示

在这里插入图片描述

五、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()方法设置的局部变量副本,可以继承到其子线程的局部变量副本中。

十、关于内存泄漏问题

我们下一篇文章讲解,女朋友喊我睡觉去了,拜拜。



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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ThreadLocal是一种与线程绑定的变量,它可以解决多线程并发访问的问题。与Synchronized不同,ThreadLocal为每个线程提供了一个独立的变量副本,每个线程都可以独立地修改自己的副本,而不会影响其他线程的副本。 ThreadLocal的使用方法比较简单。我们可以通过ThreadLocal类的set()方法来设置当前线程所关联的变量的值,通过get()方法来获取当前线程所关联的变量的值。在使用完ThreadLocal后,如果不再需要这个变量,应该调用remove()方法来清除当前线程的关联变量,避免内存泄漏的问题。 ThreadLocal的原理是通过每个线程都拥有一个独立的ThreadLocalMap对象来实现的。ThreadLocalMap内部使用一个Entry数组来存储键值对,键为ThreadLocal对象,值为对应的变量副本。在获取当前线程所关联的变量时,会根据ThreadLocal对象找到对应的变量副本并返回。ThreadLocal与Thread、ThreadLocalMap之间的关系是,每个线程都有一个ThreadLocalMap对象,其中存储了与该线程关联的所有ThreadLocal对象及其对应的变量副本。 ThreadLocal的常见使用场景包括但不限于: - 解决线程安全问题:可以将需要在多个线程中共享的数据存储在ThreadLocal变量中,每个线程访问自己的变量副本,避免了线程安全问题。 - 传递上下文信息:可以将一些需要在多个方法中共享的上下文信息存储在ThreadLocal变量中,在方法调用链中方便地获取这些上下文信息。 - 数据库连接管理:可以将数据库连接存储在ThreadLocal变量中,在每个线程中独立管理数据库连接,避免了线程间的冲突。 总之,ThreadLocal提供了一种方便的方式来实现线程间的数据隔离和传递,能有效地解决多线程并发访问的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [史上最全ThreadLocal 详解](https://blog.csdn.net/qq_43842093/article/details/126715922)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [ThreadLocal详解](https://blog.csdn.net/m0_49508485/article/details/123234587)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

理想万岁万万岁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值