2

JDK成长记11:ThreadLocal (上)

 2 years ago
source link: https://segmentfault.com/a/1190000040841552
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一节你应该学习了thread的基本知识和源码原理,熟悉了线程的应用场景。这一节来学习下和Thread相关的一个类,ThreadLocal。

什么是ThreadLocal?

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">什么是ThreadLocal?</span></h3></div>

字面意思是线程本地变量的意思。用一句话解释就是:线程本地的变量副本,属于每个线程自己独有的。

为什么说是变量副本呢?因为每个线程使用ThreadLocal设置自己的值,设置的值互相之间不受影响,但是使用的是同一个ThreadLocal对象。所以设置的每个变量,是给每个线程一个独有的变量副本。

你可以画一个图来理解下:

当你知道了什么是ThreadLocal后,让我们简单来使用一下它,看下他的使用效果。

Hello ThreadLocal

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> Hello ThreadLocal</span></h3></div>

下面通过一段Hello ThreadLocal小程序,让你回顾下ThreadLocal的使用。假设有这么一个场景:

线程启动了2个线程,使用threadLocal设置了一个Loan对象,main线程也设置了自己的loan对象。线程2和main线程在设置前尝试访问threadLocal中的数据。

代码实现如下:

public class HelloThreadLocal {

  private static ThreadLocal<Loan> threadLocal = new ThreadLocal<Loan>();

  public static void main(String[] args) {
    //线程1 使用threadLocal设置自己的变量副本
    new Thread(() -> {
      threadLocal.set(new Loan("zhangsan", "1000.00"));
      System.out.println("线程-1loan:"+threadLocal.get());

    }).start();


    //线程2 使用threadLocal设置自己的变量副本
    new Thread(() -> {
   try {
        Thread.sleep(1 * 1000);
      } catch (InterruptedException e) {
     }

      HelloThreadLocal.Loan loan = threadLocal.get();
      System.out.println("线程-2loan:"+loan);

      threadLocal.set(new Loan("lisi", "2000.00"));
      loan = threadLocal.get();
      System.out.println("线程-2loan:"+loan);
    }).start();


    try {
      Thread.sleep(5 * 1000);
    } catch (InterruptedException e) {

    }

    System.out.println("main-线程loan:"+threadLocal.get());
    threadLocal.set(new Loan("wangwu", "1000.00"));
    System.out.println("main-线程loan:"+threadLocal.get());

  }

  @Data
  @AllArgsConstructor
  public static class Loan {

    private String name;

    private String amount;

  }

}

上面的代码输出结果如下:

线程-1loan:HelloThreadLocal.Loan(name=zhangsan, amount=1000.00)

线程-2loan:null

线程-2loan:HelloThreadLocal.Loan(name=lisi, amount=2000.00)

main-线程loan:null

main-线程loan:HelloThreadLocal.Loan(name=wangwu, amount=1000.00)

可以看出,每个线程无法获取到其他线程设置的loan对象,哪怕是使用同一个ThreadLocal设置的。为什么会这样呢?其实就是因为每个线程的变量副本,ThreadLocal只是一个工具,操作了线程本地的变量副本而已。具体原理如下图所示:

file

ThreadLocal源码剖析get方法脉络

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> ThreadLocal源码剖析get方法脉络</span></h3></div>

相信你通过上面的例子,已经理解ThreadLocal的作用了。它的底层是如何做到的呢?你需要分析一下它的源码了。

这里我们把栗子简化下,可以更好的分析get、set方法的源码。简化HelloThreadLocal代码如下:

public class ThreadLocalGetMethod {

  private static ThreadLocal<Loan> threadLocal = new ThreadLocal<ThreadLocalGetMethod.Loan>();


  public static void main(String[] args) {
   //线程1
   new Thread(() -> {
     System.out.println("线程-1loan:"+threadLocal.get()); //输出null
   }).start();

  }

 

  @Data
  @AllArgsConstructor
  public static class Loan {

   private String name;

   private String amount;

  }

} 

按照上面的例子,你先new了一个ThreadLocal对象,所以需要看下ThreadLocal构造函数,做了什么事情没有,很明显什么都没做。

  public ThreadLocal() {

  }

之后线程1会直接执行了threadLocal.get操作。让我们看下他的源码:

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

你可以看到,上面的get方法的脉络主要如下:

1. 获取当前线程的一个变量ThreadLocalMap

2. 如果map为空调用setInitialValue返回默认值,并创建map

3. 如果map非空获取entry中key的对应的value值

你可以先画一个图,之后再来分别看下每一步。threadLocal的get方法核心脉络如图所示:

file

ThreadLocal源码剖析get方法细节

<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> ## ThreadLocal源码剖析get方法细节</span></h3></div>

这个是总的脉络图,接下来看一下每一步的细节。

1、获取当前线程的一个变量ThreadLocalMap。

 public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //暂时省略
}

这两句代码第一句是获取到当前运行的线程对象,第二句获取了如下map,可以从注释看出来,实际就是获取了thread对象t的一个属性,这属性是一个ThreadLocalMap。代码如下:

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

不知道各位还记得上一节的thread创建的场景么?当中有一些细节并没有讲,thread除了状态、名字、线程id以外,还有两个比较关键的属性,threadLocals和inheritableThreadLocals。代码如下:

  public class Thread implements Runnable {
     //......(其他源码)
     /* 
     * ThreadLocal使用,当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     */
     ThreadLocal.ThreadLocalMap threadLocals = null;
   
     /*
      * InheritableThreadLocal使用,自父线程继承而来的ThreadLocalMap,
      * 主要用于父子线程间ThreadLocal变量的传递
      */
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......(其他源码)
   }

注释写的清楚,一个用于是父子线程传递的变量副本Map,一般是InheritableThreadLocal才会使用,一个是自己线程变量副本Map一般ThreadLocal使用。

上一节还有一个细节我没有讲,inheritableThreadLocals这个变量在创建线程的时候调用init方法的时候会判断,如果父线程有值复制到子线程一份。代码如下:

/**
   * 初始化一个线程.此函数有两处调用,
   * 1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true
   * 2、传递AccessControlContext,inheritThreadLocals=false
   */
  private void init(ThreadGroup g, Runnable target, String name,
    long stackSize, AccessControlContext acc,
    boolean inheritThreadLocals) {
    //......(其他代码)
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    //......(其他代码)
}

inheritableThreadLocals这个变量的应用在请求跟踪,传递traceId的时候可以被用到。这里不做过多关注,核心还是关注threadLocals这个基本的线程变量副本。

你可以看下整个Map是什么?是一个ThreadLocalMap,它是ThreadLocal的内部类。所以你可以得到如下图所示结论:

file

回过头来再看下,这两行代码,非常关键的一点就是,虽然使用了ThreadLocal的get,但是操作的实际是当前线程的threadLocals本地变量副本的Map,这一点是很重要的。

 public T get() {

    Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    //暂时省略

}

getMap方法执行完成后,流程如下图所示:

file

2、如果map为空,调用setInitialValue返回默认值,并创建map

由于当前线程的获取到副本变量map为null,所以会执行到setInitialValue这个分支,如下所示:

file

所以你要来看下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;
  }

你看完这个代码后,可以发现这个方法核心脉络做了两件事情,一个是初始化值,一个是创建map。

如下图所示:

file

首先上面第一句代码是调用了initialValue方法,从名字上看就是一个初始化的动作,可以看下它的源码,非常简单:

  protected T initialValue() {
    return null;
  }

默认返回null,可以通过重写这个方法或者一个匿名内部类(jdk1.8)来设置一个初始值。

这里我给出大家设置初始值的方式。

使用重写initialValue方式

  private static ThreadLocal<Loan> threadLocal =  new ThreadLocal<Loan>() {
    @Override public Loan initialValue() {
      return new Loan("默认值", "1000.00");
    }
  };

或者使用withInitial,匿名内部类

private static ThreadLocal<Loan> threadLocal = ThreadLocal.withInitial(() -> new Loan("默认值", "1000.00"));  

setInitialValue执行到这里,逻辑很简单,如下:

file

接着进行了if判断,当前线程的本地变量副本threadLocals通过getMap获取到的肯定默认是null,所以会执行创建Map如下图:

file

所以会创建map,执行如下代码

  void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
  }
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
      table = new Entry[INITIAL_CAPACITY];
      int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      table[i] = new Entry(firstKey, firstValue);
      size = 1;
      setThreshold(INITIAL_CAPACITY);
    }

上面这个创建map 的细节我们不过去深入了,ThreadLocalMap不是我们要讲的重点,有兴趣的同学可以看下他的源码,这里给出ThreadLocalMap核心点:

  • 底层层是数组,默认大小16,默认扩容阈值10,扩容阈值计算方式:threshold = len * 2 / 3= 10
  • key是threadLocal,vlaue是ThreadLocal的泛型对象
  • 寻址算法firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  • hash值算法:firstKey.threadLocalHashCode,底层是用了AtomicInteger和一个增量值HASH_INCREMENT,保证
  • Hash冲突使用开放寻址法(HashMap是单链表法)
  • 开放寻址的核心是位置有元素了就换位置

这里value应该是为null,因为initialValue方法没有指定初始化值。

file

最后我们回到get源码的脉络图。经过getMap()、setInitialValue()方法调用后,最终线程threadLocal.get()会输出null的值。如下所示:

file

好了,今天ThreadLocal就学习到这里,下一节我们来探索下:

  • ThreadLocal的set源码原理
  • JVM的中的强引用、弱引用、软引用、虚引用
  • 弱引用在ThreadLocal的应用
  • ThreadLocal内存泄漏问题分析
  • ThreadLocal应用场景举例

    本文由博客一文多发平台 OpenWrite 发布!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK