同步和Java内存模型 (三)可见性

简介:

原文:http://gee.cs.oswego.edu/dl/cpj/jmm.html 第三章
作者:Doug Lea 译者:程晓明 校对:方腾飞

只有在下列情况时,一个线程对字段的修改才能确保对另一个线程可见:

一个写线程释放一个锁之后,另一个读线程随后获取了同一个锁。本质上,线程释放锁时会将强制刷新工作内存中的脏数据到主内存中,获取一个锁将强制线程装载(或重新装载)字段的值。锁提供对一个同步方法或块的互斥性执行,线程执行获取锁和释放锁时,所有对字段的访问的内存效果都是已定义的。

注意同步的双重含义:锁提供高级同步协议,同时在线程执行同步方法或块时,内存系统(有时通过内存屏障指令)保证值的一致性。这说明,与顺序程序设计相比较,并发程序设计与分布式程序设计更加类似。同步的第二个特性可以视为一种机制:一个线程在运行已同步方法时,它将发送和/或接收其他线程在同步方法中对变量所做的修改。从这一点来说,使用锁和发送消息仅仅是语法不同而已。


如果把一个字段声明为volatile型,线程对这个字段写入后,在执行后续的内存访问之前,线程必须刷新这个字段且让这个字段对其他线程可见(即该字段立即刷新)。每次对volatile字段的读访问,都要重新装载字段的值。

一个线程首次访问一个对象的字段,它将读到这个字段的初始值或被某个线程写入后的值。
此外,把还未构造完成的对象的引用暴露给某个线程,这是一个错误的做法 (see ?.1.2)。在构造函数内部开始一个新线程也是危险的,特别是这个类可能被子类化时。Thread.start有如下的内存效果:调用start方法的线程释放了锁,随后开始执行的新线程获取了这个锁。如果在子类构造函数执行之前,可运行的超类调用了new Thread(this).start(),当run方法执行时,对象很可能还没有完全初始化。同样,如果你创建且开始一个新线程T,这个线程使用了在执行start之后才创建的一个对象X。你不能确信X的字段值将能对线程T可见。除非你把所有用到X的引用的方法都同步。如果可行的话,你可以在开始T线程之前创建X。

线程终止时,所有写过的变量值都要刷新到主内存中。比如,一个线程使用Thread.join来终止另一个线程,那么第一个线程肯定能看到第二个线程对变量值得修改。

注意,在同一个线程的不同方法之间传递对象的引用,永远也不会出现内存可见性问题。
内存模型确保上述操作最终会发生,一个线程对一个特定字段的特定更新,最终将会对其他线程可见,但这个“最终”可能是很长一段时间。线程之间没有同步时,很难保证对字段的值能在多线程之间保持一致(指写线程对字段的写入立即能对读线程可见)。特别是,如果字段不是volatile或没有通过同步来访问这个字段,在一个循环中等待其他线程对这个字段的写入,这种情况总是错误的(see ?.2.6)。

在缺乏同步的情况下,模型还允许不一致的可见性。比如,得到一个对象的一个字段的最新值,同时得到这个对象的其他字段的过期的值。同样,可能读到一个引用变量的最新值,但读取到这个引用变量引用的对象的字段的过期值。
不管怎样,线程之间的可见性并不总是失效(指线程即使没有使用同步,仍然有可能读取到字段的最新值),内存模型仅仅是允许这种失效发生而已。因此,即使多个线程之间没有使用同步,也不保证一定会发生内存可见性问题(指线程读取到过期的值),java内存模型仅仅是允许内存可见性问题发生而已。在很多当前的JVM实现和java执行平台中,甚至是在那些使用多处理器的JVM和平台中,也很少出现内存可见性问题。共享同一个CPU的多个线程使用公共的缓存,缺少强大的编译器优化,以及存在强缓存一致性的硬件,这些都会使线程更新后的值能够立即在多线程之间传递。这使得测试基于内存可见性的错误是不切实际的,因为这样的错误极难发生。或者这种错误仅仅在某个你没有使用过的平台上发生,或仅在未来的某个平台上发生。这些类似的解释对于多线程之间的内存可见性问题来说非常普遍。没有同步的并发程序会出现很多问题,包括内存一致性问题。

原文

Visibility
Changes to fields made by one thread are guaranteed to be visible to other threads only under the following conditions:
A writing thread releases a synchronization lock and a reading thread subsequently acquires that same synchronization lock.
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.

Note the double meaning of synchronized: it deals with locks that permit higher-level synchronization protocols, while at the same time dealing with the memory system (sometimes via low-level memory barrier machine instructions) to keep value representations in synch across threads. This reflects one way in which concurrent programming bears more similarity to distributed programming than to sequential programming. The latter sense of synchronized may be viewed as a mechanism by which a method running in one thread indicates that it is willing to send and/or receive changes to variables to and from methods running in other threads. From this point of view, using locks and passing messages might be seen merely as syntactic variants of each other.

If a field is declared as volatile, any value written to it is flushed and made visible by the writer thread before the writer thread performs any further memory operation (i.e., for the purposes at hand it is flushed immediately). Reader threads must reload the values of volatile fields upon each access.

The first time a thread accesses a field of an object, it sees either the initial value of the field or a value since written by some other thread.
Among other consequences, it is bad practice to make available the reference to an incompletely constructed object (see ?.1.2). It can also be risky to start new threads inside a constructor, especially in a class that may be subclassed. Thread.start has the same memory effects as a lock release by the thread calling start, followed by a lock acquire by the started thread. If a Runnable superclass invokes new Thread(this).start() before subclass constructors execute, then the object might not be fully initialized when the run method executes. Similarly, if you create and start a new thread T and then create an object X used by thread T, you cannot be sure that the fields of X will be visible to T unless you employ synchronization surrounding all references to object X. Or, when applicable, you can create X before starting T.

As a thread terminates, all written variables are flushed to main memory. For example, if one thread synchronizes on the termination of another thread using Thread.join, then it is guaranteed to see the effects made by that thread (see ?.3.2).
Note that visibility problems never arise when passing references to objects across methods in the same thread.
The memory model guarantees that, given the eventual occurrence of the above operations, a particular update to a particular field made by one thread will eventually be visible to another. But eventually can be an arbitrarily long time. Long stretches of code in threads that use no synchronization can be hopelessly out of synch with other threads with respect to values of fields. In particular, it is always wrong to write loops waiting for values written by other threads unless the fields are volatile or accessed via synchronization (see ?.2.6).

The model also allows inconsistent visibility in the absence of synchronization. For example, it is possible to obtain a fresh value for one field of an object, but a stale value for another. Similarly, it is possible to read a fresh, updated value of a reference variable, but a stale value of one of the fields of the object now being referenced.

However, the rules do not require visibility failures across threads, they merely allow these failures to occur. This is one aspect of the fact that not using synchronization in multithreaded code doesn’t guarantee safety violations, it just allows them. On most current JVM implementations and platforms, even those employing multiple processors, detectable visibility failures rarely occur. The use of common caches across threads sharing a CPU, the lack of aggressive compiler-based optimizations, and the presence of strong cache consistency hardware often cause values to act as if they propagate immediately among threads. This makes testing for freedom from visibility-based errors impractical, since such errors might occur extremely rarely, or only on platforms you do not have access to, or only on those that have not even been built yet. These same comments apply to multithreaded safety failures more generally. Concurrent programs that do not use synchronization fail for many reasons, including memory consistency problems. 

目录
相关文章
|
12月前
|
安全 Java 数据库连接
一把锁的两种承诺:synchronized如何同时保证互斥与内存可见性?
临界区指多线程中访问共享资源的代码段,需通过互斥机制防止数据不一致与竞态条件。Java用`synchronized`实现同步,保证同一时刻仅一个线程执行临界区代码,并借助happens-before规则确保内存可见性与操作顺序,从而保障线程安全。
445 11
|
12月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
494 4
|
12月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
1348 3
|
缓存 监控 Kubernetes
Java虚拟机内存溢出(Java Heap Space)问题处理方案
综上所述, 解决Java Heap Space溢出需从多角度综合施策; 包括但不限于配置调整、代码审查与优化以及系统设计层面改进; 同样也不能忽视运行期监控与预警设置之重要性; 及早发现潜在风险点并采取相应补救手段至关重要.
1144 17
|
存储 监控 算法
Java垃圾回收机制(GC)与内存模型
本文主要讲述JVM的内存模型和基本调优机制。
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
边缘计算 算法 Java
Java 绿色计算与性能优化:从内存管理到能耗降低的全方位优化策略与实践技巧
本文探讨了Java绿色计算与性能优化的技术方案和应用实例。文章从JVM调优(包括垃圾回收器选择、内存管理和并发优化)、代码优化(数据结构选择、对象创建和I/O操作优化)等方面提出优化策略,并结合电商平台、社交平台和智能工厂的实际案例,展示了通过Java新特性提升性能、降低能耗的显著效果。最终指出,综合运用这些优化方法不仅能提高系统性能,还能实现绿色计算目标,为企业节省成本并符合环保要求。
427 0
|
监控 Kubernetes Java
最新技术栈驱动的 Java 绿色计算与性能优化实操指南涵盖内存优化与能效提升实战技巧
本文介绍了基于Java 24+技术栈的绿色计算与性能优化实操指南。主要内容包括:1)JVM调优,如分代ZGC配置和结构化并发优化;2)代码级优化,包括向量API加速数据处理和零拷贝I/O;3)容器化环境优化,如K8s资源匹配和节能模式配置;4)监控分析工具使用。通过实践表明,这些优化能显著提升性能(响应时间降低40-60%)同时降低资源消耗(内存减少30-50%,CPU降低20-40%)和能耗(服务器功耗减少15-35%)。建议采用渐进式优化策略。
735 2
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
714 0

热门文章

最新文章