1. 说说你对volatile关键字的理解
volatile是java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 保证有序性
2. 能不能详细说下什么是内存可见性,什么又是重排序呢?
2.1 内存可见性
- 这个需要从Java内存模型说起
- 在目前的计算机中,CPU的计算速度远远大于计算机存储数据的速度,但是内存访问的速度就慢了很多。为了提升整体性能,在CPU和内存之间加入了高速缓存。
- CPU将计算需要用到的数据暂存进缓存中。当计算结束后再将缓存中的数据存入到内存中。这样CPU的运算可以在缓存中高速进行。
- 说回JMM,也就是Java内存模型,JMM规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存
- 使用了volatile后,操作数据的线程先从主内存中把数据read到总线bus,然后load到自己的工作内存中。如果有线程对volatile修饰的变量进行操作并且写回了主内存,则其他已读取该变量的线程中,该变量副本将会失效。其他线程需要从主内存中重新加载一份最新的变量值。
- Volatile保证了共享变量的可见性。当有的线程修改了Volatile修饰的变量值并写回到主内存后,其他线程能立即看到最新的值。
2.2 指令重排
- 系统为了提升执行效率,在不影响最终结果的前提下,系统会对要执行的指令进行重排序。
重排序分为以下几种:
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:由于数据读写过程中涉及到多个缓冲区,这使得加载和存储的操作看上去可能是乱序执行,于是需要内存系统的重排序。
JMM主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的,通过解决这三个问题,可以解除缓存不一致的问题。
3. 那你具体说说这三个特性
- 原子性:Java中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行;赋值是原子操作,还只能是用数字赋值,用变量的话还多了一步读取变量值的操作
- 可见性:Java就是利用volatile来提供可见性的。当有的线程修改了Volatile修饰的变量值并写回到主内存后,其他线程能立即看到最新的值。 其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
- 有序性:为了提升执行效率,JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。
4. 可见性的解决方案?
- 给代码加锁:使用synchronized的代码前后,线程会获得锁,清空工作内存。read将数据读到工作内存并load成为最新的副本,再通过store和write将数据写会主内存。而获取不到锁的线程会阻塞等待,所以变量的值一直都是最新的
- volatile修饰共享变量:使用了volatile后,操作数据的线程先从主内存中把数据read到工作内存,load成副本。如果有线程对volatile修饰的变量进行操作并且写回了主内存,则其他已读取该变量的线程中,该变量副本将会失效。其他线程需要从主内存中重新加载一份最新的变量值
5. volatile能保证可见性吗?
- Volatile能保证可见性
- 使用了volatile后,操作数据的线程先从主内存中把数据read到工作内存,load成副本中。
- 如果有线程对volatile修饰的变量进行操作并且写回了主内存,则其他已读取该变量的线程中,该变量副本将会失效。其他线程需要从主内存中重新加载一份最新的变量值
6. volatile能保证原子性吗?如何保证原子性?
- volatile不能保证原子性
- 这两步操作在左边的线程执行完第一步,但还没执行第二步时右边的线程抢过CPU控制权开始完成+1的操作后写入到主内存,于是左边的线程工作内存中的count副本失效了,相当于左边这一次+1的操作就被覆盖掉了。因此,Volatile不能保证原子性。
- 保证原子性,需要加锁
7. 什么是指令重排?
系统为了提升执行效率,在不影响最终结果的前提下,系统会对要执行的指令进行重排序
重排序分为以下几种:
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:由于数据读写过程中涉及到多个缓冲区,这使得加载和存储的操作看上去可能是乱序执行,于是需要内存系统的重排序。
8. as-if-serial语义
不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守“as-if-serial语义”。编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果
9. volatile能如何保证有序性?
- Volatile通过设置内存屏障(Memory Barrier),可以禁止指令重排,避免多线程环境下程序出现乱序执行的现象
- 通过插入内存屏障,禁止在内存屏障前后的指令执行重排序优化。
- Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
- 总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。
10. MESI缓存一致性协议?
- volatile具有可见性,线程是怎么知道数据被其他线程更新的呢?这就跟MESI缓存一致性协议有关系。
- M(Modified修改):当cpu2对变量进行修改时,现在cpu内的缓存行中上锁,并向总线发信号,此时cpu2中的变量状态为M
- E(Exclusive独享):当cpu1读取一个变量时,该变量在工作内存中的状态是E
- S(Shared共享):当cpu2读取该变量时,两个cpu中该变量的状态由E转为S。
- I(Invalid无效):cpu1嗅探到变量被其他cpu修改的信号,于是将自己缓存行中的变量状态设置为i,即失效。则cpu1再从内存中获取最新数据。
11. 总线风暴是什么?
- 由于Volatile的MESI缓存⼀致性协议,需要不断的从主内存嗅探和cas不断循环,⽆效交互会导致总线带宽达到峰值。
- 所以不要⼤量使⽤Volatile,⾄于什么时候去使⽤Volatile,什么时候使⽤锁,根据场景区分。
12.什么时候去使⽤Volatile?
- 某个属性被多个线程共享,其中有⼀个线程修改了此属性,其他线程可以⽴即得到修改后的值,⽐如作为触发器,状态量标记,实现轻量级同步
- volatile可以在单例双重检查中实现可⻅性和禁⽌指令重排序,可以解决单例双重检查对象初始化代码执⾏乱序问题,从⽽保证安全性。
13. Volatile和Synchronized区别?
- volatile只能修饰实例变量和类变量,只能作⽤于属性,⽽synchronized可以修饰⽅法和代码块。
- volatile保证数据的可⻅性,⽤于禁⽌指令重排序,但是不保证原⼦性;⽽synchronized是⼀种互斥的机制。
- volatile属性的读写操作都是⽆锁的,不需要花费时间在获取锁和释放锁上,所以说它是低成本的
- volatile可以看做是轻量版的synchronized,volatile不保证原⼦性,但是如果是对⼀个共享变量进⾏多个线程的赋值,⽽没有其他的操作,那么就可以⽤volatile来代替synchronized,因为赋值本身是有原⼦性的,⽽volatile⼜保证了可⻅性,所以就可以保证线程安全了。
14. volatile能替代synchronized吗?
- volatile属性的读写操作都是⽆锁的,
- 它没有提供原⼦性和互斥性,它不能替代synchronized
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65624.html