1.1 内存可见性
意味着当一个线程更新了共享数据时对其他线程可见。
1.1.1 可见性
可见性是一种复杂的属性。在单线程环境中,如果向某个变量写入值,然后在没有其他写入操作的情况下读取这个变量,总能得到相同的值。但是,在多线程环境中,读操作和写操作在不同的线程中执行,情况却并非如此。
代码示例:
public static void main(String[] args) throws InterruptedException {
FlagClass fc = new FlagClass();
new Thread(() -> {
while (!fc.isFlag()) { // 读取flag的值为true时结束循环
}
System.out.println("---->end");
}).start();
TimeUnit.MILLISECONDS.sleep(200);
fc.setFlag(true); // 修改flag的值为true
}
private static class FlagClass {
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
启动读操作的线程可能会一直循环下去,不会执行打印语句。因为无法保证执行读操作的线程能够实时的看到其他(main)线程写入(更新)的值,因此对于读操作的线程来说是不可见的。
1.1.2 失效数据
在上面的程序中可能产生错误结果的一种情况:失效数据。当读线程查看flag变量时,可能会得到一个已经失效的值(因为flag变量已经被其他线程修改过)。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值,从而导致程序得到错误的值,无法正常结束。
1.1.3 可见性的两种解决方式
1. 加锁与可见性
当线程执行同步代码块获取锁时,重新去主内存读取数据;释放锁之前更新主内存。
简单来说:当一个线程执行由锁保护的同步代码块时,可以看到上一个线程在同一个同步代码块中的所有操作结果。
private static class FlagClass {
private boolean flag = false;
public synchronized boolean isFlag() { // 加锁
return flag;
}
public synchronized void setFlag(boolean flag) { // 加锁
this.flag = flag;
}
}
加锁(加锁机制)的含义不仅仅局限于互斥行为(原子性),还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
2. Volatile关键字
一种比 synchronized 关键字轻量级的同步机制,用来确保变量的更新操作通知到其他线程。
private static class FlagClass {
private volatile boolean flag = false; // 变量声明为volatile类型
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
在访问 volatile 变量时不会执行加锁操作,因此在多线程修改变量时不能确保原子性。
禁止重排序:不会将该变量上的操作与其他内存操作一起重排序。
重排序:编译器、处理器运行时对代码的执行顺序进行优化。
总结
加锁机制可以确保原子性和可见性,而 volatile 变量只能确保可见性。
如果只是为了多线程之间的通信效果,而不是为了互斥访问,就使用 volatile ,它更加简洁,性能也更好。
volatile 变量使用场景:
- 用于标记可变状态和结果
- 多个线程读变量,单个线程去更新变量(不能保证原子性)
- 不需要加锁(加锁可以确保可见性)