volatile 的可见性

volatile 的可见性

volatile 的可见性作用

关于 volatile 的可见性作用,必须意识到:

  • 被 volatile 修饰的变量对所有线程总是立即可见的;
  • 对 volatile 变量进行写操作,能立即反映到其他线程中;
  • 但是,对 volatile 变量运算操作在多线程环境中,并不能保证安全性。

场景一

public class volatileVisibility {
    public static volatile int value = 0;
    public static void increase() {
		value++;
	}
}

分析:

  • 若存在多线程同时调用 increase 方法,上面代码便会出现线程安全问题,因为 value++ 这个操作并不具备 原子性。
  • value++ 是先读取值,再写回一个新值,相当于把原来的值加上 1 分为两个步骤来完成。
  • 若第二个线程在第一个线程读取旧值和写回新值之间,读取 value 的值,第二个线程就会与第一个线程看到同一个值,并执行相同的 加1 操作,进而引发了线程安全问题。

因此,increase() 方法必须加上 synchronized 关键字修饰,以保证线程安全。

注意:

  • synchronized 解决的是执行控制问题,它会阻止其他线程获取当前对象的 监控锁。使得当前对象中被 synchronized 保护的代码块无法被其他线程访问,也就无法并发执行。
  • 更重要的是,synchronized 关键字还会创建一个内存屏障,内存屏障指令保证了所有cpu结果都会刷新到主内存中,从而保证了操作内存的可见性。同时使得先获得锁的线程的所有操作都 happens-before 于随后获得该锁的线程的操作。一旦使用 synchronized 关键字修饰方法后,由于 synchronized 本身也具备与 volatile 相同的 可见性 特性。因此,在这种情况下,完全可以省去 volatile 修饰。
更改为如下代码
public class volatileVisibility {
    public static int value = 0;
    public synchronized static void increase() {
		value++;
	}
}

场景二

使用 volatile 修饰,可以达到线程安全的目的

public class volatileVolatileSafe {
    volatile boolean shutdown;
    
    public void close() {
    	shutdown = true;
    }
    
    public void dowork() {
    	while (!shutdown) {
    		System.out.println("safe......");
		}
    }
}

代码分析:

  • 由于对 boolean 变量,即 shutdown 的值的修改 属于原子操作,因此可以使用 volatile 修饰shutdown变量,使得对该变量的修改对其他线程立即可见,从而达到线程安全的目的。

volatile 变量为何立即可见

  • 当写一个 volatile 变量时,JMM 会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取一个 volatile 变量时,JMM 会把该线程对应的工作内存置为无效.

内存屏障

是一个 cpu 指令。作用有以下两个:

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性

volatile 如何禁止重排优化

  • 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
  • 强制刷出各种 cpu 的缓存数据,因此任何 cpu 上的线程都能读取到这些数据的最新版本

总之,volatile 变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

单例的双重检测实现

面试常要求,即常见的实现线程安全的单例写法。

public class Singleton {
	private static Singleton instance;
	
	private Singleton () {}

	public static Singleton getInstance () {
		// 第一次检测
		if (instance == null) {
			// 同步
			synchronized (Singleton.class) {
				if (instance == null) {
					// 多线程环境下,可能会出现问题的地方
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

代码分析

  • 通过引入 synchronized 代码块,试图解决 多线程请求单例时,带来的重复创建单例的隐患。
  • 这段代码看似没有多大问题,但是在多线程环境下,依然会有隐患。原因是:某一个线程执行到 第一次检测时,instance 不为空时,instance 引用的对象可能还没完全初始化,因为 instance = new Singleton() 这步可以分为下面三步完成,伪代码如下:
// 伪代码
memory = allocate();// 1.分配对象内存空间
instance(memory);// 2.初始化对象
instance = memory; // 3. 设置instance 指向刚分配的内存地址,此时 intance != null

这时,可能会发生指令重排序,变成下面伪代码。

// 伪代码
memory = allocate();// 1.分配对象内存空间
instance = memory; // 3. 设置instance 指向刚分配的内存地址,此时 intance != null,但是对象还没有初始化完成!
instance(memory);// 2.初始化对象

第三步提前,而第二步延后了。

  • 这时因为步骤2 和步骤3 之间不存在数据依赖的关系,而且无论排前排后,执行的结果在单线程中并没有改变。因此这种重排序优化是允许的。但是指令重排,只能保证串行语义的一致性,即单线程语义的一致性。但并不会关心多线程的语义一致性。
  • 所以当一个线程访问 instance 不为空时,由于实例未必已经完成初始化,就造成了线程的安全问题。那么该如何解决?

解决方法

  • 使用 volatile 去禁止 instance 变量执行指令重排序即可。就是不能将 第2步和第3步颠倒过来。

修改后代码如下:

public class Singleton {
	// 禁止指令重排优化
	private  volatile static  Singleton instance;
	
	private Singleton () {}

	public static Singleton getInstance () {
		// 第一次检测
		if (instance == null) {
			// 同步
			synchronized (Singleton.class) {
				if (instance == null) {
					// 多线程环境下,可能会出现问题的地方
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

volatile 和 synchronized 的区别

  • volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止。
  • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法和类级别
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量修改的可见性和原子性
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值