并发环境下读取值时为何需要读锁?无锁读取自定义Counter类会引发哪些问题?
并发读取与读锁问题解答
1. 为什么并发环境中读取值时需要读锁?
在并发场景下,读锁的核心作用是解决三个关键问题:
- 可见性保证:每个线程都有自己的工作内存,会缓存共享变量的值。如果没有读锁这类同步机制,读线程可能一直从本地工作内存读取旧值,永远看不到其他线程写入主内存的最新数据。
- 避免拆分读写的中间态:对于Java中的
long和double这类64位类型,JVM允许把单次写操作拆分成两个32位的写操作执行。如果没有读锁和写锁的互斥,读线程可能恰好读到写线程只完成一半的数值——得到一个既不是旧值也不是新值的错误“垃圾值”。 - 读写操作的逻辑一致性:读锁能和写锁互斥,保证在读操作的整个过程中,不会有写线程修改目标数据。举个例子,如果你的业务是读取数值后要做计算,没有读锁的话,刚读到的数值可能在计算中途就被写线程修改,导致逻辑结果完全不符合预期。
2. 不使用锁读取Counter类的val会出现的问题
先针对你的假设逐一验证,再补充你可能遗漏的情况:
你的假设验证
- 读取到非最新值:完全正确,而且情况可能比你想的更严重——不是“至少落后一个版本”,而是读线程可能永远看不到更新。因为没有读锁的内存屏障约束,JVM不会强制读线程从主内存刷新数据,它会一直复用本地工作内存的缓存值。
- 读取到写入过程中的中间垃圾值:在Java中确实会发生!刚才也提到了,64位的
long写操作可能被拆成两次32位写,读线程刚好在这两次写之间读取,就会得到一个高位新值、低位旧值(或者反过来)的错误数值。 - 关于WriteLock的内存屏障:这里你有点误解——WriteLock的解锁操作确实会把写线程的工作内存数据刷新到主内存,但读线程如果不用读锁(或
volatile等同步手段),没有强制从主内存读取的语义,依然会读取本地缓存的旧值,所以还是存在看不到写线程修改的风险。
你遗漏的其他情况
- 指令重排序导致的逻辑混乱:JVM在没有同步约束的情况下,可能对读取指令和后续业务逻辑指令进行重排序。比如,读线程读取
val之后,本该基于这个值做的计算,可能被提前到读取之前执行,导致逻辑完全错乱。 - 多变量读取的一致性问题:如果后续给Counter类增加关联变量(比如同时维护
count和totalAmount),没有读锁的话,可能出现读到count是最新值,但totalAmount还是旧值的情况,破坏了数据的逻辑一致性。读锁能保证在整个读操作期间,所有共享数据都是一个一致的快照。
内容的提问来源于stack exchange,提问作者user6412004
相关产品推荐
相关产品推荐

