一.现象
先上伪代码,如下:
@Component
public class SpringBeanUtil {
public static JedisUtil jedisUtil;
@Resource
public void setJedisUtil(JedisUtil jedisUtil) {
SpringBeanUtil.jedisUtil = jedisUtil;
}
}
@Component
public class RedisDelayQueue {
/**
* 监听定时任务执行
*/
@PostConstruct
public void listenDelayTask(){
//另起线程执行,避免阻塞无法启动
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
while (true){
if(SpringBeanUtil.jedisUtil!=null){
//todo 具体业务
}
}
});
}
}
springboot在启动时,@PostConstruct修饰的方法在启动时会去执行,内部另起线程去执行业务逻辑,现在的问题是SpringBeanUtil.jedisUtil!=null,这个地方一直是false,也就是SpringBeanUtil.jedisUtil一直是空的,SpringBeanUtil.jedisUtil在项目启动时也是会初始化的,也就是可能的情况是开始是空的,后面是有值的,listenDelayTask方法里面是循环在执行判断,项目在我本地启动执行是正常的,在测试环境就出现问题了。
二.分析
1.在测试环境中@PostConstruct方法没有执行?后加日志打印排除这个问题
2.if(SpringBeanUtil.jedisUtil!=null)一直是false?猜想后突然想到java内存模型中的可见性问题
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
JVM程序运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
摘自https://www.jianshu.com/p/6abcddd04f4e
三.解决
由上述找到原因,需要解决变量在内存中的可见性问题,即变量发生修改后,需要立即同步到其它线程,于是采用Volatile关键字对变量进行修饰。然后测试环境重新发布后,正常执行。
@Component
public class SpringBeanUtil {
public volatile static JedisUtil jedisUtil;
@Resource
public void setJedisUtil(JedisUtil jedisUtil) {
SpringBeanUtil.jedisUtil = jedisUtil;
}
}
四.总结
测试环境在项目启动阶段,有个初始化方法中另起一个线程一直读取不到另一个bean中变量更新后的值,一直读取的是初始的值。
这个现象在我本地是不存在的,并且我的另一个项目同样的写法在测试环境也是正常的(没有Volatile关键字修饰),这个确实是比较玄学。
,