|
| 1 | +[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) |
| 2 | + |
| 3 | +[深入理解 Java 之 ThreadLocal 工作原理](<https://allenwu.itscoder.com/threadlocal-source>) |
| 4 | + |
| 5 | +## ThreadLocal |
| 6 | + |
| 7 | +### ThreadLocal简介 |
| 8 | + |
| 9 | +通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** |
| 10 | + |
| 11 | +**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** |
| 12 | + |
| 13 | +再举个简单的例子: |
| 14 | + |
| 15 | +比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 |
| 16 | + |
| 17 | +### ThreadLocal示例 |
| 18 | + |
| 19 | +相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 |
| 20 | + |
| 21 | +```java |
| 22 | +import java.text.SimpleDateFormat; |
| 23 | +import java.util.Random; |
| 24 | + |
| 25 | +public class ThreadLocalExample implements Runnable{ |
| 26 | + |
| 27 | + // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 |
| 28 | + private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); |
| 29 | + |
| 30 | + public static void main(String[] args) throws InterruptedException { |
| 31 | + ThreadLocalExample obj = new ThreadLocalExample(); |
| 32 | + for(int i=0 ; i<10; i++){ |
| 33 | + Thread t = new Thread(obj, ""+i); |
| 34 | + Thread.sleep(new Random().nextInt(1000)); |
| 35 | + t.start(); |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + @Override |
| 40 | + public void run() { |
| 41 | + System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); |
| 42 | + try { |
| 43 | + Thread.sleep(new Random().nextInt(1000)); |
| 44 | + } catch (InterruptedException e) { |
| 45 | + e.printStackTrace(); |
| 46 | + } |
| 47 | + //formatter pattern is changed here by thread, but it won't reflect to other threads |
| 48 | + formatter.set(new SimpleDateFormat()); |
| 49 | + |
| 50 | + System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); |
| 51 | + } |
| 52 | + |
| 53 | +} |
| 54 | + |
| 55 | +``` |
| 56 | + |
| 57 | +Output: |
| 58 | + |
| 59 | +``` |
| 60 | +Thread Name= 0 default Formatter = yyyyMMdd HHmm |
| 61 | +Thread Name= 0 formatter = yy-M-d ah:mm |
| 62 | +Thread Name= 1 default Formatter = yyyyMMdd HHmm |
| 63 | +Thread Name= 2 default Formatter = yyyyMMdd HHmm |
| 64 | +Thread Name= 1 formatter = yy-M-d ah:mm |
| 65 | +Thread Name= 3 default Formatter = yyyyMMdd HHmm |
| 66 | +Thread Name= 2 formatter = yy-M-d ah:mm |
| 67 | +Thread Name= 4 default Formatter = yyyyMMdd HHmm |
| 68 | +Thread Name= 3 formatter = yy-M-d ah:mm |
| 69 | +Thread Name= 4 formatter = yy-M-d ah:mm |
| 70 | +Thread Name= 5 default Formatter = yyyyMMdd HHmm |
| 71 | +Thread Name= 5 formatter = yy-M-d ah:mm |
| 72 | +Thread Name= 6 default Formatter = yyyyMMdd HHmm |
| 73 | +Thread Name= 6 formatter = yy-M-d ah:mm |
| 74 | +Thread Name= 7 default Formatter = yyyyMMdd HHmm |
| 75 | +Thread Name= 7 formatter = yy-M-d ah:mm |
| 76 | +Thread Name= 8 default Formatter = yyyyMMdd HHmm |
| 77 | +Thread Name= 9 default Formatter = yyyyMMdd HHmm |
| 78 | +Thread Name= 8 formatter = yy-M-d ah:mm |
| 79 | +Thread Name= 9 formatter = yy-M-d ah:mm |
| 80 | +``` |
| 81 | + |
| 82 | +从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 |
| 83 | + |
| 84 | +上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 |
| 85 | + |
| 86 | +```java |
| 87 | + private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){ |
| 88 | + @Override |
| 89 | + protected SimpleDateFormat initialValue() |
| 90 | + { |
| 91 | + return new SimpleDateFormat("yyyyMMdd HHmm"); |
| 92 | + } |
| 93 | + }; |
| 94 | +``` |
| 95 | + |
| 96 | +### ThreadLocal原理 |
| 97 | + |
| 98 | +从 `Thread`类源代码入手。 |
| 99 | + |
| 100 | +```java |
| 101 | +public class Thread implements Runnable { |
| 102 | + ...... |
| 103 | +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 |
| 104 | +ThreadLocal.ThreadLocalMap threadLocals = null; |
| 105 | + |
| 106 | +//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 |
| 107 | +ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; |
| 108 | + ...... |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 |
| 113 | + |
| 114 | +`ThreadLocal`类的`set()`方法 |
| 115 | + |
| 116 | +```java |
| 117 | + public void set(T value) { |
| 118 | + Thread t = Thread.currentThread(); |
| 119 | + ThreadLocalMap map = getMap(t); |
| 120 | + if (map != null) |
| 121 | + map.set(this, value); |
| 122 | + else |
| 123 | + createMap(t, value); |
| 124 | + } |
| 125 | + ThreadLocalMap getMap(Thread t) { |
| 126 | + return t.threadLocals; |
| 127 | + } |
| 128 | +``` |
| 129 | + |
| 130 | +通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** |
| 131 | + |
| 132 | +**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 |
| 133 | + |
| 134 | +```java |
| 135 | +public class Thread implements Runnable { |
| 136 | + ...... |
| 137 | +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 |
| 138 | +ThreadLocal.ThreadLocalMap threadLocals = null; |
| 139 | + |
| 140 | +//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 |
| 141 | +ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; |
| 142 | + ...... |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +`ThreadLocalMap`是`ThreadLocal`的静态内部类。 |
| 147 | + |
| 148 | + |
| 149 | + |
| 150 | +### ThreadLocal 内存泄露问题 |
| 151 | + |
| 152 | +`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 |
| 153 | + |
| 154 | +```java |
| 155 | + static class Entry extends WeakReference<ThreadLocal<?>> { |
| 156 | + /** The value associated with this ThreadLocal. */ |
| 157 | + Object value; |
| 158 | + |
| 159 | + Entry(ThreadLocal<?> k, Object v) { |
| 160 | + super(k); |
| 161 | + value = v; |
| 162 | + } |
| 163 | + } |
| 164 | +``` |
| 165 | + |
| 166 | +**弱引用介绍:** |
| 167 | + |
| 168 | +> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 |
| 169 | +> |
| 170 | +> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 |
0 commit comments