前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发-ThreadLocal

Java并发-ThreadLocal

作者头像
lpe234
发布2021-03-02 15:34:46
3870
发布2021-03-02 15:34:46
举报
文章被收录于专栏:若是烟花若是烟花

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

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

1 ThreadLocal

1.1 源码简单分析
1.1.1 ThreadLocal#set方法
代码语言:javascript
复制
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方法
代码语言:javascript
复制
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方法
代码语言:javascript
复制
public void remove() {
  // 拿到当前Thread的`ThreadLocalMap`对象
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    m.remove(this);
}

调用ThreadLocalMap.remove方法,移除ThreadLocal

1.1.4 ThreadLocalMap静态内部类

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

代码语言:javascript
复制
// 内部类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方法
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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

代码语言:javascript
复制
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类

代码语言:javascript
复制
/* 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类

代码语言:javascript
复制
// 继承自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目标场景。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 ThreadLocal
    • 1.1 源码简单分析
      • 1.1.1 ThreadLocal#set方法
      • 1.1.2 ThreadLocal#get方法
      • 1.1.3 ThreadLocal#remove方法
      • 1.1.4 ThreadLocalMap静态内部类
      • 1.1.5 ThreadLocalMap#set方法
      • 1.1.6 ThreadLocalMap#getEntry
      • 1.1.7 ThreadLocalMap#remove
    • 1.2 Java中的引用类型
      • 1.2.1 强引用(StrongReference)
      • 1.2.2 软引用(SoftReference)
      • 1.2.3 弱引用(WeakReferencw)
      • 1.2.4 虚引用(PhantomReference)
      • 1.2.5 引用队列(ReferenceQueue)
    • 1.3 内存泄露
      • 1.4 使用示例
        • 1.5 总结
        • 2 InheritableThreadLocal
        • 3 TransmittableThreadLocal
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档