ThreadLocal场景分析及原理分析

ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。

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

Spring使用ThreadLocal解决线程安全问题。通常只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。一般的Web应用划分为控制层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

所以在JDBCUtils中使用ThreadLocal后,代码如下:

public class JdbcUtils {
    private static DruidDataSource dataSource;
    private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();

    static {
        try {
            Properties properties = new Properties();
            //读取jdbc.properties属性配置文件
            InputStream inputStream = JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            //从流中加载数据
            properties.load(inputStream);
            //创建数据库连接池
            dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);
//            System.out.println(dataSource.getConnection());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    //这是以前不用事务时的写法
//    public static Connection getConnection(){
//        Connection conn =null;
//        try {
//            conn= dataSource.getConnection();
//        } catch (SQLException throwables) {
//            throwables.printStackTrace();
//        }
//        return conn;
//    }
    /*
     * 获取数据库连接池中的连接
     * */
    public static Connection getConnection(){
        Connection conn =conns.get();
        if(conn==null){    //第一次从连接池中取,之后就直接从ThreadLocal中获取
            try {
                conn = dataSource.getConnection();  //从数据库连接池中获取连接
                conns.set(conn);  //保存到ThreadLocal对象中,供后面的jdbc操作使用
                conn.setAutoCommit(false);  //设置为手动管理
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        return conn;
    }

    /**
     * 提交事务,并关闭释放连接
     */
    public static void commitAndClose(){
        Connection connection = conns.get();
        if(connection!=null){  //说明之前已经使用过连接操作过数据库
            try {
                connection.commit();  //提交事务
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }finally {
                try {
                    connection.close();  //关闭连接,释放资源
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }
        conns.remove();  //一定要执行remove操作,不然会操作,因为Tomcat底层使用了线程池操作
    }
    /**
     * 回滚事务,并关闭释放连接
     */
    public static void rollbackAndClose(){
        Connection connection = conns.get();
        if(connection!=null){  //说明之前已经使用过连接操作过数据库
            try {
                connection.rollback();  //回滚事务
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }finally {
                try {
                    connection.close();  //关闭连接,释放资源
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }
        conns.remove();  //一定要执行remove操作,不然会操作,因为Tomcat底层使用了线程池操作
    }
    /**
     * 关闭连接,放回数据库连接池中
     */
    //不用事务之前的处理办法
//    public static void close(Connection conn){
//        if(conn !=null){
//            try {
//                conn.close();
//            } catch (SQLException throwables) {
//                throwables.printStackTrace();
//            }
//        }
//    }
}

不同的线程在使用DAO的时候,会先判断是否为空,如果为null,则新建一个connection,反之则从中获取之前创建的,这样就保证了每一个线程都只有一个Connection,不会去使用其他线程的Connnection,但是这样其实只能保证同一个线程中使用的是同一个Connection,并不能保证不用的线程共享使用Connection。

对于ThreadLocal的存储,就需要提及Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。其关系可以用下图来展示。
在这里插入图片描述
每个Thread中有个成员变量ThreadLocalMap,一个Thread只有一个ThreadLocalMap,一个ThreadLocalMap里面存放着多个Entry, key,为ThreadLocal的引用, value为ThreadLocal要存储的值,例如connection或者一个对象等等,如果需要在TheadLocalMap中存放多个与线程相关的值,那么久创建多个ThreadLocal实例就行,每个ThreadLocal对应这一个Value,这样一个线程就可以对应多个value值

接下来看源码能进一步理解

get()方法

public T get() {
    //获取到当前线程
    Thread t = Thread.currentThread();
    //获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果线程内之前没创建过 ThreadLocalMap,就创建
    return setInitialValue();
}

然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。

值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。

getMap()方法

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

从这里可以验证,Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。

set()方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。

可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。

内存泄露

查看TheadLocalMap类的源码

    static class ThreadLocalMap {
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

下面以图片的形式展示ThreadLocal的内存模型
在这里插入图片描述
图中的虚线为弱引用,在ThreadLocal有一个比较重要的操作就是必须要在每次用完后,必须要执行ThreadLocal.remove操作来释放value,因为通常我们在jdbc中都会使用线程池,如果用完后不及时回收,那么在GC垃圾回收的时候,会把ThreadLocal实例进行回收,相当于弱引用断开,而线程Thread由于线程池的复用,会造成Thread与ThreadLocalMap之间的强联系会一直保持,那么里面的value一直停留在内存中而不会被清理,所以要避免内存泄露的话,记得每次使用完ThreadLocal取完相应的value后,执行ThreadLocal.remove操作来释放value就行。

参考文章
https://www.jianshu.com/p/1a5d288bdaee
https://www.jianshu.com/p/f956857a8304
https://blog.csdn.net/Rinvay_Cui/article/details/111035071

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值