从实例和源码角度简析 ThreadLocal

来源:互联网 发布:福州原子网络传销 编辑:程序博客网 时间:2024/06/08 19:48
  • ThreadLocal 是什么
  • ThreadLocal 的使用
  • ThreadLocal 源码解析
  • ThreadLocal 使用场景

注:此文源码摘自 sun jdk 1.8

ThreadLocal 是什么

打开 ThreadLocal 的源码我们可以看到如下的注释:

这里写图片描述

大致翻译如下:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。例如,以下类生成对每个线程唯一的局部标识符。 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。  import java.util.concurrent.atomic.AtomicInteger; public class UniqueThreadIdGenerator {     private static final AtomicInteger uniqueId = new AtomicInteger(0);     private static final ThreadLocal < Integer > uniqueNum =          new ThreadLocal < Integer > () {             @Override protected Integer initialValue() {                 return uniqueId.getAndIncrement();         }     };     public static int getCurrentThreadId() {         return uniqueId.get();     } } // UniqueThreadIdGenerator每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

简单而言,ThreadLocal 是一个线程自身内部的数据存储类,其它线程是无法访问该线程的数据的。这样说起来还是挺抽象的,我们下面来介绍一个例子。

ThreadLocal 的使用

代码如下:

public class Test {    private static final ThreadLocal<String> threadLocal = new ThreadLocal<String>() {        @Override        protected String initialValue() {            return "default";        }    };    public static void main(String[] args) {        threadLocal.set("thread#0");        System.out.println("thread#0 " + threadLocal.get());        new Thread("thread#1") {            @Override            public void run() {                System.out.println(currentThread().getName() + " " + threadLocal.get());            }        }.start();        new Thread("thread#2") {            @Override            public void run() {                threadLocal.set("thread#2");                System.out.println(currentThread().getName() + " " + threadLocal.get());            }        }.start();    }}

我们首先创建一个 ThreadLocal<String> 对象,并重写它的 initialValue() 方法,这样它默认情况下就是返回一个字符串为 default 的字段。然后我们再 main() 方法里面创建两个线程,线程 thread#1thread#2,当然,加上它本身,一共是三个线程,我们分别在主线程调用 set("thread#0")thread#1 线程不调用 set() 方法;thread#2 调用 set("thread#2") 方法。我们发现,输出结果如下:

这里写图片描述

对于主线程,由于我们调用了 set("thread#0") 方法,所以 threadLocal.get() 的值就是字符串 thread#0;对于 thread#1 线程没有调用 set() 方法,故 threadLocal.get() 返回的就是默认值 default 字符串;对于 thread#2 线程,我们调用了 set("thread#2") 方法,故 threadLoca.get() 返回的就是字符串 thread#2

所以我们可以发现,即使是同一个 ThreadLocal 对象,我们的 set()get() 方法都是仅对当前线程可见的,各个线程之间不可见不可相互影响。

ThreadLocal 源码解析

要想搞清楚 ThreadLocal 为什么会有这样的特性,我们其实只需要搞清楚我们上面所使用到的 set()get() 方法即可。set() 方法源码如下:

这里写图片描述

第一行代码不解释了。我们看到第二行代码可以获取到一个 ThreadLocalMap,我们不妨戳进 getMap() 方法里面看一下,ThreadLocalMap 是如何和 Thread 参数关联在一起的,源码如下:

这里写图片描述

我们再戳进 Thread 类看一下 ——

这里写图片描述

这下清楚了,先获取当前的线程对象,再获取这个对象中的 ThreadLocalMap 对象,并且调用它的 set() 方法将当前的 ThreadLocal 对象和传入的 value 值存入。那么 ThreadLocalMap 又是个什么呢?它的 set() 方法肯定是个关键点,又是怎样的呢?我们再来看看 ——

这里写图片描述

ThreadLocalMap 其实就是 ThreadLocal 的一个内部类

这里写图片描述

这里写图片描述

到这里就一目了然了,ThreadLocalMap 中维护了一个 Entry[]Entry 是一个泛型类,其构造函数需要传入两个参数,一个是 ThreadLocal 对象,作为 Reference,另一个是 Object 对象,作为 value),然后我们就只需要将当前的 ThreadLocal 对象和传入的值放入这个数组的某个位置就可以了。

接下来我们再来看看 get() 方法,其源码如下:

这里写图片描述

这个源码很简单了,就是获取到当前线程对象的所持有的 ThreadLocalMap 对象,传入当前的 ThreadLocal 对象由此获得到对应的 ThreadLocalMap.Entry 对象,再取出它的 value 即可。

到此所有的流程已经走了一遍了,我们再来理一遍思路:首先创建一个全局共享的 ThreadLocal 对象,因为 ThreadLocal 不应该是依赖某一个具体的线程的,然后假设我们使用了三个线程 A、B、C 来对该 ThreadLocal 对象进行操作,线程 A 先调用 ThreadLocal 对象的 set() 方法,那么线程 A 中的 ThreadLocalMap 里面的 Entry 数组就会在某个位置保存这个 ThreadLocal 对象和 set() 进来的值;然后线程 B 调用 ThreadLocal 对象的 set() 方法,那么线程 B 中的 ThreadLocalMap 里面的 Entry 数组就会在某个位置保存这个 ThreadLocal 对象和 set() 进来的值;然后线程 C 调用 ThreadLocal 对象的 set() 方法,那么线程 C 中的 ThreadLocalMap 里面的 Entry 数组就会在某个位置保存这个 ThreadLocal 对象和 set() 进来的值。每个线程所持有的 ThreadLocalMap 对象是不同的,故其 ThreadLocalMap 里面的 Entry 数组也是不同的,故它们之间的 set()get() 方法是互不相见的。

ThreadLocal 使用场景

在 Android 中,ThreadLocal 比较知名的一个场景就是 Looper 的使用,我们可以在 Lopper 类的源码中找到 ThreadLocal 的影子,因为一个线程对应一个 Looper,这时使用 ThreadLocal 就再好不过了。那么为什么要在 Looper 中使用 ThreadLocal,而不能是别的什么替代方案呢?假如我们不使用 ThreadLocal,那么我们可以使用一个全局的哈希表来维护线程和 Looper 的关系,这样显然不如 ThreadLocal 来的方便。一般来说,当某些数据是以线程为作用域并且不同线程之间有不同的数据副本的话,就可以采用 ThreadLocal

扩展链接:理解Java中的ThreadLocal

1 0
原创粉丝点击