参考视频:https://www.bilibili.com/video/BV1N741127FH?p=1
ThreadLocal介绍
介绍
ThreadLocal类的作用是提供线程内部的局部变量,在多线程环境下访问时保证各个线程的变量相对独立于其他线程内的变量,不同线程之间不会相互干扰,保证线程间局部变量的隔离性。这些变量在线程的声明周期内起作用。
基本使用
看以下代码:
public class Demo01 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo01.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "-->获取" + demo01.getContent());
}
}, "线程"+i);
thread.start();
}
}
}
输出结果:
多个线程在访问同一变量时出现了异常,线程间的数据没有隔离。
ThreadLoacl保证线程隔离
public class Demo02 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String content;
public String getContent() {
//return content;
return threadLocal.get();
}
public void setContent(String content) {
//this.content = content;
threadLocal.set(content);
}
public static void main(String[] args) {
Demo02 demo02 = new Demo02();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo02.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "-->获取" + demo02.getContent());
}
}, "线程"+i);
thread.start();
}
}
}
输出结果:
synchronized加锁
public class Demo03 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Demo03.class) {
demo01.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "-->获取" + demo01.getContent());
}
}
}, "线程"+i);
thread.start();
}
}
}
输出结果:
ThreadLocal类与synchronized
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用“时间换空间”的方式,只提供了一份变量,多个线程排队访问 | ThreadLocal采用“空间换时间”的方式,为每个变量都提供了一个变量的副本,从而实现同时访问互不干扰 |
侧重点 | 多个线程之间资源的同步 | 多个线程中每个线程之间数据隔离性 |
ThreadLocal原理
常见误解(早期的ThreadLoacl)
最早期的ThreadLoca设计:每个Thread都维护一个ThreadLocalMap,然后用ThreadLoacl作为ThreadLocalMap的key,要隔离的局部变量作为ThreadLocalMap的value,这样达到各线程的局部变量隔离效果。
现在的设计
JDK8中对ThreadLocal的设计进行了优化:每个Thread对象都拥有一个ThreadLocalMap,ThreadLocal实例本身作为ThreadLocalMap的key,要隔离的局部变量作为ThreadLocalMap的value。ThreadLocalMap是由ThreadLocal对象维护的,ThreadLocal对象负责向ThreadLocalMap获取和设置线程的变量值。
这样设计的优势是:
- 每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
源码剖析
Thread的部分源码如下:
public class Thread implements Runnable{
......
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}
ThreadLocal的核心方法
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocalMap分析
ThreadLocalMap是ThreadLocal的静态内部类。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
......
}
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是说key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和Thread对象的生命周期解绑。
弱引用和内存泄漏
弱引用相关概念
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
内存泄漏相关概念
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak: 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
内存泄漏跟Entry中使用了弱引用的key有关系?
如果key使用的是强引用,如图:
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
如果key使用弱引用的话:
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
所以,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。内存泄漏的真正原因是ThreadLocalMap的生命周期跟Thread一样长,如果没有手动remove对应的Entry就会导致内存泄漏。
要避免内存泄漏有两种方式:
- 使用完ThreadLocal,调用其remove方法删除对应的Entry
- 使用完ThreadLocal,当前Thread也随之运行结束
使用弱引用的原因
只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
hash冲突的解决
ThreadLocalMap使用线性探测法来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。