ThreadLocal的浅理解

一、ThreadLocal简介

1.ThreadLocal介绍

        ThreadLocal是线程变量在ThreadLocal中声明的变量仅属于当前线程,对其他线程是隔离的。ThreadLocal为变量在每个线程中创建了一个副本,每个线程可以访问自己内部的副本变量。

  2. 注意点

  • 因为每个ThreadLocal内有自己的实例副本,且该副本只能当前Thread使用
  • 每个Thread有自己的实例副本,且其他Thread不可访问,不存在多线程共享问题

        ThreadLocal变量通常被private static修饰,当一个线程结束时,它所使用的所有ThreadLocal相对应的副本都可被回收。

3.ThreadLocal使用状态图

 ThreadLocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(ThreadLocal,value),虽然不同的线程之间ThreadLocal的key值一样,但是不同的线程所拥有的ThreadLocalMap是唯一的,不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而实现了线程间变量隔离的目的,在同一个线程中value的地址是一样的。

二、ThreadLocal与Synchronized区别

ThreadLocal与Synchronized都可以解决多线程并发访问。

区别:

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

三、ThreadLocal中的核心方法

1.ThreadLocal中的set方法

public void set(T value) {
        /*  返回对当前执行的线程对象引用  */
        Thread t = Thread.currentThread();
        /*  获取线程中的属性threadLocalMap,如果threadLocalMap 不为空
        * 则直接更新要保存的变量值  */
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            /* 初始化threadLocalMap,并赋值 */
            createMap(t, value);
        }
    }

        从上面代码可以看出,ThreadLocal set赋值时首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value,如果map为空,则实例化threadLocalMap,并将value值初始化。

2.ThreadLocalMap

 static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

在上面代码可以看出,ThreadLocalMap是ThreadLocal的内部静态类,它的构成主要是用Entry来保存数据,而且还是继承的弱引用。在Entry内部,使用ThreadLocal作为key,使用我们设定的值作为value。

    ThreadLocalMap中,使用的key是ThreadLocal的弱引用,弱引用的特点是如果这个只存在弱引用,那么在下一次垃圾回收时的时候必然会被清理掉。

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

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

3.ThreadLocal中的get()方法

       

public T get() {
        /*  获取当前线程  */
        Thread t = Thread.currentThread();
        /* 获取当前线程的ThreadLocalMap */
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            /*  获取ThreadLocalMap中存储的值   */
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        /* 如果数据为null,则初始化,初始化的结果,ThreadLocalMap中存放的Key值是ThreadLocal,值为null */
        return setInitialValue();
    }

    4.ThreadLocal中的remove()方法

  public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null) {
            m.remove(this);
        }
    }

  在上面的代码可以看出,remove方法,直接将ThreadLocal对应的值从当前的ThreadLocalMap中删除,关于为什么删除,涉及到内存泄露问题。

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

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

五、Spring使用ThreadLocal解决线程安全问题

        一般的Web应用划分为M - mode 对象层,封装到 domain 里。V - view 展示层,但因为目前都是前后端分离的项目,几乎不会在后端项目里写 JSP 文件了。C - Controller 控制层,对外提供接口实现类。DAO 算是单独拿出来用户处理数据库操作的层。在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。

这样根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一个ThreadLocal变量都是当前线程所绑定的。

public class TopicDao {
    /* 使用ThreadLocal保存Connection变量 */
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
    public static Connection getConnection(){
        /* 如果connThreadLocal没有本线程对应的Connection,就需要创建一个新的
        * 并保存到线程本地变量  */
        if (connThreadLocal.get() == null){
             /* 假设存在的类*/
            Connection connection = ConnectionManager.getConnection();
            connThreadLocal.set(connection);
            return connection;
        }else {
            /* 直接返回线程本地变量 */
            return connThreadLocal.get();
        }
    }
    public void addTopic(){
        /* 从ThreadLocal中获取线程对应的 */
        Statement statement = getConnection().createStatement();
    }
}

六、ThreadLocal 内存泄露的原因

 1.ThreadLocal使用原理

 ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal,但是实际上使用的值value是自己独有一份。

  当线程使用threadLocal时,是将threadLocal当做当前线程thread的属性ThreadLocalMap中的一个Entry的key值,实际上存放的变量是Entry的value值,实际使用的值是value

2.内存泄漏原因

Entry将ThreadLocal作为Key,值作为value保存,继承自WeakReference,在下面构造函数的第一行super(k),意味着ThreadLocal对象是一个弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

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

3.为什么 key 要用弱引用

  key如果是强引用,假设在使用完ThreadLocal,ThreadLocal ref被回收,但是因为threadlocalMap的Entry使用的是强引用threadLocal(key是threadLocal),造成ThreadLocal无法回收。在没有手动删除Entry以及CurrentThread仍然运行前提下,始终有强引用链CurrentThread Ref -> CurrentThread -> Map(ThreadLocalMap) -> entry,Entry就不会被回收。

   事实上,在ThreadLocalMap中的set/getEntry方法中,会对Key为null(ThreadLocal为null)进行判断,如果为null的话,那么也会把value置为null。这也就是说,使用ThreadLocal,CurrentThread仍然运行的情况下,就算忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收。对应value在下一次ThreadLocal调用get()/set()/remove()中的任意一个方法的时候会被清除,从而避免内存泄漏。

4.如何正确使用ThreadLocal

        1、将ThreadLocal变量定义为private static,这样ThreadLocal的生命周期就更长。这样就能保证根据ThreadLocal的弱引用访问到Entry的value值,然后remove,防止内存泄漏。

        2.每次使用完ThreadLocal,都调用remove()方法。

七、参考来源

参考文章

单例和多例是设计模式中常用的概念,它们分别指的是一个类只有一个实例和每个请求使用一个新的实例。它们的好处和应用场景如下所示: 单例的好处: - 节省资源:单例模式可以避免重复创建对象,节省了系统资源的开销。 - 数据共享:单例模式可以实现数据的共享,多个模块可以共享同一个实例,方便数据的传递和操作。 - 简化调用:单例模式可以提供一个全局访问点,简化了对象的调用和管理。 多例的好处: - 隔离数据:多例模式可以实现数据的隔离,每个请求使用一个新的实例,避免了数据的混乱和冲突。 - 并发安全:多例模式在多线程环境下可以保证每个线程使用独立的实例,避免了线程安全问题。 - 灵活性:多例模式可以根据需求创建多个实例,提供了更大的灵活性和扩展性。 确定使用单例还是多例取决于具体的业务需求和设计考虑: - 如果需要共享数据或者节省资源,可以选择单例模式。 - 如果需要隔离数据或者保证并发安全,可以选择多例模式。 范例:<<引用:单例:所有请求用同一个对象来处理。通过单例模式,可以保证系统中一个类只有一个实例。,比如我们常用的service和dao层的对象通常都是单例的。 多例:每个请求用一个新的对象来处理。比如action。 2、为什么用单例多例 [^1]。引用:单例模式多线程不安全,解析ThreadLocal类。在上面谈到了对ThreadLocal的一些理解,那我们下面来看一下具体ThreadLocal是如何实现的。 [^2]。>> 单例模式的好处: - 节省资源:单例模式可以避免重复创建对象,节省了系统资源的开销。 - 数据共享:单例模式可以实现数据的共享,多个模块可以共享同一个实例,方便数据的传递和操作。 - 简化调用:单例模式可以提供一个全局访问点,简化了对象的调用和管理。 多例模式的好处: - 隔离数据:多例模式可以实现数据的隔离,每个请求使用一个新的实例,避免了数据的混乱和冲突。 - 并发安全:多例模式在多线程环境下可以保证每个线程使用独立的实例,避免了线程安全问题。 - 灵活性:多例模式可以根据需求创建多个实例,提供了更大的灵活性和扩展性。 确定使用单例还是多例取决于具体的业务需求和设计考虑: - 如果需要共享数据或者节省资源,可以选择单例模式。 - 如果需要隔离数据或者保证并发安全,可以选择多例模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值