ThreadLocal
使用场景
- 每个线程需要一个独享的对象(通常是工具类,典型的类有 SimpleDateFormat 和 Random) ,要重写initialValue()方法
- 每个线程内需要保存全局变量(例如在连接器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。这些信息在同一个线程内相同,但是在不同的线程内是不同的,所以不能使用 static 方法来解决。使用 ConcurrentHashMap,synchronized 等会对性能有影响。不需要重写initialValue()方法,但是要手动调用set()方法
当ThreadLocal中的对象初始化的时机由我们控制,不受外界干扰,就选择场景一,而对象初始化的时机受外界影响,如拦截器里生成的用户信息,则选择场景二,但究其本质,源码中都是调用map.set方法设置值
使用ThreadLocal的好处
- 达到线程安全
- 不需要加锁,执行效率高
- 更高效的利用内存,节省开销
- 免去传参的繁琐,降低代码的耦合
Thread,ThreadLocalMap,ThreadLocal辨析
- 每一个Thread实例里都有一个ThreadLocalMap属性,名称叫ThreadLocals,初始值是null,访问的时候如果是null,就会创建对象.一个Thread实例维护一个ThreadLocalMap对象.
- ThreadLocalMap实例里维护的是一个或者多个Map<k,v>,存放ThreadLocal实例
ThreadLocal常用方法
initialValue()
- 该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候才会触发
- 如果get之前,线程使用了set()方法,在这种情况下,initialValue()不会触发
- 通常每个线程只调用一次initialValue()方法,但如果调用了remove()后,再调用get会再次触发
-
如果不重写本方法,这个方法会返回null,一般以匿名内部类来重写
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取到了值直接返回resule
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//没有获取到才会进行初始化
return setInitialValue();
}
private T setInitialValue() {
//获取initialValue生成的值,并在后续操作中进行set,最后将值返回
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);
}
get()
- get()方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,去除map中属于本ThreadLocal的Value
- 注意这个map是保存再Thread中的而不是保存在ThreadLocal中的
set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
remove()
- 只删掉一个ThreadLocal,而不是删除ThreadLocalMap
ThreadLocalMap
ThreadLocalMap是Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:键是ThreadLocal,值就是我们需要的成员变量
弱引用和阿里规约
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap.
Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key.
每个key都弱引用指向threadlocal,在对键值对赋值的时候,调用super(key),而父类就是WeakReference
所以当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收
依然出现的内存泄露问题
虽然上述的弱引用解决了key,也就是线程的ThreadLocal能及时被回收,但是value却依然存在内存泄漏的问题。
当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收.
map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露,
因为存在一条从current thread连接过来的强引用
只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收(但在线程池中,线程很难被回收)
解决方法(阿里规约)
Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的remove,set的时候都会清除线程Map里所有key为null的value。
但最怕的情况就是: threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。
所以当线程的某个localThread使用完了,马上调用threadlocal的remove方法,就是所谓阿里规约
ThreadLocal的空指针异常问题
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
//当get前面的是long,就会报空指针异常
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
System.out.println(threadLocalNPE.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,造成空指针问题的原因在于使用者。
共享对象问题
如果在每个线程中ThreadLocal.set()进去的东西本来就是多个线程共享的同一对象,比如static
对象,那么多个线程调用ThreadLocal.get()
获取的内容还是同一个对象,还是会发生线程安全问题。
优先使用框架的支持,而不是自己创造
例如在Spring框架中,如果可以使用RequestContextHolder
,那么就不需要自己维护ThreadLocal
,因为自己可能会忘记调用remove()
方法等,造成内存泄漏。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。