对象的共享
可见性
通常,无法保证读取线程会及时看到另一个线程写入的值,甚至根本无法看到。为了保证跨线程内存写入的可见性,必须使用同步。
例子:
虽然 NoVisibility 将打印 42 看起来很明显,但实际上它可能会打印零,或者根本不会终止!因为它没有使用足够的同步,所以不能保证主线程写入的 ready 和 number 的值对读取线程可见。这里不能保证一个线程中的操作将按照程序给定的顺序执行。
每当跨线程共享数据时,始终使用合适的同步。
非原子的64位操作
当一个线程在没有同步的情况下读取一个变量时,它可能会看到一个过时的值,但至少它看到的是某个线程实际放置在那里的值,而不是某个随机值。这种安全保证被称为最低安全性。
最低安全性适用于所有变量,但有一个例外:未声明为 volatile 的 64 位数字变量(double 和 long)。Java 内存模型要求 fetch 和 store 操作是原子的,但是对于nonvolatile long 和 double 变量,允许 JVM 将 64 位读取或写入视为两个单独的 32 位操作。
加锁与可见性
加锁不仅仅是互斥;它还与内存可见性有关。为确保所有线程都能看到共享可变变量的最新值,读写线程必须在公共锁上同步。
Volatile变量
Java 语言还提供了另一种较弱的同步形式, volatile 变量,以确保对变量的更新可预测地传播到其他线程。 当一个字段声明为 volatile 时,编译器和运行时 注意到这个变量是共享的,对它的操作不应该 与其他内存操作一起重新排序。 volatile变量不缓存在 寄存器或高速缓存中,它们对其他处理器是隐藏的,所以读取一个 volatile 变量总是返回任何线程的最新写入。
但是,我们不建议过分依赖 volatile 变量来提高可见性;依赖易失性变量来实现任意状态可见性的代码比使用锁定的代码更脆弱且更难理解。
volatile 变量很方便,但也有局限性。 volatile 变量最常见的用途是作为完成、中断或状态标志。volatile 的语义不足以使增量操作 (count++) 成为原子操作,除非您可以保证该变量仅从单个线程写入。
加锁可以保证可见性和原子性; volatile 变量只能保证可见性。
仅当满足以下所有条件时才可以使用 volatile 变量:
• 对变量的写入不依赖于其当前值,或者您可以确保只有一个线程更新该值;
• 该变量不参与与其他状态变量的不变量;
• 在访问变量时不需要出于任何其他原因进行锁定。
发布和逃逸
发布对象意味着使其可供当前范围之外的代码使用。
在不应该发布的情况下发布的对象被认为是逃逸对象。
在构造过程中不要允许 this 引用逃逸。
在构造过程中让 this 引用逃逸的一个常见错误是从构造函数启动线程。
如果你想在构造函数中注册一个事件侦听器或启动一个线程,你可以通过使用私有构造函数和公共工厂方法来避免不正确的构造。