一、初识ThreadLocal
1、什么是threadlocal变量?
- ThreadLocal变量是线程的局部变量,同一个threadLocal所包含的对象在不同的Thread中有不同的副本。需要注意:
- 因为每个thread内有自己的是咧副本,且该副本只能由当前thread使用。
- 既然每个thread都有自己的副本,且其它thread不可访问,那就不存在多线程间共享的问题;
- ThreadLocal提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本;
- TheadLocal变量通常被private static修饰,当一个线程结束时,它所拥有ThreadLocal相对的实例副本都可被回收;
- ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在在线程间隔离而在方法或类间共享的场景;
2、ThreadLocal实现原理
- ThreadLocal是一个泛型类,保证可以接受任何类型的对象;
- 因为一个线程可以存放多个ThreadLocal对象,所以其实ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法;
- 最终的变量是存放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值;
3、内存溢出问题
- 实际上ThreadLocalMap中使用的key为ThreadLocal的弱引用(弱引用特点:如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉);
- 所有如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap中使用这个ThreadLocal的key也会被清理掉。但是value是强引用,不会被清理掉,这样一来就会出现key为null的value;
- 上面的情况,ThreadLocalMap已经考虑了,在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。如果说会出现内存泄漏,那只有在出现了key为null的记录后,没有手动调用remove()方法,并且之后也没有再调用get()、set()、remove()方法的情况下;
- 解决内存泄漏:回收自定义的ThreadLocal变量,通过try-finally块进行垃圾回收。(列如:尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存溢出的风险);
private static ThreadLocal threadLocal = new ThreadLocal(); threadlocal.set(变量); try{ //... } finally{ threadLocal.remove(); }
4、使用场景
- 每个线程需要有自己单独的实列;(方式很多,ThreadLocal可以非常方便的形式满足该需求)
- 实列需要在多个方法中共享,但不希望被多线程共享;(在满足第一点,每个线程有单独实列的条件下,通过方法间引用传递的形式实现。ThreadLocal使得代码耦合度更低,且实现更优雅)
举例子:
存储用户Session
public class Demo1 { private static ThreadLocal threadLocal = new ThreadLocal(); public static Session getSession(){ Session s = (Session)threadLocal.get(); try{ if (null==s) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); HttpSession session = request.getSession(); threadLocal.set(session); } }catch (Exception e){ threadLocal.remove();//手动清理,避免内存溢出 throw new GlobalHandleException(ResultCode.ERROR); } return s; } }解决线程安全问题,比如在jdk7中的SimpleDateFormat不是线程安全的,可以使用ThreadLocal来解决。(注:jdk8的java.time.format.DateTimeFormatter是线程安全的)
public class Demo2 { private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String formatData(Date date){ return simpleDateFormatThreadLocal.get().format(date); } public static void main(String[] args) { //高并发多线程下,Demo2.formatData(new Date())就是线程安全的 System.out.println(Demo2.formatData(new Date())); } }
5、threadLocalRandom
ThreadLocalRandom使用ThreadLocal的原理,让每一个线程内持有一个本地的种子变量,该种子变量只有在使用随机数的时候才会被初始化,多线程下计算新种子的时候根据自己线程内维护的种子变量进行更新,从而避免竞争。下面是用法:
public class Demo3 { public static void main(String[] args) { //获取100里面的随机数,在多线程情况线程安全,避免竞争 int i = ThreadLocalRandom.current().nextInt(100); System.out.println(i); } }
二、原理分析
1、源码解读
每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储当前线程中所有ThreadLocal对象及其对应值。源码:
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; }而ThreadLocalMap中的核心就是一个Entry对象(ThreadLocalMap是ThreadLocal的一个静态内部类)。源码:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k,Object v){ super(k); value = v; } } }通过一张图来表明ThreadLocal的引用关系
通过代码实列来表明使用和不使用ThreadLocal的效果
/** * @des: 下面这个实例:体现ThreadLocal的作用,即保证多线程之间变量不共享 */ public class Demo4 { private String data;//没有使用ThreadLocal时,线程之间出现了变量混用 private String getDate(){ return this.data; } private void setData(String data){ this.data = data; } //_________________________________________________// //使用ThreadLocal时,线程之间没有出现了变量混用,各自线程使用自己ThreadLocal维护的变量 ThreadLocal<String> threadLocal = new ThreadLocal<>(); private String getDate1(){ return this.threadLocal.get(); } private void setData1(String data1){ this.threadLocal.set(data1); } //多运行几次看效果 public static void main(String[] args) { final Demo4 demo4 = new Demo4(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //存数据 demo4.setData(Thread.currentThread().getName().concat("的数据")); demo4.setData1(Thread.currentThread().getName().concat("的数据")); //取数据 System.out.println(Thread.currentThread().getName().concat("拿走了[").concat(demo4.getDate()).concat("]")); System.err.println(Thread.currentThread().getName().concat("拿走了[").concat(demo4.getDate1()).concat("]")); } }); thread.setName("线程"+i); thread.start(); } } } //经多次运行后测试发现: //1、不使用ThreadLocal时,线程之间会出现变量的混用; //2、而使用了后,各线程只会使用各自线程维护的变量;总结:Thread提供线程内部的局部变量,不同线程之间不会相互干扰,这种变量仅在线程的生命周期内起作用。
2、Thread Local内存泄漏问题
内存泄漏和内存溢出关系
- 内存泄漏(Memory Leak):程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成的系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致严重的内存溢出;
- 内存溢出(Out Of Memory):无法给声明的对象提供足够的内存空间,程序无法再正常运行;
Java对象的四大引用
- 强引用(Strong Refrence):最普遍使用的引用,垃圾回收器不会回收一个持有强引用的对象。即使内存空间不足时,Java虚拟机宁愿抛出Out of Memory,终止程序运行,也不会依靠回收具有强引用的对象来解决内存空间不足的问题。
- 软引用(Soft Refrence):一个只持有软引用的对象,只要内存空间充足,垃圾回收器就不会回收它,一旦内存空间不足,就会回收这些对象的内存来保证程序的运行。
- 弱引用(Weak Refrence):只持有弱引用的对象比只持有软引用的对应拥有更加短暂的生命周期。当垃圾回收器线程扫描到只具有弱引用的对象,不管当前内存空间是否充足,都会回收这些对象的内存。不过,垃圾回收线程是一个优先级很低的线程,所以不一定会很快发现那些只具有弱引用的对象。
- 虚引用(Phantom Refrence):就是字面意思,与其它引用都不同的是虚引用不会决定对象的生命周期,也就是说只持有虚引用的对象就和没有任何引用一样,任何时候都可能被垃圾回收器回收,所以虚引用必须和引用队列(Reference Quene)联合使用来发挥自己的作用;
内存泄漏的根本原因
- 所有Entry对象都被ThreadLoclMap类的实例化对象threadLocals持有,当ThreadLocal对象不再使用时,ThreadLocal对象不再使用时(线程的生命周期结束或者一个接口的请求响应结束),ThreadLocal对象在栈中的引用就会被回收,一旦没有任何引用指向ThreadLocal对象,Entry只持有弱引用的Key就会在下一次YGC时被回收,而此时持有强引用的Enety对象并不会被回收;
- 简而言之,threadLocals对象中的entry对象不在使用后,没有及时remove该entry对象,然而程序自身也无法通过垃圾回收机制自动清除,从而导致内存泄漏;
- 解决方案:只要在使用完ThreadLocal对象后,调用其remove方法删除对应的Entry;
对象已经已经不会再使用,垃圾回收器不能清除
垃圾回收器不能清除代表这个对象肯定还可达,也就是还有GC root可以到这个对象引用链。但这个对象对于我们程序员来说已经没有用了,我们不会再使用这个对象;
ThreadLocal的set是存在当前线程对象的ThreadLocalMap中,当栈中对ThreadLocal对象的引用释放后,GC后ThreadLocalMap中的Key就指向了null,但是value还是指向你set的对象。注意对当前线程对象的引用还在,也就是还有当前线程对象一直可达,这样就导致当前线程对象的ThreadLocalMap中的Key为null,value不为null。value一直没有办法释放;

