非最终字段的同步


91

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

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)
     {  
     }  
   }  
 }  

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

Answers:


127

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

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

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


1
我的意思是,如果您在非最终字段上进行同步,应该意识到以下事实:代码段的运行具有对o到达同步块时所引用的对象的独占访问权。如果o引用的对象发生更改,则可以引入另一个线程并执行同步的代码块。
aioobe

42
我不同意您的经验法则-我更喜欢在一个唯一目的是保护其他状态的对象上进行同步。如果您除了对对象进行锁定之外从未对它执行任何操作,请确定没有其他代码可以锁定它。如果您锁定了随后调用其方法的“真实”对象,则该对象也可以在自身上进行同步,这使推理锁定变得更加困难。
乔恩·斯基特

9
正如我在回答说,我觉得我需要有它合理的非常仔细地给我,你为什么会想要做这样的事情。而且,我也不建议在上this进行同步-我建议仅出于锁定目的而在类中创建最终变量,这将阻止其他任何人锁定同一对象。
乔恩·斯基特

1
那是另一个好处,我同意。锁定非最终变量绝对需要谨慎的理由。
aioobe

我不确定有关更改用于同步的对象的内存可见性问题。我认为您会遇到麻烦,即更改对象,然后依靠代码正确查看更改,以使“同一部分代码可以并行运行”。我不确定与内存同步模块中访问的变量相比,内存模型将哪些保证(如果有的话)扩展到了用于锁定的字段的内存可见性。我的经验法则是,如果您对某些内容进行同步,那么它应该是最终的。
Mike Q

47

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

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

  • 线程1进入同步块。是的-它具有对共享数据的独占访问权...
  • 线程2调用setO()
  • 线程3(或仍然是2 ...)进入同步块。ek!它认为它具有对共享数据的独占访问权,但是线程1仍在使用它...

您为什么发生这种情况?也许在某些非常特殊的情况下这是有道理的……但是,在我满意之前,您必须向我展示一个特定的用例(以及减轻我上面给出的那种情况的方法)它。


2
@aioobe:但是,线程1可能仍在运行一些使列表发生变化的代码(并经常引用o),并且在执行过程中,有一部分代码开始对另一个列表进行变化。怎么会是个好主意?我认为我们根本不同意锁定以其他方式触摸的对象是否是个好主意。我希望能够在不知道其他代码在锁定方面有任何了解的情况下对我的代码进行推理。
乔恩·斯基特

2
@Felype:听起来您应该将一个更详细的问题作为一个单独的问题提出-但是,是的,我经常像锁一样创建单独的对象。
乔恩·斯基特

3
@VitBernatik:否。如果线程X开始修改配置,线程Y更改正在同步的变量的值,然后线程Z开始修改配置,那么X和Z都会同时修改配置,这很不好。
乔恩·斯基特

1
简而言之,如果总是将这样的锁定对象声明为final,那会更安全,对吗?
圣安塔里奥州

2
@LinkTheProgrammer:“一个同步的方法同步实例中的每个对象” –不,它不是。事实并非如此,您应该重新了解同步。
乔恩·斯基特

12

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

规则1:如果字段为非最终字段,请始终使用(私有)最终锁定虚拟对象。

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

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

但是,当使用最终锁定虚拟对象时,还有另一个问题:您可能会得到错误的数据,因为非最终对象仅在调用sync(ize)对象时才与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


1
我认为您必须o用synchronized(LOCK)包装setter,以在设置和读取对象之间建立“先于先后”的关系o。我在一个类似的我的问题中讨论这个问题:stackoverflow.com/questions/32852464/…–
Petrakeas

我使用dataObject来同步对dataObject成员的访问。怎么了 如果dataObject开始指向不同的地方,我希望它在新数据上同步,以防止并发线程对其进行修改。有什么问题吗?
哈曼2015年

2

我在这里看不到正确的答案,也就是说,这样做是完全可以的。

我什至不知道为什么要警告,它没有错。JVM确保在读取值时获得一些有效的对象(或null),并且可以在任何对象上进行同步。

如果计划在使用锁时实际更改锁(而不是例如在开始使用锁之前从init方法更改锁),则必须制作要更改的变量volatile。然后,所有你需要做的是在同步两个旧的和新的对象,你可以放心地更改值

public volatile Object lock;

...

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

那里。这并不复杂,实际上,使用锁(互斥锁)编写代码非常容易。在没有它们的情况下编写代码(无锁代码)是很难的。


这可能不起作用。假设o以对O1的引用开始,然后线程T1锁定o(= O1)和O2,并将o设置为O2。同时,线程T2锁定O1,并等待T1对其进行解锁。当它收到锁O1时,会将o设置为O3。在这种情况下,在T1释放O1和T2锁定O1之间,O1对于通过o锁定无效。此时,另一个线程可以使用o(= O2)进行锁定,并在不中断与T2竞争的情况下继续进行。
GPS

2

编辑:因此,该解决方案(由乔恩·斯凯特(Jon Skeet)提出)可能会在对象引用更改时实现“ synchronized(object){}”实现的原子性问题。我另外询问,根据埃里克森先生的说法,这不是线程安全的-请参阅:输入同步块是原子的吗?。因此,以它为例,如何不做-用链接为什么;)

查看代码,如果syncnised()是原子的,它将如何工作:

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();
    }

}

1
可能没问题-但我不知道在内存模型中是否可以保证您同步的值是最近写入的值-我不认为可以原子地“读取并同步”任何保证。为了简单起见,我个人尝试避免在具有其他用途的监视器上进行同步。(通过使用单独的字段,代码变得明显正确,而不必仔细地进行推理。)
Jon Skeet 2015年

@乔恩 谢谢!我听到你的担心。对于这种情况,我同意外部锁可以避免“同步原子性”的问题。因此将是优选的。尽管在某些情况下您想在运行时中引入更多配置,并为不同的线程组共享不同的配置(尽管不是我的情况)。然后,该解决方案可能会变得有趣。我发布了问题stackoverflow.com/questions/29217266/…
synced

2

AtomicReference适合您的要求。

从有关原子包的Java文档中:

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

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问题:

何时在Java中使用AtomicReference?


1

如果o在的实例的生命周期内永不更改,则X无论是否涉及同步,第二个版本都是更好的样式。

现在,如果不知道该类中还有什么其他情况,就无法回答第一个版本是否有问题。我倾向于同意编译器,它看起来确实容易出错(我不会重复别人说的话)。


1

只需加上我的两分钱:当我使用通过设计器实例化的组件时,我曾收到此警告,因此它的字段不能真正是最终的,因为构造函数无法获取参数。换句话说,我有没有final关键字的准决赛场。

我认为这就是警告的原因:您可能做错了什么,但也可能是对的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.