zoukankan      html  css  js  c++  java
  • ThreadLocal的使用场景分析

    目录

    一.ThreadLocal介绍

    二.使用场景1——数据库事务问题

      2.1 问题背景

      2.2 方案1-修改接口传参

      2.3 方案2-使用ThreadLocal

    三.使用场景2——日志追踪问题

    四.其他使用场景

    一.ThreadLocal介绍

      我们知道,变量从作用域范围进行分类,可以分为“全局变量”、“局部变量”两种:

      1.全局变量(global variable),比如类的静态属性(加static关键字),在类的整个生命周期都有效;

      2.局部变量(local variable),比如在一个方法中定义的变量,作用域只是在当前方法内,方法执行完毕后,变量就销毁(释放)了;

      使用全局变量,当多个线程同时修改静态属性,就容易出现并发问题,导致脏数据;而局部变量一般来说不会出现并发问题(在方法中开启多线程并发修改局部变量,仍可能引起并发问题);

      再看ThreadLocal,从名称上就能知道,它可以用来保存局部变量,只不过这个“局部”是指“线程”作用域,也就是说,该变量在该线程的整个生命周期中有效。

    二.使用场景1——数据库事务问题

    2.1问题背景

      下面介绍示例,UserService调用UserDao删除用户信息,涉及到两张表的操作,所以用到了数据库事务:

      数据库封装类DbUtils

    public class DbUtils {
    
        // 使用C3P0连接池
        private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");
    
        public static Connection getConnectionFromPool() throws SQLException {
            return dataSource.getConnection();
        }
    
        // 省略其他方法.....
    }
    

      UserService代码如下:  

    public class UserService {
    
        private UserDao userDao;
    
        public void deleteUserInfo(Integer id, String operator) {
            Connection connection = null;
            try {
                // 从连接池中获取一个连接
                connection = DbUtils.getConnectionFromPool();
                // 因为涉及事务操作,所以需要关闭自动提交
                connection.setAutoCommit(false);
    
                // 事务涉及两步操作,删除用户表,增加操作日志
                userDao.deleteUserById(id);
                userDao.addOperateLog(id, operator);
    
                connection.commit();
            } catch (SQLException e) {
                // 回滚操作
                try {
                    if (connection != null) {
                        connection.rollback();
                    }
                } catch (SQLException ex) {
                }
            } finally {
                DbUtils.freeConnection(connection);
            }
        }
    }

      下面是UserDao,省略了部分代码:

    package cn.ganlixin.dao;
    import cn.ganlixin.util.DbUtils;
    import java.sql.Connection;
    
    /**
     * @author ganlixin
     * @create 2020-06-12
     */
    public class UserDao {
    
        public void deleteUserById(Integer id) {
            // 从连接池中获取一个数据连接
            Connection connection = DbUtils.getConnectionFromPool();
    
            // 利用获取的数据库连接,执行sql...........删除用户表的一条数据
    
            // 归还连接给连接池
            DbUtils.freeConnection(connection);
        }
    
        public void addOperateLog(Integer id, String operator) {
            // 从连接池中获取一个数据连接
            Connection connection = DbUtils.getConnectionFromPool();
    
            // 利用获取的数据库连接,执行sql...........插入一条记录到操作日志表
    
            // 归还连接给连接池
            DbUtils.freeConnection(connection);
        }
    }
    

      上面的代码乍一看,好像没啥问题,但是仔细看,其实是存在问题的!!问题出在哪儿呢?就出在从数据库连接池获取连接哪个位置。

      1.UserService会从数据库连接池获取一个连接,关闭该连接的自动提交;

      2.UserService然后调用UserDao的两个接口进行数据库操作;

      3.UserDao的两个接口,都会从数据库连接池获取一个连接,然后执行sql;

      注意,第1步和第3步获得的连接不一定是同一个!!!!这才是关键。

      如果UserService和UserDao获取的数据库连接不是同一个,那么UserService中关闭自动提交的数据库连接,并不是UserDao接口中执行sql的数据库连接,当userService中捕获异常,即使执行rollback,userDao中的sql已经执行完了,并不会回滚,所以数据已经出现不一致!!!

    2.2方案1-修改接口传参

      上面的例子中,因为UserService和UserDao获取的连接不是同一个,所以并不能保证事务原子性;那么只要能够解决这个问题,就可以保证了

      可以修改userDao中的代码,不要每次在UserDao中从数据库连接池获取连接,而是增加一个参数,该参数就是数据库连接,有UserService传入,这样就能保证UserService和UserDao使用同一个数据库连接了

    public class UserDao {
    
        public void deleteUserById(Connection connection, Integer id) {
            // 利用传入的数据库连接,执行sql...........删除用户表的一条数据
        }
    
        public void addOperateLog(Connection connection, Integer id, String operator) {
            // 利用传入的数据库连接,执行sql...........插入一条记录到操作日志表
        }
    }
    

      UserService调用接口时,传入数据库连接,修改代码后如下:

    // 事务涉及两步操作,删除用户表,增加操作日志
    // 新增参数传入数据库连接,保证UserService和UserDao使用同一个连接
    userDao.deleteUserById(connection, id);
    userDao.addOperateLog(connection, id, operator);

      这样做,的确是能解决数据库事务的问题,但是并不推荐这样做,耦合度太高,不利于维护,修改起来也不方便;

    2.3使用ThreadLocal解决

      ThreadLocal可以保存当前线程有效的变量,正好适合解决这个问题,而且改动的点也特别小,只需要在DbUtils获取连接的时候,将获取到的连接存到ThreadLocal中即可:

    public class DbUtils {
    
        // 使用C3P0连接池
        private static ComboPooledDataSource dataSource = new ComboPooledDataSource("dev");
    
        // 创建threadLocal对象,保存每个线程的数据库连接对象
        private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    
        public static Connection getConnectionFromPool() throws SQLException {
            if (threadLocal.get() == null) {
                threadLocal.set(dataSource.getConnection());
            }
    
            return threadLocal.get();
        }
    
        // 省略其他方法.....
    }
    

      然后UserService和UserDao中,恢复最初的版本,UserService和UserDao中都调用DbUtils获取数据库连接,此时他们获取到的连接则是同一个Connection对象,就可以解决数据库事务问题了。

    三.使用场景2——日志追踪问题

      如果理解了场景1的数据库事务问题,那么对于本小节的日志追踪,光看标题就知道是怎么回事了;

      开发过程时,会在项目中打很多的日志,一般来说,查看日志的时候,都是通过关键字去找日志,这就需要我们在打日志的时候明确的写入某些标识,比如用户ID、订单号、流水号...

      如果业务比较复杂,那么一个请求的处理流程就会比较长,如果将这么一长串的流程给串起来,也可以通过前面说的用户ID、订单号、流水号来串,但有个问题,某些接口没有用户ID或者订单号作为参数!!!!这个时候,当然可以像场景1中给接口增加用户ID或者订单号作为参数,但是这样实现起来,除非想被炒鱿鱼,否则就别这样做。

      此时可以就可以使用ThreadLocal,封装一个工具类,提供唯一标识(可以是用户ID、订单号、或者是分布式全局ID),示例如下:

    package cn.ganlixin.util;
    
    /**
     * 描述:
     * 日志追踪工具类,设置和获取traceId,
     * 此处的traceId使用snowFlake雪花数算法,详情可以参考:https://www.cnblogs.com/-beyond/p/12452632.html
     *
     * @author ganlixin
     * @create 2020-06-12
     */
    public class TraceUtils {
        // 创建ThreadLocal静态属性,存Long类型的uuid
        private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    
        // 全局id生成器(雪花数算法)
        private static final SnowFlakeIdGenerator generator = new SnowFlakeIdGenerator(1, 1);
    
        public static void setUuid(String uuid) {
            // 雪花数算法
            threadLocal.set(generator.nextId());
        }
    
        public static Long getUuid() {
            if (threadLocal.get() == null) {
                threadLocal.set(generator.nextId());
            }
            return threadLocal.get();
        }
    }
    

      

      使用示例:

    @Slf4j
    public class UserService {
    
        private UserDao userDao;
    
        public void deleteUserInfo(Integer id, String operator) {
            log.info("traceId:{}, id:{}, operator:{}", TraceUtils.getUuid(), id, operator);
            
            //.....
        }
    }
    
    @Slf4j
    public class UserDao {
    
        public void deleteUserById(Connection connection, Integer id) {
            log.info("traceId:{}, id:{}", TraceUtils.getUuid(), id);
        }
    }
    

      

     四.其他场景

      其他场景,其实就是利用ThreadLocal“线程私有且线程间互不影响”特性,除了上面的两个场景,常见的还有用来记录用户的登录状态(当然也可以用session或者cookie实现)。

      原文地址:https://www.cnblogs.com/-beyond/p/13111015.html 

  • 相关阅读:
    【Java基础】9、Enumeration接口和Iterator接口的区别
    【Java基础】4、java中的内部类
    【Java基础】3、Java 位运算(移位、位与、或、异或、非)
    【高并发解决方案】6、数据库水平切分的实现原理解析
    【Java深入研究】6、CGLib动态代理机制详解
    【Java深入研究】4、fail-fast机制
    【Java深入研究】2、JDK 1.8 LinkedList源码解析
    【算法】1、快速排序
    Linux下查看系统版本号信息的方法
    Kubernetes1.2如何使用iptables
  • 原文地址:https://www.cnblogs.com/-beyond/p/13111015.html
Copyright © 2011-2022 走看看