Java:引用同步对象需要volatile / final吗?
这似乎是一个非常基本的问题,但我找不到明确的确认。
假设我有一个正确同步的类:
public class SyncClass { private int field; public synchronized void doSomething() { field = field * 2; } public synchronized void doSomethingElse() { field = field * 3; } }
如果我需要引用该类的实例,在线程之间共享, 我仍然需要声明该实例是volatile还是final ,我是对的吗? 如:
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(); } }
Final或volatile是强制性的, MyClass
同步对其自己成员的访问这一事实并不豁免确保在线程之间共享引用。 是对的吗?
与 Java中的volatile和synchronized之间的 差异的 差异
1-引用的问题是关于同步和volatile作为替代,对于相同的字段/变量,我的问题是如何正确使用已经正确同步的类(即已选择同步),考虑调用者需要考虑的含义,可能在已经同步的类的引用上使用volatile / final。
2-换句话说,提到的问题/答案是关于锁定/挥发相同的对象,我的问题是:我怎样才能确定不同的线程实际上看到相同的对象? 在锁定/访问它之前。
当引用问题的第一个答案明确指向易失性引用时,它是关于不同步的不可变对象。 第二个答案限于原始类型。 我发现它们很有用(见下文),但还不够完整,不足以对我在这里给出的案件表示怀疑。
3-所提到的答案对于一个非常开放的问题是非常抽象和学术性的解释,根本没有代码; 正如我在介绍中所说,我需要明确确认实际代码,引用一个特定的,虽然很常见的问题。 当然,它们是相关的,但正如教科书与特定问题有关。 (我在打开这个问题之前实际上已经阅读了它,并且发现它很有用,但我仍然需要讨论一个特定的应用程序。)如果教科书解决了所有人可能已经应用它们的问题/疑问,我们可能根本不需要stackoverflow。
考虑到,在multithreading中,你不能“只是尝试一下”,你需要一个正确的理解并确保细节,因为竞争条件可能会发生一千次,然后在一千+一次的时间里出现可怕的错误。
是的,你是对的。 您还必须访问变量也是线程安全的。 您可以通过使其成为final
或volatile
来执行此操作,或者确保所有线程在同步块内再次访问该变量。 如果你不这样做,可能是一个线程’看到’已经是变量的新值,但另一个线程可能仍然’看”,例如。
因此,关于您的示例,当线程访问mySharedObject
变量时,有时可能会出现NullPointerException
。 但这可能只发生在具有多个缓存的多核机器上。
Java内存模型
这里的要点是Java内存模型。 它声明一个线程只保证看到另一个线程的内存更新,如果该更新在所谓的发生在之前的关系中读取该状态之前发生 。 可以通过使用final
, volatile
或synchronized
来强制执行之前发生的关系。 如果不使用任何这些结构,则任何其他线程都不会保证一个线程的变量赋值可见。
您可以认为线程在概念上具有本地缓存,并且只要您不强制执行multithreading的缓存同步,线程就会读取和写入其本地缓存。 这可能导致两个线程在从同一字段读取时看到完全不同的值的情况。
请注意,还有一些其他方法可以强制实现内存更改的可见性,例如,使用静态初始化程序。 此外,新创建的线程始终可以看到其父线程的当前内存,而无需进一步同步。 因此,您的示例甚至可以在没有任何同步的情况下工作,因为在初始化字段之后,会以某种方式强制创建线程。 然而,依赖于这样一个微妙的事实是非常危险的,并且如果您稍后重构代码而不考虑这些细节,则很容易破坏。 在Java语言规范中描述了关于先发生关系的更多细节(但很难理解)。
如果我需要对该类的实例进行refence,在线程之间共享,我仍然需要声明该实例是volatile还是final,我是对的吗?
是的,你是对的。 在这种情况下,您有两个共享变量:
private int field
private SyncClass mySharedObject
由于您定义了SyncClass
的方式,对SyncClass
任何引用SyncClass
将为您提供该SyncClass
最新值。
如果您没有正确地同步对mySharedObject
的访问(非最终的,非易失性)字段并且您更改mySharedObject
的值,则可能会得到一个过期的mySharedObject
。
这完全取决于如何共享此变量的上下文。
这是一个简单的例子,它很好:
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之前,那么hb(x,y) 。
在启动线程中的任何操作之前发生对线程的
start()
调用。
换句话说,如果对mySharedObject
的赋值发生在启动新线程的同一线程上,则新线程被强制要求查看赋值,而不管同步如何。
但是,如果您希望,例如,可以在与调用doSomething
的线程不同的线程上调用init
,那么您可能会遇到竞争条件。
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
。 哪个是合适的。
要理解以下两点需要注意的事项:
- 竞争您的参考变量与成员字段没有概念上的区别。
- 共享参考变量需要仔细处理安全发布
它不是必须使用它们中的任何一个,但如果你想编写适当的multithreading代码,你应该知道它们。
最后
final
表示你不能再次重新初始化该变量,所以当你说
final SyncClass mySharedObject = new SyncClass();
你不能在下面的代码的其他部分再次初始化mySharedObject
mySharedObject = new SyncClass(); // throws compiler error
即使您无法将mySharedObject
引用重新分配给其他对象,您仍然可以通过调用其上的方法来更新它的状态(字段计数器变量),因为field
不是最终的。
同步和volatile只是构造,以确保一个线程对共享可变对象(在这种情况下更新field
计数器)的任何更改对所有其他线程可见。
同步
synchronized
方法意味着任何尝试调用该方法的线程都应该获取对定义该方法的对象的锁定。
所以在你的情况下,如果thread-1试图执行mySharedObject.doSomething()
,它将获取对mySharedObject
锁定,而thread-2必须等到thread-1释放锁定同一对象才能执行mySharedObject.doSomethingElse()
即在任何给定的时间点使用同步,只有一个线程将更新对象的状态。 在方法结束时,就在释放锁之前,所有由thread-1完成的更改都会刷新到主内存,以便thread-2可以处理最近的状态。
挥发物
另一方面, volatile
确保所有线程的读/写可见性。 对volatile变量的任何读写操作总是刷新到主存储器。
如果SyncClass
的field
变量是易失性的,那么thread-1可以看到任何类似field++
by thread-1的更新,但我不确定它是如何应用于对象引用的。
由于volatile仅保证可见性而非primefaces性,因此thread-1和thread-2可能同时尝试更新field
计数器,并且最终更新的值可能不正确。