再看volatile关键字

volatile关键字

Java中, 允许多线程访问同一个共享变量(§17.1 shared variables), 为了保证共享变量在多线程间保持一致, 通常情况会通过锁来对共享变量的保护, 线程操作的时候需要获得锁才可以对共享变量的操作, 这样就保证了共享变量在多线程间保持一致.

Java还提供另外一种做法可以确保共享变量在多线程间保持一致的, 这个做法就是将变量声明为volatile变量, 对于一些场景, 这是一种比锁更方便的做法.

volatile的作用: 当一个变量被声明为volatile, 将确保所有线程看到变量一致结果, 也就是说声明为volatile的变量变更时对所有线程是可见的.

volatile重要的两个特性:

  • 可见性
  • 禁止指令的重排序

还有需要注意的是:

  • 被声明为final的变量不能同时被声明为volatile
  • volatile不能代替锁

对于volatile的详细介绍具体可以查阅:The Java ® Language Specification [Java SE 10 Edition] (8.3.1.4 volatile Fields)

可见性

可见性是指当多个线程访问同一个变量时, 一个线程修改了这个变量的值, 其他线程能够立即看得到修改的值.

这里通过一个简单的例子来说明可见性的问题.
但在说明这个例子之前, 首先需要知道:

Java内存模型中规定了所有的变量都存储在主内存中, 每条线程还有自己的工作内存(可以与处理器的高速缓存类比), 线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝, 线程对变量的所有操作(读取,赋值)都必须在工作内存中进行, 而不能直接读写主内存中的变量. 不同线程之间无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要在主内存来完成.

示例:

// 线程1操作
int i = 0; // (1)
i = 1;     // (2)

// 线程2操作
int j = i; // (3)

线程1执行完操作(1)会将初始值0加载到主内存, 后执行操作(2)进行赋值, 将1赋值给i, 这样线程1工作内存中的i的值变为1, 如果线程1此时还没将i=1的值同步到主内存中, 线程2执行操作(3), 将主内存中i=0拷贝到线程2工作内存中, 然后赋值给j, 结果j=0.

这就是可见性问题, 线程1对i的修改, 线程2是没有立即看到这个修改.

如果将i声明为volatile类型, 上述可见性问题就不存在了, 在Java内存模型中, 当变量一旦被声明为volatile, 它将会保证所有的线程看到一致的结果.

可见性

当变量被声明为volatile的时候, 需要读取变量时, 每次都会从主内存读取, 一旦变量发生改变, 会立即同步到主内存, 同时其它线程中工作内存中的变量缓存行失效, 当其它线程再次读取变量的时候发现缓存行失效, 就会从主内存读取, 这就保证了可见性.

可见性原理

处理器为了提高处理速度, 不直接和内存进行通讯, 而是将系统内存的数据独到内部缓存后再进行操作, 但操作完后不知什么时候会写到内存.
如果对声明了volatile变量进行写操作时, JVM会向处理器发送一条Lock前缀的指令, 将这个变量所在缓存行的数据写会到系统内存. 这一步确保了如果有其他线程对声明了volatile变量进行修改, 则立即更新主内存中数据.
但这时候其他处理器的缓存还是旧的, 所以在多处理器环境下, 为了保证各个处理器缓存一致, 每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期, 当处理器发现自己缓存行对应的内存地址被修改了, 就会将当前处理器的缓存行设置成无效状态, 当处理器要对这个数据进行修改操作时, 会强制重新从系统内存把数据读到处理器缓存里. 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的.

禁止指令重排序

volatile关键字能禁止指令重排序, 所以volatile能在一定程度上保证有序性.

指令重排序

为了使得处理器内部的运算单元能尽可能被充分利用, 处理器可能会对指令进行乱序执行(Out-Of-Order Execution)优化, 处理器会在计算之后将对指令乱序执行的结果进行重组, 保证结果准确性, 在Java中也有类似的机制, 这就是指令重排序(Instruction Reorder).

在执行程序时为了提高性能, 编译器和处理器经常会对指令进行重排序.
而重排序分成三种类型:
1. 编译器优化的重排序: 编译器在不改变单线程程序语义放入前提下, 可以重新安排语句的执行顺序.
2. 指令级并行的重排序: 现代处理器采用了指令级并行技术来将多条指令重叠执行. 如果不存在数据依赖性, 处理器可以改变语句对应机器指令的执行顺序.
3. 内存系统的重排序: 由于处理器使用缓存和读写缓冲区, 这使得加载和存储操作看上去可能是在乱序执行.

volatile禁止指令重排序的原理

如果对声明了volatile变量进行写操作时, JVM会向处理器发送一条Lock前缀的指令, Lock前缀指令实际上相当于一个内存屏障也称内存栅栏, 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 也不会把前面的指令排到内存屏障的后面; 即在执行到内存屏障这句指令时, 在它前面的操作已经全部完成.

参考致谢

参考书籍: Java并发编程的艺术 - 方腾飞
网上资料: https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
网上资料: http://www.importnew.com/24082.html
网上资料: The Java ® Language Specification [Java SE 10 Edition] (8.3.1.4 volatile Fields)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值