欢迎关注公众号 【11来了】 ,持续 中间件源码、系统设计、面试进阶相关内容
在我后台回复 「资料」 可领取 编程高频电子书!
在我后台回复「面试」可领取 30w+ 字的硬核面试笔记!感谢你的关注!
隐蔽的 synchronized 并发错误
在使用 synchronized 进行单机并发控制时,有一种特殊情况会出现并发控制异常
接下来会说一下为何会出现锁失效问题,通过对字节码进行反编译,查看对应的字节码指令,来寻找并发异常出现的原因!
并发问题复现
代码流程为:
- 创建两个线程,去执行任务,对 cnt 进行累加操作
- 为了并发的安全,通过 synchronized 对 cnt 加锁来保证并发安全
public class SynErr implements Runnable {
public static Integer cnt = 0;
static SynErr instance = new SynErr();
@Override
public void run() {
for (int i = 0; i <10000; i ++) {
synchronized (cnt) {
cnt++;
}
}
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(cnt);
}
}
最终输出结果 理应为 20000 ,但是由于出现了并发问题,导致 cnt 的值总是不到 20000
原因分析
通过在 cnt 对象上加锁,之后在加锁代码内部对 cnt ++,从 代码层面上 来看的话,没有发现什么问题,可以通过对字节码进行反编译,通过 底层的字节码 指令来查找原因
这里可以使用 IDEA 的 jclasslib 工具来查看 class 文件对应的字节码:
接下来查看 run() 方法对应字节码:
接下来解释一下对应字节码:
- 14 行:
monitorenter
即 synchronized 关键字对应的加锁命令,从 14 行开始,表明进入了 synchronized 代码块 - 26 行:
iadd
表明将 cnt 和 1 进行相加,也就是执行了 cnt ++ 的命令 - 27 行:执行了
Integer.valueOf
方法,新建了一个 Integer 对象,并且在 31 行通过 putstatic 指令赋值给了 cnt ,那么此时 cnt 是一个新的 Integer 对象了
通过 27 行的字节码指令,可以看到,每次执行 cnt ++
之后,都会创建一个新的 Integer 对象赋值给 cnt,因此两个线程加锁的 cnt 不是一个对象,导致出现并发问题
如何解决呢,对 intance 对象加锁,不对 cnt 加锁就可以了:
synchronized (cnt)
---> 修改
synchronized (intance)
总结
对 cnt 加锁,看似逻辑上没有问题,但最终却出现了并发问题,是因为 JDK 将 java 代码编译为字节码指令,编译后的字节码指令与我们的预期不符,导致出现并发问题,虽然工作中也不会碰到,但是可以借此进一步了解 JVM 中的字节码指令
字节码为了计算方便,为通过栈进行操作数的计算,在第 15 行通过 getstatic 获取 cnt 的 int 值压入栈中,在 25 行将常数 1 压入栈中,在 26 行将栈顶两个整数相加,执行完成 cnt ++
这行代码,之后将得到的结果赋值给新的 Integer 变量,从而导致 synhronized 锁失效