volatile关键字的作用_volatile 关键字 - 可见性保证

Java的关键字 volatile 用于将变量标记为“存储于主内存中”。更确切地说,对 volatile 变量的每次读操作都会直接从计算机的主存中读取,而不是从 cpu 缓存中读取;同样,每次对 volatile 变量的写操作都会直接写入到主存中,而不仅仅写入到 cpu 缓存里。

实际上,从 Java 5 开始关键字 volatile 除了能确保 volatile 变量直接从主存中进行读写,还有以下几个作用。

可见性保证

关键字 volatile 能确保数据变化在线程之间的可见性。

在多线程的应用中多个线程对 non-volatile 变量进行操作,线程在对它们进行操作的时候为了提高性能会将变量从主存复制到 cpu 缓存中。如果你的电脑包含的 cpu 不止一个, 那么每个线程可能会运行于不同的 cpu 上。这意味着,不同线程会将变量复制到不同 cpu 的缓存里。如下图:

f94f2b462eca5a956f7f0fbf3ff45f54.png

no-volatile 变量不能保证 Java 虚拟机(JVM)何时从主存中将数据读入cpu 缓存,也不能保证何时将数据从 cpu 缓存写入到主存中。这会带来一些问题,我将在下面解释。

想象一个场景,两个或两个以上线程可访问同一个共享对象,这个对象含有一个如下的计数器变量:

public class SharedObject{

public int counter = 0;

}

再假设有2个线程 Thread1 和 Thread2,只有 Thread1 能增大 counter ,而 Thread1 和 Thread2 都可以在任何时刻读取 counter 的值。

如果 counter 没被声明为 volatile ,将不能保证什么时候 counter 变量的值会从 cpu 缓存回写到主存内。也就是说,变量 counter 在 cpu 缓存中的值可能和主存内的不一致。 如下图:

003d57b77812d5b56483df9cd77f3724.png

这种由于线程还未将变量的值回写到主内存(Main Memory)而导致其他线程不能看到该变量的最新值的问题,称为可见性问题。一个线程的更新操作对其他线程不可见。

通过将变量 counter 声明为 volatile ,对其进行的所有写操作都会马上回写至主存中。同时,所有 counter 的读操作也将直接在主存中进行。声明方式如下:

public class SharedObject{

public volatile int counter = 0;

}

这样,将变量声明为 volatile 保证了写操作对其他线程的可见性。

volatile 并不能满足所有场景

即使关键字 volatile 能保证对它的所有读操作都是直接从主存中读取,所有写操作也都是直接写入主存中,还是有些仅将变量声明为 volatile 不能满足的场景。

在前面讨论的例子中只有线程A 对共享变量 counter 进行写操作,将 counter 声明为 volatile 就足以保证线程B 能看到新写入的值。

实际上,多线程甚至在同时对共享的 volatile 变量进行写操作时,只要新值的写入不依赖它之前的值,就仍然能保持主存中的值是正确的。换句话说,一个线程将一个值写入共享 volatile 变量中,不需要首先读取原来值来计算下一个新值。

只要线程需要先读取 volatile 的值, 然后基于这个值来生成新值,那么这个 volatile 变量就不再能保证其正确的可见性。读取 volatile 和写入新值之间的时间间隙产生了 竞态条件 ,多个线程可能读取到相同的值, 然后生成新值,接着在将值回写到主存中的时候就会覆盖彼此的值了。

多线程增加同一个计数器 counter 就恰好是 volatile 不够用的一个场景。接下来我们来更详细的解释这个例子。

假设线程1 读取值为0的共享变量 counter 到它的 cpu 缓存中,增加值到1,但还没把改变的值回写到主存中。 线程2 接着也能从主存中读取值还是0这个 counter,并将其存入它自己的 cpu 缓存里。接着线程2 也将 counter 的值增加到1,同样也还没回写主存。这个场景如下图:

9debfda8d027b1ada850130f782503ed.png

线程1 和线程2 现在就是切实的不同步。共享变量 counter 实际的值应该 2,但是每个线程在各自的 cpu 缓存中的值为 1,主存中的值还是 0。乱成一团了!即使最后线程都把它们持有的值回写到主存中,counter 的值也是错的。

什么时候单单使用 volatile 就够了?

就如之前提到的,如果两个线程都对共享变量进行读写,那么只使用关键字 volatile 就不能满足要求了。这种情况你需要用 synchronized 来保证读写变量的原子性。对 volatile 变量的读写操作并不会阻塞其他线程的读写。如果需要阻塞,你就必须在临界区周围使用关键字 synchronized

如果不想用 synchronized 代码块,你可以从包java.util.concurrent中找到很多有用的原子数据类型,如 AtomicLong,AtomicReference 或者其他。

假设只有一个线程同时读写 volatile 变量,其他线程只读取,那么只读线程一定能看到最新写入到 volatile 变量的值。如果不将变量声明为 volatile ,这就得不到保证。

关键字 volatile 确定能在 32位和64位变量上正常运行。

volatile 的性能考虑

volatile 变量的读取和写入操作导致变量直接在主存中读写。从主存中读取和写入到主存中比在 cpu 缓存中代价更高 。访问 volatile 变量也阻止了常规的性能优化技术对指令的重排序。所以,你应该只在确实需要加强变量的可见性的时候使用 volatile

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值