简述
jdk 源码注解中有这样一段描述:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
这个类提供线程局部变量. 这些变量与其正常的对应方式不同, 因为访问一个的每个线程 (通过其 get 或 set 方法) 都有自己独立初始化的变量副本. ThreadLocal 实例通常是希望将状态与线程关联的类中的私有静态字段(例如, 用户 ID 或事务 ID)
需要明确的是 ThreadLocal 不是用于解决共享变量的问题的, 也不是为了协调线程同步而存在, 而是为了方便每个线程处理自己的状态而引入的一个机制
ThreadLocal 使用示例
- public class SeqCount {
- private static ThreadLocal seqCount = new ThreadLocal(){
- // 实现 initialValue()
- public Integer initialValue() {
- return 0;
- }
- };
- public int nextSeq(){
- seqCount.set(seqCount.get() + 1);
- return seqCount.get();
- }
- public void remove() {
- seqCount.remove();
- }
- public static void main(String[] args){
- SeqCount seqCount = new SeqCount();
- SeqThread thread1 = new SeqThread(seqCount);
- SeqThread thread2 = new SeqThread(seqCount);
- SeqThread thread3 = new SeqThread(seqCount);
- SeqThread thread4 = new SeqThread(seqCount);
- thread1.start();
- thread2.start();
- thread3.start();
- thread4.start();
- }
- private static class SeqThread extends Thread{
- private SeqCount seqCount;
- SeqThread(SeqCount seqCount){
- this.seqCount = seqCount;
- }
- public void run() {
- try {
- for(int i = 0 ; i <3 ; i++){
- System.out.println(Thread.currentThread().getName() + "seqCount :" + seqCount.nextSeq());
- }
- } finally {
- seqCount.remove();
- }
- }
- }
- }
复制代码
运行结果:
- Thread-0 seqCount :1
- Thread-0 seqCount :2
- Thread-0 seqCount :3
- Thread-1 seqCount :1
- Thread-1 seqCount :2
- Thread-1 seqCount :3
- Thread-3 seqCount :1
- Thread-3 seqCount :2
- Thread-3 seqCount :3
- Thread-2 seqCount :1
- Thread-2 seqCount :2
- Thread-2 seqCount :3
复制代码
从结果可以得知, ThreadLocal 确实是可以达到线程隔离机制, 保证了变量的安全性
ThreadLocal 实现原理
ThreadLocal 是为每一个线程创建一个单独的变量副本, 所以每个线程都可以独立地改变自己所拥有的变量副本, 而不会影响其他线程所对应的副本. 从其几个方法入手
set 方法
- public void set(T value) {
- // 获取当前线程
- Thread t = Thread.currentThread();
- // 通过当前线程实例获取 ThreadLocalMap 对象
- ThreadLocalMap map = getMap(t);
- // 若 map 不为 null, 则以当前 threadLocal 为键, value 为值存放
- if (map != null)
- map.set(this, value);
- // 若 map 为 null, 则创建 ThreadLocalMap, 以当前 threadLocal 为键, value 为值
- else
- createMap(t, value);
- }
复制代码
获取当前线程实例, 调用 getMap()获取此线程的 ThreadLocalMap
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
复制代码
然后判断 map 是否为 null, 若为 null 则还需创建 threadLocalMap, 以当前 threadLocal 为键, value 为值存放在 threadLocalMap 中, 若不为 null 直接存储即可
get 方法
- public T get() {
- // 获取当前线程
- Thread t = Thread.currentThread();
- // 获取线程关联的 ThreadLocalMap
- ThreadLocalMap map = getMap(t);
- // 若 map 不为 null, 从 map 中获取以当前 threadLocal 实例为 key 的数据
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- // 若 map 为 null 或者 entry 为 null, 则调用此方法初始化
- return setInitialValue();
- }
复制代码
get 方法获取当前线程关联的 ThreadLocalMap. 若 map 不为 null, 以 threadLocal 实例为 key 获取数据; 若 map 为 null 或 entry 为 null 调用 setInitialValue()方法
- 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 方法差不多, 但多了 initialValue()方法, 此方法需要子类重写
- protected T initialValue() {
- return null;
- }
复制代码
remove 方法
- public void remove() {
- // 根据当前线程获取其所关联的 ThreadLocalMap
- ThreadLocalMap m = getMap(Thread.currentThread());
- // 若 map 不为 null, 删除以当前 threadLocal 为 key 的数据
- if (m != null)
- m.remove(this);
- }
复制代码
ThreadLocalMap
从 ThreadLocal 那几个核心方法来看, 其实现都基于内部类 ThreadLocalMap
ThreadLocalMap 属性
- // 初始化容量
- private static final int INITIAL_CAPACITY = 16;
- // 哈希表
- private Entry[] table;
- // 元素个数
- private int size = 0;
- // 扩容阈值(threshold = 底层哈希表 table 的长度 len * 2 / 3)
- private int threshold;
复制代码
内部类 entry
- static class Entry extends WeakReference> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal k, Object v) {
- super(k);
- value = v;
- }
- }
复制代码
从源码中可以得知 Entry 的 key 是 Threadlocal, 并且 Entry 继承 WeakReference 弱引用. 注意 Entry 中并没有 next 属性, 相对于 HashMap 采用链地址法处理冲突, ThreadLocalMap 采用开放定址法
set 方法
- private void set(ThreadLocal key, Object value) {
- Entry[] tab = table;
- int len = tab.length;
- // 根据 ThreadLocal 的 hashcode 值, 寻找对应 Entry 在数组中的位置
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal k = e.get();
- // 若找到对应 key, 替换旧值返回
- if (k == key) {
- e.value = value;
- return;
- }
- // 若 key == null, 因为 e!=null 肯定存在 entry
- // 说明之前的 ThreadLocal 对象已经被回收
- if (k == null) {
- // 替换旧 entry
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 创建新 entry
- tab[i] = new Entry(key, value);
- // 元素个数 + 1
- int sz = ++size;
- // cleanSomeSlots 清除旧 Entry(key == null)
- // 如果没有要清除的数据, 元素个数仍然大于阈值则扩容
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- rehash();
- }
复制代码
每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode, 每初始化一个 ThreadLocal 对象, hash 值就增加一个固定的大小 0x61c88647. 在插入过程中先根据 threadlocal 对象的 hash 值, 定位哈希表的位置:
1, 若此位置是空的, 就创建一个 Entry 对象放在此位置上, 调用 cleanSomeSlots()方法清除 key 为 null 的旧 entry, 若没有要清除的旧 entry 则判断是否需要扩容
2, 若此位置已经有 Entry 对象了, 如果这个 Entry 对象的 key 正好是所要设置的 key 或 key 为 null, 则替换 value 值
3, 若此位置 Entry 对象的 key 不符合条件, 寻找哈希表此位置 + 1(若到达哈希表尾则从头开始)
我们可以发现 ThreadLocalMap 采用了开放定址法来解决冲突, 一旦发生了冲突, 就去寻找下一个空的散列地址, 而 HashMap 采用链地址法解决冲突在原位置利用链表处理
getEntry 方法
- private Entry getEntry(ThreadLocal key) {
- // 定位
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- // 若此位置不为空且与 entry 的 key 返回 entry 对象
- if (e != null && e.get() == key)
- return e;
- else
- return getEntryAfterMiss(key, i, e);
- }
复制代码
理解了 set,getEntry 很好理解. 先根据 threadlocal 对象的 hash 值, 定位哈希表的位置. 若此位置 entry 的 key 和查找的 key 相同的话就直接返回这个 entry, 若不符合调用 getEntryAfterMiss()继续向后找, getEntryAfterMiss 方法如下:
- private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
- while (e != null) {
- ThreadLocal k = e.get();
- // 找到和所需 key 相同的 entry 则返回
- if (k == key)
- return e;
- // 处理 key 为 null 的 entry
- if (k == null)
- expungeStaleEntry(i);
- else
- // 继续找下一个
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
复制代码
remove 方法
- 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)]) {
- // 若找到所需 key
- if (e.get() == key) {
- // 将 entry 的 key 置为 null
- e.clear();
- // 将 entry 的 value 置为 null 同时 entry 置空
- expungeStaleEntry(i);
- return;
- }
- }
- }
复制代码
定位在哈希表的位置, 找到相同 key 的 entry, 调用 clear 方法将 key 置为 null, 调用 expungeStaleEntry 方法删除对应位置的过期实体, 并删除此位置后 key = null 的实体
- private int expungeStaleEntry(int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- // 将此位置的 entry 对象置空以及 value 置空
- tab[staleSlot].value = null;
- tab[staleSlot] = null;
- // 元素个数 - 1
- size--;
- // Rehash until we encounter null
- Entry e;
- int i;
- // 清除此位置后 key 为 null 的 entry 对象以及 rehash 位置不同的 entry 直至有位置为空为止
- for (i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal k = e.get();
- if (k == null) {
- e.value = null;
- tab[i] = null;
- size--;
- } else {
- int h = k.threadLocalHashCode & (len - 1);
- if (h != i) {
- tab[i] = null;
- // Unlike Knuth 6.4 Algorithm R, we must scan until
- // null because multiple entries could have been stale.
- while (tab[h] != null)
- h = nextIndex(h, len);
- tab[h] = e;
- }
- }
- }
- return i;
- }
复制代码
内存泄漏
先附上四种引用与 gc 关系
引用类型 | 回收机制 | 用途 | 生存时间 |
强引用 | 从不回收 | 对象状态 | JVM 停止运行时 |
软引用 | 内存不足时回收 | 对象缓存 | 内存不足时终止 |
弱引用 | 对象不被引用时回收 | 对象缓存 | GC 后终止 |
虚引用 | 对象不被引用时回收 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
先看下面代码
- Son son = new Son();
- Parent parent = new Parent(son);
复制代码
当我们把 son 置空, 由于 parent 持有 son 的引用且 parent 是强引用, 所以 gc 并不回收 son 所分配的内存空间, 这就导致了内存泄露
如果是弱引用那么上述例子, GC 就会回收 son 所分配的内存空间. 而 ThreadLocalMap 采用 ThreadLocal 弱引用作为 key, 虽然 ThreadLocal 是弱引用 GC 会回收这部分空间即 key 被回收, 但是 value 却存在一条从 Current Thread 过来的强引用链. 因此只有当 Current Thread 销毁时, value 才能 得到释放
那么如何有效的避免呢?
在上述中我们可以看到 ThreadLocalMap 中的 set/getEntry 方法中, 会对 key 为 null(即 ThreadLocal 为 null)进行判断, 如果为 null 的话, 那么是会对 value 置为 null 的. 当然也可以通过调用 ThreadLocal 的 remove 方法进行释放.
总结
ThreadLocal 不是用来解决共享对象的多线程访问问题, 而是为了方便每个线程处理自己的状态而引入的一个机制. 它为每一个线程都提供一份变量的副本, 从而实现同时访问而互不影响. 另外 ThreadLocal 可能存在内存泄漏问题, 使用完 ThreadLocal 之后, 最好调用 remove 方法
感谢
- https://www.jianshu.com/p/377bb840802f
- https://www.jianshu.com/p/ee8c9dccc953
- http://cmsblogs.com/?p=2442
来源: https://juejin.im/post/5b87f5a2e51d4538815f0814