详解-ThreadLocal-底层原理

1,289 阅读5分钟

ThreadLocal到底是什么

  • 案例演示

我们发现所有线程都是操作的同一个变量,现在有一个需求我们想要这些线程操作的变量是一个确切的值,所以我们引入 ThreadLocal 类

  • 案例演示

ThreadLocal原理分析

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal而言即为 Integer 类型变量),在不同的 Thread 中有不同的副本(实际上是不同的实例):

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。
  • 既然其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都会被回收。

因此ThreadLocal 非常适用于这样的场景:每个线程需要自己独立的实例且该实例需要在多个方法中使用。

ThreadLocal源码分析

分析源码首先要找到入口,我们就以 threadLocal.get(); 方法作为切入点。

  • ThreadLocal 是如何存储每个线程的实例
  • ThreadLocal 是什么时候进行初始化的
   public T get() {
  		//拿到当前线程
        Thread t = Thread.currentThread();
  		//拿到 ThreadLocalMap 属性的对象,看名字和 Map 有关系
        ThreadLocalMap map = getMap(t);
  		//判断拿到的对象是否为空
        if (map != null) {
  			//从 Map 中拿到当前 key 为当前对象的 Entry 
            ThreadLocalMap.Entry e = map.getEntry(this);
  			//如果不等于 null,拿到他的value并返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
  		//如果为空则调用 setInitialValue() 方法
        return setInitialValue();
    }
  • 如果这个线程第一次调用getMap()方法,返回值一定为null
  ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  //初始值为null
  ThreadLocal.ThreadLocalMap threadLocals = null;
  • 所以第一次一定会调用setInitialValue()方法
    private T setInitialValue() {
  		//调用重写的initialValue()方法	
        T value = initialValue();
  		//拿到当前线程
        Thread t = Thread.currentThread();
  		//再尝试获取一次
        ThreadLocalMap map = getMap(t);
  		//如果获取到了,就更新value
        if (map != null)
            map.set(this, value);
  	 	//没获取到就创建一个Map
        else
            createMap(t, value);
  		//把拿到的值返回回去
        return value;
    }
  • 先看一下initialValue()方法,他是一个空壳方法默认返回 null
    protected T initialValue() {
        return null;
    }

我们在创建 ThreadLocal 的时候通过ThreadLocal.withInitial(() -> { return 0; }); 来创建的

    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
      return new SuppliedThreadLocal<>(supplier);
  }
  //我们可以看到 ThreadLocal 的静态内部类继承了 ThreadLocal 类,而且覆写了 initialValue 方法
  static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
      private final Supplier<? extends T> supplier;
      SuppliedThreadLocal(Supplier<? extends T> supplier) {
          this.supplier = Objects.requireNonNull(supplier);
      }
      @Override
      protected T initialValue() {
          return supplier.get();
      }
  }
  • createMap(t, value);方法创建了 ThreadLocalMap 对象并且赋值给线程的 threadLocals 属性
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 刨根问底,再看一下 ThreadLocalMap 对象的组成
       ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  			//创建一个 Entry 集合默认长度为16
            table = new Entry[INITIAL_CAPACITY];
  			//这句话很重要,后面的set()方法再分析
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  			//创建Entry节点放入table属性中
            table[i] = new Entry(firstKey, firstValue);
  			//设置已存在的节点个数
            size = 1;
  		    //设置触发扩容的条件
            setThreshold(INITIAL_CAPACITY);
        }
  • 接着看创建Entry节点
  static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

重点来了,Entry节点继承了 WeakReference 类,WeakReference这个类代表的弱引用,也就是说,如果这个 threadlocal 对象设为null,正常情况下是不会被GC的,因为他被threadLocalMap 引用了,但是他设置为了弱引用,那么就会忽略这个引用进行GC。

  • 到此位置调用get()方法的流程分析完毕了,以图的形式来更好地理解

  • 既然 ThreadLocalMap 是个集合那么就一定会有多个 ThreadLocal 对象

  • 接着分析threadlocal.set()方法

	private void set(ThreadLocal<?> key, Object value) {
  			//拿到当前线程的Entry集合
            Entry[] tab = table;
  			//得到threadLocals的长度
            int len = tab.length;
  			//简单理解为计算Hash值,但和普通的计算方法不一样
            int i = key.threadLocalHashCode & (len-1);
			//拿到Hash值对应的Entry对象
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				//更新value值
                if (k == key) {
                    e.value = value;
                    return;
                }
				//如果 key 为null,也就是说 threadlocal 对象被设置为null,清空对应的Entry
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//如果为Entry对象为null,则创建Entry
            tab[i] = new Entry(key, value);
			//Entry对象的个数加一
            int sz = ++size;
			//进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
  • 计算Hash值的方法

可以看到和我们的 HashMap 计算方式有所区别,他会根据集合的长度,每次调用得到一个小于长度,并且不会和以前相同的数,也就是说他没有链表的结构,由于 ThreadLocal 他并不是一个专门存储数据的结构,他仅仅只是为了解决数据共享的安全问题,所以他认为不需要再使用链表来增加复杂度。

  • 简单了解一下扩容方法
  1. 创建一个新的 Entry集合将原来的长度扩大为2倍
  2. 重新计算hash值,将原集合中的数据放入对应的Entry中
  3. 更新 threadLocal 对象中的属性
  • 简单了解 remove 方法
  1. 计算threadLocal对象对应的hash值
  2. 删除节点
  • 需要注意的问题 ThreadLocal 他仅仅只能解决基本类型的安全问题,如果你在 withInitial() 方法中返回了一个已经存在的对象,那么每个线程操作的依然是唯一的那个对象,所以解决不了对象的安全问题。