ThreadLocal应用与实现

ThreadLocal是线程本地变量,用于隔离线程间的数据。它通过ThreadLocalMap存储线程私有的变量,创建、存取和读取都有特定实现。在Android的Looper和Spring的事务管理中有实际应用。注意,ThreadLocalMap的key使用弱引用防止内存泄漏,但需及时调用remove避免value泄露。当发生冲突时,ThreadLocalMap会在数组中寻找空位插入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal应用与实现

什么是ThreadLocal以及如何使用

ThreadLocal 直译过来就是线程本地变量的意思。它是一个容器类,可以存入其他对象类型。
它的作用就是控制多个线程对同一个ThreadLocal变量读取和写入时,可以做到相互独立,互不影响。并且对多次get()操作返回的值是同一个。

ThreadLocal有两种初始化方式:

  1. 使用ThreadLocal的set(Object)方法设置初始值。
  2. 重写initialValues()方法,返回初始值。当调用get()方法获取值时,如果发现没有用set(Object)设置过值,则会调用initialValues()获取初始值。

示例代码如下:

//方式1

//在主线程中创建一个ThreadLocal变量,并初始化为1
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
threadLocal.set(1);

//那么在主线程可以获得该值: value = 1
Integer value = threadLocal.get();

//在子线程中无法获取该值: valueChild = null
Integer valueChild = threadLocal.get();
//方式2

//在主线程通过复写initialValue()初始化ThreadLocal
AtomicInteger nextId = new AtomicInteger(0);
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>() {
    
    @override
    protected Integer initialValue() {
        return nextId.getAndInrement();
    }
};

//在子线程第一次调用get()方法时会回调initialValue()获得初始值,并且多次调用不变
Integer id = threadLocal2.get();

源码实现

ThreadLocal实现线程变量隔离的核心在于get()方法,那么接下来通过get()方法的源码追踪其实现原理。

public T get() {
   //获取当前调用线程对象
   Thread t = Thread.currentThread();
   //根据线程对象获取其关联的ThreadLocal字典,说是字典,其实是数组实现的
   //这个数组存储了在当前线程环境创建的ThreadLocal变量
   //数组下标通过ThreadLocal对象的哈希表计算出来的
   //这个ThreadLocalMap变量被存储到了Thread对象的threadLocal变量中。
   ThreadLocalMap map = getMap(t);
   //如果ThreadLocalMap不为空,则以ThreadLocal为键取出其中的值。
   if (map != null) {
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   //如果ThreadLocalMap为空,或者ThreadLocal对应的值不存在,则调用setInitialValue()方法
   return setInitialValue();
}


private T setInitialValue() {
   //回调initialValue()获取初始值
   T value = initialValue();
   Thread t = Thread.currentThread();
   //再次获取ThreadLocalMap
   ThreadLocalMap map = getMap(t);
   //如果ThreadLocalMap不为空,则将初始值存进去
   if (map != null)
       map.set(this, value);
   //如果ThreadLocalMap为空,则创建一个ThreadLocalMap,并赋值给当前Thread对象的threadLocal变量
   else
       createMap(t, value);

   //最终返回初始值
   return value;
}

下面我们着重关注几个操作:

创建ThreadLocalMap

创建ThreadLocalMap非常简单。就是新建一个ThreadLocalMap对象,并赋值给线程的threadLocals变量。

//第一次创建ThreadLocalMap
//t 为当前线程对象
//fisrtValue 为第一次存入的值
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

ThreadLocalMap定义如下:

private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap内部是通过Entry数组存储ThreadLocal变量的。数组初始化容量为16,触发扩容的阈值为容量的2/3,扩容后的容量为原容量的2倍。

向ThreadLocalMap存入

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;
  //注意:因为Entry数组的容量也总是2的幂次方,所以计算数组下标的方式与HashMap一致。
  int i = key.threadLocalHashCode & (len-1);

//遍历数组中已有的Entry
  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;
      }
    //如果键为空,说明被回收(Entry中的key采用了弱引用)了。
    //则在该数组位置上存入新的Key和Value,同时还会清除其他位置上key为空的Entry
      if (k == null) {
          replaceStaleEntry(key, value, i);
          return;
      }
      //再,如果key不为空,并且也不同,说明发生的冲突,那么继续循环寻找下一个空位置nextIndex()
  }

//如果没有相同key,则新建Entry存入Entry数组中
  tab[i] = new Entry(key, value);
  int sz = ++size;
  //最后做一遍无效key的Entry清理。
  //如果没有需要清理的,并且数组长度超过了阈值,就会触发扩容
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
}

从ThreadLocalMap读取

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

相对存入,读取过程就比较简单了,计算出数组下标就可以拿到对应的Entry对象了。
不过,由于弱引用的存在,有可能在读取时key被回收了,所以还需要走一遍清除的流程。

应用场景

Android中的Looper

如果要是一个变量在一个线程中只能存在一份,并且与其他线程不能共用,可以使用ThreadLocal。

示例:Android中的Looper对象。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

//不同的线程创建不同的Looper,一个线程只能存在一个Looper
private static void prepare(boolean quitAllowed) {
   if (sThreadLocal.get() != null) {
       throw new RuntimeException("Only one Looper may be created per thread");
   }
   sThreadLocal.set(new Looper(quitAllowed));
}

//在线程内部多次调用myLooper()返回的是同一个Looper对象。
public static @Nullable Looper myLooper() {
   return sThreadLocal.get();
}

Spring中的事务管理

事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性

注意事项

为什么Entry的key采用弱引用的方式?

防止内存泄漏。

因为ThreadLocal对象是存储到线程对象Thread中的,所以它的生命周期是和当前线程一致的。如果当前线程被缓存住了(线程池),会导致当前线程会一直持有ThreadLocal引用,从而产生内存泄漏。
当ThreadLocal采用弱引用后,在GC时会回收ThreadLocal的引用,从而导致Entry的key为null,这样在下次get()或set()操作时就会清除掉该Entry。

有人会问,如果之后不再调用get()或set()方法了,是不是就会产生内存泄漏?
是的,尽管ThreadLocal由于弱引用的原因可以被回收,但是value是强引用,所以value引用的对象还是会产生泄漏。
所以当不再使用ThreadLocal后,要主动调用remove()方法,从ThreadLocalMap中移除掉对应的Entry。

如果Entry被回收了,ThreadLocal的get()方法会返回null吗?

不存在这种情况,Entry被回收了,说明没有引用指向ThreadLocal了,也就没有办法调用get()方法了。

如果在Entry存入数组中时,发生了冲突怎么办?

不像HashMap中的Entry是一个链表,这里的Entry就是一个单个对象,所以发生冲突时就需要在从发生冲突的位置开始,向后寻找空的位置,如果到达了数组默认,从返回数组开始位置继续寻找。
因为当数组长度到达容量的2/3时就会发生扩容,所以当前时间点肯定存在空位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值