volatile足以保证数据同步吗

本文探讨了计算机系统的四种存储介质:寄存器、高级缓存、RAM和ROM,并介绍了引入多级存储机制的原因。进一步类比Java内存模型中的主存与工作内存,解析了数据同步问题以及volatile关键字的作用。

在讨论之前必须先搞清四种存储介质:寄存器、高级缓存、RAM和ROM。

RAM与ROM大家都比较熟悉了,可以看成是我们经常说的内存与硬盘,寄存器属于处理器里面的一部分,而高级缓存cache是CPU设计者为提高性能引入的一个缓存,也可以说是属于处理器的一部分。在利用CPU进行运算时必定涉及操作数的读取,假如CPU直接读取ROM,那么这个读取速度简直是无法忍受的,于是引入了内存RAM,这样做确实让速度提高了很多,但由于CPU发展十分迅猛而另一方面RAM的发展受到技术及成本的限制发展缓慢,此时产生了一个很难调和的矛盾,CPU运算速度比从RAM读取数据的速度快了几个数量级,木桶原理我们都很熟悉了,桶的容量大小取决于最短的那块,这必将影响处理器的效率,于是又引入了高级缓存,直接在CPU添加了几个级别的缓存,他们的速度虽然无法与寄存器比较,但是速度已经提升很多,基本能跟CPU的计算速度相匹配。总结成一句话就是,为了解决CPU运算速度与读取速度的矛盾,引入了多级存储机制。

如图所示,机器的四种存储介质是有关系的,一般程序运行时会将ROM相关的程序数据都读进RAM中,而需要运算的数据或运算过程中即将要用到的数据则会被读进高速缓存或寄存器中,假如要进行的运算所需要的所有数据及指令都在寄存器和高速缓存中,则这个运算过程则表现得非常平坦,并不存在性能瓶颈,因为运算速度跟读取速度基本匹配了。读取速度快慢的排序如下:寄存器>cache>RAM>ROM,用一个比较好理解但不完全正确的概念来解释,因为寄存器是离CPU最近的,所以读取最快,高速缓存次之,RAM第三,ROM离得最远,自然速度最慢(当然不能完全用距离来说明这个问题,但用距离是比较好理解的,另外的还因为这些存储介质的硬件设计不同、工作方式不同)。从另一个角度来看,CPU读取数据的顺序是先尝试读寄存器,如果不存在则尝试读高速缓存,如果还不存在则读RAM,最后才是读ROM。一些CPU有三级cache,读取时是一级一级往下直到找到需要的操作数,一般做的比较好的CPU3级缓存已经能让命中率高达95%以上。

这里写图片描述

有了上面的知识再往下探索就水到渠成了,如果把Java内存模型与多级存储机制类比将发现为了提高性能java引入了工作内存的概念,提高了线程执行时读取数据的速度,这样就可以把java模型中的主存和工作内存分别于RAM和高速缓存或寄存器对应起来,每条线程的工作内存预先把需要的数据复制到高速缓存或寄存器(但是不保证所有的工作内存的变量副本都是放在高速缓存,也可能在RAM,具体的还要看JVM是如何实现的),这样在多线程并发时性能得到保证。当然寄存器和高速缓存由于成本原因存在容量大小限制的问题,这个也是考验JVM实现的一个难题。

一般引入一种机制解决了一个问题,但同时也会带来另外一个问题,数据同步即是带来的另一个问题,即是否能保证当前运算使用的变量值总是当前时刻最新的值。如果变量值并非最新值,将会导致数据的脏读,最终可能导致计算结果大相径庭。这时可能有人会想起java中有个volatile关键词,毫无疑问它能保证可见性,让每个线程得到的都是主存中最新的变量值,但它就足以保证数据的同步性了吗?举个典型例子,伪代码如下:

private volatile int count=0;
private void increase(){count++}
public static void main(String args){
    创建30条线程执行;
    每条线程任务都是执行10000次increase()方法;
}

执行完所有线程任务,我们期望的结果会是30*10000,但实际却是一个小于30*10000的数,刚开始看到一定觉得有点奇怪,但仔细一想就清楚了,count++编译后最终并非一个原子操作,它由几个指令一起组合实现。下图能较清晰地说明此点,在Java内存模型中,count++被分割成5个步骤(当然这个并不是确切的指令执行步骤),这5步不具有原子性,假如在完成过程中,其他线程就去读了主存的count变量,那明显导致了一个脏读现象。

这里写图片描述

导致这个问题的原因其实是因为volatile不具备锁操作,要解决此问题其实不难,就是将这五步变为原子操作,即保证线程一完成之前不能有其他线程读取count变量,对count变量加一个互斥锁即可达到,线程一在执行第①步前对count加锁,其他线程无法对count进行访问,线程一执行完第⑤步后释放锁,此刻开始才允许其他线程获取此变量。

Volatile是一个很容易搞混的关键词,很多经验丰富的开发人员都不能正确使用它,这节从机器结构讲到对应的java内存模型,再引出主存与工作内存之间数据同步的问题,进而更好地解释了volatile的确切含义——它只保证可见性,它不足以保证数据的同步性。

========广告时间========

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以购买。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

=========================

欢迎关注:

这里写图片描述

### Java 中 `volatile` 关键字的线程安全保障分析 #### 1. 可见性保障 在 Java 的多线程环境中,`volatile` 关键字的主要作用之一是确保变量的 **可见性**。这意味着当一个线程修改了一个被 `volatile` 修饰的变量时,该修改会立即反映到主内存中,并且其他线程可以及时看到最新的值[^1]。 然而需要注意的是,虽然 `volatile` 能够保证多个线程之间的可见性,但它无法完全解决线程安全问题。这是因为即使某个线程看到了最新值,也不能防止其他线程在同一时间对该变量进行操作而引发的竞争条件[^2]。 #### 2. 防止指令重排序 除了提供可见性外,`volatile` 还能通过禁止某些类型的指令重排序来增强程序的行为一致性。具体来说,在 Java 内存模型 (JMM) 下,编译器和 CPU 可能会对代码中的语句重新排列以优化性能。这种行为通常不会改变单线程下的逻辑结果,但在多线程场景下可能导致不可预期的结果。为此,`volatile` 提供了一种轻量级同步手段——它不仅阻止了特定变量上的写入与读取之间发生重排现象,还间接地维持了一些必要的全局顺序关系[^3]。 尽管如此,仅靠 `volatile` 并不足以应对复杂的并发控制需求;对于更严格的约束情况,则需借助诸如 `synchronized` 或者显式的机制 (`Lock`) 来实现真正的互斥访问以及更强有力的一致性和原子性保护[^4]。 #### 3. 不足之处 - 缺乏原子性支持 值得注意的一个方面在于,即便使用了 `volatile` ,如果存在复合操作(比如先读再写的组合动作),那么这些单独的操作本身仍然可能被打断或者交错执行从而破坏整体的数据完整性。例如经典的 “懒汉式” 单例模式里涉及到两次检查实例是否存在并创建新对象的过程就需要额外加才能达到绝对意义上的线程安全性。 综上所述,`volatile` 主要是用来处理那些只需要简单的状态标志位更新而不涉及复杂计算或资源竞争的情形之下的一种高效解决方案。但对于需要高度精确协调的任务而言,还是应该考虑采用更加全面可靠的同步方法如上述提到的各种形式的定技术。 ```java // 使用 volatile 实现简单计数器的例子 public class VolatileExample { private static volatile int counter = 0; public void increment() { // 此处虽然是 volatile, 但由于 ++ 是非原子操作仍可能存在竞态条件 counter++; } public int getCounterValue(){ return counter; } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超人汪小建(seaboat)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值