likes
comments
collection
share

浅谈ThreadLocal(一)

作者站长头像
站长
· 阅读数 14

前言

ThreadLocal是什么?它可以用来解决什么问题?它是怎么解决这些问题的?如果你也有上述疑问的话,希望本系列文章可以帮你带来一些帮助。

ps: ThreadLocal源码虽然不多但是讲清楚却不容易,因此我打算写为ThreadLocal写系列文章。而且我也不会一上来就直接分析ThreadLocal的set()get()ThreadLocalMap等源码 ,那样不过是粗暴的扒了下源码却体会不到ThreadLocal的设计思想。我更愿意从ThreadLocal作者的角度去分析它,以作者的角度去解答What is ThreadLocal?

正文

1. ThreadLocal是什么

首先ThreadLocal这个变量名容易让人费解,“线程本地”是个啥意思,是“本地线程“吗?网上翻译成中文大部分叫它“线程本地变量”/“线程变量”。但个人理解ThreadLocal是用来管理线程变量的工具类。

1.1 作者眼中的ThreadLocal

每个技术的出现肯定都是为了解决现实问题的,咱们看看作者是怎么介绍的。

浅谈ThreadLocal(一)

大致含义就是ThreadLocal这个类提供线程本地变量(注意图中variables用的是复数,即多个变量),这些变量彼此之间不一样。怎么不一样呢,每个线程都可以通过set()get()操作属于自己的变量。而且ThreadLocal实例一般会设置成静态私有属性,以达到状态和线程关联的目的。

大白话:ThreadLocal用于管理变量副本,这些变量副本在线程之间是独立隔离的。ThreadLocal实例一般会设置成静态私有属性

接着,作者给了我们例子说明,如图:

浅谈ThreadLocal(一)

看代码注释,这个例子是利用ThreadLocal管理了每个线程的id。

public class ThreadId{
    // Atomic integer 存储下一个线程ID
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // threadId存储每个线程的id,可以看到这里ThreadLocal设置成了静态私有属性,一般都会这么用
    private static final ThreadLocal<Integer> threadId =
            new ThreadLocal<Integer>() {
                @Override
                protected Integer initialValue() {
                    return nextId.getAndIncrement();
                }
            };
    // 返回当前线程id
    public static int get() {
        return threadId.get();
    }
    public static void main(String[] args) {
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
    }
}

我们把该案例测试运行一下,结果如下图:

浅谈ThreadLocal(一)

结果: main()中我们创建了4个线程,每个线程都调用了ThreadId.get()获取了id,然后得到了每个线程的ID。

Q1: ThreadId类中的静态变量threadId,它是随着类加载而加载的。也就是说threadId在内存中只有一份,但是为何它能存多个值呢?

Q2: 即使它能存多个值,但是按理说initialValue()是在threadId加载到内存中时才被调用,而类只加载一次,threadId也就只会实例化一次,那么initialValue()也就只调用一次。那为什么程序运行的结果是“0,1,2,3”而不是“0,0,0,0”呢?

A1: 也有Q1问题的小伙伴说明你还记得static修饰符对变量的生命周期的影响。确实,如果把threadId的类型换成任意一个基本数据类型,那么它是不可能可以存多个值的。但是它是TheadLocal呀,它的内部其实是用了一个Map去存数据的。千万别误会这个map是不是用了多个key存不同线程的id。因为这个map只有一个key就是threadLocal。而在本例中threadLocal是静态的,那么它在内存中只有一份,也就是说4个线程的key相同的。真正的奥义在于这个map是不同的。这里先给结论:ThreadLocal是利用ThreadLocalMap存值的,而它能存多个线程id的原因是每个线程都有自己的ThreadLocalMap,这样即使key相同,但是这个key存在不同map中从而实现存多个线程id的目的。 具体实现我们后面看源码。

A2: 我们先看看作者对initialValue()的说明

浅谈ThreadLocal(一)

第一段大致是说:这个方法返回当前线程“thread-local”变量的“初始值”。 这个方法被调用的情况:

  1. 第一次调用set()前调用了get()去取值。也就是没有设置值时就去取值了那么会先调用initialValue()初始化一个值。

一般情况下,这个方法每个线程最多被调用一次,除非调用remove()后马上又调用了get()

第二段大致是说:这个方法一般返回null,如果程序员想要返回其他值那么必须继承ThreadLocal类去重写initialValue()。一般我们会用匿名内部类的方式重写initialValue()

结论: 看到这大致懂了,首先上述案例里我们没有调用set()直接get()去取值,因此initialValue()会执行。其次,我们用匿名内部类的方式重写了initialValue(),让它每次返回不同的线程id。等等,好像还是没解释initialValue()为什么会执行多次,按作者的话说每个线程最多执行一次。眼尖的小伙伴可能已经发现了,每个线程最多执行一次,我们有4个线程,因此执行4次也是无可厚非的。那么为什么每启动一个线程就会调用一次呢?ThreadLocal和Thread之间的关系到底如何?

下篇:传送门