1、ThreadLocal
1.1 定义
用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get或者set方法访问)时能保证各个线程的变量对应相对独立于其他线程内的变量。
作用是:提供了线程内部的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
1.2 示例
需求:线程隔离
在多线程并发的场景下,每个线程中变量都是相互隔离的
- 线程A:设置变量1,获取变量1
- 线程B:设置变量2,获取变量2
1.2.1 错误实现
public class ThreadLockDemo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.setName(Thread.currentThread().getName() + "的数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}, "线程" + i).start();
}
}
}
运行结果
线程4--->线程10的数据
线程3--->线程10的数据
线程8--->线程10的数据
线程10--->线程10的数据
线程2--->线程10的数据
线程9--->线程10的数据
线程7--->线程10的数据
线程5--->线程10的数据
线程6--->线程10的数据
线程1--->线程10的数据
1.2.2 使用 ThreadLocal 解决
ThreadLocal
1、set() 将变量绑定到当前线程中
2、get() 获取当前线程绑定的变量
public class ThreadLockDemo {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String name;
// public String getName() {
// return name;
// }
//
// public void setName(String name) {
// this.name = name;
// }
public String getName() {
return threadLocal.get();
}
public void setName(String name) {
threadLocal.set(name);
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.setName(Thread.currentThread().getName() + "的数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}, "线程" + i).start();
}
}
}
运行结果
线程8--->线程8的数据
线程1--->线程1的数据
线程5--->线程5的数据
线程6--->线程6的数据
线程7--->线程7的数据
线程10--->线程10的数据
线程3--->线程3的数据
线程9--->线程9的数据
线程2--->线程2的数据
线程4--->线程4的数据
1.3 使用 synchronize 来处理
1.3.1 使用synchronize 加锁来实现
使用 synchronize 加锁也能解决这个问题
public class ThreadLockDemo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
ThreadLockDemo demo = new ThreadLockDemo();
for (int i = 1; i <= 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (ThreadLockDemo.class){
demo.setName(Thread.currentThread().getName() + "的数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
}
}
}, "线程" + i).start();
}
}
}
运行也没有问题,但是加锁后,各个线程需要排队进入运行,性能会降低,造成该操作不是并发运行。
1.3.2 ThreadLocal 与 synchronize 的区别
都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
synchronize | ThreadLocal | |
---|---|---|
原理 | 同步机制采用 以时间换空间的方式,只提供了一份变量,让不容的线程排队访问 | ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据互相隔离 |
2 在数据库连接案例中使用到了 ThreadLocal
- 传递数据:保存每个线程绑定的数据,在需要的地方直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
3 ThreadLocal 的内部结构
JDK8 中 ThreadLocal 的设计是:
- 每一个Thread线程内部都有一个Map(ThreadLocalMap)
- Map 里边存储 ThreadLocal 对象(key)和线程的变量副本(value)
- Thread 内部的Map是由ThreadLocal维护的,由ThreadLocal负责向 map 获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4 为什么要使用弱引用
无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免出现内存泄漏。
要避免内存泄漏有两种方式:
- 使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
- 使用完 ThreadLocal,当前Thread也随之运行结束
第二种方式是不好控制,特别是使用线程池的时候,线程结束不会销毁的。
也就说,在使用完后,记得调用 remove,无论是强引用还是弱引用都不会有问题,那为什么要使用弱引用呢?
在 ThreadLocalMap 中的 set/getEntry 的方法中,会对 key 为 null(即 ThreadLocal 为 null)进行判断,如果为 null 的话,那么会对 value 设置为 null。
即在使用完 ThreadLocal ,CurrentThread 依然运行的情况下,就算忘记调用 remove 方法,弱引用比强引用多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set/get/remove中的任意方法时都会被清除,从而避免内存泄漏。
总结:ThreadLocal内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除掉对应的 key 就会导致内存泄漏。