Java并发-ThreadLocal

原创
2021/02/16 23:23
阅读数 85

线程局部变量。每个访问该变量的线程都有自己独立的初始化副本。ThreadLocal实例通常是类中私有静态字段,将状态与线程(用户ID、事务ID等)想关联。

每个线程内部都有一个ThreadLocalMap,每个ThreadLocalMap里面都有一个Entry[]数组,Entry对象由ThreadLocal数据组成。

1 ThreadLocal

1.1 源码简单分析

1.1.1 ThreadLocal#set方法
public void set(T value) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 从当前线程获取ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null)
    // map不为空设置值
    map.set(this, value);
  else
    // 初始化map
    createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

获取当前线程,然后根据当前线程获取ThreadLocalMap。后续操作均针对ThreadLocalMap

1.1.2 ThreadLocal#get方法
public T get() {
  Thread t = Thread.currentThread();
  // 拿到当前线程的ThreadLocalMap
  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;
    }
  }
  // 获取值时,若map为空则初始化map
  return setInitialValue();
}

private T setInitialValue() {
  // 初始化null值
  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() {
  // 初始化null值
  return null;
}

调用ThreadLocalMap.get方法,获取当前值。

1.1.3 ThreadLocal#remove方法
public void remove() {
  // 拿到当前Thread的`ThreadLocalMap`对象
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    m.remove(this);
}

调用ThreadLocalMap.remove方法,移除ThreadLocal

1.1.4 ThreadLocalMap静态内部类

从以上setgetremove方法来看,所有操作都是基于该内部类来实现功能的。

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

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

// 初始化容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组(开放地址法处理冲突)
private Entry[] table;
// 元素个数
private int size = 0;
// 扩容阈值 table长度*2/3
private int threshold; // Default to 0
private void setThreshold(int len) {
  threshold = len * 2 / 3;
}

Entry的key为ThreadLocal,并且继承WeakReference弱引用。ThreadLocalMap使用开放地址法处理哈希冲突。

1.1.5 ThreadLocalMap#set方法
private void set(ThreadLocal<?> key, Object value) {

  // We don't use a fast path as with get() because it is at
  // least as common to use set() to create new entries as
  // it is to replace existing ones, in which case, a fast
  // path would fail more often than not.

  Entry[] tab = table;
  int len = tab.length;
  // 跟ThreadLocal的HashCode和(len-1)进行`与`操作,得到数组索引值。(需确保len为2的倍数)
  int i = key.threadLocalHashCode & (len-1);

  // 因为使用开放地址法
  // 所以需要从当前索引位置开始,不断的向后查找,直到key值相等或者遇到Entry=null值
  // 因为有阈值threshold的存在,所以不会形成死循环(肯定存在null值)
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    // 找到key,更新value
    if (k == key) {
      e.value = value;
      return;
    }

    // 若key为null,此时Entry不为null,说明ThreadLocal已被回收
    if (k == null) {
      // 替换陈旧条目
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  // 生成新Entry
  tab[i] = new Entry(key, value);
  // size增加
  int sz = ++size;
  // 清除数据,若没有 并且 size不小于阈值 则进行rehash
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

private void rehash() {
  // 清除所有已经废弃的节点
  expungeStaleEntries();

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

每个ThreadLocal对象都有一个threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加0x61c88647大小。插入时根据threadLocal对象的hash值,定位哈希表Entry[] table上的位置:

  1. 若位置为null,则新建一个Entry放置到给位置。调用cleanSomeSlots清除key为null,entry不为null的Entry,若没有要清除的则判断后续是否要进行扩容
  2. 若位置已经有Entry对象,则判断key是否相同,相同则直接将value替换为新值。
  3. 若不相同,则说明可能没有该key或者已经发生冲突。则继续向后走,直到遇到相同key或者遇到null值。
1.1.6 ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
  // 找到Entry[] table中的哈希索引位置
  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;

  // 直到Entry为null时,停止查找(说明该key值不存在)
  while (e != null) {
    ThreadLocal<?> k = e.get();
    // 找到key 直接返回
    if (k == key)
      return e;
    // key为null时,此时entry不为null,清除该陈旧entry
    if (k == null)
      expungeStaleEntry(i);
    else
      // key值不相等,继续查找下一个
      i = nextIndex(i, len);
    e = tab[i];
  }
  // key值不存在,返回null
  return null;
}

getEntry大概流程:

  1. 根据ThreadLocal的hashCode查找到该Entry[] table数组的索引位置,若该索引值不为null,则判断key是否相同。key值相同则直接返回。
  2. 若不同则向后不断遍历,直至Entry为null,说明不存在key,返回null。
  3. 若向后遍历过程中遇到key,则直接返回;若中间遇到key为null时,说明ThreadLocal已被释放需清除;若未遇到key则继续向后查找。
1.1.7 ThreadLocalMap#remove
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) {
      // Entry清除,key置为null
      e.clear();
      // 清除key为null,entry不为null的陈旧Entry
      expungeStaleEntry(i);
      return;
    }
  }
}

1.2 Java中的引用类型

Java中除了原始数据类型的变量,其他的都是引用类型。共有四种引用类型:强引用、软引用、弱引用、虚引用。

1.2.1 强引用(StrongReference)

被强引用的对象不会被垃圾回收器主动回收,即使抛出OOM异常,使程序终止。

1.2.2 软引用(SoftReference)

软引用的生命周期比强引用的短一些,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。JVM会确保在抛出OOM异常前,清理软引用对象。可以和一个引用队列(ReferenceQueue)联合使用。

应用场景:通常用来实现对内存敏感的缓存。还有空闲内存可暂时保留,内存不足时直接清理掉。

1.2.3 弱引用(WeakReferencw)

弱引用生命周期比软引用更短一些。在垃圾回收器扫描内存时,发现有弱引用对象会直接回收。可以和一个引用队列(ReferenceQueue)联合使用。

应用场景:可用于内存敏感的缓存。

1.2.4 虚引用(PhantomReference)

无法通过虚引用来访问对象的任何属性或函数。虚引用仅仅提供了一直确保对象被finalize后,做某些事情的机制。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾收集器准备回收某个对象时,若发现它还有虚引用,则会在回收对象的内存之前,将这个虚引用加入到与之关联的引用队列中。

应用场景:可用来跟踪对象被垃圾回收器回收的活动。

1.2.5 引用队列(ReferenceQueue)

引用队列可以和软引用、弱引用、虚引用一起配合使用,当垃圾回收器回收一个对象时,若发现它还有引用,就会在回收对象之前将这个引用加入到与之关联的引用队列中去。可以通过判断引用队列中是否已加入了引用,来判断对象是否将要被垃圾回收,就可以在对象被回收之前做一些操作。

1.3 内存泄露

严格来说,ThreadLocal没有内存泄露问题,本身提供了remove方法来移除。一般来说线程的生命周期比较长,若不主动remove则不会被释放。在get/set方法中可以看到,当发现有key==null && entry!=null的情况时,会主动释放。为了避免出现内存泄露问题,使用完毕后一定要主动调用remove释放。

强依赖关系:Thread-->ThreadLocalMap-->Entry-->value

1.4 使用示例

SimpleDateFormat不是线程安全的。主要是因为在SimpleDateFormat的父类DateFormat中的Calendar对象使用int fields[]来存储当前设置的时间值,并发访问时有可能出现数据异常,故称之为线程不安全。

解决方法有好几个:①将SimpleDateFormat定义为局部变量,每次调用时创建,调用结束后销毁;②方法加同步锁,避免多线程同时访问;③使用ThreadLocal,使每个线程都有自己的SimpleDateFormat

ThreadLocal<SimpleDateFormat> dateFormatThreadLocal1 = new ThreadLocal<SimpleDateFormat>() {
	@Override
	protected SimpleDateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	}
};

ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() ->  new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

1.5 总结

  • ThreadLocal使用开放地址法解决哈希冲突,数组默认大小16,扩容阈值为2/3,扩容大小为之前的2倍。
  • 使用完毕后要主动调用remove进行清除数据。
  • ThreadLocal无法向子线程中传递数据。
  • 存在线程复用时,需谨慎使用ThreadLocal

2 InheritableThreadLocal

InheritableThreadLocal主要解决了ThreadLocal在父子线程之间无法传值的问题,其实就是无法在子线程中获取父线程的ThreadLocal

Thread类

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
  // ...
  // 当前线程(父线程)
  Thread parent = currentThread();
  // ...
  // 子线程拷贝父线程inheritableThreadLocals
  if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  // ...
}

父线程在调用Thread构造方法生成一个子线程时,构造方法最终会调用Thread#init方法。如果父线程中存在inheritableThreadLocals,则将值设置(拷贝)到子线程中。

InheritableThreadLocal类

// 继承自ThreadLocal类

// 获取map时,返回线程的inheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
  return t.inheritableThreadLocals;
}

// 创建map时,赋值给inheritableThreadLocals
void createMap(Thread t, T firstValue) {
  t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

3 TransmittableThreadLocal

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

ThreadLocal的需求场景即TransmittableThreadLocal的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值』则是TransmittableThreadLocal目标场景。

https://github.com/alibaba/transmittable-thread-local 阿里开源项目,后续再做研究~

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部