源码分析:ThreadLocal的使用以及源码分析

最近项目中用到了ThreadLocal,一直对它的使用原理有疑惑,比如:到底是怎么实现线程间隔离、各个名词之间是什么关系?今天看了看源码,在这里总结一下,分为三个大部分去记录,分别是:简单使用和源码分析

简单使用

使用这块直接上代码了,下边代码模拟了一个service,然后两个线程去跑去修改各自的变量,每个变量不受各自的修改影响,需要说明的是主线程独立于两个线程,这两个线程相当于service两个独立的请求。

package com.momo.learn.learn.test;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author:txxs
 * @description:模拟类
 * @date: Created in 下午12:58 2019/6/22
 */

public class TestService {

  /**
   * 不要static,否则threadLocal没意义
   */
  private String param = "各个线程各自的业务变量";

  /**
   * 加上static,所有线程公用一个就可以
   */
  private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

  /**
   * 业务方法
   */
  public void busMethod(){
    param = "param" + "业务变量修改" + Thread.currentThread().getName();
    System.out.println(Thread.currentThread().getName()+":业务修改变量:"+param);
    threadLocal.set(param);
    System.out.println(Thread.currentThread().getName()+":业务获取变量:"+threadLocal.get());
  }


  /**
   * 模拟同一个服务多个线程跑
   * 1、每个线程有各自变量:param不是static的
   * 2、各个线程修改各自的值,互不受影响
   */
  public static void main(String[] args) {

    ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(100), new ThreadPoolExecutor.CallerRunsPolicy());

    for (int i = 0; i < 2; i++) {
      executor.execute(() -> {
        new TestService().busMethod();
      });
    }

    executor.shutdown();

  }

}

上述代码执行结果

pool-1-thread-1:业务修改变量:param业务变量修改pool-1-thread-1
pool-1-thread-2:业务修改变量:param业务变量修改pool-1-thread-2
pool-1-thread-1:业务获取变量:param业务变量修改pool-1-thread-1
pool-1-thread-2:业务获取变量:param业务变量修改pool-1-thread-2

那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?

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 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。

原理源码分析

在源码分析之前先来一个图,下边这个图说明了Thread、ThreadLocal、ThreadLocalMap、Entry、WeakReference这五者的关系

关系
Thread、ThreadLocal、ThreadLocalMap、Entry、WeakReference

如上图ThreadLocal内部包含ThreadLocalMap,ThreadLocalMap中包含Entry,Entry实现了WeakReference,Thread引用了ThreadLocalMap。

看一下堆栈占用使用情况

栈中有Thread线程和ThreadLocal两者的引用,这两者可以随时被销毁,每次调用都会被创建。堆中存储变量相关的值,当栈对应的引用被销毁的时候,堆中相应的数据也会被回收

ThreadLocal对外暴露了三个重要方法get、set和remove,我们就是通过这三个方法对ThreadLocal进行操作的。现在我们就顺着这三个方法去看,首先看一下set

 /**
   * 范型可以传入各种类型
   */
  public void set(T value) {
    /**
     * 获取当前线程
     */
    Thread t = Thread.currentThread();
    /**
     * 获取当前线程是否已经有ThreadLocalMap对象
     * 1、有的话直接set进去相关的值
     * 2、没有的话,创建一个新的
     * 3、通过这个代码我们可以看到一个Thread只有一个ThreadLocalMap,ThreadLocalMap保存了各个ThreadLocal对应的变量
     */
    ThreadLocalMap map = getMap(t);
    if (map != null)
      map.set(this, value);
    else
      createMap(t, value);
  }

  /**
   * 一个线程t只有一个变量threadLocals
   */
  ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
  }

  /**
   * 给当前线程t,创建了一个ThreadLocalMap对象,并赋值
   */
  void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
  }

  /**
   * ThreadLocalMap的构造方法
   * 这块代码表明ThreadLocalMap的key是ThreadLocal,所以
   * 1、一个ThreadLocal作为key可以存在于多个Thread中
   * 2、一个Thread中又有多个ThreadLocal的key对应的值
   * 3、实际我们就是可以声明多个ThreadLocal进行使用
   */
  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);
  }

  /**
   * ThreadLocalMap的set方法,这个实现和hashmap的实现类似
   */
  private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
      ThreadLocal<?> k = e.get();

      if (k == key) {
        e.value = value;
        return;
      }

      if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
      }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
  }

再看下get方法:

  /**
   * get方法同样是更具当前线程获取它的ThreadLocalMap
   * 1、如果不为空则已当前ThreadLocal为key获取对应的值map.getEntry(this);
   * 2、如果为空则返回初始值
   */
  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();
  }

  /**
   * 根据key获取相关的值
   * 内部都是一个数组
   */
  private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
      return e;
    else
      return getEntryAfterMiss(key, i, e);
  }
  
  /**
   * 设置初始值,同时会把value置为空,会防止内存泄露
   */
  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;
  }

  /**
   * 返回空
   */
  protected T initialValue() {
    return null;
  }

最后再看一下remove方法

  /**
   * 移除key对应的值
   */
  public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
      m.remove(this);
  }

  /**
   * ThreadLocalMap的remove方法
   * 找到对应的值后,通过e.clear();将引用置为空,从而删除相关的Entry
   */
  private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
      if (e.get() == key) {
        e.clear();
        expungeStaleEntry(i);
        return;
      }
    }
  }

  public void clear() {
    this.referent = null;
  }

其他问题

我们看下边的entry类,key是弱引用的,也就是说内存资源在紧张的时候会把key回收调,那么value作为强引用就会一直在内存中不会被销毁

  static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }

那怎么办呢

1、调用remove方法,这个很好理解,看源码就可以

2、调用get()方法,这个就牛逼了,上源码:

  private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
      return e;
    else
      return getEntryAfterMiss(key, i, e);
  }


  /**
   * 上一步是因为不满足e != null && e.get() == key才沦落到调用getEntryAfterMiss的,所以首先e如果为null的话,
   * 那么getEntryAfterMiss还是直接返回null的,如果是不满足e.get() == key,那么进入while循环,这里是不断循环,如果e一直不为空,那么就调用nextIndex,不断递增i,在此过程中一直会做两个判断:
   * 1、如果k==key,那么代表找到了这个所需要的Entry,直接返回;
   * 2、如果k==null,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry清理该Entry。
   * 3、即ThreadLocal Ref销毁时,ThreadLocal实例由于只有Entry中的一条弱引用指着,那么就会被GC掉,Entry的key没了,value可能会内存泄露的,其实在每一个get,set操作时都会不断清理掉这种key为null的Entry的。
   */
  private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
      ThreadLocal<?> k = e.get();
      if (k == key)
        return e;
      if (k == null)
        expungeStaleEntry(i);
      else
        i = nextIndex(i, len);
      e = tab[i];
    }
    return null;
  }

  /**
   * 1、expunge entry at staleSlot:
   * 这段主要是将i位置上的Entry的value设为null,Entry的引用也设为null,那么系统GC的时候自然会清理掉这块内存;
   * 2、Rehash until we encounter null:
   * 这段就是扫描位置staleSlot之后,null之前的Entry数组,清除每一个key为null的Entry,同时若是key不为空,做rehash,调整其位置。
   */
  private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
      ThreadLocal<?> k = e.get();
      if (k == null) {
        e.value = null;
        tab[i] = null;
        size--;
      } else {
        int h = k.threadLocalHashCode & (len - 1);
        if (h != i) {
          tab[i] = null;

          // Unlike Knuth 6.4 Algorithm R, we must scan until
          // null because multiple entries could have been stale.
          while (tab[h] != null)
            h = nextIndex(h, len);
          tab[h] = e;
        }
      }
    }
    return i;
  }

借用总结:

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

参考:

ThreadLocal造成OOM内存溢出案例演示与原理分析 https://blog.csdn.net/xlgen157387/article/details/7829884

ThreadLocal源码深度剖析 https://juejin.im/post/5a5efb1b518825732b19dca4

手撕面试题ThreadLocal!!! https://blog.csdn.net/lirenzuo/article/details/92821256

正确理解Thread Local的原理与适用场景  http://www.jasongj.com/java/threadlocal/

发布了223 篇原创文章 · 获赞 308 · 访问量 84万+

猜你喜欢

转载自blog.csdn.net/maoyeqiu/article/details/93330238