网上讲volatile的很多,大多都是从定义和内存模型上来介绍,本文就举两个例子来说下我的心得吧
1.状态标志位:
volatile字段常用于多线程环境下的状态标志位。例如,一个线程修改了某个标志位的值,其他线程可以立即看到最新的值,从而做出相应的处理。在这里,我们举个简单的例子。假设我们有一个MyThread类,该类包含一个isRunning字段和一个run方法,如下所
在上述示例中,一个线程通过设置isRunning字段为false来停止另一个线程的执行。由于isRunning字段被声明为volatile,其他线程在访问该字段时能够立即看到最新的值,从而及时停止执行。如果中没有使用volatile修饰isRunning字段,那么其他线程可能无法立即看到该字段的最新值,从而继续执行循环,导致无法正常停止。(可能会多运行一段时间) 使用volatile关键字修饰isRunning字段可以解决这个问题。volatile关键字保证了字段的可见性,即当一个线程修改了isRunning字段的值时,其他线程能够立即看到最新的值,从而及时停止执行。
2.双重检查锁-单例
在这个例子中,如果不使用volatile修饰instance字段,可能会导致多线程环境下的线程安全问题。 当多个线程同时调用getInstance()方法时,如果instance字段没有被volatile修饰,那么就可能存在以下情况: 线程A检查到 instance 在线程A创建实例的过程中,另一个线程B也检查到 instance synchronized 当线程A完成实例的创建并释放锁之后,线程B获得锁,然后也开始创建实例。 线程B创建出来的实例会覆盖掉线程A创建的实例,从而导致单例模式失效。
3.防止指令重排,还是上面的例子
举个例子来说明,假设有两个线程A和B,它们都通过getInstance()方法获取单例对象。在某一时刻,线程A执行到如下代码:
instance = new Singleton();
按照正常的顺序,这行代码应该依次完成分配内存空间、初始化对象、将引用赋值给变量的操作。然而,由于指令重排优化,编译器和处理器可能会改变执行顺序,将赋值操作(将引用赋值给变量)提前到内存空间分配和对象初始化之前。这样,在线程A执行完赋值操作之前,线程B就已经检查到instance
不为null,然后开始使用这个对象。
如果此时线程B访问了未完全初始化的对象,就可能会出现问题。因为对象的初始化可能包含一些关键的操作,例如为实例变量赋初值或连接数据库等。如果线程B使用了未完全初始化的对象,就会出现数据不一致或其他错误。
而如果我们使用volatile关键字修饰了instance
字段,就能够防止指令重排优化。具体而言,对于被volatile修饰的字段,编译器和处理器会确保读写操作按照程序的先后顺序执行,从而保证了对象的完全初始化。这样,在一个线程完成实例创建后,其他线程在获取instance
时能够立即看到最新的值,避免了另一个线程获取到未完全初始化的对象。
总之,没有volatile关键字修饰的情况下,编译器和处理器可能会对指令进行重新排序优化,导致另一个线程获取到未完全初始化的对象。而使用volatile关键字修饰可以防止指令重排,确保对象的完全初始化,并保证多线程环境下的正确性和可见性。