假设我们有一个非常简单的Java类MyClass
。
public class MyClass {
private int number;
public MyClass(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
有三种方法可以构造具有某种状态的线程安全Java类:
使其真正不可变
public class MyClass { private final int number; public MyClass(int number) { this.number = number; } public int getNumber() { return number; } }
将字段
number
设为volatile
。public class MyClass { private volatile int number; public MyClass(int number) { this.number = number; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } }
使用
synchronized
块。在实践中的Java并发第4.3.5章中描述了这种方法的经典版本。有趣的是,这本书的勘误表中提到的例子中有一个错误。public class MyClass { private int number; public MyClass(int number) { setNumber(number); } public synchronized int getNumber() { return number; } public synchronized void setNumber(int number) { this.number = number; } }
还有一个事实应该添加到讨论的背景中。在多线程环境中,JVM可以自由地对synchronized
块之外的指令进行重新排序,以保留逻辑序列,并且发生在JVM指定的关系之前。它可能会导致将尚未正确构造的对象发布到另一个线程。
关于第三个案例,我有几个问题。
它是否等效于以下代码:
public class MyClass { private int number; public MyClass(int number) { synchronized (this){ this.number = number; } } public synchronized int getNumber() { return number; } public synchronized void setNumber(int number) { this.number = number; } }
在第三种情况下,是否会阻止重新排序,或者JVM有可能重新排序内部结构,从而在字段
number
中发布具有默认值的对象?如果第二个问题的答案是肯定的,那么我还有一个问题。
public class MyClass { private int number; public MyClass(int number) { synchronized (new Object()){ this.number = number; } } public synchronized int getNumber() { return number; } public synchronized void setNumber(int number) { this.number = number; } }
这个奇怪的synchronized (new Object())
看起来应该是为了防止重新排序的效果。它行得通吗?
需要明确的是,所有这些例子都没有任何实际应用。我只是好奇多线程的细微差别。
synchronized(new Object())
不会做任何事情,因为同步只在您同步的对象上。因此,如果线程A在oneObject
上同步,线程B在anotherObject
上同步,那么它们之间之前不会发生任何事情。因为我们可以知道,在您创建的new Object()
上,没有其他线程会同步,所以这不会在任何其他线程之间建立以前发生过的事情。
关于构造函数中的synchronzied
,如果您的对象安全地发布到另一个线程,则不需要它;如果不是,你可能会遇到麻烦。我在并发兴趣列表上问了这个问题,得到了一个有趣的线程。请特别参阅这封电子邮件,它指出,即使您的构造函数同步,在没有安全发布的情况下,另一个线程也可能在您的字段中看到默认值,而这封电子邮件(imho)将整个事情联系在一起。
在问题#3中,synchronized(new Object())
是一个无操作,不会阻止任何操作。编译器可以确定没有其他线程可能在该对象上同步(因为没有其他线程可以访问该对象。)这是Brian Goetz的论文"Java理论与实践:Mustang中的同步优化"中的一个明确例子。
即使您确实需要在构造函数中同步,即使您的synchronized(new Object())
块是有用的(即,您在不同的长期对象上同步),由于您的其他方法在this
上同步,如果您不在同一变量上同步,您也会出现可见性问题。也就是说,您确实希望您的构造函数也使用synchronized(this)
。
旁白:
在this
上同步被认为是一种糟糕的形式。相反,在一些私有的最终字段上同步。调用者可能会在您的对象上同步,这可能会导致死锁。考虑以下内容:
public class Foo
{
private int value;
public synchronized int getValue() { return value; }
public synchronized void setValue(int value) { this.value = value; }
}
public class Bar
{
public static void deadlock()
{
final Foo foo = new Foo();
synchronized(foo)
{
Thread t = new Thread() { public void run() { foo.setValue(1); } };
t.start();
t.join();
}
}
}
对于Foo
类的调用方来说,这不会导致死锁。最好将您的锁定语义保持为类的内部和私有语义。