1.ThreadLocal 作用和用法
ThreadLocal 也叫局部变量,目的就是解决在多线程的时候,解决共享变量线程安全问题.通过将变量保存在线程局部,从而达到多线程之间独有一份,不产生竞争.
这个时候就会有一个疑问,那直接在线程内声明局部变量不就可以了?
直接到线程内定义局部变量确实可以解决线程安全问题,但是如果是夸类,方法等调用的时候,需要时时刻刻带着该变量,导致业务极度耦合,而使用ThreadLocal就可以很好的规避这个问题.
// ThreadLocal的本质其实ThreadLocal.ThreadLocalMap 而ThreadLocal只是起到一个桥接的作用
// 通常我们在使用ThreadLocal的时候,都会将其设置为static final,主要是为了可以让多线程共同访问,多次实例化ThreadLocal就违背了线程共享的本质
static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) {
THREAD_LOCAL.set("我是主线程");
// 通过THREAD_LOCAL到当前线程ThreadLocal.ThreadLocalMap去获取数据,意味着该数据是存储在当前线程内部的,从而实现线程隔离
log.info(THREAD_LOCAL.get());
// 在使用完以后,需要手动remove,避免不必要的内存泄露
THREAD_LOCAL.remove();
new Thread(() -> {
// 这里获取的是子线程中ThreadLocal.ThreadLocalMap中的数据,ThreadLocal中的数据可以夸方法,类共享,但是不能夸线程共享
THREAD_LOCAL.set("我是子线程");
log.info(THREAD_LOCAL.get());
THREAD_LOCAL.remove();
}).start();
}
那为什么不通过方法参数传递变量?
这样会导致类与类之间的紧耦合,特别是在跨多个方法的时候
为什么 TheadLocal 常作为私有静态使用?
如果 TheadLocal 不作为静态变量,而属于某个线程的实例类,就失去了线程之间共享的本质属性,同时 ThreadLocal 的作用只是标识变量以及访问 ThreadLocalMap ,多份的 ThreadLocal 没有必要
2.TheadLocal 实践场景
1.透传全局上下文 2.解决并发下 SimpleDateFormat 的线程不安全 等问题
3.ThreadLocal的源码
- ThreadLocalMap本质
static class ThreadLocalMap {
/**
* Entry继承了WeakReference,实现键弱应用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// 键是ThreadLocal本身,而值就是需要存储的值,将这个保存在Thread中
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量为16
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 要调整大小的下一个大小值
*/
private int threshold; // Default to 0
/**
* 达到2/3的时候开始扩容
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
ThreadLocalMap本质是一个Entry对象,而Entry又继承了WeakReference,实现了键弱引用,而值还是强引用,初始容量为16,当容量达到2/3时,会开始扩容,ThreadLocalMap并不像hashMap一样,通过拉链法解决hash冲突问题,而是采用开放地址法解决hash冲突
- set方法源码
public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}
- get方法源码
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}
private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}
- remove方法的实现
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
public void remove() {
//获取当前线程绑定的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
m.remove(this);
}
4.ThreadLocal内存泄露问题
因为线程到 ThreadLocalMap 再到 Entry 再到 局部变量存在强引用的关系,当线程不死亡也不对局部变量进行 remove 操作,局部变量是不会被回收的。
虽然在 TheadLocalMap 中,Entry 的 key 是由一个虚引用指向相应的 TheadLocal 对象,当 ThreadLocal 变量没有强引用指向它时,指向 ThreadLocal 会被下一次 GC 回收,但尽管如此对于 Entry 中 Value 还是存在强引用是不会被回收的。更何况 ThreadLocal 变量一般由 private static 修饰,生命周期至少不会随着线程结束而结束。
总之就是,线程中的局部变量 Value 无法在在该回收时自动回收,线程持续执行或者呆在线程程池中占用不必要的内存,导致内存泄漏
由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
虽然TheadLocalMap的键是弱应用 会被GC掉,但是value还是强引用无法被GC,从而导致内存泄露问题,更何况平时正确使用ThreadLocal姿势,都是static final进行修饰,导致ThreadLocal和类共生死,拉长其生命周期,所以并不会随着线程死亡还回收
这需要实际的时候使用完毕及时调用remove()方法避免内存泄漏。
5.ThreadLocal不支持父子线程共享
同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)