Java/JUC进阶/Java 并发 - 04 ThreadLocal

本文主要结合源码讲述ThreadLocal 的使用场景和内部结构,以及ThreadLocalMap 的内部结构。

一、使用场景

通常情况下避免多线程问题的三种方法

  • 不使用共享状态变量
  • 状态变量为不可变的
  • 访问共享变量的使用同步

而ThreadLocal 则是通过每个线程独享状态变量的方式,即不使用共享状态变量,来消除多线程的问题:

package com.spring.test;


import java.util.concurrent.TimeUnit;

/**
 * @Author wangli 
 * @Descrintion:
 * @Date : Created in 15:48 2019/6/1
 * @
 */

public class TestThreadLocal {

    private static ThreadLocal<String> local = ThreadLocal.withInitial(() -> "init");

    public static void main(String[] args) throws Exception {
        Runnable r = new RR();
        new Thread(r, "thread1").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(r, "thread2").start();
        System.out.println("exit");
    }

    private static class RR implements  Runnable{

        @Override
        public void run() {
            System.out.println(local.get());
            local.set(Thread.currentThread().getName());
            System.out.println("set local and get:"+ local.get());
        }
    }

}

打印信息 

通过打印结果我们看到线程1 和 线程2 虽然使用的是同一个 ThreadLocal 变量,但是他们之间却没有相互影响;其原因就是每个使用 ThreadLocal 变量的线程都会在各自的线程中保存一份独立的副本,所以各个线程之间没有相互影响;

二、ThreadLocal 结构概述

ThreadLocal 的大体结构如图所示:

  • 在使用ThreadLocal 的时候,是首先获得当前线程
  • 取的线程的成员变量ThreadLocalMap (可以理解和 WeakHashMap相似)
  • 以当前的ThreadLocal 变量作为key ,取到 Entry
  • 最后返回Entry 中的value
/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }



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


/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocalMap.Entry

 static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        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, 并且没有传入 ReferenceQueue;   Reference 单独讲解

WeakReference 表示当传入的 referenet (这里就是ThreadLocal 自身),变成弱引用的时候(即没有强引用指向他的时候); 下一次GC将自动回收弱引用;这里没有传入 ReferenceQueue, 也就是代表不能集中监测回收已弃用的Entry, 而需要再次访问到对应的位置时才能检测到, 具体内容下面还有讲到,注意这也是和 WeakHashMap 最大的两个区别;

注意如果没有手动移除ThreadLocal, 而他又一直以强引用状态存活,就会导致value 无法回收, 至最终OOM; 所以在使用 ThreadLocal 的时候, 最后一定要手动移除

三、ThreadLocalMap 结构概述

1. set

ThreadLocalMap 看名字大致可以知道是类似 于HashMap 的数据结构; 但是有一个重要的区别是 HashMap 使用拉链法解决哈希冲突,而ThreadLocalMap 是使用线性探测法解决哈希冲突;

如图所示, ThreadLocalMap 里面没有链表的结构,当使用 threadLocalHashCode & (len-1) ; 定位到哈希槽时,如果该位置为空则直接插入,如果不为空则检查下一个位置,直到遇到空的哈希槽; 

另外它和我们通常见到的线性探测有点区别,在插入和删除的时候,会有哈希槽的移动;

public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);    // 延迟初始化
}

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();
  
    // 如果 threadLocal 已经存在,则直接用新值替代旧值
    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();
  }
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;
  Entry e;

  int slotToExpunge = staleSlot;
  
  // 以 staleSlot 为基础,向前查找到最前面一个弃用的哈希槽,并确立清除开始位置
  for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
    if (e.get() == null) slotToExpunge = i;

  // 以 staleSlot 为基础,向后查找已经存在的 ThreadLocal
  for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    
    // 如果向后还有目标 ThreadLocal,则交换位置
    if (k == key) {
      e.value = value;

      tab[i] = tab[staleSlot];
      tab[staleSlot] = e;

      // 刚交换的位置如果等于清除开始位置,则将其指向目标位置之后
      if (slotToExpunge == staleSlot) slotToExpunge = i;
      
      // 从开始清除位置开始扫描全表,并清除
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      return;
    }
    
        // 如果在目标位置后面未找到目标 ThreadLocal,则 staleSlot 仍然是目标位置,并将开始清除位置指向后面
    if (k == null && slotToExpunge == staleSlot)
      slotToExpunge = i;
  }
  
  // 在目标位置替换
  tab[staleSlot].value = null;
  tab[staleSlot] = new Entry(key, value);

  // 如果开始清除的位置,不是目标位置,则扫描全表并清除
  if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

其总体思路是:

  • 如果目标位置为空,则直接插入;
  • 如果不为空,则向后查询,看是否有目标key存在,如果存在则交换位置,并插入;
  • 另外还需要确定一个跳跃扫描全表的起始位置,必须是启用的哈希槽,如果目标位置前面有就找最前面的,如果没有就用后面的;

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

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

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

从源码里面也可以看到上面讲的逻辑:

  • 首先获取 ThreadLocalMap, 如果map 为空则初始化,也可以使用 Thread.withInitial(Supplier<? extends S> supplier) ;工厂方法创建以初始值的 ThreadLocal , 或则 直接覆盖 Thread.initialValue() 方法;
  • 然后用哈希定位哈希槽, 如果命中则返回,未命中则向后一次查询;
  • 如果最终未查到,则用 Thread.initivalValue() 方法返回初始值;

3. remove 方法

public void remove() {
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null) m.remove(this);
}

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

移除的逻辑也与HashMap 类似:

  • 首先查找目标哈希槽,然后清楚;
  • 注意这里的清除并非直接将Entry 置为null , 而是先将WeakReferenc的 referent 置为空,在扫描全表;其实是在模拟了WeakReference 清除的过程, 如果ThreadLocal 变成弱引用,在访问一次 ThreadLocalMap , 其清除郭恒一样的;
  • 另外注意这里清除后和HashMap 一样,容量是不会缩小的;

4. ThreadLocal 哈希计算

int index = key.threadLocalHashCode & (len-1);

private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

这里哈希槽的定位仍然是使用的除留余数法,当容量是2的幂时,  hash % length = hash & (length-1); 但是ThreadLocalMap 和 HashMap 有点区别的是, ThreadLocalMap 的key 都是ThreadLocal, 如果这里使用通常意义的哈希计算方法,那肯定每个key 都会发生哈希碰撞; 所以需要用一种方法将相同的key 分开, 并均匀的分布到 2 的幂的数组中;所有就看到了上面的计算方法,ThreadLocal 的哈希值每次增加 0x61c88647 ; 其目的就是将key 均匀的分布到2的幂的数组中。

5. 清除方法

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

private boolean cleanSomeSlots(int i, int n) {
  boolean removed = false;
  Entry[] tab = table;
  int len = tab.length;
  do {
    i = nextIndex(i, len);
    Entry e = tab[i];
    if (e != null && e.get() == null) {
      n = len;
      removed = true;
      i = expungeStaleEntry(i);
    }
  } while ( (n >>>= 1) != 0);
  return removed;
}

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

expungeStaleEntry:

  • 首先清除目标位置
  • 然后向后依次扫描,直到遇到空的哈希槽
  • 如果遇到已弃用的哈希槽则清除,如果遇到因哈希冲突后移ThreadLocal, 则前移;

cleanSomeSlots 则是向后偏移调用 expungeStaleEntry 方法 log(n) 次, cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  连用就可以扫描全表清除已弃用的哈希槽;

6. 扩容方法

private void rehash() {
  expungeStaleEntries();

  // Use lower threshold for doubling to avoid hysteresis
  if (size >= threshold - threshold / 4) resize();
}

private void expungeStaleEntries() {
  Entry[] tab = table;
  int len = tab.length;
  for (int j = 0; j < len; j++) {
    Entry e = tab[j];
    if (e != null && e.get() == null) expungeStaleEntry(j);
  }
}

private void resize() {
  Entry[] oldTab = table;
  int oldLen = oldTab.length;
  int newLen = oldLen * 2;
  Entry[] newTab = new Entry[newLen];
  int count = 0;

  for (int j = 0; j < oldLen; ++j) {
    Entry e = oldTab[j];
    if (e != null) {
      ThreadLocal<?> k = e.get();
      if (k == null) {
        e.value = null; // Help the GC
      } else {
        int h = k.threadLocalHashCode & (newLen - 1);
        while (newTab[h] != null)
          h = nextIndex(h, newLen);
        newTab[h] = e;
        count++;
      }
    }
  }

  setThreshold(newLen);
  size = count;
  table = newTab;
}

扩容时:

  • 首先扫描全表清除已弃用的哈希槽
  • 如果清楚后仍然超过阀值,则扩容
  • 扩容时,容量增加1倍(初始容量为16, 所以变量一直是2的幂),然后将旧表中的值,依次查到新表中

四、InheritableThreadLocal

InheritableThreadLocal 是可以被继承的ThreadLocal ; 在 Thread 中有成员变量用来继承父类的ThreadLocalMap; 

ThreadLocal.ThreadLocalMap  inheritableThreadLocals ;

public class TestThreadlocal {
  private static InheritableThreadLocal<String> local = new InheritableThreadLocal();

  public static void main(String[] args) throws InterruptedException {
    Runnable r = new TT();

    local.set("parent");
    log.info("get: {}", local.get());
    Thread.sleep(1000);
    new Thread(r, "child").start();
    log.info("exit");

  }

  private static class TT implements Runnable {
    @Override
    public void run() {
      log.info(local.get());
      local.set(Thread.currentThread().getName());
      log.info("set local name and get: {}", local.get());
    }
  }
}

总结

  • ThreadLocal 通过线程独占的方式, 也就是隔离的方式, 避免了多线程问题
  • 在使用ThreadLocal 的时候一定要手动移除, 以避免内存泄漏

猜你喜欢

转载自blog.csdn.net/wszhongguolujun/article/details/90368770