专项攻克——ThreadLocal全解

📅 发布时间:2026/8/27 11:59:15
专项攻克——ThreadLocal全解
文章目录1. Threadlocal作用2. API3. 使用案例4. 源码分析5. ThreadLocal内存泄露5.1 ThreadLocalkey是弱引用5.2 为什么不将key设计为强引用5.2.1 假如key 设计成强引用5.3 为什么 key 要设计成弱引用5.4 ThreadLocal如何避免内存泄露6、应用场景6.1 存储用户登录session6.2 数据库连接处理数据库事务6.3 数据跨层传递controller,service, dao6.4 Spring使用ThreadLocal解决线程安全问题1. Threadlocal作用简单的说一个ThreadLocal在一个线程中是共享的在不同线程之间又是隔离的每个线程都只能看到自己线程的值。如下图2. APIThreadLocal类的API非常的简单在这里比较重要的就是get()、set()、remove()、initiaValue()。其中initialValue方法会在第一次调用时被触发用于初始化当前变量值默认返回null。3. 使用案例ThreadLocal在一个线程中是共享的在不同线程之间是隔离的每个线程都只能看到自己线程的值。隔离性案例如下在类中创建了一个静态的 “ThreadLocal变量”在主线程中创建两个线程在这两个线程中分别设置ThreadLocal变量为2和3。然后等待一号和二号线程执行完毕后在主线程中查看ThreadLocal变量的值。publicclassThreadLocalTest_1{publicstaticThreadLocalIntegerthreadLocal_AnewThreadLocalInteger();publicstaticThreadLocalIntegerthreadLocal_BnewThreadLocalInteger();static{threadLocal_A.set(1);threadLocal_B.set(11);}publicstaticvoidmain(String[]args){System.out.println(主线程-AthreadLocal_A.get());System.out.println(主线程-BthreadLocal_B.get());newThread(newRunnable(){Overridepublicvoidrun(){System.out.println(线程2-A前threadLocal_A.get());System.out.println(线程2-B前threadLocal_B.get());threadLocal_A.set(2);threadLocal_B.set(22);System.out.println(线程2-A后threadLocal_A.get());System.out.println(线程2-B后threadLocal_B.get());}}).start();newThread(newRunnable(){Overridepublicvoidrun(){System.out.println(线程3-A前threadLocal_A.get());System.out.println(线程3-B前threadLocal_B.get());threadLocal_A.set(3);threadLocal_B.set(33);System.out.println(线程3-A后threadLocal_A.get());System.out.println(线程3-B后threadLocal_B.get());}}).start();System.out.println(主线程-A结束threadLocal_A.get());System.out.println(主线程-B结束threadLocal_B.get());}}结果4. 源码分析ThreadLocal 也叫线程变量意思是ThreadLocal中填充的变量属于当前线程该变量对其他线程是隔离的是当前线程独有的变量。怎么做到隔离的呢每个Thread里都有一个变量ThreadLocalMap threadLocals变量threadLocals能以(keythreadLocalvaluevalue)的形式存储threadLocal在当前线程内的数据。ThreadLocalMap threadLocals 在ThreadLocal对象的set方法去插入threadLocal对象和数据也由ThreadLocal来维护。其他set赋值的时候首先会获取当前线程thread并获取thread线程中的ThreadLocalMap属性。如果map属性不为空则直接更新value值如果map为空则实例化threadLocalMap并将value值初始化。remove方法直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除这涉及到内存泄露的问题。publicclassThreadLocalT{privatefinalintthreadLocalHashCodenextHashCode();privatestaticAtomicIntegernextHashCodenewAtomicInteger();privatestaticfinalintHASH_INCREMENT0x61c88647;...publicThreadLocal(){}publicTget(){ThreadtThread.currentThread();ThreadLocalMapmapgetMap(t);if(map!null){ThreadLocalMap.Entryemap.getEntry(this);if(e!null){SuppressWarnings(unchecked)Tresult(T)e.value;returnresult;}}returnsetInitialValue();}privateTsetInitialValue(){TvalueinitialValue();ThreadtThread.currentThread();ThreadLocalMapmapgetMap(t);if(map!null)map.set(this,value);elsecreateMap(t,value);returnvalue;}//对当前线程的ThreadLocalMap设置key为当前threadLocal值为valuepublicvoidset(Tvalue){ThreadtThread.currentThread();ThreadLocalMapmapgetMap(t);if(map!null)map.set(this,value);elsecreateMap(t,value);}publicvoidremove(){ThreadLocalMapmgetMap(Thread.currentThread());if(m!null)m.remove(this);}ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}/** 内部类 */staticclassThreadLocalMap{...}}5. ThreadLocal内存泄露补充知识内存泄露、内存溢出内存泄漏memory leak是指程序在申请内存后无法释放已申请的内存空间一次内存泄漏似乎不会有大的影响但内存泄漏堆积后的后果就是内存溢出内存溢出out of memory指程序申请内存时没有足够的内存供申请者使用。比如给了一块存储int类型数据的存储空间却存储long类型的数据结果就是内存不够用会报错OOM即所谓的内存溢出。补充知识 java中的四种引用强引用 如果一个对象具有强引用它就不会被垃圾回收器回收。即使当前内存空间不足JVM也不会回收它而是抛出 OutOfMemoryError错误使程序异常终止。如果想中断强引用和某个对象之间的关联可以显式地将引用赋值为null这样一来的话JVM在合适的时间就会回收该对象弱引用 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收一旦发现弱引用对象无论当前内存空间是否充足都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程所以并不一定能迅速发现弱引用对象软引用 在使用软引用时如果内存的空间足够软引用就能继续被使用而不会被垃圾回收器回收只有在内存不足时软引用才会被垃圾回收器回收。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露增强程序的健壮性虚引用 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用那么它就和没有任何引用一样在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列ReferenceQueue联合使用。当垃圾回收器准备回收一个对象时如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中。注意哦其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作比如说资源释放等。5.1 ThreadLocalkey是弱引用staticclassEntryextendsWeakReferenceThreadLocal?{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);valuev;}}Entry将ThreadLocal作为Key值作为value保存它继承自WeakReference注意构造函数里的第一行代码super(k)这意味着ThreadLocal对象是一个「弱引用」。ThreadLocalMap的生命周期跟Thread注意线程池中的Thread一样长。如果没有手动remove 对应keyvalue是不会被回收的一定会导致内存泄漏解释线程使用结束归还给线程池了其中的KV不再被使用但又不会GC回收可认为是内存泄漏。弱引用回收value内存泄露当弱引用ThreadLocal等于null时ThreadLocal 会被GC回收而对应的value在下一次ThreadLocalMap调用set,get,remove方法时才被清除。但是如果线程是线程池里的核心线程由于线程的周期特别长线程一直被重复利用entry(nullvalue)的对象越来越多线程中Entry对象中的value就可能一直得不到回收发生内存泄露。原因补充Java8优化在ThreadLocal的get()、set()、remove()方法调用时会清除线程ThreadLocalMap中所有Entry中Key为null的Value并将整个Entry设置为null利于下次内存回收。5.2 为什么不将key设计为强引用5.2.1 假如key 设计成强引用为什么ThreadLocalMap的key要设计成弱引用呢其实很简单如果key设计成强引用且没有手动remove()那么key会和value一样伴随线程的整个生命周期。解释假设在业务代码中使用完ThreadLocal, ThreadLocal本该被回收了但是threadLocalMap的Entry强引用了threadLocal造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)- entryEntry就不会被回收( Entry中包括了ThreadLocal实例和value)导致Entry内存泄漏。5.3 为什么 key 要设计成弱引用事实上在 ThreadLocalMap 中的set/getEntry 方法中会对 key 为 null也即是 ThreadLocal 为 null 进行判断如果为 null 的话那么会把 value 置为 null 的这就意味着使用threadLocal , CurrentThread 依然运行的前提下就算忘记调用 remove 方法弱引用比强引用可以多一层保障弱引用的 ThreadLocal 会被回收对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除从而避免内存泄漏.key使用强引用/弱引用的区别key 使用强引用引用的ThreadLocal的对象被回收了但是ThreadLocalMap还持有ThreadLocal的强引用如果没有手动删除ThreadLocal不会被回收导致Entry内存泄漏。key 使用弱引用引用的ThreadLocal的对象被回收了由于ThreadLocalMap持有ThreadLocal的弱引用即使没有手动删除ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。5.4ThreadLocal如何避免内存泄露ThreadLocal避免内存泄露的方法将ThreadLocal变量定义成private static的这样就随时可以根据ThreadLocal访问到Entry的value值然后remove() 防止内存泄露。每次使用完ThreadLocal都调用它的remove()方法清除数据。6、应用场景ThreadLocal 适用于如下两种场景每个线程需要有自己单独的实例实例需要在多个方法中共享但不希望被多线程共享应用场景举例6.1 存储用户登录sessionprivatestaticfinalThreadLocalthreadSessionnewThreadLocal();publicstaticSessiongetSession()throwsInfrastructureException{Sessions(Session)threadSession.get();try{if(snull){sgetSessionFactory().openSession();threadSession.set(s);}}catch(HibernateExceptionex){thrownewInfrastructureException(ex);}returns;}6.2 数据库连接处理数据库事务6.3 数据跨层传递controller,service, dao每个线程内需要保存类似于全局变量的信息例如在拦截器中获取的用户信息可以让不同方法直接使用避免参数传递的麻烦却不想被多线程共享因为不同线程获取到的用户信息不一样。例如用 ThreadLocal 保存一些业务内容用户权限信息、从用户系统获取到的用户名、用户ID 等这些信息在同一个线程内相同但是不同的线程使用的业务内容是不相同的。在线程生命周期内都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象避免了将这个对象如 user 对象作为参数传递的麻烦。比如说我们是一个用户系统那么当一个请求进来的时候一个线程会负责执行这个请求然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4()这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。packagecom.kong.threadlocal;publicclassThreadLocalDemo05{publicstaticvoidmain(String[]args){UserusernewUser(jack);newService1().service1(user);}}classService1{publicvoidservice1(Useruser){//给ThreadLocal赋值后续的服务直接通过ThreadLocal获取就行了。UserContextHolder.holder.set(user);newService2().service2();}}classService2{publicvoidservice2(){UseruserUserContextHolder.holder.get();System.out.println(service2拿到的用户:user.name);newService3().service3();}}classService3{publicvoidservice3(){UseruserUserContextHolder.holder.get();System.out.println(service3拿到的用户:user.name);//在整个流程执行完毕后一定要执行removeUserContextHolder.holder.remove();}}classUserContextHolder{//创建ThreadLocal保存User对象publicstaticThreadLocalUserholdernewThreadLocal();}classUser{Stringname;publicUser(Stringname){this.namename;}}执行的结果 service2拿到的用户:jack service3拿到的用户:jack6.4 Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下只有无状态的Bean才可以在多线程环境下共享在Spring中绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean中非线程安全的“状态性对象”采用ThreadLocal进行封装让它们也成为线程安全的“状态性对象”因此有状态的Bean就能够以singleton的方式在多线程中正常工作了如用户信息Context、RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等。这样用户就可以根据需要将一些非线程安全的变量以ThreadLocal存放在同一次请求响应的调用线程中所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。