面试中的 ThreadLocal 原理和使用场景

论坛 期权论坛 期权     
纯洁的微笑   2019-6-10 02:38   2913   0
点击上方“蓝字”
技术的故事还有很多  
只想与你
静静分享  


N


公众号后台回复“java”,获得作者Java 知识体系/面试必看资料

相信大家不管是在网上做题还是在面试中都经常被问过 ThreadLocal 的原理和用法,虽然一直知道这个东西的存在但是一直没有好好的研究一下原理,没有自己的知识体系。今天花点时间好好学习了一下,分享给有需要的朋友。
[h2]
  1. ThreadLocal
复制代码
是什么[/h2]ThreadLocal 是 JDK
  1. java.lang
复制代码
包中的一个用来实现相同线程数据共享不同的线程数据隔离的一个工具。 我们来看下 JDK 源码中是如何解释的:
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).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
大致的意思是
ThreadLocal 这个类提供线程局部变量,这些变量与其他正常的变量的不同之处在于,每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本;ThreadLocal 实例变量通常采用
  1. private static
复制代码
在类中修饰。
只要 ThreadLocal 的变量能被访问,并且线程存活,那每个线程都会持有 ThreadLocal 变量的副本。当一个线程结束时,它所持有的所有 ThreadLocal 相对的实例副本都可被回收。
一句话说就是 ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用(相同线程数据共享),也就是变量在线程间隔离(不同的线程数据隔离)而在方法或类间共享的场景。
[h2]
  1. ThreadLocal
复制代码
使用[/h2]我们先通过两个例子来看一下
  1. ThreadLocal
复制代码
的使用
例子 1 普通变量
  1. [/code][code]import java.util.concurrent.CountDownLatch;
复制代码
  1. [/code][code]
复制代码
  1. public class MyStringDemo {
复制代码
  1.     private String string;
复制代码
  1. [/code][code]    private String getString() {
复制代码
  1.         return string;
复制代码
  1.     }
复制代码
  1. [/code][code]    private void setString(String string) {
复制代码
  1.         this.string = string;
复制代码
  1.     }
复制代码
  1. [/code][code]    public static void main(String[] args) {
复制代码
  1.         int threads = 9;
复制代码
  1.         MyStringDemo demo = new MyStringDemo();
复制代码
  1.         CountDownLatch countDownLatch = new CountDownLatch(threads);
复制代码
  1.         for (int i = 0; i < threads; i++) {
复制代码
  1.             Thread thread = new Thread(() -> {
复制代码
  1.                 demo.setString(Thread.currentThread().getName());
复制代码
  1.                 System.out.println(demo.getString());
复制代码
  1.                 countDownLatch.countDown();
复制代码
  1.             }, "thread - " + i);
复制代码
  1.             thread.start();
复制代码
  1.         }
复制代码
  1. [/code][code]    }
复制代码
  1. [/code][code]}
复制代码
  1. [/code]程序的运行的随机结果如下:
  2. [list][*][*][*][*][*][*][*][*][*][*][*][*][*][/list][code]
复制代码
  1. thread - 1
复制代码
  1. thread - 2
复制代码
  1. thread - 1
复制代码
  1. thread - 3
复制代码
  1. thread - 4
复制代码
  1. thread - 5
复制代码
  1. thread - 6
复制代码
  1. thread - 7
复制代码
  1. thread - 8
复制代码
  1. [/code][code]Process finished with exit code 0
复制代码
  1. [/code]从结果我们可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 [code]ThreadLocal
复制代码
变量的方式来解决这个问题的例子。
例子 2
  1. ThreadLocal
复制代码
变量
  1. [/code][code]import java.util.concurrent.CountDownLatch;
复制代码
  1. [/code][code]
复制代码
  1. public class MyThreadLocalStringDemo {
复制代码
  1.     private static ThreadLocal threadLocal = new ThreadLocal();
复制代码
  1. [/code][code]    private String getString() {
复制代码
  1.         return threadLocal.get();
复制代码
  1.     }
复制代码
  1. [/code][code]    private void setString(String string) {
复制代码
  1.         threadLocal.set(string);
复制代码
  1.     }
复制代码
  1. [/code][code]    public static void main(String[] args) {
复制代码
  1.         int threads = 9;
复制代码
  1.         MyThreadLocalStringDemo demo = new MyThreadLocalStringDemo();
复制代码
  1.         CountDownLatch countDownLatch = new CountDownLatch(threads);
复制代码
  1.         for (int i = 0; i < threads; i++) {
复制代码
  1.             Thread thread = new Thread(() -> {
复制代码
  1.                 demo.setString(Thread.currentThread().getName());
复制代码
  1.                 System.out.println(demo.getString());
复制代码
  1.                 countDownLatch.countDown();
复制代码
  1.             }, "thread - " + i);
复制代码
  1.             thread.start();
复制代码
  1.         }
复制代码
  1.     }
复制代码
  1. [/code][code]}
复制代码
  1. [/code][code]
复制代码
程序运行结果
  1. thread - 0
复制代码
  1. thread - 1
复制代码
  1. thread - 2
复制代码
  1. thread - 3
复制代码
  1. thread - 4
复制代码
  1. thread - 5
复制代码
  1. thread - 6
复制代码
  1. thread - 7
复制代码
  1. thread - 8
复制代码
  1. [/code][code]Process finished with exit code 0
复制代码
  1. [/code]从结果来看,这次我们很好的解决了多线程之间数据隔离的问题,十分方便。
  2. 这里可能有的朋友会觉得在例子 1 中我们完全可以通过加锁来实现这个功能。是的没错,加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是多线程共享数据的问题。假如我们这里除了[code]getString()
复制代码
之外还有很多其他方法也要用到这个 String,这个时候各个方法之间就没有显式的数据传递过程了,都可以直接中
  1. ThreadLocal
复制代码
变量中获取,这才是
  1. ThreadLocal
复制代码
的核心,相同线程数据共享不同的线程数据隔离。
由于
  1. ThreadLocal
复制代码
是支持泛型的,这里采用的是存放一个
  1. String
复制代码
来演示,其实可以存放任何类型,效果都是一样的。
[h2]
  1. ThreadLocal
复制代码
源码分析[/h2]在分析源码前我们明白一个事那就是对象实例与
  1. ThreadLocal
复制代码
变量的映射关系是由线程
  1. Thread
复制代码
来维护的,对象实例与
  1. ThreadLocal
复制代码
变量的映射关系是由线程
  1. Thread
复制代码
来维护的,对象实例与
  1. ThreadLocal
复制代码
变量的映射关系是由线程
  1. Thread
复制代码
来维护的。重要的事情说三遍。换句话说就是对象实例与
  1. ThreadLocal
复制代码
变量的映射关系是存放的一个
  1. Map
复制代码
里面(这个
  1. Map
复制代码
是个抽象的
  1. Map
复制代码
并不是
  1. java.util
复制代码
中的
  1. Map
复制代码
),而这个
  1. Map
复制代码
  1. Thread
复制代码
类的一个字段!而真正存放映射关系的
  1. Map
复制代码
就是
  1. ThreadLocalMap
复制代码
。下面我们通过源码的中几个方法来看一下具体的实现。
  1. [/code][code]//set 方法
复制代码
  1. public void set(T value) {
复制代码
  1.     Thread t = Thread.currentThread();
复制代码
  1.     ThreadLocalMap map = getMap(t);
复制代码
  1.     if (map != null)
复制代码
  1.         map.set(this, value);
复制代码
  1.     else
复制代码
  1.         createMap(t, value);
复制代码
  1. }
复制代码
  1. [/code][code]//获取线程中的ThreadLocalMap 字段!!
复制代码
  1. ThreadLocalMap getMap(Thread t) {
复制代码
  1.     return t.threadLocals;
复制代码
  1. }
复制代码
  1. [/code][code]//创建线程的变量
复制代码
  1. void createMap(Thread t, T firstValue) {
复制代码
  1.      t.threadLocals = new ThreadLocalMap(this, firstValue);
复制代码
  1. }
复制代码
  1. [/code]在 [code]set
复制代码
方法中首先获取当前线程,然后通过
  1. getMap
复制代码
获取到当前线程的
  1. ThreadLocalMap
复制代码
类型的变量
  1. threadLocals
复制代码
,如果存在则直接赋值,如果不存在则给该线程创建
  1. ThreadLocalMap
复制代码
变量并赋值。赋值的时候这里的
  1. this
复制代码
就是调用变量的对象实例本身。
  1. [/code][code]public T get() {
复制代码
  1.     Thread t = Thread.currentThread();
复制代码
  1.     ThreadLocalMap map = getMap(t);
复制代码
  1.     if (map != null) {
复制代码
  1.         ThreadLocalMap.Entry e = map.getEntry(this);
复制代码
  1.         if (e != null) {
复制代码
  1.             @SuppressWarnings("unchecked")
复制代码
  1.             T result = (T)e.value;
复制代码
  1.             return result;
复制代码
  1.         }
复制代码
  1.     }
复制代码
  1.     return setInitialValue();
复制代码
  1. }
复制代码
  1. [/code][code]
复制代码
  1. private T setInitialValue() {
复制代码
  1.     T value = initialValue();
复制代码
  1.     Thread t = Thread.currentThread();
复制代码
  1.     ThreadLocalMap map = getMap(t);
复制代码
  1.     if (map != null)
复制代码
  1.         map.set(this, value);
复制代码
  1.     else
复制代码
  1.         createMap(t, value);
复制代码
  1.     return value;
复制代码
  1. }
复制代码
  1. [/code][code]get
复制代码
方法也比较简单,同样也是先获取当前线程的
  1. ThreadLocalMap
复制代码
变量,如果存在则返回值,不存在则创建并返回初始值。
[h2]
  1. ThreadLocalMap
复制代码
源码分析[/h2]
  1. ThreadLocal
复制代码
的底层实现都是通过
  1. ThreadLocalMap
复制代码
来实现的,我们先看下
  1. ThreadLocalMap
复制代码
的定义,然后再看下相应的
  1. set
复制代码
  1. get
复制代码
方法。
  1. [/code][code]static class ThreadLocalMap {
复制代码
  1. [/code][code]    /**
复制代码
  1.      * The entries in this hash map extend WeakReference, using
复制代码
  1.      * its main ref field as the key (which is always a
复制代码
  1.      * ThreadLocal object).  Note that null keys (i.e. entry.get()
复制代码
  1.      * == null) mean that the key is no longer referenced, so the
复制代码
  1.      * entry can be expunged from table.  Such entries are referred to
复制代码
  1.      * as "stale entries" in the code that follows.
复制代码
  1.      */
复制代码
  1.     static class Entry extends WeakReference k = e.get();
复制代码
  1. [/code][code]        //k 相等则覆盖旧值
复制代码
  1.         if (k == key) {
复制代码
  1.             e.value = value;
复制代码
  1.             return;
复制代码
  1.         }
复制代码
  1. [/code][code]        //此时说明此处 Entry 的 k 中的对象实例已经被回收了,需要替换掉这个位置的 key 和 value
复制代码
  1.         if (k == null) {
复制代码
  1.             replaceStaleEntry(key, value, i);
复制代码
  1.             return;
复制代码
  1.         }
复制代码
  1.     }
复制代码
  1. [/code][code]    //创建 Entry 对象
复制代码
  1.     tab[i] = new Entry(key, value);
复制代码
  1.     int sz = ++size;
复制代码
  1.     if (!cleanSomeSlots(i, sz) && sz >= threshold)
复制代码
  1.         rehash();
复制代码
  1. }
复制代码
  1. [/code][code]
复制代码
  1. //获取 Entry
复制代码
  1. private Entry getEntry(ThreadLocal key) {
复制代码
  1.     int i = key.threadLocalHashCode & (table.length - 1);
复制代码
  1.     Entry e = table[i];
复制代码
  1.     if (e != null && e.get() == key)
复制代码
  1.         return e;
复制代码
  1.     else
复制代码
  1.         return getEntryAfterMiss(key, i, e);
复制代码
  1. }
复制代码
  1. [/code][code]
复制代码
至此我们看完了
  1. ThreadLocal
复制代码
相关的 JDK 源码,我自己也有了更深入的了解,也希望能帮助到大家。
[h2]小结[/h2]在平时忙碌的工作中我们经常解决的是一个业务的需求,往往很少会涉及到底层的源码或者框架的具体实现代码。 其实这是很不好的,其实很多的东西的原理都是一样的,我们需要经常去看一下源码,了解一些底层的实现,不能总是停留在表层,代码看到多了,才能写出好的代码,并且还能学到很多东西。 随着我们知道的越来越多,我们会发现我们不知道的也越来越多。加油,共勉!

作者介绍:子悠,一个有点文艺有点技术宅的深漂程序员。

作者赞赏码




Java 极客技术公众号,是由一群热爱 Java 开发的技术人组建成立,专注分享原创、高质量的 Java 文章。如果您觉得我们的文章还不错,请帮忙赞赏、在看、转发支持,鼓励我们分享出更好的文章。



分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:68
帖子:6
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP