Java教程

JDK成长记11:ThreadLocal (上)

本文主要是介绍JDK成长记11:ThreadLocal (上),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

file

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

file

什么是ThreadLocal?

什么是ThreadLocal?

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

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

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

file

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

Hello ThreadLocal

Hello ThreadLocal

下面通过一段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方法脉络

ThreadLocal源码剖析get方法脉络

相信你通过上面的例子,已经理解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方法细节

## ThreadLocal源码剖析get方法细节

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

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应用场景举例
这篇关于JDK成长记11:ThreadLocal (上)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!