volatile,它是 Java 中的一个关键字,是一种同步机制。当某个变量是共享变量,且这个变量被 volatile 修饰的,那么在修改了这个变量的值之后,再读取该变量的值时,可以保证获取到的是修改后的最新的值,而不是过期的值。
相比于 synchronized 或者 Lock,volatile 是更轻量的,因为使用 volatile 不会发生上下文切换等开销很大的情况,不会让线程阻塞。但正是由于它的开销相对比较小,所以它的效果,也就是能力,相对也小一些。
volatile 不适用:a++,我们仅靠 volatile 是不能保证 a++ 的线程安全的。
public class DontVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new DontVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((DontVolatile) r).a);
System.out.println(((DontVolatile) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
a++;
realA.incrementAndGet();
}
}
}
如果某个共享变量自始至终只是被各个线程所赋值或读取,而 没有其他的操作(比如读取并在此基础上进行修改这样的复合操作)的话,那么我们就可以使用 volatile
来代替 synchronized
或者代替原子类,因为赋值操作自身是具有原子性的,volatile 同时又保证了可见性,这就足以保证线程安全了。
一个比较典型的场景就是布尔标记位的场景,例如 volatile boolean flag。因为通常情况下,boolean 类型的标记位是会被直接赋值的,此时不会存在复合操作(如 a++),只存在单一操作,就是去改变 flag 的值,而一旦 flag 被 volatile 修饰之后,就可以保证可见性了,那么这个 flag 就可以当作一个标记位,此时它的值一旦发生变化,所有线程都可以立刻看到,所以这里就很适合运用 volatile 了。
总结一下 volatile 的作用:
- 保证可见性
如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。
- 禁止重排序
不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序。
volatile 和 synchronized 的关系
1,volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全。实际上,对 volatile 字段的每次读取或写入都类似于“半同步”——读取 volatile 与获取 synchronized 锁有相同的内存语义,而写入 volatile 与释放 synchronized 锁具有相同的语义。
2,但是在更多的情况下,volatile 是不能代替 synchronized 的,volatile 并没有提供原子性和互斥性。
3,volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以说它是高性能的,比 synchronized 性能更好。
单例模式的双重检查锁模式为什么必须加 volatile?
单例模式指的是,保证一个类只有一个实例,并且提供一个可以全局访问的入口。
使用单例的理由就是为了节省内存、节省计算,为了保证结果的正确。
有一个私有的 Singleton 类型的 singleton 对象;同时构造方法也是私有的,为了防止他人调用构造函数来生成实例;另外还会有一个 public 的 getInstance 方法,可通过这个方法获取到单例。单例模式的类结构如下图所示:
双重检查锁模式的写法
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要 double-check?去掉任何一次的 check 行不行?
有两个线程同时调用 getInstance 方法,由于 singleton 是空的 ,因此两个线程都可以通过第一重的 if 判断;然后由于锁机制的存在,会有一个线程先进入同步语句,并进入第二重 if 判断 ,而另外的一个线程就会在外面等待。当第一个线程执行完 new Singleton() 语句后,就会退出 synchronized 保护的区域,这时如果没有第二重 if
(singleton == null)
判断的话,那么第二个线程也会创建一个实例,此时就破坏了单例,这肯定是不行的。
在双重检查锁模式中为什么需要使用 volatile
关键字?
主要就在于 singleton = new Singleton()
,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
1,给 singleton 分配内存空间;
2,调用 Singleton 的构造函数等,来初始化 singleton;
3,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。
使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的 读取到不完整对象 的问题的发生。
使用 volatile 的意义主要在于它可以避免拿到未完成初始化的对象,从而保证了线程安全。