问题
当多个线程并发同时进行set、get时,其它线程能否感知到flag的变化
public class ThreadSafeCache {
boolean flag = true;//默认设置true
public boolean isFlag() {
return flag;
}
public synchronized ThreadSafeCache setFlag(boolean flag) {
this.flag = flag;
return this;
}
public static void main(String[] args) {
ThreadSafeCache threadSafeCache = new ThreadSafeCache();
//循环创建多个线程
for (int i = 0;i < 10;i++){
new Thread(() -> {
int j = 0;
while(threadSafeCache.isFlag()){
j++;
}
System.out.println(j);
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadSafeCache.setFlag(false);
}
}
运行结果
可以看到程序是卡死了,一直没有退出
分析
这个类非常简单,里面有一个属性,有两个方法,set、get,并且在set方法上添加了synchronized。
多线程并发的同时进行set、get操作,A线程调用set、B线程调用get能感知到flag发生变化吗?
说到这里,问题就变成了synchronized能否保证上下文可见性!!!
关键词synchronized的用法
- 指定加锁对象:对给定的对象进行加锁,进入同步代码前需要获得给定对象的锁。
- 直接作用于实例方法:相当于对当前对象的实例加锁,进入同步代码前需要获得当前对象实例的锁
- 直接作用于静态方法:相当于对当前类进行加锁,进入同步代码前需要获得当前类的锁。
从代码中,我们可以看到只对set方法加了同步锁,多个线程调用set方法时,由于存在锁,会一个一个的进行set,但对于get来说,并没有加锁,多个线程无需获得该实例的锁,就可以直接获取到flag的值,那么我们就需要考虑某一个线程set之后的flag对其它线程是否可见!!!
Java内存模型happens-before原则
JSR-133内存模型使用happens-before原则的概念来阐述操作之间的内存可见性。在JMM(JAVA Memory Model)中,如果一个执行的结果需要对另一个操作可见,那么这两个操作直接必须要存在happens-before关系。两个操作可以是同一个线程内的也可以是不同线程中的。
happens-before(之前发生)原则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值的手段检测到线程是否已经终止执行。
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
注意:两个操作之间存在happens-before关系,并不一定前一个操作必须要在后一个操作执行!!!
happens-before仅仅要求前一个操作的执行结果对后一个操作可见,且前一个操作的执行顺序排在后一个操作之前(因为java虚拟机重排不相关的指令)。
volatile
volatile可见性
前面的happens-before原则中提到了volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。因此,volatile保证了多线程下的可见性!!!
volatile禁止内存重排序
下面是JMM针对编译器制定的volatile重排序规则:
是否能重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
通过上面的分析我们添加关键字volatile来试试
结论
多线程并发的同时进行set、get操作,A线程调用set方法,B线程并不一定能对这个改变可见,上面的代码中,如果get也添加synchronized也是可见的,还是happens-before的监视器规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。只是volatile对比synchronized更轻量级,所以本例使用volatile,但是对于符合非原子操作i++这里还是不行的,还得用synchronized。
不过使用volatile也会限制一些调优