多年来,Java并发编程一直是一个让开发者头疼不已的问题。无论使用哪种编程模型,线程安全问题总会随时出现。而我们之所以难以掌握多线程并发的真相,很大一部分原因就是因为Java内存模型(JMM)的存在。
JMM定义了Java线程如何访问共享变量,以及变量值的传播规则。这对我们理解线程安全至关重要。本文将带你深入剖析JMM的工作原理,揭开它给并发编程带来的影响。这对你理解并控制多线程程序的行为将很有帮助。
首先,我们来回顾一下单线程程序中变量值的传播:
代码语言:java复制int x = 0;
x = 3;
int y = x;
在单线程环境中,我们期望的结果是y的值为3。但在多线程环境中,情况就不一定了。
这是因为JVM允许重排序指令,以提高性能。例如,一个线程修改变量x的值,另一个线程可能首先读取变量y,而非x,导致看到的结果不一致。
JMM通过 Happen-Before 原则来约束指令重排序:
- 程序顺序规则:一个线程内,按照程序顺序进行的操作,后面操作的结果对前面可见
- 监视器锁规则:对一个共享变量的写与对这个变量的监视器锁的解锁具有Happens-Before关系
- volatile变量规则:对一个volatile变量的写与后续读/写该变量的线程间形成Happens-Before关系
- 线程开始规则:Thread对象的start()方法的调用与线程内首次访问特定变量的 Happens-Before关系
- 线程中断规则:对线程interrupt()方法的调用与被中断线程检测interrupt状态的Happens-Before关系
- 线程终止规则:线程中最后一个指令的完成与线程结束的Happens-Before关系
这些规则限制了指令重排序,保证了一个线程修改变量值后,其他线程可以看到最新值。
但是,规则也不能消除所有并发问题。一个典型案例:
代码语言:java复制int x = 0, y = 0;
thread1(){
x = 1;
y = 1;
}
thread2(){
if(x==1)
System.out.println("x equals 1");
if(y==1)
System.out.println("y equals 1");
}
这里x=1和y=1语句没有同步控制,线程2打印结果顺序不确定。这时我们就需要同步控制来保证程序顺序:
代码语言:java复制synchronized(this){
x = 1;
y = 1;
}
所以,理解JMM的Happen-Before原则,并掌握如何使用同步控制来保证程序顺序,这对我们正确编写线程安全程序至关重要。这也意味着并发编程的难点不在语法层面,而在于我们如何采取必要的同步措施。
在后续文章中,我将通过介绍常见的多线程BUG casos,如死锁,锁投毒,线程不安全等,来帮助大家进一步理解并发编程的难点所在,以及如何利用JMM和同步工具像synchronized,volatile, lock等更妥善解决线程安全问题。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!