1.简述
volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile的缺点:
- 频繁更改、改变、写入volatile字段 有可能导致性能问题(volatile字段未修改时,读取没有性能问题的)。
- 限制现代JVM的JIT编译器对这个字段优化(volatile字段必须遵守一定顺序,放置顺序指令重拍导致bug例如单例双检测bug)。
volatile适用场景:
- 适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。
- 适用于读多写少的场景。
- 可用作状态标志。
- JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。
volatile的特性:
- volatile具有可见性、有序性,不具备原子性。
- volatile不具备原子性,这是volatile与synchronized、Lock最大的功能差异。
synchronized和volatile比较:
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。
- 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁。
- synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
2.volatile的作用
(1)可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
非volatile修饰变量示例:
public class Test { static boolean bool = false;//非volatile变量 public static void main(String[] args) throws Exception{ new Thread(new Runnable() { @Override public void run() { while(!bool){ }; System.out.println("子线程运行完毕"); } }).start(); Thread.sleep(100); bool = true; System.out.println("主线程运行完毕"); } }
运行上面的示例可以看出,主线程修改了bool为true,但子线程却一直不退出循环,因为主线程的修改子线程是不可见的(子线程中获取的bool一直为false,因为子线程的变量副本一直未更新)。
volatile修饰变量示例:
public class Test { static volatile boolean bool = false;//非volatile变量 public static void main(String[] args) throws Exception{ new Thread(new Runnable() { @Override public void run() { while(!bool){ }; System.out.println("子线程运行完毕"); } }).start(); Thread.sleep(100); bool = true; System.out.println("主线程运行完毕"); } }
运行上面的示例可以看出,使用volatile修饰变量后,主线程修改了bool为true,子线程可见(子线程可以退出循环,因为使用volatile修饰后主线程修改了变量会导致子线程的变量副本失效,从而子线程会从主内存中拿去最新值)。
(2)有序性(禁止指令重排序)
即程序执行的顺序按照代码的先后顺序执行。当代码执行顺序不同时,多线程就会出现问题。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
指令重排序遵循的规则:
- 重排序不会对存在依赖关系的操作进行重排。
- 重排序目的是优化性能,不管怎样重排,单线程下的程序执行结果不会变。
指令重排序示例:
public class Test { public void get(){ int a = 1; //1 int b = a + 1; //2 因为依赖 1指令的结果,因此1和2都不会排序 } public void get2(){ int a = 1; //1 int b = 2; //2 1和2没有依赖关系,可能发生重排 int c = a + b; //3 单线程情况下运行的结果永远是3 } }
double-check(懒汉式)单例模式示例:
class Singleton { private volatile static Singleton singleton; /**构造函数私有,禁止外部实例化 */ private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { //1.第一次检查是否为空 synchronized (Singleton.class) { //2.singleton对象上锁,初始化是由三步操作组成的复合操作,为了保障这三个步骤不可中断,使用synchronized加锁。 if (singleton == null) { //3.第二次检查是否为空,防止二次初始化 singleton = new Singleton();//4.实例化 } } } return singleton; } }
singleton = new Singleton();由三步操作组合而成,如果不使用volatile修饰,可能发生指令重排序。步骤3在步骤2之前执行,singleton引用的是还没有被初始化的内存空间,别的线程调用单例的方法就会引发未被初始化的错误。
(3)原子性
volatile只能保证单次读、写操作的原子性,多线程就会出现问题。
不能保证原子性示例:
public class Test { public static volatile int num = 0; public static void main(String[] args) throws Exception { //开启30个线程进行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++) num++;//自加操作 } }.start(); } while(Thread.activeCount() > 1) //线程是否执行完成 Thread.yield(); System.out.println(num); } }
在复合操作的情景下,原子性的功能是维持不了。但是volatile对于读、写操作都是单步的情景,所以还是能保证原子性的。
要想保证原子性,只能借助于synchronized、Lock以及并发包下的atomic的原子操作类了,即对基本数据类型的自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。