如何避免非最终字段的同步?

时间:2015-09-29 19:42:03

标签: java multithreading synchronization synchronized final

如果我们有2个类在不同线程下对同一个对象进行操作,并且我们想要避免竞争条件,我们必须使用具有相同监视器的synchronized块,如下例所示:

class A {
    private DataObject mData; // will be used as monitor

    // thread 3
    public setObject(DataObject object) {
       mData = object;
    }

    // thread 1
    void operateOnData() {
        synchronized(mData) {
            mData.doSomething();
            .....
            mData.doSomethingElse();
        }
    }
}

  class B {
    private DataObject mData;  // will be used as monitor

    // thread 3
    public setObject(DataObject object) {
       mData = object;
    }

    // thread 2
    void processData() {
        synchronized(mData) {
            mData.foo();
            ....
            mData.bar();
        }
    }
}

我们将操作的对象将通过调用setObject()进行设置,然后不会更改。我们将使用该对象作为监视器。但是,intelliJ会警告非最终字段的同步。

在这种特殊情况下,非本地字段是否是可接受的解决方案?

上述方法的另一个问题是,在线程3设置后,线程1或线程2不能保证监视器(mData)被观察,因为“发生 - 在“设置和读取显示器之间尚未建立关系之前”。例如,它可以被线程1视为null。我的推测是否正确?

关于可能的解决方案,使DataObject线程安全不是一种选择。在类的构造函数中设置监视器并声明它final可以正常工作。

编辑从语义上讲,所需的互斥与DataObject相关。这就是我不想拥有辅助监视器的原因。一种解决方案是在lock()上添加需要调用的unlock()DataObject方法,然后再进行操作。在内部,他们将使用Lock对象。因此,operateOnData()方法变为:

 void operateOnData() {
     mData.lock()
     mData.doSomething();
     .....
     mData.doSomethingElse();
     mData.unlock();
 }

3 个答案:

答案 0 :(得分:1)

您可以创建一个包装器

class Wrapper
{
    DataObject mData;

    synchronized public setObject(DataObject mData)
    {
        if(this.mData!=null) throw ..."already set"
        this.mData = mData;
    }

    synchronized public void doSomething()
    {
        if(mData==null) throw ..."not set"

        mData.doSomething();
    }

创建包装器对象并将其传递给A和B

class A 
{
    private Wrapper wrapper; // set by constructor

    // thread 1
    operateOnData() 
    {
        wrapper.doSomething();
    }

线程3也有对包装器的引用;它会在setObject()可用时调用。{/ p>

答案 1 :(得分:0)

一个简单的解决方案是定义一个公共静态最终对象用作锁。声明如下:

/**Used to sync access to the {@link #mData} field*/
public static final Object mDataLock = new Object();

然后在程序中同步mDataLock而不是mData。

这非常有用,因为将来有人可能会更改mData,使其值 更改,然后您的代码会出现一系列奇怪的线程错误。

这种同步方法消除了这种可能性。它的成本也很低。

将锁定为静态意味着该类的所有实例共享一个锁。在这种情况下,这似乎是你想要的。

请注意,如果您有许多这些类的实例,这可能会成为瓶颈。由于所有实例现在共享一个锁,因此只有一个实例可以一次更改任何mData。所有其他实例都必须等待。

一般来说,我认为类似于你想要同步的数据的包装器是一种更好的方法,但我认为这样可行。

如果您有多个这些类的并发实例,则尤其如此。

答案 2 :(得分:0)

某些平台提供显式的内存屏障原语,这将确保如果一个线程写入字段然后执行写入屏障,则可以保证从未检查过相关对象的任何线程 看到那个写的效果。不幸的是,截至上次我提出这样一个问题Cheapest way of establishing happens-before with non-final field时,Java唯一能够提供任何线程语义保证而不需要代表阅读线程采取任何特殊操作就是使用final字段。 Java保证通过final字段对对象进行的任何引用都会看到在引用存储在最终字段之前对该对象的最终字段或非字段执行的任何存储,但该关系不是传递。因此,给定

class c1 { public final c2 f; 
           public c1(c2 ff) { f=ff; } 
         }
class c2 { public int[] arr; }
class c3 { public static c1 r; public static c2 f; }

如果写入c3的唯一内容是执行代码的线程:

c2 cc = new c2();
cc.arr = new int[1];
cc.arr[0] = 1234;
c3.r = new c1(cc);
c3.f = c3.r.f;

第二个线程执行:

int i1=-1;
if (c3.r != null) i1=c3.r.f.arr[0];

并且第三个线程执行:

int i2=-1;
if (c3.f != null) i2=c3.f.arr[0];

Java标准保证第二个线程如果if条件产生true,则将i1设置为1234.但是,第三个线程可能会看到非空c3.f的值,但null的值c3.arrc3.f.arr[0]中的值为0。即使存储在c3.f中的值已从c3.r.f读取,并且需要读取final引用c3.r.f的任何内容,以查看在此之前对该对象所做的任何更改编写了引用c3.r.f,Java Standard中的任何内容都不会禁止JIT将第一个线程的代码重新排列为:

c2 cc = new c2();
c3.f = cc;
cc.arr = new int[1];
cc.arr[0] = 1234;
c3.r = new c1(cc);

这样的重写不会影响第二个线程,但可能会对第三个线程造成严重破坏。