并发编程(二十四):并发设计模式之线程本地存储:ThreadLocal

Java 语言提供的线程本地存储(ThreadLocal)就能够做到避免共享

1.ThreadLocal的工作原理

一般来讲,我们的设计思路可能是ThreadLocal会持有一个Map,map的key是Thread对象,然后value存储具体数据。但是这样真是最好的设计吗?

设计的代码可能如下:

class MyThreadLocal<T> {
  Map<Thread, T> locals = 
    new ConcurrentHashMap<>();
  // 获取线程变量  
  T get() {
    return locals.get(
      Thread.currentThread());
  }
  // 设置线程变量
  void set(T t) {
    locals.put(
      Thread.currentThread(), t);
  }
}

但是这样可能会发生什么问题呢?

  1. 内存泄漏:如果Thread已经执行完毕了,但是如果ThreadLocal没有得到释放,其持有的map已经引用了已执行完成的Thread,此时就会导致Thread无法得到清理,从而导致内存泄漏

Java是如何设计的呢?

JDK中让Thread去持有上述的map,ThreadLocal其实操作的是当前线程Thread所持有的Map对象,这样就不需要使用Thread对象去作为key,能够通过当前的Thread直接获得map去给ThreadLocal类去读写,并且通过源码我们可以看到Thread持有的这个map的key的计算是根据ThreadLocal去计算的,所以get方法需要传入ThreadLocal对象去取值,代码如下:

为什么要用ThreadLocal对象去作为map的key,因为可能同时创建了多个ThreadLocal对象,如果不以其作为key,则无法区分是根据哪一个ThreadLocal对象去set的数据

image-20210818214458355
class Thread {
  // 内部持有 ThreadLocalMap
  ThreadLocal.ThreadLocalMap 
    threadLocals;
}
class ThreadLocal<T>{
  public T get() {
    // 首先获取线程持有的
    //ThreadLocalMap
    ThreadLocalMap map =
      Thread.currentThread()
        .threadLocals;
    // 在 ThreadLocalMap 中
    // 查找变量
    Entry e = 
      map.getEntry(this);
    return e.value;  
  }
  static class ThreadLocalMap{
    // 内部是数组而不是 Map
    Entry[] table;
    // 根据 ThreadLocal 查找 Entry
    Entry getEntry(ThreadLocal key){
      // 省略查找逻辑
    }
    //Entry 定义
    static class Entry extends
    WeakReference<ThreadLocal>{
      Object value;
    }
  }
}

2.ThreadLocal的内存泄漏

Java的上述实现不一定能够完全避免内存泄漏,虽然Thread持有的ThreadLocalMap中的Entry对ThreadLocal是弱引用,ThreadLocal被回收了,entry也能被回收,但是entry所存储的value是强引用,因此如果Thread得不到回收,那么ThreadLocalMap中的Entry肯定也无法回收,这样数据就一直存在,从而导致内存泄漏

解决方法:try{}finally{}方案了,finally中去清理数据(调用ThreadLocal的remove操作)

3.InheritableThreadLocal 与继承性

ThreadLocal会存在一个问题,那就是子线程无法去读取父线程中存储的数据,因为子线程没有拷贝父线程的ThreadLocalMap。因此我们只需要再子线程创建时,将父线程的ThreadLocalMap拷贝一份就行了,代码如下:

在Thread的初始化中,会将父线程inheritableThreadLocals赋值给子线程的inheritableThreadLocals,然后InheritableThreadLocal 重写了ThreadLocal的getMap的方法,这样InheritableThreadLocal.get就能拿到线程的inheritableThreadLocals(存储父线程的ThreadLocalMap数据)去读写,也就解决了子线程无法获取父线程ThreadLocal中的数据的问题

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {
    
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

inheritableThreadLocals的重写get如下:

/**
 * Get the map associated with a ThreadLocal.
 *
 * @param t the current thread
 */
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

但是不建议使用inheritableThreadLocals,因为:线程池中线程的创建是动态的,很容易导致继承关系错乱。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×