ThreadLocal底层原理学习

1. 是什么?

首先ThreadLocal类是一个线程数据绑定类, 有点相似于HashMap<Thread, 你的数据> (但实际上并不是如此), 它全部线程共享, 但读取其中数据时又只能是获取线程本身的数据, 写入也只能给线程本身的数据java

2. 怎么用?

public class ThreadLocalDemo {
    
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                threadLocal.set("zhazha" + Thread.currentThread().getName());
                String s = threadLocal.get();
                System.out.println("threadName = " + Thread.currentThread().getName()  + " [ threadLocal = "  + threadLocal + "\t data = " + s + " ]");
            }, "threadName" + i).start();
        }
    }
}

从他的输入来看, ThreadLocal是同一个, 数据存的是线程本身的名字, 因此和threadName是同样的名称shell

threadName = threadName9 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName9 ]
threadName = threadName3 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName3 ]
threadName = threadName7 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName7 ]
threadName = threadName0 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName0 ]
threadName = threadName6 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName6 ]
threadName = threadName1 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName1 ]
threadName = threadName2 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName2 ]
threadName = threadName4 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName4 ]
threadName = threadName5 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName5 ]
threadName = threadName8 [ threadLocal = java.lang.ThreadLocal@43745e1f     data = zhazhathreadName8 ]

3. 有什么使用场景

咱们使用获取到一个保存数据库请求, tomcat会有一个线程去操做数据库保存数据和响应数据给客户, 而操做数据库须要存在一个数据库连接Connection对象, 只要是同一个数据库连接, 就能够获得同一个事务
但一个线程是如何获取同一个Connection从而获取同一个事务 ?
方法其实很简单, 使用 ThreadLocal绑定在线程中, 相似于Map<Thread, Connection>去存储数据库

4. 底层源码分析

get方法分析数组

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取ThreadLocalMap
    ThreadLocal.ThreadLocalMap map = getMap(t);
    // map不为null
    if (map != null) {
        // 根据this获取咱们的entry
        ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若是map获取为空, 则初始化
    return setInitialValue();
}

根据上面源码分析发现ThreadLocal底层使用的不是相似Map<Thread, Data> 这种结构而是

每一个线程都有一个属于本身的ThreadLocalMap结构
而他的结构是这样的缓存

其中的table数组在上面的 setInitialValue() 方法建立详细源码在这tomcat

private T setInitialValue() {
    // 这个方法在咱们的用例中没写, 因此默认放回 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 获取线程单独的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 若是咱们初始化了initialValue() 方法, 那么它默认初始化的值会被设置到这里, 
        // 可是实际上咱们用例为null, 因此不会执行这段代码
        map.set(this, value);
    } else {
        // 线程ThreadLocalMap 没被建立, 须要建立出来, 
        // 其中的 table 数组在这里被建立
        createMap(t, value);
    }
    // 这里我没分析, 忽略了
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

他会在ThreadLocalMap中调用构造方法初始化网络

// 其中 firstValue是咱们的值
void createMap(Thread t, T firstValue) {
    // 关注下 this , 它是ThreadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 咱们的table在这里被建立, INITIAL_CAPACITY == 16
    table = new Entry[INITIAL_CAPACITY];
    // 获取不超过16的hashCode
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 根据计算出来的HashCode设置到对应的table数组中, 这里key是ThreadLocal, value是咱们的值
    table[i] = new Entry(firstKey, firstValue);
    // 初始时, 已经有一个值了, 因此size = 1
    size = 1;
    // 设置扩容阈值加载因子 threshold = len * 2 / 3; 默认为长度的三分之二
    setThreshold(INITIAL_CAPACITY);
}

从这段代码能够发现, firstKey实际上是咱们ThreadLocalMap中的key, 而firstKey就是咱们的ThreadLocal, 而value就是咱们 initialValue() 方法返回的值, 这里默认为null, 因此咱们能够得出这样一幅图jvm

总结下
每一个线程都有一个属于本身的 ThreadLocalMap类, 他用于关联多个以 ThreadLocal对象为 key, 以 你的数据valueEntry对象, 且该对象的 key是一个 弱引用对象

接下来咱们分析下这个类Entry, 它继承了弱引用类WeakReferenceide

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        // ThreadLocal被设置为弱引用
        super(k);
        // 保存value
        value = v;
    }
}

发现 ThreadLocal 被设置为弱引用源码分析

存在什么问题?

为何前面的Entry须要继承弱引用类WeakReference呢?
首先了解下什么是引用

简单了解下强、软、弱和虚引用

  • 强引用: 若是引用变量没被指向null则, 引用对象将被停留在堆中, 没法被虚拟机回收Object obj = new Object()
  • 软引用: 若是虚拟机堆内存不够用了(在发生内存溢出以前), 虚拟机能够选择回收软引用对象, 虚拟机提供SoftReference类实现软引用, 通常用于相对比较重要但又能够不用的对象, 好比: 缓存
  • 弱引用: 生于系统回收以前, 死于系统回收完毕以后, 弱引用须要依附于强引用或者软引用才可以防止被虚拟机回收, 好比放到一个引用队列(ReferenceQueue)中或者对象中, 好比: ThreadLocalMapEntry对象, 须要依附于ThreadLocal才可以不被删除掉
  • 虚引用: 能够理解为跟强引用对象没了引用变量同样, 随时能够被回收, 只要依附于引用队列中才不会被回收, 一般用于网络通信的NIO上, 用于引用直接内存, java提供类PhantomReference来实现虚引用

为什么Entry对象须要为弱引用?

答案很明显, 防止内存泄漏[1], 咱们来详细分析分析
首先, 咱们知道ThreadLocalMap中存放的是一个一个Entry对象, 而 Entry对象中的key(ThreadLocal)被设计成弱引用若是key被设置成null
(好比: 外部的测试用例中的private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();这个对象被设置为 threadLocal = null) 则, 你会发现此时Entry的对象key = null value = xxxx(此时这个Entry实质上是没有用的, 连key都给设置成null, 它的value还有什么用?) 而ThreadLocalMap中存储的仍是Entry对象的地址, 此Entry不会被回收, 但Entry对象的key被设置成弱引用, 就不同了, 直接会被回收掉它

[1]内存泄漏: 程序中己动态分配的堆内存因为某种缘由程序未释放或没法释放,形成系统内存的浪费,致使程序运行速度减慢甚至系统崩溃等严重后果

那么这样就没有问题了么???(打脸篇)

再次强调, 下面这段话别信, 仔细看到最后, 你会发现这被打脸了

其实应该是没什么问题了(__被本身打脸了, 别信这句话__), 只不过不少网友以为Entry中的key虽然是弱引用, 但Entry可能不会被回收, 由于entryvalue是强引用, 可能致使线程下的entry没法被回收掉, 最好推荐使用threadLocal.remove方法删除掉, 前面说的threadLocal = null方法不推荐使用, 那么为了以防万一吧, 仍是手动调用下remove方法比较好一点

下面是我对threadLocal = null方式的代码测试:

public class ThreadLocalDemo {
    
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "1";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "2";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        // 获取ThreadLocalMap
        Thread thread = Thread.currentThread();
        Class<? extends Thread> clazz = thread.getClass();
        Field threadLocals = clazz.getDeclaredField("threadLocals");
        threadLocals.setAccessible(true);
        Object threadLocalsObj = threadLocals.get(thread);
        // 获取ThreadLocalMap下的table数组
        Class<?> threadLocalsMapClass = threadLocalsObj.getClass();
        Field tableField = threadLocalsMapClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] tableObj = (Object[]) tableField.get(threadLocalsObj);
        threadLocal1.set("zhazha");
        threadLocal2.set("xixi");
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
        // 在这里下一个断点看看ThreadLocal被回收, Entry是否被回收
        threadLocal1 = null;
        threadLocal2 = null;
        System.gc();
        Thread.sleep(5000);
        System.out.println(tableObj);
        System.out.println("主线程结束");
    }
}

输出是这样的:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.zhazha.threadlocal.ThreadLocalDemo (file:/D:/program/codes/java/Concurrentcy/reviewjuc/target/classes/) to field java.lang.Thread.threadLocals
WARNING: Please consider reporting this to the maintainers of com.zhazha.threadlocal.ThreadLocalDemo
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
zhazha
xixi
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@aecb35a
主线程结束

若是上面的代码不调用gc方法, 很长一段时间内不会被回收, 应该是jvm gc还没开始被动回收

但!!!但!!!但!!! 看调试代码

数组中的referent字段仍是存在的, 下图是gc回收以前查看数组中的元素发现, 字段referent(也就是ThreadLocal) 它还在

gc方法执行完毕后, referent被回收掉了, referent = null

可是那个对象怎么回事??? 没被回收掉?? 打脸了??? 求助广大网友给我看看

那让咱们试试 remove方法试试?

好了, 直接没了, 找不到那两个属性了

An illegal reflective access operation has occurred这个问题怎么帮? 这回真不知道了, 应该不影响咱们的代码么?

算了为了把这个红色的字改没掉, 改了改源码

public class ThreadLocalDemo {
    
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "1";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "2";
        }
        
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("threadLocal1被回收");
        }
    };
    
    private static Unsafe unsafe;
    
    static {
        Class<Unsafe> unsafeClass = Unsafe.class;
        Unsafe unsafe = null;
        try {
            Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            ThreadLocalDemo.unsafe = (Unsafe) unsafeField.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException {
        Thread thread = Thread.currentThread();
        long threadLocalsFieldOffset = unsafe.objectFieldOffset(Thread.class.getDeclaredField("threadLocals"));
        Object threadLocalMapObj = unsafe.getObject(thread, threadLocalsFieldOffset);
        long tableOffset = unsafe.objectFieldOffset(threadLocalMapObj.getClass().getDeclaredField("table"));
        Object tableObj = unsafe.getObject(threadLocalMapObj, tableOffset);
        threadLocal1.set("zhazha");
        threadLocal2.set("xixi");
        System.out.println(threadLocal1.get());
        System.out.println(threadLocal2.get());
        threadLocal1 = null;
        threadLocal2 = null;
        // threadLocal1.remove();
        // threadLocal2.remove();
        System.gc();
        System.out.println(tableObj);
        System.out.println("主线程结束");
    }
}

好了没这个问题了

zhazha
xixi
threadLocal1被回收
threadLocal1被回收
[Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;@7dc222ae
主线程结束
与目标VM断开链接, 地址为: ''127.0.0.1:58958',传输: '套接字'', 传输: '{1}'

进程已结束,退出代码0
相关文章
相关标签/搜索