ThreadLocal底层原理学习 1. 是什么? 2. 怎么用? 3. 有什么使用场景 4. 底层源码分析 存在什么问题?

首先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> 这种结构而是
ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?
每个线程都有一个属于自己的ThreadLocalMap结构
而他的结构是这样的

ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?

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

ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?

总结下
每个线程都有一个属于自己的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) 它还在

ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?

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

ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?

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

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

ThreadLocal底层原理学习
1. 是什么?
2. 怎么用?
3. 有什么使用场景
4. 底层源码分析
存在什么问题?

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

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