繁体   English   中英

如何在Java中创建线程安全对象数组?

[英]How to create thread safe object array in Java?

我已经搜索了这个问题,但只找到了原始类型数组的答案。

假设我有一个名为MyClass的类,并且希望在另一个类中具有其对象的数组。

class AnotherClass {
    [modifiers(?)] MyClass myObjects;

    void initFunction( ... ) {
         // some code
         myObjects = new MyClass[] { ... };
    }

    MyClass accessFunction(int index) {
        return myObjects[index];
    }
}

我在某处读到,声明一个数组易失性并不能对它的字段提供易失性访问,但是给数组一个新值是安全的。 因此,如果我对它的理解很好,如果我在示例代码中为数组提供一个volatile修饰符,那将是(某种程度上)安全的。 如果我从不使用[]运算符更改其值。

还是我错了? 如果我想改变其价值之一,该怎么办? 我应该在初始分配中创建数组的新实例,然后用新值替换旧值吗?

AtomicXYZArray不是一个选择,因为它仅适用于原始类型数组 AtomicIntegerArray对get()和set()使用本机代码,因此对我没有帮助。

编辑1:我认为Collections.synchronizedList(...)是一个不错的选择,但现在我正在寻找数组。

编辑2:从另一个类调用initFunction()。 AtomicReferenceArray似乎是一个很好的答案。 到目前为止,我还不知道。 (我仍然对我的示例代码能够与volatile修饰符一起使用(在数组之前)仅从其他位置调用这两个函数一起感兴趣。)


这是我的第一个问题。 我希望我能达到正式要求。 谢谢。

是的,当您说易失性单词不能满足您的情况时,您是对的,因为它可以保护对数组的引用,而不是数组的元素。

如果两者都需要,Collections.synchronizedList(...)或同步的collections是最简单的方法。

像这样倾向于使用修饰符不是这样做的方法,因为这样不会影响元素。

如果确实需要,请像这样使用数组: new MyClass[]{ ... };

然后, AnotherClass是需要对其安全负责的那个,您可能正在这里寻找较低级别的同步:同步的关键字和锁。

同步化的关键字更容易,您可以创建锁定对象或默认情况下类实例的块和方法。

在更高级别上,您可以使用Streams为您执行工作。 但是最后,如果您已经在使用数组,我建议您使用arraylist的同步版本。 并在必要时对其进行可变引用。 如果在创建类后不更新对数组的引用,则不需要volatile,最好尽可能将其设为final

为了使数据具有线程安全性,您需要确保没有同时发生的数据:

  1. 写/写操作
  2. 读/写操作

通过线程到同一个对象。 这被称为读者/作家问题 请注意,两个线程同时从同一对象同时读取数据是非常好的。

在正常情况下,可以通过在方法和成员中使用synced修饰符(充当对象的锁定)和atomic构造(“瞬时”执行操作)来将上述属性强制达到令人满意的水平。 这实质上确保了没有两个线程可以以导致不良交错的方式同时访问同一资源。

如果我在示例代码中给数组一个易失修饰符,那将是(kinda?)安全的。

volatile关键字会将数组引用放置在主内存中,并确保没有线程可以在其私有内存中缓存它的本地副本,这有助于提高线程可见性,尽管它不能单独保证线程安全。 另外,除非经验丰富的程序员使用,否则应很少使用volatile ,因为它可能会对程序造成意想不到的影响。

如果我想改变其价值之一,该怎么办? 我应该在初始分配中创建数组的新实例,然后用新值替换旧值吗?

如果需要更改类的可变成员的同步变量方法,或者使用类中原子对象提供的方法,请创建同步的可变方法。 这是更改数据而不会引起任何意外副作用的最简单方法(例如,在线程访问要删除的对象中的数据时,从数组中删除对象)。

在这种情况下,Volatile确实确实起作用,但需要注意:在MyClass上的所有操作只能读取值。

与您可能了解的所有有关volatile的内容相比,它在JMM中具有一个目的:创建事前发生的关系。 它仅影响两种操作:

  • 易失性读取(例如,访问字段)
  • 易失性写入(例如,分配给字段)

而已。 直接来自JLS§17.4.5的事前发生关系:

  • 可以通过事前发生关系来排序两个动作。 如果一个动作发生在另一个动作之前,则第一个动作对第二个动作可见 ,并在第二个动作之前排序。
  • 在随后每次对该字段进行读取之前,都会写入一个易失字段(第8.3.1.4节)。
  • 如果x和y是同一线程的动作,并且x按程序顺序位于y之前,则hb(x,y)。

这些关系是可传递的。 综上所述,这暗示了一些要点:对单个线程执行的所有操作均在该线程对该字段进行易失性写入之前发生(上面的第三点)。 在读取该字段之前(第二点),会发生字段的易失性写入。 因此,任何其他读取volatile字段的线程都会看到所有更新,包括所有引用的对象(在这种情况下,例如数组元素)都是可见的(第一点)。 重要的是,仅保证他们在写入字段时看到更新。 这意味着,如果您完全构造一个对象,然后将其分配给一个volatile字段,然后再不对其进行突变或它所引用的任何对象,则它永远不会处于不一致的状态。 使用上述警告是安全的:

class AnotherClass {
    private volatile MyClass[] myObjects = null;

    void initFunction( ... ) {
         // Using a volatile write with a fully constructed object.
         myObjects = new MyClass[] { ... };
    }

    MyClass accessFunction(int index) {
        // volatile read
        MyClass[] local = myObjects;
        if (local == null) {
            return null; // or something else
        }
        else {
            // should probably check length too
            return local[index];
        }
    }
}

我假设您只调用一次initFunction。 即使您多次调用它,也只是破坏那里的值,它永远不会处于不一致的状态。

您还很正确,因为不允许更改数组,因此更新此结构不是很简单。 如您所说,复制并替换是很常见的。 假设只有一个线程将更新值,则可以简单地获取对当前数组的引用,将值复制到新数组中,然后将新构造的值重新分配给volatile引用。 例:

private void add(MyClass newClass) {
    // volatile read
    MyClass[] local = myObjects;
    if (local == null) {
        // volatile write
        myObjects = new MyClass[] { newClass };
    }
    else {
        MyClass[] withUpdates = new MyClass[local.length + 1];
        // System.arrayCopy
        withUpdates[local.length] = newClass;
        // volatile write
        myObjects = withUpdates;
    }
}

如果将要更新多个线程,那么您将遇到以下问题:由于两个线程可以复制和旧数组,因此将丢失对数组的添加,使用新元素创建新数组,然后最后写入会赢。 在这种情况下,您需要使用更多同步或AtomicReferenceFieldUpdater

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM