forked from Snailclimb/JavaGuide
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
cb69a9d
commit 2eabc71
Showing
1 changed file
with
158 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) | ||
|
||
[深入理解 Java 之 ThreadLocal 工作原理](<https://allenwu.itscoder.com/threadlocal-source>) | ||
|
||
## ThreadLocal | ||
|
||
### ThreadLocal简介 | ||
|
||
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** | ||
|
||
**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** | ||
|
||
再举个简单的例子: | ||
|
||
比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 | ||
|
||
### ThreadLocal示例 | ||
|
||
相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 | ||
|
||
```java | ||
import java.text.SimpleDateFormat; | ||
import java.util.Random; | ||
|
||
public class ThreadLocalExample implements Runnable{ | ||
|
||
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 | ||
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); | ||
|
||
public static void main(String[] args) throws InterruptedException { | ||
ThreadLocalExample obj = new ThreadLocalExample(); | ||
for(int i=0 ; i<10; i++){ | ||
Thread t = new Thread(obj, ""+i); | ||
Thread.sleep(new Random().nextInt(1000)); | ||
t.start(); | ||
} | ||
} | ||
|
||
@Override | ||
public void run() { | ||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); | ||
try { | ||
Thread.sleep(new Random().nextInt(1000)); | ||
} catch (InterruptedException e) { | ||
e.printStackTrace(); | ||
} | ||
//formatter pattern is changed here by thread, but it won't reflect to other threads | ||
formatter.set(new SimpleDateFormat()); | ||
|
||
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); | ||
} | ||
|
||
} | ||
|
||
``` | ||
|
||
Output: | ||
|
||
``` | ||
Thread Name= 0 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 0 formatter = yy-M-d ah:mm | ||
Thread Name= 1 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 2 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 1 formatter = yy-M-d ah:mm | ||
Thread Name= 3 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 2 formatter = yy-M-d ah:mm | ||
Thread Name= 4 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 3 formatter = yy-M-d ah:mm | ||
Thread Name= 4 formatter = yy-M-d ah:mm | ||
Thread Name= 5 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 5 formatter = yy-M-d ah:mm | ||
Thread Name= 6 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 6 formatter = yy-M-d ah:mm | ||
Thread Name= 7 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 7 formatter = yy-M-d ah:mm | ||
Thread Name= 8 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 9 default Formatter = yyyyMMdd HHmm | ||
Thread Name= 8 formatter = yy-M-d ah:mm | ||
Thread Name= 9 formatter = yy-M-d ah:mm | ||
``` | ||
|
||
从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 | ||
|
||
上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 | ||
|
||
```java | ||
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){ | ||
@Override | ||
protected SimpleDateFormat initialValue() | ||
{ | ||
return new SimpleDateFormat("yyyyMMdd HHmm"); | ||
} | ||
}; | ||
``` | ||
|
||
### ThreadLocal原理 | ||
|
||
从 `Thread`类源代码入手。 | ||
|
||
```java | ||
public class Thread implements Runnable { | ||
...... | ||
//与此线程有关的ThreadLocal值。由ThreadLocal类维护 | ||
ThreadLocal.ThreadLocalMap threadLocals = null; | ||
|
||
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 | ||
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; | ||
...... | ||
} | ||
``` | ||
|
||
从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 | ||
|
||
`ThreadLocal`类的`set()`方法 | ||
|
||
```java | ||
public void set(T value) { | ||
Thread t = Thread.currentThread(); | ||
ThreadLocalMap map = getMap(t); | ||
if (map != null) | ||
map.set(this, value); | ||
else | ||
createMap(t, value); | ||
} | ||
ThreadLocalMap getMap(Thread t) { | ||
return t.threadLocals; | ||
} | ||
``` | ||
|
||
通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** | ||
|
||
**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key的键值对。这里解释了为什么每个线程访问同一个`ThreadLocal`,得到的确是不同的数值。另外,`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。** | ||
|
||
`ThreadLocalMap`是`ThreadLocal`的静态内部类。 | ||
|
||
![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg) | ||
|
||
### ThreadLocal 内存泄露问题 | ||
|
||
`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 | ||
|
||
```java | ||
static class Entry extends WeakReference<ThreadLocal<?>> { | ||
/** The value associated with this ThreadLocal. */ | ||
Object value; | ||
|
||
Entry(ThreadLocal<?> k, Object v) { | ||
super(k); | ||
value = v; | ||
} | ||
} | ||
``` | ||
|
||
**弱引用介绍:** | ||
|
||
> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 | ||
> | ||
> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 |