有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何避免在非最终字段上进行同步?

如果我们有两个类在不同线程下在同一对象上运行,并且我们想要避免竞争条件,那么我们必须使用同步块和同一监视器,如下面的示例所示:

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设置监视器(mData)后,线程1或线程2将观察到监视器(mData),因为在设置和读取监视器之间尚未建立“之前发生”关系。例如,线程1仍然可以将其观察为null。我的猜测正确吗

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

EDIT语义上,所需的互斥与DataObject有关。这就是我不想要二次监视器的原因。一种解决方案是在DataObject上添加lock()unlock()方法,这些方法在处理之前需要调用。在内部,他们将使用Lock对象。因此,operateOnData()方法变成:

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

共 (3) 个答案

  1. # 1 楼答案

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

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

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

    这是非常有用的,因为将来可能会有人更改mData,使其值发生变化,那么您的代码将有大量奇怪的线程错误

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

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

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

    总的来说,我认为对于想要同步的数据来说,像包装器这样的东西是一种更好的方法,但我认为这是可行的

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

  2. # 2 楼答案

    你可以创建一个包装器

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

  3. # 3 楼答案

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

    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的非空值,而c3.arrnull值或c3.f.arr[0]中的零。即使存储在c3.f中的值是从c3.r.f读取的,并且任何读取final引用c3.r.f的操作都需要在写入引用c3.r.f之前查看对其标识的对象所做的任何更改,Java标准中的任何内容都不会禁止JIT将第一个线程的代码重新排列为:

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

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