ThreadLocal-线程安全利器

原文链接:https://www.jylt.cc/#/detail?activityIndex=2&id=9df3fd62d6ee13ff555c30157798b092

ThreadLocal是什么

ThreadLocal用来提供线程内部的局部变量,是各个线程独有的,该变量无法被其他线程访问。主要有以下作用:

  • 解决共享变量线程安全问题
  • 线程生命周期内全局参数传递

其中最典型的应用是对数据库连接池的处理。可以参考这篇文章阅读:ThreadLocal在数据库连接中的应用

ThreadLocal怎么用

ThreadLocal主要有以下几个方法:

public void set(T value); // 存值
public T get(); // 取值
public void remove(); // 移除线程局部变量的引用

ThreadLocal底层原理

多线程的时候 ThreadLocal 为什么能够做到线程数据的隔离呢?原因是由于每个线程Thread都维护了一个 ThreadLocal.ThreadLocalMap 的引用,而 ThreadLocalMap 就是存放 ThreadLocal 值的地方。引用关系如下图:https://oss.jylt.cc/img/content/32ef85ebfc3b3eb8ef83903ee3757cc0/a09b1f73-34ce-4394-b78c-4697d1a1683f.png

下面根据源码来解释以下上面的引用关系。

Thread类

public class Thread implements Runnable {
    // Thread拥有ThreadLocal.ThreadLocalMap的引用
    ThreadLocal.ThreadLocalMap threadLocals = null
} 

ThreadLocal 类结构

注意上图的红线部分和 Entry extends WeakReference。Entry的key被包装成了弱引用是什么原因呢?

首先要知道弱引用的作用,我们都知道平时我们创建对象 Object o = new Object() ,这种方式是强引用,在对象 o使用完之前,该对象是不会被垃圾回收的,因为该对象是可达状态;该对象使用完之后,是可以被垃圾回收的,因为该对象是不可达的。如果 o是弱引用对象,并且没有其他强引用对象对其引用时,不管任何收执行GC,对象 o都会被垃圾回收掉。

可以看出红线部分的设计是为了防止key长时间无法被GC,导致内存溢出。

public class ThreadLocal<T> {
	// ThreadLocal的内部类,这个也就是上面Thread里面持有的threadLocals对象
	static class ThreadLocalMap {
	// Entry是具体存放ThreadLocal数据的容器,可以发现Entry的数据结构跟Hash Map的是比较像的,都是<key,value>形式。此处的Entry的key是ThreadLocal对象,下面会说到
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  
	// table是Entry数组的原因是,每个线程可能会有多个ThreadLocal对象,这个时候,需要将不同ThreadLocal对象对应的值放到不同下标的Entry数组中,具体如何存放的下面会说到
	private Entry[] table;
    }
}

方法解析:

set(T value)

该方法是向ThreadLocal中存放值的,如下:

ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(1);

具体设置值的逻辑:

public void set(T value) {
	// 获取当前线程对象
	Thread t = Thread.currentThread();
	// 获取当前线程引用的 threadLocals 对象
	ThreadLocalMap map = getMap(t);
	if (map != null)
		// 设置值
		map.set(this, value);
	else
		// 创建新的引用
		createMap(t, value);
}


ThreadLocalMap getMap(Thread t) {
	// 获取当前线程持有的threadLocals
	return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
	// 初始化一个 ThreadLocalMap 赋值给当前线程的 threadLocals
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
  
  	// 计算数据应该存放的位置
    int i = key.threadLocalHashCode & (len-1);

  	// 在进行设置值的时候,为了解决hash冲突,使用了 线性探测法
  	// 如果第 i 个位置已经有值了,则判断下一个位置有没有值,没有值则将数据放入该位置
	// 整个循环的意思是,从上面获取的hash下标开始向后遍历,在遍历过程中如果当前下标的Entry没有值,如果有值,判断Entry的key是不是当前threadLocal对象,如果是,则给当前ThreadLocal设置新的value;如果Entry的key为空,说明该Entry已经没有引用的ThreadLoca了,无法再被访问到,将该无效Entry移除,然后赋值新的key和value
    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) {
        // 说明当前的 threadLocal 对象已经被GC清理,移除失效的 Entry,下面会说到
        replaceStaleEntry(key, value, i);
        return;
      }
    }
    // 说明当前下标的Entry还没有值,初始化一个新Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
 }
T get()
public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    // map.getEntry 通过循环遍历的方式查找当前 ThreadLocal
    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)
	// Entry的key==当前threadLocal,说明是要查询的Entry
         return e;
      else
	// 通过线性探测法,循环获取下一个下标的Entry,并判断是不是目标Entry
          return getEntryAfterMiss(key, i, e);
}
void 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) {
      // 移除 key 的引用
      e.clear();
      // 移除 value 的引用
      expungeStaleEntry(i);
      return;
    }
  }
}
  • 注意事项

使用完 ThreadLocal 之后, 一定要手动调用 remove 方法 ,不然可能会导致内溢出。前面说了 Entry 里的 key 是弱引用对象,可以避免了内存溢出。但是 value 是强引用对象,如果 value 的对象还被其他对象引用,value 会一直不被 GC 回收,如果这样的 value 比较多的时候,会导致内存溢出。

value可能被长时间引用的原因是Thread的生命周期要比对象的生命周期长的多,在整个生命周期内,可能会创建了许许多多的ThradLocal,这时value对象就会特别多,而且不会被垃圾回收。

移除失效 Entry 的逻辑
private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;

  // 将当前 Entry 的 value 置为 null
  tab[staleSlot].value = null;
  // 将当前 Entry 置为 null
  tab[staleSlot] = null;
  // Entry 数量 -1
  size--;
  
  Entry e;
  int i;
  // 通过线性探测法将 table 中所有失效的 Entry 都做清理
  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;
        while (tab[h] != null)
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

ThreadLocal 子线程无法继承父线程数据问题

如果在线程中创建了子线程,那么子线程与父线程的 ThreadLocal 数据是不能共享的,比如下面的代码:

public static void main(String[] args) {
  ThreadLocal<Integer> local = new ThreadLocal<>();
  local.set(1);
  System.out.println("父线程get=" + local.get());

  new Thread(() -> {
    System.out.println("子线程get=" + local.get());
  }).start();
}

// 输出结果:
// 父线程get=1
// 子线程get=null

如何在子线程中使用父线程 ThreadLocal 数据呢?可以使用 InheritableThreadLocal,如下代码:

InheritableThreadLocal<Integer> local1 = new InheritableThreadLocal<>();
local1.set(1);
System.out.println("父线程get1=" + local1.get());

new Thread(() -> {
  System.out.println("子线程get1=" + local1.get());
}).start();

// 打印结果
// 父线程get1=1
// 子线程get1=1

其原理是因为在调用 Thread 的构造方法的时候,会将父线程的局部变量赋值给子线程,实现了在子线程能够使用到父线程数据。

但这种方法不能在线程池中使用,线程池中的线程不一定是当前线程创建的。

public class Thread implements Runnable {
  
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
  public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
  }
  
  private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
  }
  
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
     Thread parent = currentThread();
     //  parent.inheritableThreadLocals 的值是在调用 set 方法时设置的
     if (inheritThreadLocals && parent.inheritableThreadLocals != null)
       		// 将父线程的局部变量赋值给子线程
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
   }
}

线程池中如何使用父线程的ThreadLocal局部变量呢

可以参考阿里的开源项目:Gitee 极速下载/transmittable-thread-local

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记忆旅途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值