ThreadLocal再探

222 阅读4分钟

    之前写过一篇文章介绍过ThreadLocal(juejin.cn/post/685639…)

几个问题,现在结合代码和源码来深入分析一下,分析之前先看一段代码

package com.my.test.thread.ThreadLocal;

public class ThreadLocalTest  {

    static ThreadLocal<String> local = new ThreadLocal<String>();
    
    static int var = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
                local.set("local1");
                var = 11;
                System.out.println(local.toString()); // java.lang.ThreadLocal@7db811c1
        });
        
        t1.start();
        t1.join();
        System.out.println(local.get());// null
        System.out.println(var); // 11
        System.out.println(local.toString()); // java.lang.ThreadLocal@7db811c1
    }
}
   启动2个线程t1和main,t1线程给local赋值,main线程取值,结果是null,但是int类型的变量在main
线程里取出来的是修改过的值,那么使用ThreadLocal修饰的变量为什么一个线程对数据的操作对另一个
线程是不可见的呢?这个就是我们要讨论的问题

1、为什么ThreadLocal可以隔离线程数据

  t1线程往ThreadLocal里赋值的时候调用这个方法
  public void set(T value) {
        Thread t = Thread.currentThread(); // 当前线程t1
        ThreadLocalMap map = getMap(t); // 取出当前线程的的一个变量threadLocals,是一个Map
        if (map != null)
            map.set(this, value);  // map不为空直接赋值
        else
            createMap(t, value);  // 为空要创建map
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;  // 默认就是null
    }

    ThreadLocal.ThreadLocalMap threadLocals = null;

    // local.set("local1");最终执行的代码在这里
    // new一个对象,构造方法里面传2个参数,this是当前的ThreadLocal对象local,firstValue即set的值
    // 这就是ThreadLocal它将自己作为key,set的值作为value存储在当前线程的ThreadLocalMap里
    void createMap(Thread t, T firstValue) {        
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

   // main线程执行的时候,判断map为null
   // 执行 return setInitialValue();
   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();
    }
    // 执行到initialValue();
    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;
    }
    // 返回null
    protected T initialValue() {
        return null;
    }
总结:
    不管是set还是get方法,难免都要对threadLocals进行操作,而threadLocals是Thread的一个变量
    每个线程线程都有自己的threadLocals变量,简单的说就是你操作你的map我操作我的map

2、为什么有哈希冲突

    threadLocals本质上还是一个map只要是采用hash算法的就有可能产生hash冲突,线程于线程之间的
肯定不存存在冲突,那么如果一个线程里面有好几个ThreadLocal或者说一个线程想绑定好几个值那么不
就有可能产生hash冲突吗。local1、local2、local3都作为key往map里存
ThreadLocal<String> local1 = new ThreadLocal<String>();
ThreadLocal<String> local2 = new ThreadLocal<String>();
ThreadLocal<String> local3 = new ThreadLocal<String>();

3、如何解决哈希冲突

    解决hash冲突的方式有2种,像HsahMap的链表法,ThreadLoac采用的是线性探测法。线性探测法:在初始化ThreadLocal时候hash值就增加一个固定的HASH_INCREMENT(0x61c88647)大小,根据key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

      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);
        }
      private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

4、为什么存在内存泄漏

    我们往ThreadLocal里set值的时候最终是吧值放在一个Entry类型的数组里的

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

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    我们通过阅读源码已经知道了ThreadLocal它将自己作为key,set的值作为value存储在当前线程的
ThreadLocalMap里,ThreadLocalMap的数据结构又是Entry数组,值得我们注意的是Entry数组的key被设
计成了弱引用也就是ThreadLocal对象是包裹在WeakReference对象里的,而当垃圾收集器工作时候,无
论当前内存是否足够,都会回收掉被弱引用关联的对象。那么就存在一种可能就是ThreadLocal对象已经
被回收了,但是set的值一直没有回收就导致了内存泄漏,因为threadlocals也就是ThreadLocalMap是线
程的一个变量,它的生命周期和线程是一致的。如果线程迟迟不结束,那么set的值将一直得不到回收

5、如何解决内存泄漏

     使用完之后立马remove掉,或者将ThreadLocal定义成static类型

6、使用场景

    hibernate的session每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,

    当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。

    使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景