ThreadLocal 详解

一、介绍

  通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

再举个简单的例子:

  比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

二、ThreadLocal的存储结构

  前面说了 ThreadLocal 就是一个专为单个线程存储的值,不会受其他线程的影响,而我们现在想问,这个值存在哪里?如何去取用?

  每个线程是有自己存储数据的数据栈,而这个栈只有自己可见,而堆则是所有线程都可见,所以既然有了这个特点,那 ThreaLocal 对应的值是存在栈中的么,答案是否定的,其实值还是存在于堆中,只不过利用了一些手段使得只有对应线程可见

  

  那如何存,如何取?

  ThreadLocal的数量可能有多个,而每一个ThreadLocal对应的是不同的值,所以线程利用了类似 HashMap 的结构(不是完全一样),在一个线程中有 ThreadLocalMap

来存储,可以看一下源码:

1 public class Thread implements Runnable {
2     //......
3     //与此线程有关的ThreadLocal值。由ThreadLocal类维护
4     ThreadLocal.ThreadLocalMap threadLocals = null;
5 
6     //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
7     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
8     //......
9 }

这是存储数据的地方,初始值都为null,当我们用ThreadLocal调用了get()或者set()方法后就会分配内存,我们接下来看一下 get() 和 set() 方法

get()方法:

 1 public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);
 6             if (e != null) {
 7                 @SuppressWarnings("unchecked")
 8                 T result = (T)e.value;
 9                 return result;
10             }
11         }
12         return setInitialValue();
13     }

set()方法:

1  public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t);
4         if (map != null) {
5             map.set(this, value);
6         } else {
7             createMap(t, value);
8         }
9     }

  如果读懂了上述 get() 和 set() 方法,会发现 Map 存储在线程之上,在 ThreadLocal 中通过 Thread.currentThread()来得到此线程,然后通过这个线程来获取这个线程对应的 Map ,而Map上的数据存储方式采用的是 <key,value> ,其中 key 的值是 ThreadLocal 自己,而value的值是我们想要获取的值,如下图所示:

 

 

 

 

可以看出这个存储结构与HashMap极为相似,但还是有区别的,区别的重点就在于 HashMap 利用拉链法来防止 Hash 冲突,这个结构没有链表,如何防止冲突呢?

  其实它用了另外一种避免 Hash 冲突的方法:开放地址法中的线性探索法:当存入数据出现 Hash 冲突的时候,指针就会在 Hash 冲突的位置采用 +1 或者 -1 的操作,直到找到空余位置,就会将数据存入,这样就避免的 Hash 冲突。

  因为 Key 值 ThreadLocal 一般不会很多,所以用这种方法的查询效率在这种情况下也是可以保证的

  

三、深入理解ThreadLocal

  1、如何让其他线程可以访问 ThreadLocal 的值?

  这里我们首先回到前面的代码:

 

1 public class Thread implements Runnable {
2     //......
3     //与此线程有关的ThreadLocal值。由ThreadLocal类维护
4     ThreadLocal.ThreadLocalMap threadLocals = null;
5 
6     //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
7     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
8     //......
9 }

 

  我们会发现在创建 ThreadLocals 的同时,创建了一个 inheritableThreadLocals ,这个就是问题的关键,我们继续看给这个变量什么样的值:

1 public class Thread implements Runnable {
2   ……
3    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
4       this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
5   ……
6 }

我们发现当 inheritThreadLocals 和 parent.inheritableThreadLocals(父程序的)都存在的时候,我们就可以用子程序的 inheritThreadLocals 来接受 父程序的inheritThreadLocals,这样就实现了父子程序之间的值的传递,举个例子:

 

 1 private void test() {    
 2 final ThreadLocal threadLocal = new InheritableThreadLocal();       
 3 threadLocal.set("帅得一匹");    
 4 Thread t = new Thread() {        
 5     @Override        
 6     public void run() {            
 7       super.run();            
 8       Log.i( "张三帅么 =" + threadLocal.get());        
 9     }    
10   };          
11   t.start(); 
12 } 

我们在主程序(父线程)里创建的 InheritableThreadLocal 在子线程是可以正常访问的。

 

2、如何理解 ThreadLocal 内存泄漏的问题

我们先看一下 Map 里的引用是怎么定义的:

1 static class Entry extends WeakReference<ThreadLocal<?>> {
2     /** The value associated with this ThreadLocal. */
3     Object value;
4 
5     Entry(ThreadLocal<?> k, Object v) {
6         super(k);//设置K为弱引用
7         value = v;
8     }
9 }

  可以看到这里继承了 WeakReference 这个类,将 K 设置成了弱引用,那什么是弱引用?

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

 

所以可以得出 Key 是一个弱引用,而 Value 则是一个强引用,当线程本身消亡时,这两个引用当然会被解除,不会造成内存泄漏

但如果类似线程池里面,线程得到了复用,而 Key 值在下一次GC就会被消灭,Value值因为线程得到了复用始终没有得到消除,就产生了 key 值为 NULL,而Value值占用了空间的情况,此时就造成了内存的泄漏

 

那如何防止内存泄漏?

 

  其实之所将 Key 值设为弱引用就是想利用这个特征来防止内存泄漏,当弱引用的 key 被GC回收后,在 ThreadLocal 调用 get() 或 set() 方法后会自动清除 key 值为null的value值。

  此外也可以通过调用 remove() 方法来清楚这个值

 

posted @ 2022-03-05 16:07  空心小木头  阅读(419)  评论(0编辑  收藏  举报