这似乎是一个非常基本的问题,但我找不到明确的确认。
假设我有一个正确同步的课程:
public class SyncClass {
private int field;
public synchronized void doSomething() {
field = field * 2;
}
public synchronized void doSomethingElse() {
field = field * 3;
}
}
如果我需要对该类的实例进行引用,在线程之间共享,我仍然需要声明该实例是volatile还是最终,我是对的?如:
public class MainClass { // previously OuterClass
public static void main(String [ ] args) {
final SyncClass mySharedObject = new SyncClass();
new Thread(new Runnable() {
public void run() {
mySharedObject.doSomething();
}
}).start();
new Thread(new Runnable() {
public void run() {
mySharedObject.doSomethingElse();
}
}).start();
}
}
或者,如果mySharedObject
不能是最终的,因为它的实例化取决于一些其他条件(与GUI的交互,来自套接字的信息等),事先不知道:
public class MainClass { // previously OuterClass
public static void main(String [ ] args) {
volatile SyncClass mySharedObject;
Thread initThread = new Thread(new Runnable() {
public void run() {
// just to represent that there are cases in which
// mySharedObject cannot be final
// [...]
// interaction with GUI, info from socket, etc.
// on which instantation of mySharedObject depends
if(whateverInfo)
mySharedObject = new SyncClass();
else
mySharedObject = new SyncClass() {
public void someOtherThing() {
// ...
}
}
}
});
initThread.start();
// This guarantees mySharedObject has been instantied in the
// past, but that still happened in ANOTHER thread
initThread.join();
new Thread(new Runnable() {
public void run() {
mySharedObject.doSomething();
}
}).start();
new Thread(new Runnable() {
public void run() {
mySharedObject.doSomethingElse();
}
}).start();
}
}
最终或易失性是强制性的,MyClass
同步对其自己成员的访问这一事实,并不豁免确保在线程之间共享引用。是吗?
Difference between volatile and synchronized in Java
的差异1-引用的问题是关于同步和易失性作为替代,对于相同的字段/变量,我的问题是如何正确使用已经正确同步的类(即已选择同步),考虑需要考虑的含义由调用者,可能在已经同步的类的引用上使用volatile / final。
2-换句话说,提到的问题/答案是关于锁定/挥发相同的对象,我的问题是:我怎样才能确定不同的线程实际上看到相同的对象?在锁定/访问它之前。
当引用问题的第一个答案明确提到易失性引用时,它是关于不可变对象而没有同步。第二个答案仅限于原始类型。 我发现它们很有用(见下文),但还不够完整,不能对我在这里提供的案件表示怀疑。
3-所提到的答案对于一个非常开放的问题是非常抽象和学术性的解释,完全没有代码;正如我在介绍中所说,我需要明确确认实际代码,引用一个特定的,虽然很常见的问题。当然,它们是相关的,但正如教科书与特定问题有关。 (我实际上是在打开这个问题之前阅读它,并发现它很有用,但我仍然需要讨论一个特定的应用程序。)如果教科书解决了人们可能已经应用它们的所有问题/疑问,我们可能根本不需要stackoverflow。
考虑到,在多线程中,你不能“只是尝试一下”,你需要一个正确的理解并确保细节,因为竞争条件可能会发生一千次,然后在一千+一次的时间里出现可怕的错误。 / p>
答案 0 :(得分:6)
是的,你是对的。您还必须访问变量也是线程安全的。您可以通过将其设置为final
或volatile
来执行此操作,或者确保所有线程在同步块内再次访问该变量。如果你不这样做,可能是一个线程看到'已经是变量的新值,但另一个线程可能仍然会看到'例如null
。
因此,关于您的示例,当线程访问NullPointerException
变量时,有时可能会获得mySharedObject
。但这可能只发生在具有多个缓存的多核机器上。
Java内存模型
这里的要点是Java内存模型。它声明一个线程只能保证看到另一个线程的内存更新,如果在所谓的发生在关系之前读取该状态之前发生了更新。可以使用final
,volatile
或synchronized
强制执行之前发生的关系。如果您不使用任何这些结构,则任何其他线程都不会保证一个线程的变量赋值可见。
您可以认为线程在概念上具有本地缓存,并且只要您不强制执行多线程的缓存同步,线程就会读取和写入其本地缓存。这可能会导致两个线程在从同一个字段读取时看到完全不同的值。
请注意,还有一些其他方法可以强制实现内存更改的可见性,例如,使用静态初始化程序。此外,新创建的线程始终可以看到其父线程的当前内存,而无需进一步同步。因此,您的示例甚至可以在没有任何同步的情况下工作,因为在初始化字段之后,以某种方式强制创建线程。 然而依赖于这样一个微妙的事实是非常危险的,如果你以后重构你的代码而没有考虑到这些细节,那么很容易破解。在Java Language Specification中描述了关于先发生关系的更多细节(但很难理解)。
答案 1 :(得分:2)
如果我需要对该类的实例进行反对,在线程之间共享,我仍然需要声明该实例是volatile还是final,我是对的吗?
是的,你是对的。在这种情况下,您有两个共享变量:
private int field
private SyncClass mySharedObject
由于您定义SyncClass
的方式,对SyncClass
的任何引用都会为您提供SyncClass
的最新值。
如果您未正确同步mySharedObject
的访问权限(非最终,非易失性)字段,并且您更改mySharedObject
的值,则可能会获得mySharedObject
这已经过时了。
答案 2 :(得分:1)
这完全取决于如何共享此变量的上下文。
这是一个简单的例子:
class SimpleExample {
private String myData;
public void doSomething() {
myData = "7";
new Thread(() -> {
// REQUIRED to print "7"
// because Thread#start
// mandates happens-before ordering.
System.out.println(myData);
}).start();
}
}
您的例子可能属于这种情况。 17.4.5:
如果 x 和 y 是同一个帖子的操作,而 x 是 y 之前的 y 程序顺序,然后 hb(x,y)。
线程上的
start()
调用发生在已启动线程中的任何操作之前。
换句话说,如果对mySharedObject
的赋值发生在启动新线程的同一线程上,则新线程被强制要求查看赋值而不管同步。
但是,如果您希望,例如,可以在与调用init
的线程不同的线程上调用doSomething
,那么您可能会遇到竞争条件。
public static void main(String[] args) {
final OuterClass myOuter = new OuterClass();
Thread t1 = new Thread( () -> myOuter.init(true) );
Thread t2 = new Thread( () -> myOuter.doSomething() );
t1.start(); // Does t1#run happen before t2#run? No guarantee.
t2.start(); // t2#run could throw NullPointerException.
}
SyncClass
具有同步方法的事实与mySharedObject
引用的保证状态完全无关。读取该引用是在同步块之外执行的。
如有疑问,请使用final
或volatile
。无论哪个合适。
答案 3 :(得分:1)
为了理解,请记住以下两点:
答案 4 :(得分:0)
它不是必须使用它们中的任何一个,但如果你想编写适当的多线程代码,你应该知道它们。
<强>最终强>
final
表示您无法再次重新初始化该变量,因此当您说
final SyncClass mySharedObject = new SyncClass();
您无法在代码的其他部分(例如下面的
)中再次初始化mySharedObject
mySharedObject = new SyncClass(); // throws compiler error
即使您无法将mySharedObject
引用重新分配给其他对象,您仍然可以通过调用其上的方法来更新它的状态(字段计数器变量),因为field
不是最终的
同步和volatile只是构造,以确保所有其他线程都可以看到一个线程对共享可变对象(在这种情况下更新field
计数器)的任何更改。
<强>同步强>
synchronized
方法意味着任何尝试调用该方法的线程都应该获取对定义该方法的对象的锁定。
所以在你的情况下,如果thread-1试图做mySharedObject.doSomething()
,它将获得mySharedObject
上的锁定,而线程2必须等到线程1释放锁定同一个对象为了能够mySharedObject.doSomethingElse()
,即在任何给定的时间点使用同步,只有一个线程将更新对象的状态。在方法结束时,就在释放锁之前,所有由thread-1完成的更改都会刷新到主内存,以便thread-2可以处理最近的状态。
<强>挥发性强>
另一方面, volatile
确保所有线程的读/写可见性。对volatile变量的任何读写操作总是刷新到主存储器。
如果field
内的SyncClass
变量是易变的,则线程1可以看到线程1之类的field++
更新,但我不确定它是如何应用于对象引用。
由于volatile仅保证可见性而非原子性,因此线程1和线程2可能同时尝试更新field
计数器,并且最终更新的值可能不正确。