对象的成员不需要 volatile 而只需要原始成员吗?
Is volatile not needed for objects' members but only on primitive members?
我的代码是
package threadrelated;
import threadrelated.lockrelated.MyNonBlockingQueue;
public class VolatileTester extends Thread {
MyNonBlockingQueue mbq ;
public static void main(String[] args) throws InterruptedException {
VolatileTester vt = new VolatileTester();
vt.mbq = new MyNonBlockingQueue(10);
System.out.println(Thread.currentThread().getName()+" "+vt.mbq);
Thread t1 = new Thread(vt,"First");
Thread t2 = new Thread(vt,"Secondz");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Thread.currentThread().getName()+" "+vt.mbq);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" before "+mbq);
mbq = new MyNonBlockingQueue(20);
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" after "+mbq);
}
}
输出是
main threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz before threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
First before threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz after threadrelated.lockrelated.MyNonBlockingQueue@7100650c
First after threadrelated.lockrelated.MyNonBlockingQueue@7100650c
main threadrelated.lockrelated.MyNonBlockingQueue@7100650c
说明当First线程将成员变量赋值给new对象时,同样对其他线程可见。即使 "mbq" 没有被声明为 volatile.
我使用断点来尝试不同的操作顺序。但我的观察是一个线程可以立即看到另一个线程的影响。
class 对象成员不需要 volatile 吗?它们总是与主存同步吗?仅原始成员变量(int、long、boolean 等?)
才需要 Volatile
它对于引用和基元一样都是必需的。您的输出没有显示可见性问题这一事实并不能证明不存在。一般来说,很难证明不存在并发错误。但这里有一个简单的反证明,显示 volatile
:
的必要性
public class Test {
static volatile Object ref;
public static void main(String[] args) {
// spin until ref is updated
new Thread(() -> {
while (ref == null);
System.out.println("done");
}).start();
// wait a second, then update ref
new Thread(() -> {
try { Thread.sleep(1000); } catch (Exception e) {}
ref = new Object();
}).start();
}
}
该程序运行一秒钟,然后打印 "done"。删除 volatile
并且它不会终止,因为第一个线程永远不会看到更新的 ref
值。 (免责声明:与任何并发测试一样,结果可能会有所不同。)
一般来说,您此时没有看到某事发生,并不意味着以后不会发生。 特别是 并发代码。您可以使用 jcstress
库,它会尝试向您展示您的代码可能存在的问题。
volatile 变量不同于其他变量,因为它在 CPU 级别引入了 memory barries
。没有这些,就无法保证 when 或 what 线程看到来自另一个线程的更新。用简单的话来说,这些被称为 StoreLoad|StoreStore|LoadLoad|LoadStore
.
所以使用 volatile 保证了可见性效果,实际上它是你唯一可以依赖的可见性效果(除了使用 Unsafe
和 locks/synchronized 关键字)。您还必须考虑到您正在针对 一个特定的 CPU 进行测试,很可能是 x86
。但是对于不同的 CPU(比如说 ARM),事情会崩溃得更快。
您的代码不是对 volatile 的有用测试。无论是否使用 volatile,它都可以正常工作,这不是偶然的,而是根据规范。
包含的代码可以更好地测试 volatile 关键字,因为该字段是否可变会产生影响。如果您采用该代码,使该字段成为非易失性的,并在循环中插入一个 println,那么您应该会看到从另一个线程设置的该字段的值是可见的。这是因为 println 在打印流上同步,插入内存屏障。
在您的示例中还有另外两件事插入了这些障碍,导致更新跨线程可见。
Java Language Specification 列出了这些先行关系:
A call to start() on a thread happens-before any actions in the started thread.
All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
这意味着您发布的代码中不需要 volatile。新启动的线程可以看到从 main 传入的队列,一旦线程完成,main 就可以看到对队列的引用。在线程启动时间和 println 执行时间之间有一个 window,字段的内容可能是陈旧的,但代码中没有任何内容正在测试它。
但是不,说引用不需要 volatile 是不准确的。 volatile 存在先行关系:
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
规范不区分包含引用的字段和包含基元的字段,该规则适用于两者。这又回到了 Java 是按值调用,引用是值。
我的代码是
package threadrelated;
import threadrelated.lockrelated.MyNonBlockingQueue;
public class VolatileTester extends Thread {
MyNonBlockingQueue mbq ;
public static void main(String[] args) throws InterruptedException {
VolatileTester vt = new VolatileTester();
vt.mbq = new MyNonBlockingQueue(10);
System.out.println(Thread.currentThread().getName()+" "+vt.mbq);
Thread t1 = new Thread(vt,"First");
Thread t2 = new Thread(vt,"Secondz");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Thread.currentThread().getName()+" "+vt.mbq);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" before "+mbq);
mbq = new MyNonBlockingQueue(20);
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" after "+mbq);
}
}
输出是
main threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz before threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
First before threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz after threadrelated.lockrelated.MyNonBlockingQueue@7100650c
First after threadrelated.lockrelated.MyNonBlockingQueue@7100650c
main threadrelated.lockrelated.MyNonBlockingQueue@7100650c
说明当First线程将成员变量赋值给new对象时,同样对其他线程可见。即使 "mbq" 没有被声明为 volatile.
我使用断点来尝试不同的操作顺序。但我的观察是一个线程可以立即看到另一个线程的影响。
class 对象成员不需要 volatile 吗?它们总是与主存同步吗?仅原始成员变量(int、long、boolean 等?)
才需要 Volatile它对于引用和基元一样都是必需的。您的输出没有显示可见性问题这一事实并不能证明不存在。一般来说,很难证明不存在并发错误。但这里有一个简单的反证明,显示 volatile
:
public class Test {
static volatile Object ref;
public static void main(String[] args) {
// spin until ref is updated
new Thread(() -> {
while (ref == null);
System.out.println("done");
}).start();
// wait a second, then update ref
new Thread(() -> {
try { Thread.sleep(1000); } catch (Exception e) {}
ref = new Object();
}).start();
}
}
该程序运行一秒钟,然后打印 "done"。删除 volatile
并且它不会终止,因为第一个线程永远不会看到更新的 ref
值。 (免责声明:与任何并发测试一样,结果可能会有所不同。)
一般来说,您此时没有看到某事发生,并不意味着以后不会发生。 特别是 并发代码。您可以使用 jcstress
库,它会尝试向您展示您的代码可能存在的问题。
volatile 变量不同于其他变量,因为它在 CPU 级别引入了 memory barries
。没有这些,就无法保证 when 或 what 线程看到来自另一个线程的更新。用简单的话来说,这些被称为 StoreLoad|StoreStore|LoadLoad|LoadStore
.
所以使用 volatile 保证了可见性效果,实际上它是你唯一可以依赖的可见性效果(除了使用 Unsafe
和 locks/synchronized 关键字)。您还必须考虑到您正在针对 一个特定的 CPU 进行测试,很可能是 x86
。但是对于不同的 CPU(比如说 ARM),事情会崩溃得更快。
您的代码不是对 volatile 的有用测试。无论是否使用 volatile,它都可以正常工作,这不是偶然的,而是根据规范。
在您的示例中还有另外两件事插入了这些障碍,导致更新跨线程可见。 Java Language Specification 列出了这些先行关系:
A call to start() on a thread happens-before any actions in the started thread.
All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
这意味着您发布的代码中不需要 volatile。新启动的线程可以看到从 main 传入的队列,一旦线程完成,main 就可以看到对队列的引用。在线程启动时间和 println 执行时间之间有一个 window,字段的内容可能是陈旧的,但代码中没有任何内容正在测试它。
但是不,说引用不需要 volatile 是不准确的。 volatile 存在先行关系:
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
规范不区分包含引用的字段和包含基元的字段,该规则适用于两者。这又回到了 Java 是按值调用,引用是值。