一、一次离谱的log记录
相关的代码,可以简化成如下所示:
Thread t = new Thread(() -> {
Integer a = 0;
while (true) {
System.out.println(a);
try {
TimeUnit.SECONDS.sleep(1);
} catch (Throwable ignore) {
}
}
});
t.start();
简单来说,线程内有一个Integer类型的变量a,然后每隔1s,打印一下这个变量a的值。那么不出意外的话,这段代码的结果应该每秒打印一个0,但是不出意外的话,意外出现了。
0
0
0
0
0
1
1
1
1
在中间某一个时刻,突然打印出来变量的值变成了1,当时看到结果,我人都裂开了😢。裂开的原因如下:
- 线程内部完全没有修改这个变量的操作
- 这个变量是在方法内部创建的,并没有暴露给外部类
二、一次更离谱的定位问题过程
先从一个IDEA的智能提示开始,对于下面这个方法定义:
public static void hahaha(Integer s){
}
如果我们在调用的时候这么写:
hahaha(new Integer(0));
此时,IDEA会机智的提示你:
如果你按照IDEA的指示,移除"不必要的装箱",那么就会变成下面这种调用方式:
hahaha(0);
这么一看,好像也没什么问题对吧?我理解也是,IDEA真智能👍,我就这么改了。
接下来回到问题的开始,我发现,我的程序就是调用了一个下面这样的方法后,打印出来的a变量的值就发生了变化
public native int GetConfFile(String var1, int var2, StringBuffer var4);
注意:
- 这是一个native方法,内部是通过JNI调用的C++的so文件
- 通常来说,c++都是通过指针操作,然后返回值也可能放在参数列表中,所以这个var2虽然是传递进去的参数,但同样也是这个方法的返回值
接下来,我就按照IDEA的指示,这么调用了此方法:
StringBuffer sb = new StringBuffer();
int code = ConfigAccess.GetConfFile("myConfigFile", 0, sb);
这么一看好像,也没有什么问题,但就是调用了此方法以后,所有值等于0的Integer类,都变成了1!!!!😨
三、原来是这样!
《吃瓜吃到八股文头上是一种怎样的体验?》
四、我们简单用Java模拟复现一下
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(() -> {
Integer a = 0;
while (true) {
System.out.println(a);
try {
TimeUnit.SECONDS.sleep(1);
} catch (Throwable ignore) {
}
}
});
t.start();
TimeUnit.SECONDS.sleep(5);
Integer b = 0;
// 获取Unsafe的实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
// 获取对象的字段
Field field = Integer.class.getDeclaredField("value");
// 计算字段在对象中的偏移量
long offset = unsafe.objectFieldOffset(field);
// 修改字段的值
unsafe.putInt(b, offset, 1);
System.out.println("+++++" + unsafe.getInt(b, offset));
}
见证奇迹的时刻到了
0
0
0
0
0
+++++1 //此时修改了内存中的值
1
1
1
1
1
五、小结
- 八股文有些时候还是有用的
- Java和C++打交道真的要谨慎、谨慎再谨慎,各种离奇的bug都能出现
- 当我循环打印一个没有任何改变的变量却发现内容变化时,我整个人真的裂开了😑