zoukankan      html  css  js  c++  java
  • ThreadLocal底层原理学习

    1. 是什么?

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

    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 + "	 data = " + s + " ]");
    			}, "threadName" + i).start();
    		}
    	}
    }
    

    从他的输入来看, ThreadLocal是同一个, 数据存的是线程自己的名字, 所以和threadName是一样的名称

    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() 方法创建详细源码在这

    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, 所以我们可以得出这样一幅图

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

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

    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
    
  • 相关阅读:
    图像处理之基础---卷积及其快速算法的C++实现
    嵌入式c语言笔试
    逻辑题
    多媒体开发之---h264 图像参数级语义
    多媒体开发之---h264 取流解码实现
    多媒体开发之---live555 分析客户端
    多媒体开发之---如何确定slice_header slice_type 的位置
    图像处理之基础---很好的一个开源文档库
    多媒体开发之---h264 高度和宽度获取
    Flutter实战视频-移动电商-65.会员中心_订单区域UI布局
  • 原文地址:https://www.cnblogs.com/bangiao/p/13204983.html
Copyright © 2011-2022 走看看