java线程的真正实现方式以及实际开发中解决【TransmittableThreadLocal解决异步方法执行前后用户信息不一致问题】

目录

项目场景:某财险

问题描述:数据导入时获取用户信息异常

原因分析:异步方法执行前后未初始化用户信息

解决方案:获取用户信息之前初始化


前言:java线程的真正实现方式

    对于这个问题相信只要是学习java的程序猿应该都知道,只不过可能每个人说出来实现的方式和有几种实现的方式不一样,一般在面试初中级开发的话回答三种就可以了,学的不错的猴宝宝回答四种更不错;但是其实从原理上来讲,java线程实现的方式只有一种,这个我们最后在讨论,我们先来说说四种线程实现的方式:

1.继承thread类,重写run方法:

public class ThreadTest extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("线程1");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public static void main(String[] args) throws Exception{
        //1、继承Thread类,重写run方法
        new ThreadTest().start();
    }
}

2.实现Runnable接口,重写run方法:

public static void main(String[] args) throws Exception{
        //2、实现Runnable接口,重写run方法
        new Thread(()->{
            while (true){
                System.out.println("Runnable多线程1");
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
        new Thread(()->{
            while (true){
                System.out.println("Runnable多线程2");
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

3.实现Callable接口,重写run方法,然后利用FutureTask类接受返回值

public static void main(String[] args) throws Exception{
       //3、实现Callable接口,重写run方法
        FutureTask<String> futureTask1 = new FutureTask<>(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("Callable线程1正在执行" + (i + 1));
            }
            return "Callable线程1执行完毕";
        });
        //开启线程的就绪状态
        new Thread(futureTask1).start();
        FutureTask<String> futureTask2 = new FutureTask<>(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("Callable线程2正在执行" + (i + 1));
            }
            return "Callable线程2执行完毕";
        });
        new Thread(futureTask2).start();
        //通过FutureTask类来接受线程的返回值
        System.out.println(futureTask1.get());
        System.out.println(futureTask2.get());
    }

   4、使用线程池来创建线程(执行速度很快,循环一万次只需要3ms)

public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        //申明线程池
        ExecutorService taskExecutor = new ThreadPoolExecutor(6, Integer.MAX_VALUE, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>());
        //开启线程池
        taskExecutor.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    System.out.println(i);
                }
            }
        });
        long endTime = System.currentTimeMillis();
        System.out.println("程序运行时间:" + (endTime - startTime) + "ms");
        //关闭线程池
        taskExecutor.shutdown();
    }

上面这几种实现方式可以说是比较简单的,一般我们工作经验五年以内在面试的时候答出来这些就可以了,但是如果你是面试的高开或者五年以上开发面试的话这样回答可能不太好,因为你的开发年限越长,你的回答就应该围绕着原理来进行剖析,所以你在回答的时候可以这样回答(下面这些些面试的话术转自java面试八股文之------Java并发夺命23问_归去来 兮的博客-CSDN博客):

        java中本质上线程的创建技术只有一种,就是利用Thread+Runnable接口来实现多线程,其他所有方式都是基于Thread+Runnable接口来改造而来,所以本质上就只有一种。为什么这么说呢,对于使用Runnable的实现方式应该没有意义,那我们就来聊一聊Thread和Callable吧,我们通过继承Thread时,需要重写run方法,实际上这个run方法就是Runnable的,进到Thread的源码就可以看到他也实现了Runnable。而对于Callable怎么说呢,通过Callable来实现多线程时,我们必须使用FutureTask类对Callable的对象进行包装,然后将FutureTask传递给Thread,这样才能够启动多线程。我们看下FutureTask的run方法就会发现,他其实调取的就是Callable的call方法,然后将返回值存取了,而FutureTask之所以有run方法,就是因为他的父类继承了接口Runnable。所以实现了Callable本质上也还是使用Runnable实现的类。其实还有线程池,线程池不管是使用Runnable还是Callable其实都是一样也都是利用的Runnable。

同样的Callable同样也实现了Runnable接口

项目场景:某财险

保险行业,嗯,不错(不知道咋描述,就直接问题描述吧)


问题描述:数据导入时获取用户信息异常

  在给某财险做内部系统时,数据是需要他们内部人员自己管理的,所有对数据的编辑、导入、导出都只能由创建人来操作(超管可以改变所有人的数据),其他人来操作时则会给"无操作数据权限"的提示,但是最近出现了一个很难找出来的BUG,就是在数据导入时,数据首先会进入预览表中,但是在预览数据没有问题后就可以点击确认导入,将该条数据落到真正的数据表中,问题来了:因为这里点击确认导入新增的时候会重新获取创建人,本来表格中的创建人是张三,预览后的创建人也是张三没错,但是真正确认导入的时候发现创建人变成了李四,头疼。

原因分析:异步方法执行前后未初始化用户信息

 我们从代码的业务逻辑层面去看的时候并没有发现错误,所以就从另一方面考虑,也就是获取当前用户人的代码是不是有误,中间看了多少遍代码就不说了(苦死本猴宝宝了!!!),但是最后终于发现了问题,直接上代码:

    /**
     * 统一登录用户信息
     */
    public static LoginUserInfo getUserInfo() {

        /**
         * 用户统一登录信息
         */
        String authData = RpcInvokeContext.getContext().getRequestBaggage(AUTH_DATA);
        log.info("用户登录信息:authData {}", authData);
        if (StringUtils.isEmpty(authData)) {
            return null;
        }
        LoginUserInfo loginUserInfo = null;
        try {
            loginUserInfo = JSON.parseObject(authData, LoginUserInfo.class);
        } catch (Exception e) {
            log.error("解析用户登录信息异常", e);
        }
        return loginUserInfo;
    }

这段代码是获取我们登录系统封装的用户信息的一段代码,就是通过SOFARPC框架中的数据透传功能将用户信息封装到RpcInvokeContext.中的context中,通过将某个接口绑定到外部授权中的认证接口即可获取到当前用户的信息,这里就不多讲了,问题重点在下一部分代码中

   private static final Logger log = LogManager.getLogger(UserInfoUtil.class);

    public static final ThreadLocal<LoginUserInfo> THREAD_LOCAL = new TransmittableThreadLocal<>();

    /**
     * 设置线程需要保存的值
     * 需要在异步线程中获取用用户信息的需要调用该方法
     */
    public static void initUserInfo() {
        THREAD_LOCAL.set(getLoginUser());
    }
    
    //获取线程中保存的值
    public static LoginUserInfo getValue() {
        return THREAD_LOCAL.get();
    }
    
    //移除线程中保存的值
    public static void remove() {
        THREAD_LOCAL.remove();
    }

 我们这里是将上部分获取到的用户信息通过set方法放入TransmittableThreadLocal这个线程中,这个线程主要是为了解决子父线程不能共用变量的问题(线程解答详情可以看下这篇文章:初识TransmittableThreadLocal_transmitthreadlocal_大鸡腿同学的博客-CSDN博客),也就是现在我的导入的功能做的是一个异步的,大家都知道异步方法执行前后会改变当前的线程,但是你的用户信息存储在当前线程中,如果你改变了当前线程,里面存储的用户信息就有可能并不是当前登录用户,也就相当于改变了当前的用户信息,也就会出现确认导入时创建人会改变的问题


解决方案:获取用户信息之前初始化

综合上面的原因分析,虽然这个线程是可以解决子父线程共享同一个变量的问题,但是如果想要在改变线程的时候共享同一个变量的话,还是需要自己初始化之后来共享,这里其实是可以由两种方法:

1、在异步方法执行前主动调用的去调用initUserInfo()初始化用户信息的方法,如下:

 2、第二种方法比较麻烦点,首先需要从当前线程中获取用户信息,在异步方法执行后在获取用户信息后做个判断,两边用户信息不一致的话就需要重新执行下初始化方法,当然你也可以直接在获取当前线程中保存的值的时候初始化,这样不管你有多少个方法是异步的,你在获取用户信息的时候都会初始化,最终拿到的肯定是TransmittableThreadLocal线程中最新的用户信息,所以这种解决方法也是可以的,但是还是比较推荐第一种哈

    //获取线程中保存的值
    public static LoginUserInfo getValue() {
        initUserInfo();
        return THREAD_LOCAL.get();
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值