volatile 的作用是什么?与 synchronized 有什么异同?

volatile是Java中的关键字,用于实现轻量级同步,确保共享变量的可见性。它不能保证原子性,适用于布尔标志位和作为触发器的场景。在单线程规则和volatile规则下,volatile能保证修改后的值对其他线程立即可见,但无法替代synchronized的同步保护。volatile在性能上优于synchronized,因为它避免了锁操作。
摘要由CSDN通过智能技术生成

volatile 是什么?

  • 首先我们就来介绍一下 volatile,它是 Java 中的一个关键字,是一种同步机制。当某个变量是共享变量,且这个变量是被 volatile 修饰的,那么在修改了这个变量的值之后,再读取该变量的值时,可以保证获取到的是修改后的最新的值,而不是过期的值。
  • 相比于 synchronized 或者 Lock,volatile 是更轻量的,因为使用 volatile 不会发生上下文切换等开销很大的情况,不会让线程阻塞。但正是由于它的开销相对比较小,所以它的效果,也就是能力,相对也小一些。
  • 虽然说 volatile 是用来保证线程安全的,但是它做不到像 synchronized 那样的同步保护,volatile 仅在很有限的场景中才能发挥作用,所以下面就让我们来看一下它的适用场景,我们会先给出不适合使用 volatile 的场景,再给出两种适合使用 volatile 的场景。

volatile 的适用场合

不适用:a++,volatile 不适合运用于需要保证原子性的场景,比如更新的时候需要依赖原来的值,而最典型的就是 a++ 的场景,我们仅靠 volatile 是不能保证 a++ 的线程安全的。

适用场合1:布尔标记位

  • 如果某个共享变量自始至终只是被各个线程所赋值或读取,而没有其他的操作(比如读取并在此基础上进行修改这样的复合操作)的话,那么我们就可以使用 volatile 来代替 synchronized 或者代替原子类,因为赋值操作自身是具有原子性的,volatile 同时又保证了可见性,这就足以保证线程安全了。
  • 一个比较典型的场景就是布尔标记位的场景,例如 volatile boolean flag。因为通常情况下,boolean 类型的标记位是会被直接赋值的,此时不会存在复合操作(如 a++),只存在单一操作,就是去改变 flag 的值,而一旦 flag 被 volatile 修饰之后,就可以保证可见性了,那么这个 flag 就可以当作一个标记位,此时它的值一旦发生变化,所有线程都可以立刻看到,所以这里就很适合运用 volatile 了。

适用场合 2:作为触发器
那么下面我们再来看第二个适合用 volatile 的场景:作为触发器,保证其他变量的可见性。

下面是 Brian Goetz 提供的一个经典例子:

Map configOptions;

char[] configText;

volatile boolean initialized = false;

 

. . .
 

// In thread A


configOptions = new HashMap();

configText = readConfigFile(fileName);

processConfigOptions(configText, configOptions);

initialized = true;

. . .


// In thread B
 
while (!initialized) 

  sleep();

// use configOptions
  • 在这段代码中可以看到,我们有一个 map 叫作 configOptions,还有一个 char 数组叫作 configText,然后会有一个被 volatile 修饰的 boolean initialized,最开始等于 false。再下面的这四行代码是由线程 A 所执行的,它所做的事情就是初始化 configOptions,再初始化 configText,再把这两个值放到一个方法中去执行,实际上这些都代表了初始化的行为。那么一旦这些方法执行完毕之后,就代表初始化工作完成了,线程 A 就会把 initialized 这个变量设置为 true。
  • 而对于线程 B 而言,它一开始会在 while 循环中反复执行 sleep 方法(例如休眠一段时间),直到 initialized 这个变量变成 true,线程 B 才会跳过 sleep 方法,继续往下执行。重点来了,一旦 initialized 变成了 true,此时对于线程 B 而言,它就会立刻使用这个 configOptions,所以这就要求此时的 configOptions 是初始化完毕的,且初始化的操作的结果必须对线程 B 可见,否则线程 B 在执行的时候就可能报错。
  • 你可能会担心,因为这个 configOptions 是在线程 A 中修改的,那么在线程 B 中读取的时候,会不会发生可见性问题,会不会读取的不是初始化完毕后的值?如果我们不使用 volatile,那么确实是存在这个问题的。
  • 但是现在我们用了被 volatile 修饰的 initialized 作为触发器,所以这个问题被解决了。根据happens-before 关系的单线程规则,线程 A 中 configOptions 的初始化 happens-before 对 initialized 变量的写入,而线程 B 中对 initialzed 的读取 happens-before 对 configOptions 变量的使用,同时根据 happens-before 关系的 volatile 规则,线程 A 中对 initialized 的写入为 true 的操作 happens-before 线程 B 中随后对 initialized 变量的读取。

volatile 的作用

上面我们分析了两种非常典型的用法,那么就来总结一下 volatile 的作用,它一共有两层作用。

  • 第一层的作用是保证可见性。Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。
  • 这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。
  • 第二层的作用就是禁止重排序。先介绍一下 as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序

volatile 和 synchronized 的关系

  • 相似性:volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全。实际上,对 volatile 字段的每次读取或写入都类似于“半同步”——读取 volatile 与获取 synchronized 锁有相同的内存语义,而写入 volatile 与释放 synchronized 锁具有相同的语义。
  • 不可代替:但是在更多的情况下,volatile 是不能代替 synchronized 的,volatile 并没有提供原子性和互斥性
  • 性能方面:volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以说它是高性能的,比 synchronized 性能更好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值