ThreadLocal详解

1 线程上下文设计

单个线程执行的任务步骤会非常多,后一个步骤的输入有可能是前一个步骤的输出,比如在单个线程多步骤(阶段)执行时,为了使得功能单一,有时候我们会采用GoF职责链设计模式:
在这里插入图片描述
有些时候后一个步骤未必会需要前一个步骤的输出结果,但是都需要将context从头到尾进行传递,假如方法参数比较少还可以容忍,如果方法参数比较多,在七八次的调用甚至十几次的调用,都需要从头到尾地传递context,很显然这是一种比较烦琐的设计,那么我们就可以尝试采用线程的上下文设计来解决这样的问题:

 private ConcurrentHashMap<Thread, ThreadContext> contexts =
         new ConcurrentHashMap<>();

 public ThreadContext getThreadContext(){
     ThreadContext threadContext = contexts.get(Thread.currentThread());
     if(threadContext == null){
         threadContext = new ThreadContext();
         contexts.put(Thread.currentThread(),threadContext);
     }
     return threadContext
     
 }

通过这种方式定义线程上下文很可能会导致内存泄漏,contexts是一个Map的数据结构,用当前线程做key,当线程的生命周期结束后,contexts中的Thread实例不会被释放,与之对应的Value也不会被释放,时间长了就会导致内存泄漏(Memory Leak)

2 ThreadLocal

自JDK1.2版本起,Java就提供了java.lang.ThreadLocal,ThreadLocal为每一个使用该变量的线程都提供了独立的副本,可以做到线程间的数据隔离,每一个线程都可以访问各自内部的副本变量。

2.1 ThreadLocal的使用场景及注意事项

一般在以下情况中会使用到ThreadLocal:

  • 在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次间的约束。
  • 线程间数据隔离,比如21.2节中描述的线程上下文ActionContext。
  • 进行事务操作,用于存储线程事务信息。

ThreadLocal并不是解决多线程下共享资源的技术,一般情况下,每一个线程的Thread-Local存储的都是一个全新的对象(通过new关键字创建),如果多线程的Thread-Local存储了一个对象的引用,那么其还将面临资源竞争,数据不一致等并发问题

2.2 ThreadLocal的方法详解及源码分析

2.2.1 简单认识
public static void main(String[] args) {
    // 1 创建一个ThreadLocal的实例
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    // 2 创建10个线程
    IntStream.range(0,10).forEach(i -> {
        new Thread(()->{
            // 每个线程都设置threadLocal,但是彼此之间是独立的
            threadLocal.set(i);
            System.out.println(Thread.currentThread().getName() + " set value " + threadLocal.get());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " get value " + threadLocal.get());
        },"线程" + i).start();
    });
}

上面的代码中定义了一个全局唯一的ThreadLocal<Integer>,然后启动了10个线程对threadLocal进行set和get操作,通过下面的输出可以发现,这10个线程之间彼此不会相互影响,每一个线程存入threadLocal中的i值也是完全不同彼此独立的:

线程0 set value 0
线程3 set value 3
线程4 set value 4
线程2 set value 2
线程1 set value 1
线程6 set value 6
线程5 set value 5
线程7 set value 7
线程9 set value 9
线程8 set value 8
线程6 get value 6
线程8 get value 8
线程0 get value 0
线程3 get value 3
线程4 get value 4
线程1 get value 1
线程9 get value 9
线程2 get value 2
线程7 get value 7
线程5 get value 5
2.2.2 常用方法

最常用的方法就是:

initialValue();
set(T t);
get();
initialValue

为ThreadLocal要保存的数据类型指定了一个初始化值,在ThreadLocal中默认返回值为null

protected T initialValue() {
	// 默认返回值为null
    return null;
}

演示:可以通过重写initialValue方法进行数据的初始化

public static void main(String[] args) {
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    System.out.println(threadLocal.get()); // null

    // 重写initialValue
    ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    System.out.println(threadLocal1.get()); // 1

}
set方法

set方法主要是为ThreadLocal指定将要被存储的数据

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
// createMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// set
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();

       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();
}
  1. 获取当前线程Thread.currentThread()。
  2. 根据当前线程获取与之关联的ThreadLocalMap数据结构。
  3. 如果map为null则进入第4步,否则进入第5步。
  4. 当map为null的时候创建一个ThreadLocalMap,用当前ThreadLocal实例作为key,将要存放的数据作为Value,对应到ThreadLocalMap中则是创建了一个Entry。
  5. 在map的set方法中遍历整个map的Entry,如果发现ThreadLocal相同,则使用新的数据替换即可,set过程结束。
  6. 在遍历map的entry过程中,如果发现有Entry的Key值为null,则直接将其逐出并且使用新的数据占用被逐出数据的位置,这个过程主要是为了防止内存泄漏
  7. 创建新的entry,使用ThreadLocal作为Key,将要存放的数据作为Value。
  8. 最后再根据ThreadLocalMap的当前数据元素的大小和阀值做比较,再次进行key为null的数据项清理工作。
get方法

get用于返回当前线程在ThreadLocal中的数据备份,当前线程的数据都存放在一个称为ThreadLocalMap的数据结构中:

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();
}
// 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过程:

  1. 首先获取当前线程Thread.currentThread()方法。
  2. 根据Thread获取ThreadLocalMap,其中ThreadLocalMap与Thread是关联的,而我们存入ThreadLocal中的数据事实上是存储在ThreadLocalMap的Entry中的。
  3. 如果map已经被创建过,则以当前的ThreadLocal作为key值获取对应的Entry。
  4. 如果Entry不为null,则直接返回Entry的value值,否则进入第5步。
  5. 如果在第2步获取不到对应的ThreadLocalMap,则执行setInitialValue()方法。
  6. 在setInitialValue()方法中首先通过执行initialValue()方法获取初始值。
  7. 根据当前线程Thread获取对应的ThreadLocalMap。
  8. 如果ThreadLocalMap不为null,则为map指定initialValue()所获得的初始值,实际上是在map.set(this,value)方法中new了一个Entry对象。
  9. 如果ThreadLocalMap为null(首次使用的时候),则创建一个ThreadLocalMap,并且与Thread对象的threadlocals属性相关联(通过这里我们也可以发现ThreadLocalMap的构造过程是一个Lazy的方式)。
  10. 返回initialValue()方法的结果,当然这个结果在没有被重写的情况下结果为null。
2.2.3 ThreadLocalMap

无论是get方法还是set方法都不可避免地要与ThreadLocalMap和Entry打交道,ThreadLocalMap是一个完全类似于HashMap的数据结构,仅仅用于存放线程存放在ThreadLocal中的数据备份,ThreadLocalMap的所有方法对外部都完全不可见。

在ThreadLocalMap中用于存储数据的是Entry,**它是一个WeakReference类型的子类,之所以被设计成WeakReference是为了能够在JVM发生垃圾回收事件时,能够自动回收防止内存溢出的情况出现,**通过Entry源码分析不难发现,在Entry中会存储ThreadLocal以及所需数据的备份。ThreadLocalMap的Entry源码如下:

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

     Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
     }
 }
ThreadLocal的内存泄漏问题分析

在早些的JDK版本中,ThreadLocalMap完全是由HashMap来充当的,在最新的JDK版本中,ThreadLocal为解决内存泄漏做了很多工作,使用了WeakReference来存储数据。

WeakReference在JVM中触发任意GC(young gc、full gc)时都会导致Entry的回收
在get数据时增加检查,清除已经被垃圾回收器回收的Entry(Weak Reference可自动回收)。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
   Entry[] tab = table;
   int len = tab.length;
	// /查找Key为null的Entry
   while (e != null) {
       ThreadLocal<?> k = e.get();
       if (k == key)
           return e;
       if (k == null)
       		// //将key为null的Entry删除
           expungeStaleEntry(i);
       else
           i = nextIndex(i, len);
       e = tab[i];
   }
   return null;
}
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    // **查找Key为null的Entry**
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 将key为null的Entry删除
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
//执行Entry在ThreadLocalMap中的删除动作
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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值