Java中通过同步块实现的线程安全类



假设我们有一个非常简单的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类:

  1. 使其真正不可变

    public class MyClass {
       private final int number;
       public MyClass(int number) {
        this.number = number;
       }
       public int getNumber() {
        return number;
       }
    }
    
  2. 将字段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;
       }
    }
    
  3. 使用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指定的关系之前。它可能会导致将尚未正确构造的对象发布到另一个线程。

关于第三个案例,我有几个问题。

  1. 它是否等效于以下代码:

    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;
       }
    }
    
  2. 在第三种情况下,是否会阻止重新排序,或者JVM有可能重新排序内部结构,从而在字段number中发布具有默认值的对象?

  3. 如果第二个问题的答案是肯定的,那么我还有一个问题。

     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类的调用方来说,这不会导致死锁。最好将您的锁定语义保持为类的内部和私有语义。

最新更新