非最终字段的同步

时间:2011-08-02 10:44:16

标签: java multithreading synchronized

每次在非最终类字段上同步时都会显示警告。这是代码:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

所以我按照以下方式更改了编码。

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

我不确定上面的代码是在非final类字段上同步的正确方法。如何同步非最终字段?

8 个答案:

答案 0 :(得分:112)

首先,我鼓励您真正努力在更高级别的抽象上处理并发问题,即使用来自java.util.concurrent的类来解决它,例如ExecutorServices,Callables,Futures等。

话虽如此,没有任何错误在非最终字段本身同步。您只需要记住如果对象引用发生更改,则相同的代码段可以并行运行。即,如果一个线程在同步块中运行代码并且有人调用setO(...),则另一个线程可以同时在同一实例上运行相同的同步块。< / p>

同步您需要独占访问的对象(或者更好的是,专用于保护它的对象)。

答案 1 :(得分:41)

这真的不是一个好主意 - 因为您的同步块不再以一致的方式真正同步。

假设同步块是为了确保一次只有一个线程访问某些共享数据,请考虑:

  • 线程1进入同步块。是的 - 它可以独家访问共享数据......
  • 线程2调用setO()
  • 线程3(或仍为2 ...)进入同步块。伊克!它认为它拥有对共享数据的独占访问权限,但是线程1仍在继续使用它......

为什么希望发生这种情况?也许有一些非常特殊的情况才有意义......但你必须向我提供一个特定的用例(以及减轻我上面给出的那种情况的方法)在我对它感到满意之前。

答案 2 :(得分:11)

我同意John的评论之一:您必须 始终 在访问非最终变量时使用最终锁定虚拟对象,以防止变量引用更改时出现不一致。因此,在任何情况下,作为第一个经验法则:

规则#1:如果某个字段是非最终字段,请始终使用(私人)最终锁定虚拟字段。

原因#1:您持有锁并自行更改变量的引用。在同步锁之外等待的另一个线程将能够进入受保护的块。

原因#2:您持有锁,另一个线程更改变量的引用。结果是一样的:另一个线程可以进入受保护的块。

但是当使用最终锁定假人时,会出现 另一个问题 :您可能会收到错误的数据,因为您的非最终对象只会在调用时与RAM同步同步(对象)。所以,作为第二个经验法则:

规则#2:当锁定非最终对象时,您始终需要同时执行这两项操作:为了RAM同步,使用最终锁定虚拟对象和非最终对象的锁定。(唯一的选择是将对象的所有字段声明为volatile!)

这些锁也称为“嵌套锁”。 请注意,您必须始终以相同的顺序调用它们,否则您将获得死锁

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

正如你所看到的,我直接在同一行上写了两个锁,因为它们总是在一起。像这样,你甚至可以做10个嵌套锁:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

请注意,如果您只是通过其他线程获取synchronized (LOCK3)之类的内部锁,则此代码不会中断。但如果你在另一个线程中调用这样的东西,它会破坏:

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

处理非最终字段时,这种嵌套锁只有一种解决方法:

规则#2 - 替代方案:将对象的所有字段声明为volatile。(我不会在此讨论这样做的缺点,例如,即使对于x级缓存中的任何存储也是如此读,aso。)

因此,aioobe非常正确:只需使用java.util.concurrent。或者开始了解有关同步的所有内容,并使用嵌套锁自行完成。 ;)

有关非最终字段同步的详细信息,请查看我的测试用例:https://stackoverflow.com/a/21460055/2012947

有关RAM和缓存需要同步的更多详细信息,请查看此处:https://stackoverflow.com/a/21409975/2012947

答案 3 :(得分:2)

我在这里找不到正确答案,也就是说,这样做完全没问题。

我甚至不确定为什么这是一个警告,它没有任何问题。 JVM确保您在读取值时返回某些有效对象(或null),并且可以在任何对象上进行同步。

如果您计划在使用锁时实际更改锁(而不是在开始使用锁之前将其从init方法更改),则必须使您计划更改的变量volatile 。然后你需要做的就是同步两者旧对象和新对象,你可以安全地改变值

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

有。这并不复杂,用锁(互斥锁)编写代码实际上非常简单。编写没有它们的代码(锁定免费代码)是很难的。

答案 4 :(得分:2)

编辑:所以这个解决方案(由Jon Skeet建议)可能存在实施&#34; synchronized(object){}&#34;的原子性问题。而对象引用正在改变。我单独询问并且根据埃里克森先生的说法,它不是线程安全的 - 请参阅:Is entering synchronized block atomic?。所以把它作为例子如何不做 - 链接为什么;)

如果synchronized()是原子的,请参阅代码如何工作:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}

答案 5 :(得分:2)

AtomicReference适合您的要求。

关于atomic包的java文档:

  

一个小型工具包,支持对单个变量进行无锁线程安全编程。本质上,此包中的类将volatile值,字段和数组元素的概念扩展为也提供表单的原子条件更新操作的那些:

boolean compareAndSet(expectedValue, updateValue);

示例代码:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

在上面的示例中,您将String替换为您自己的Object

相关的SE问题:

When to use AtomicReference in Java?

答案 6 :(得分:1)

如果oX实例的生命周期内永远不会发生变化,则无论是否涉及同步,第二个版本的样式都会更好。

现在,如果不知道该课程中还有什么其他内容,第一个版本是否有任何问题是不可能回答的。我倾向于同意编译器看起来容易出错(我不会重复其他人所说的)。

答案 7 :(得分:1)

添加我的两分钱:当我使用通过设计器实例化的组件时,我收到了这个警告,所以它的字段实际上不是最终的,因为构造函数不能接受参数。换句话说,我有一个没有最终关键字的准最终字段。

我认为这就是为什么它只是警告:你可能做错了什么,但也可能是对的。