多个线程同时读写同一共享变量存在并发问题。要解决这个问题,我们可以突破的写,没有写操作自然没有并发问题了。其实还可以突破共享变量,没有共享变量也不会有并发问题,没有共享,就没有伤害。
那如何避免共享呢?每个线程都拥有自己的变量,彼此之间不共享,也就没有并发问题了。
通过局部变量可以做到避免共享,Java 语言提供的线程本地存储(ThreadLocal)也能够做到。下面我们先看看 ThreadLocal 到底该如何使用。
ThreadLocal 的使用方法
SimpleDateFormat 不是线程安全的,那如果需要在并发场景下使用它,你该怎么办呢?
其实有一个办法就是用 ThreadLocal 来解决,下面的示例代码就是 ThreadLocal 解决方案的具体实现:
static class SafeDateFormat {
// 定义 ThreadLocal 变量
static final ThreadLocal<DateFormat>
tl=ThreadLocal.withInitial(
()-> new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss"));
static DateFormat get(){
return tl.get();
}
}
// 不同线程执行下面代码
// 返回的 df 是不同的
DateFormat df =
SafeDateFormat.get();
ThreadLocal 的工作原理
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
Thread 持有 ThreadLocalMap 的示意图
class Thread {
// 内部持有 ThreadLocalMap
ThreadLocal.ThreadLocalMap
threadLocals;
}
class ThreadLocal<T>{
public T get() {
// 首先获取线程持有的
//ThreadLocalMap
ThreadLocalMap map =
Thread.currentThread()
.threadLocals;
// 在 ThreadLocalMap 中
// 查找变量
Entry e =
map.getEntry(this);
return e.value;
}
static class ThreadLocalMap{
// 内部是数组而不是 Map
Entry[] table;
// 根据 ThreadLocal 查找 Entry
Entry getEntry(ThreadLocal key){
// 省略查找逻辑
}
//Entry 定义
static class Entry extends
WeakReference<ThreadLocal>{
Object value;
}
}
}
ThreadLocal 与内存泄露
在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?
因为ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
ThreadLocal 最佳实践
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
网友评论