ThreadLocal的那些事——IT故事汇

30 篇文章 0 订阅

关于ThreadLocal的那些事

  • 那么,ThreadLocal有什么故事呢?
    • 他的起源
    • 剖析
    • 他的用例

?看到这里,发现ThreadLocal好像故事不多,确实,他的故事并不够多,但是,他的每个故事,都沁人心脾。

一.ThreadLocal的起源


1.ThreadLocal是什么

ThreadLocal是一个类,采用ThreadLocal声明的变量也称为线程本地变量。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。即线程资源绑定


ThreadLocal是连接ThreadLocalMap和Thread的桥梁,来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。

//以下是在Thread.java中的TheadLocalMap
//与此线程相关的threadLocal的值,这个map由ThreadLocal维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程相关的InheritableThreadLocal值,此map由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

通过上述代码,我们大概可以猜到,为什么ThreadLocal能够实现线程的本地变量,就是因为在线程类中声明了ThreadLocalMap的字段,将ThreadThreadLocalMap进行了绑定。

2.他来自哪里

ThreadLocal是JDK1.2起定义的一个类。

/**
 * 该类提供线程局部变量。该局部变量不同于正常的实例局部变量,
 * 而是独属于线程的。
 * 常用于:一个线程持有一个UserId、或者一个事务ID、一个会话等等。
 * 由于是每个线程独立且私有的状态,因此最好设置为private static。
 * <p>每个线程都拥有对其本地线程副本的隐式引用
 * 变量只要线程处于活动状态且{@code ThreadLocal}
 * 实例可访问;一个线程消失后,它的所有副本
 * 线程局部实例受垃圾回收(除非其他存在对这些副本的引用)
 */
public class ThreadLocal<T> {
//里面的内容接下来会讲解
}

以下是ThreadLocalMap,该类是ThreadLocal的内部类:

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/**
 * The value associated with this ThreadLocal.
*/
     Object value;
     Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
        }
    }
}

这个ThreadLocalMap不是继承于Map,而是内部定义了一个Entry,该类继承弱引用,即将ThreadLocal作为键值,而value即为变量的值。

二.剖析源码

先来看看ThreadLocal提供的几个操作方法:

  • 先对get()方法进行解析:
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();
}
//再看getMap(t)
ThreadLocalMap getMap(Thread t) {
       return t.threadLocals;
    }

可以看到,get()方法做的事情就是:

  1. 从当前线程中获取ThreadLocalMap;
  2. 从ThreadLocalMap中取出entry;
  3. 再从entry中取出value(变量对象或者变量值);
  4. 如果map或者map中的entry为空,则初始化一个值。

  • 解析完get(),我们就来解析set()方法:
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(t, value);和map.set(this, value);
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

对上面的两个方法进行逐个解析:

  1. 获取当前线程的threadLocalMap,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。(即每个线程在对同一个threadLocal变量调用set()方法,是一个覆盖的过程)
  2. 如果threadLocalMap为空,则创建新的map,创建新的map则是通过new 一个threadLocalmap给当前线程的threadLocals。

接下来,是一个重点,在很多的讲解教程中,许多作者会有意无意地忽略这个方法,这是一个难点,只有理解了这个方法,才能真正理解ThreadLocal
方法如下,这个方法的解析采用注释进行解析,读者只需要跟着注释理解就可以了

private void set(ThreadLocal<?> key, Object value) {
           Entry[] tab = table;//table为map中的entry[]
           int len = tab.length;
           //计算出这个ThreadLocal变量的索引;
           int i = key.threadLocalHashCode & (len - 1);
           //可能有人会疑惑这里为什么使用一个循环,而不是直接进行索引后覆盖
           //原因:ThreadLocal变量可能会存在哈希冲突,因此需要进行寻址判断。
           for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
               ThreadLocal<?> k = e.get();
               //这里就是实现同个threadLocal变量下的值覆盖。
               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.我们回溯到上一层的方法调用,是在ThreadLocal进行一个set()方法的调用,如果map存在的情况下,就调用上述方法;
2.重点难点来了:为什么一个线程只维护一个threadLocalMap,而一个线程却可以持有多个ThreadLocal定义的对象?
2.1ThreadLocalMap中维护了一个Entry[] table,默认容量为16;达到2/3会扩容;
2.2每个entry实体由<ThreadLocal,Object>组成;
2.3因此,由每个entry实体的KEY不同(KEY即ThreadLocal变量),从而一个线程可以持有一个ThreadLocalMap,一个ThreadLocalMap可以保存多个Entry<?,?>,一个entry实体持有一个ThreadLocal定义的对象。
3.从以上的set()方法,我们可以看到,相同的key的value会被覆盖,因此,一个线程要持有多个本地变量,就需要用ThreadLocal定义多个对象。


  • 解析完set(),我们接着解析初始化值的方法:
protected T initialValue() {
        return null;
    }

可以看到默认的初始化值方法是空的,因此,再进行查找,发现:

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

以上方法与set()基本一致,无非就是将初始化值添加进threadLocalMap。


  • 接下来再解析一下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) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

以上方法由ThreadLocal调用,获取当前线程的threadLocalMap,移除当前线程下的threadLocalMap中的对于的entry实例,并非移除所有entry。


  • 以上的操作解析已经完毕,接下来再讲讲内存回收与内存泄漏吧。
  • 内存回收可以分为两个层面:
    • ThreadLocal 层面的内存回收:
      当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。

    • ThreadLocalMap 层面的内存回收:
      如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。

使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是默认情况下线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码:

    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
                rehash();
           }
  • 接下来讲讲内存泄漏:

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。但是,此时的value却不能回收,因为存在一条从current thread连接过来的强引用,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束之间的这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(这种问题在web的多线程容器极其常见,多线程容器,正常配置都是采用线程池,每个请求都从线程池中启用一个线程进行处理,使用完之后线程没有销毁,就会发生内存泄漏)
图示如下:

在这里插入图片描述
因此,使用的时候记得手动执行remove方法!
好了,内存方面的问题讲解到此结束!


三.用例

1.测试用例

以下是一个测试用例:


import java.util.concurrent.atomic.AtomicInteger;
import java.lang.ThreadLocal;
/**
 * @author linxu
 * @date 2019/3/27
 * 这是一个线程ID类,采用原子自增,为每个访问的线程赋予一个ID,相同线程访问ID相   * 同。
 */
public class ThreadId {
    // 包含要分配的下一个线程ID的原子整数
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    // 包含每个线程ID的线程局部常变量
    private static final ThreadLocal<Integer> threadId =
            new ThreadLocal<Integer>() {
                @Override
                protected Integer initialValue() {
                    return nextId.getAndIncrement();
                }
            };

    /**
     *
     * @return 返回当前线程的唯一ID,必要时进行分配
     */
    public static int get() {
        return threadId.get();
    }
    //这里省略了一个公开的remove方法。
}

主函数方法:

public class ThreadLocalTest {
    public static void main(String[] args) {
       testThreadId();
    }

    /**
     * 测试线程ID,可以看到,相同线程当运行两次的时候,它的ID是不会增加的;
     * 如下结果:
     * 当前线程:pool-1-thread-1,线程ID为:0
     * 当前线程:pool-1-thread-1,线程ID为:0
     * 当前线程:pool-1-thread-2,线程ID为:1
     * 当前线程:pool-1-thread-3,线程ID为:2
     *
     */
    private static void testThreadId() {
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(1024));
        Runnable target = () -> {
            System.err.println("当前线程:" + Thread.currentThread().getName() + ",线程ID为:" + ThreadId.get());
        };
        for (int i = 0; i < 4; i++) {
            pool.execute(target);
        }
        pool.shutdown();
      }
 
    }

其实用例有很多,比如在线程之间传递数据,避免在每个方法上传入参数等等;又或者数据库会话Session或者连接Connection,事务等等,每个线程都持有自己的本地数据,避免互相干扰。


总结

(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;

(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;

(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;

(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;

(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);

(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;

(7)线程死亡时,线程局部变量会自动回收内存;

(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;

(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值