线程变量引发的session混乱问题

最近不是在救火,就是在救火的路上。 也没什么特别可写的,今天记录下最近遇到的一个问题,个人觉得挺有意思, 待有缘人阅读

言归正传,售后反馈: 营业查询中付款方式为第三方支付的几条银行缴费,创建操作员和修改操作员为系统操作员,系统操作员一般只用于系统配置,不会用于处理业务, 这类异常数据会导致月底与财务报表不正确

看到这个问题的时候第一感觉就是有点蒙, 在我们的系统中的配置操作员和业务操作员是分开的。银行缴费的操作员记录的是指定的业务操作员

查看系统日志,发现日志中有问题交易流水120231116205337hF544的记录信息, 从日志分析,问题数据不是通过客户充值产生的数据,而是银行对账产生的数据。 所谓对账就是指本系统的缴费和银行系统的缴费做对比,对方有我方没有则补数据,对方没有我方有则冲正数据, 双方都有则按照银行的数据修复成一直。 通过日志定位问题数据都是对方有我方没有而补录的数据。

2023-11-17 09:41:00,997 INFO  [com.bank.server.checkAccount.CheckAccountJob] Checking detail account...
2023-11-17 09:41:01,001 INFO  [com.bank.server.checkAccount.CheckSingleContext] boss中不存在关于流水号120231116205337hF544的付款记录,重做对应交易
2023-11-17 09:41:01,004 INFO  [com.bank.server.checkAccount.CheckSingleContext] there is not detail that flowNo is 120231116205337hF544,transaction again

查看补录缴费程序的实现,公司内部框架中的持久化处理默认会设置表数据的创建操作员(operator)为登录session中的操作员信息。对账是后台任务处理的,后台任务没有操作员登录,没有操作员登录那么肯定就没有session. 处理到这儿,我开始有点儿见鬼的感觉,想不明白为什么一个后台任务突然有了session,有了登录信息,

private static void setCreateInfo(ApplicationSession session, AbstractSystemModel entity) {
		Operator createOperator = null;
		if (session == null || session.getValue("operator") == null) {
			createOperator = new Operator();
		}else {
			createOperator = (Operator) session.getValue("operator");
		}
		
		if (entity.getCreateOperator() == null || entity.getCreateOperator().getId() == null) {
			entity.setCreateOperator(createOperator);
		}
		
		if (entity.getCreateDate() == null) {
			entity.setCreateDate(new Timestamp(System.currentTimeMillis()));
		}
	}

统计稽核问题数据发现后台对账任务记录信息也很奇怪,有修改操作员记录的数据表示对账任务有session,没有修改操作员的记录表示此时对账任务没有session, 这个现象说明当前问题是偶然现象

当操作员登录后会在线程变量里缓存session信息,用于快速获取登录信息。从上面的数据库分析,执行对账的后台任务有的时候有session有的时候没有session .这个现象很像是线程变量增加session后没有清空引起的。

我们知道Jboss是通过线程池记录来减少线程的开销, 难道是现场复用引起的。 我有了一个大胆的猜测

  1. 系统管理员登录后办理业务使用A线程, 登录时设置了session到A线程的线程变量中
  2. 系统管理员完成业务操作后,因某个原因退出登录没有清空线程A的线程变量信息,也就是没有清空session
  3. 线程A回归线程池后再次被对账任务使用,此事后台任务从线程变量里取值就能错误获取线程变量里的记录信息

为了验证猜测我写了下面的一段测试代码模拟猜测场景。

  1. LocalThreadTest 实现Runable接口,并在其中设置一个线程变量
  2. 线程池有3个线程,并分配10个随机10秒的任务。 
  3. 将第2个任务的线程设置一个线程变量
  4. 观察第2个任务的线程被再次使用的时候线程变量是否存在
package com.thread.localthread;

import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LocalThreadTest implements Runnable {
    private static final ThreadLocal<String> LOCAL_SESSION = new ThreadLocal<String>();

    private Integer index;
    public LocalThreadTest(Integer i){
        index=i;
    }

    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (Integer i = 0; i < 10; i++) {
            fixedThreadPool .execute(new LocalThreadTest(i));
        }
        fixedThreadPool.shutdown();
    }

    @Override
    public void run() {
        if(index==2){
            LOCAL_SESSION.set("session is"+Thread.currentThread().getName());
        }
        System.out.println(LOCAL_SESSION.get());
        Random random = new Random();
        int randomNumber = random.nextInt(4) + 1;
        try {
            Thread.sleep(randomNumber*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果发现和想的一样当线程回归线程池后。再次使用的时候线程变量仍会被线程持有。并不会被清除。

null
null
session ispool-1-thread-3
null
session ispool-1-thread-3
null
null

查看代码还有一个疑问没有解决,开启对账任务的时候是通过start()方法来启动线程的,而不是run方法(run()方法不会新创建线程)。 如果主线程中使用ThreadLocal记录线程变量, 当使用start()方法运行线程时是真正创建一个子线程。 对于线程变量ThreadLocal对象来说,子线程不会持有主线程中ThreadLocal的信息。这么来看,对账处理中ApplicationSession 应该也没有session才对。

public void billingServiceStart(int processInstanceId,
            long billingServiceInstanceId) {
        
        try {
            //.....略部分代码
            AbstractBillingServiceContext context = (AbstractBillingServiceContext) BeanFactoryHolder
                    .get().getBean(contextName);
            context.setBillingServiceInstance(billingServiceInstance);
            Thread t = new Thread(context);
            t.start();
        } catch (Exception e) {
            logger.error("case:", e);
            BillingServiceInstance bsi = billingServiceInstanceDao
                    .queryBillingServiceInstance(billingServiceInstanceId);
            bsi.setProcessStatus(ServiceProcessStatus.ERROR_FINISHED);
            bsi.setInfoStr(e.toString() + ":" + e.getMessage());
            bsi.setEndDate(new Date());
            billingServiceInstanceDao.modifyBillingServiceInstance(bsi);
        }
    }

再进一步分析ApplicationSessionHolder的代码才解决了自己的疑惑。 ApplicationSessionHolder对象中使用的线程变量是InheritableThreadLocal对象,InheritableThreadLocal是ThreadLocal 的子类,两者的区别是InheritableThreadLocal创建时可以获取主线程的线程变量值,

public final class ApplicationSessionHolder
{
  private static final InheritableThreadLocal<ApplicationSessionHolder> LOCAL_SESSION = new InheritableThreadLocal();
  
  private boolean clear;
  
  private ApplicationSession session;
  
  private ApplicationSessionHolder(ApplicationSession session)
  {
    this.session = session;
  }
}

 

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

}

至此定位问题原因, 第一次遇到session错乱的问题, 算是涨经验了

前一篇:线程池技术总结

  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Java多线程编程中,如果多个线程访问和修改同一个全局变量,可能会导致读取值混乱问题,这是由于线程之间的竞争条件引起的。具体来说,当一个线程正在读取全局变量的值,另一个线程可能会修改该变量的值,导致读取到的值不一致或者不正确。 为了解决这个问题,可以采取以下几种方式: 1. 使用synchronized关键字:可以使用synchronized关键字来保证在同一间只有一个线程可以访问和修改全局变量。通过在读取和修改全局变量的方法或代码块上添加synchronized关键字,可以确保线程安全。 2. 使用volatile关键字:可以使用volatile关键字来修饰全局变量,它可以确保多个线程之间对该变量的可见性。当一个线程修改了volatile变量的值,其他线程可以立即看到最新的值。 3. 使用Lock对象:可以使用Lock对象来实现对全局变量的访问控制。通过在读取和修改全局变量的代码块中使用Lock对象进行加锁和解锁操作,可以确保同一间只有一个线程可以访问和修改全局变量。 4. 使用ThreadLocal类:ThreadLocal类可以实现每个线程都拥有自己独立的全局变量副本。每个线程对全局变量的修改只会影响到自己的副本,不会影响其他线程的副本,从而避免了读取值混乱问题。 总之,在多线程编程中,为了避免全局变量读取值混乱问题,需要采取适当的同步机制或者使用线程本地变量来保证线程安全。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

=PNZ=BeijingL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值