• 深入理解ThreadLocal


    一、ThreadLocal

    ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的

    因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
    既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

    ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
    在这里插入图片描述
    在这里插入图片描述

    public class SequenceNumber {
            //①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    	private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
    		public Integer initialValue(){
    			return 0;
    		}
    	};
         
            //②获取下一个序列值
    	public int getNextNum(){
    		seqNum.set(seqNum.get()+1);
    		return seqNum.get();
    	}
    	
    	public static void main(String[ ] args) 
    	{
              SequenceNumber sn = new SequenceNumber();
             
             //③ 3个线程共享sn,各自产生序列号
             TestClient t1 = new TestClient(sn);  
             TestClient t2 = new TestClient(sn);
             TestClient t3 = new TestClient(sn);
             t1.start();
             t2.start();
             t3.start();
    	}	
    	private static class TestClient extends Thread
    	{
    		private SequenceNumber sn;
    		public TestClient(SequenceNumber sn) {
    			this.sn = sn;
    		}
    		public void run() {
               //④每个线程打出3个序列值
    			for (int i = 0; i < 3; i++) {
    			System.out.println("thread["+Thread.currentThread().getName()+ "] sn["+sn.getNextNum()+"]");
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    二、ThreadLocal与Synchronized的区别

    ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

    但是ThreadLocal与synchronized有本质的区别:
    1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

    2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

    而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
    一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

    三、ThreadLocal核心方法

    ThreadLocal中set方法

    	ThreadLocal.ThreadLocalMap threadLocals = null;
    	
        public void set(T value) {
       		//返回对当前执行的线程对象的引用。
            Thread t = Thread.currentThread();
            //获取与ThreadLocal关联的映射,InheritableThreadLocal<T> extends ThreadLocal<T> 重写了getMap()方法
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
            //InheritableThreadLocal中重写了createMap->初始化当前线程的ThreadLocalMap->ThreadLocal.ThreadLocalMap
                createMap(t, value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ThreadLocal中get方法

        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            //返回初始化的值null
            return setInitialValue();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ThreadLocal中remove方法

     public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
    
    • 1
    • 2
    • 3
    • 4

    remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

    ThreadLocal中initialValue方法

         protected T initialValue() {
            return null;
        }
    
    • 1
    • 2
    • 3

    返回该线程局部变量的初始值,若使用protected限制父类的方法,则该方法仅父类和子类内部(即定义父类和子类的代码中)可以调用,所以这个方法显然是为了子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    Entry

    ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用,因此当value=null时意味着该键不再被引用可以被垃圾回收 。在Entry内部使用ThreadLocal作为key,

            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

    所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

    ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

    四、ThreadLocal与Thread,ThreadLocalMap之间的关系

    在这里插入图片描述
    (1)每个Thread线程内部都有一个Map (ThreadLocalMap)
    ( 2 ) Map里面存储ThreadLocal对象(key )和线程的变量副本( value )
    ( 3 ) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变星值。
    ( 4 ) 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

    五、ThreadLocal 常见使用场景

    1、每个线程需要有自己单独的实例
    2、实例需要在多个方法中共享,但不希望被多线程共享
    对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
    对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

    场景一 ThreadLocal来存储Session的例子

    private static final ThreadLocal threadSession = new ThreadLocal();
     
        public static Session getSession() throws InfrastructureException {
            Session s = (Session) threadSession.get();
            try {
                if (s == null) {
                    s = getSessionFactory().openSession();
                    threadSession.set(s);
                }
            } catch (HibernateException ex) {
                throw new InfrastructureException(ex);
            }
            return s;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    场景二 解决线程安全的问题

    比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:

    public class DateUtil {
        private static final String dateFormatStr = "yyyy-MM-dd HH:mm:ss";
        private static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(dateFormatStr);
            }
        };
    
        public static String formatDate(Date date) {
            return dateFormat.get().format(date);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里的DateUtil.formatDate()就是线程安全的了。(Java8里的 java.time.format.DateTimeFormatter是线程安全的,Joda time里的DateTimeFormat也是线程安全的)。

    场景三、使用切面打印日志开始到结束使用ThreadLocal解决

    @Component
    @Slf4j
    @Aspect
    public class Aspect1 {
        ThreadLocal<Long> startTime = new ThreadLocal<>();
    
        @Pointcut("execution(* com.*(..))")
        public void webLog(){}
    
        @Before(value = "webLog()")
        public void before(JoinPoint joinPoint) {
            //输出连接点的信息
            startTime.set(System.currentTimeMillis());
            //日志操作
            ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();
            log.info("****************HeaderStart***********************");
            while (headerNames.hasMoreElements()){
                String headerName = headerNames.nextElement();
                log.info("*****<{}: {}>",headerName,request.getHeader(headerName));
            }
            log.info("****************HeaderEnd***********************");
            //------------其他处理
        }
    
        @AfterReturning(returning = "ret", value = "webLog()")
        public void afterThrowing(String ret) {
            log.info("RESPONSE: {}",ret);
            log.info("SPEND TIME: {}",System.currentTimeMillis()-startTime.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    场景四、ThreadLocal在Spring事务管理中的应用

    Spring使用ThreadLocal解决线程安全问题
    在一般情况下,只有无状态的Bean从才能在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。(PS:Spring Bean实例的作用范围,一般由scope进行指定,scope配置项有5个属性,用于描述不同的作用域:1.singleton:使用该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。2.prototype:使用该属性定义Bean时,IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。)
    绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean中非线程安全的”状态性对象“采用ThreadLocal进行封装。因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
    一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。一般情况下,从接收请求到返回响应所经过的所有程序调用都属于同一个线程。
    下面的示例能够体现Spring对有状态Bean的改造思路:

    public class TopicDao{
    	private Connection conn;//一个非线程安全的变量
    	public void addTopic(){
    		Statement stat=conn.createStatement();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    由于这个conn是一个非线程安全的成员变量,因此addTopic()方法是非线程安全的,必须在使用时创建一个新的TopicDao实例。下面,使用ThreadLocal对conn这个非线程安全的状态进行改造:

    import java.sql.Connection;
    import java.sql.Statement;
    public class TopicDao{
    	private static ThreadLocal<Connection> connThreadLocal=new ThreadLocal<Connection>();//使用ThreadLocal保存Connection变量
    	public static Connection getConnection(){
    		if(connThreadLocal.get()==null){//如果connThreadLocal没有本线程对应的Connection创建一个新的Connection
    			Connection conn=ConnectionManager.getConnection();
    			connThreadLocal.set(conn);
    			return conn;
    		}
    		else{
    			return connThreadLocal.get();//直接返回线程本地变量
    		}
    	}
    	public void addTopic(){
    		Statement stat=getConnection().createStatement();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中,如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用自己独立的Connection,而不会使用其他线程的Connection,因此,这个TopicDao就可以做到singleton共享了。

    六、ThreadLocal其他几个注意的点

    ThreadLocal 内存泄露的原因

    Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。可以看图1.

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    主要两个原因
    1 . 没有手动删除这个 Entry
    2 . CurrentThread 当前线程依然运行
    第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。

    第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际上ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。

    综上, ThreadLocal 内存泄漏的根源是:
    由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.

    key 如果是强引用

    为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。

    假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

    为什么 key 要用弱引用

    事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.
    在这里插入图片描述

    如何正确的使用ThreadLocal

    1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

    2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    ThreadLocal高级面试真题

    一.ThreadLocal 是什么?
    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。

    二.为什么 ThreadLocalMap 的 key 是弱引用?
    1.key使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。

    2.key使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。

    总结:比较以上两种情况,我们可以发现:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。

    三.ThreadLocal类之如何让子类访问父线程的值?
    1.InheritableThreadLocal类

    继承自ThreadLocal,提供了一个特性,让子线程可以访问父线程中设置的本地变量。InheritableThreadLocal重写了creatMap方法,所以在这个类中inheritableThreadLocals代替了threadLocals,所以get和set的都是这个map

    2.创建子线程的时候传入父线程的变量,并将其赋值到子线程

  • 相关阅读:
    桥梁模板人工费多少钱?
    位运算相关
    Windows11 VMware上安装适用于编译Android12源代码的Ubuntu虚拟机
    A loam位姿结果缺1帧;rosbag收不到第1帧;kittihelper转的bag
    Java nio 的线程通信机制线程通信Pipe
    Java8:Effectively final
    websocket入门
    LeetCode刷题复盘笔记—一文搞懂343. 整数拆分(动态规划系列第四篇)
    分页请求时避免二次请求
    CPU性能优化干货总结
  • 原文地址:https://blog.csdn.net/weixin_56219549/article/details/125493345