Administrator
Published on 2021-12-15 / 158 Visits
0
0

JUC之ThreadLocal

JUC之ThreadLocal

为什么说 SimpleDateFormat 非线程安全的?

SimpleDateFormat类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关 String,Date 等等,都是交由 Calendar 引用来储存的,这样就会导致一个问题,如果你的 SimpleDateFormat 是 static 的,那么多个 thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用

解决方案

  • 将 SimpleDateFormat 定义成局部变量
  • 使用 ThreadLocal 包一层

Thread,ThreadLocal,ThreadLocalMap 关系

  • ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的 map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象
  • JVM内部维护了一个线程版的 Map<Thread,T> (通过ThreadLocal对象的 set 方法,把 ThreadLocal 对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个 T 的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量
  • 每个Thread对象维护着一个 ThreadLocalMap 的引用
  • ThreadLocalMap是 ThreadLocal 的内部类,用 Entry 来进行存储
  • 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
  • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  • ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

ThreadLocal内存泄露问题

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

为什么源代码用弱引用?

  • 当 function01 方法执行完毕后,栈帧销毁强引用 tl 也就没有了,但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象若这个key引用是强引用,就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收,造成内存泄漏,若这个 key 引用是弱引用就大概率会减少内存泄漏的问题,使用弱引用,可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为null
  • 此后我们调用 get,set 或 remove 方法时,就会尝试删除 key 为null的 entry,可以释放value对象所占用的内存

thread-local-m

弱引用就万事大吉了吗?

  • 当我们为threadLocal变量赋值,实际上就是当前的 Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放,Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏
  • 虽然弱引用保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
  • 从前面的 set,getEntry,remove 方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry

Comment