【问题标题】:How to create thread safe object array in Java?如何在 Java 中创建线程安全的对象数组?
【发布时间】:2018-07-08 13:50:09
【问题描述】:

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

假设我有一个名为 MyClass 的类,并且我想在我的另一个类中包含它的对象数组。

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

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

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

我在某处读到,声明一个数组 volatile 不会给它的字段提供 volatile 访问权限,但给数组一个新值是安全的。 所以,如果我理解得很好,如果我在我的示例代码中给我的数组一个 volatile 修饰符,它会(有点?)安全。如果我从不通过 [] 运算符更改其值。

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

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

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

编辑 2: initFunction() 是从不同的类调用的。 AtomicReferenceArray 似乎是一个很好的答案。到现在我都不知道。 (我仍然对我的示例代码将与 volatile 修饰符(在数组之前)一起使用仅从其他地方调用这两个函数感兴趣。)


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

【问题讨论】:

标签: java arrays multithreading object


【解决方案1】:

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

与您可能读到的所有关于 volatile 的作用相比,它在 JMM 中有一个目的:创建发生前的关系。它只影响两种操作:

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

就是这样。直接来自 JLS §17.4.5 的先发生关系:

  • 两个操作可以按发生前的关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。
  • 对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。
  • 如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

这些关系是可传递的。总而言之,这意味着一些重要的点:在单个线程上执行的所有操作都发生在该线程对该字段的 volatile 写入之前(上面的第三点)。字段的易失性写入发生在读取该字段之前(第二点)。因此,任何其他读取 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

【讨论】:

    【解决方案2】:

    为了使您的数据是线程安全的,您需要确保没有并发:

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

    通过线程到同一个对象。这被称为readers/writers problem。请注意,两个线程同时从同一个对象同时读取数据是完全可以的。

    在正常情况下,您可以通过在方法中使用 synchronized 修饰符(充当对象的锁)和原子构造(“即时”执行操作)将上述属性强制到可满足的水平和会员。这从本质上确保了没有两个线程可以同时访问相同的资源,从而导致错误的交错。

    如果我在示例代码中给数组一个 volatile 修饰符,它会(有点?)安全。

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

    如果我想更改其中一个值,我应该怎么做?我应该创建一个新的数组实例并在初始赋值中用新值替换旧值吗?

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

    【讨论】:

      【解决方案3】:

      是的,当您说 volatile 词不能满足您的情况时,您是正确的,因为它将保护对数组的引用而不是其元素。

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

      使用你喜欢做的修饰符不是这样做的方法,因为你不会影响元素。

      如果你真的,必须,像这样使用和排列:new MyClass[]{ ... };

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

      同步的关键字更容易,你可以创建块和方法来锁定一个对象,或者默认情况下在类实例中。

      在更高级别中,您可以使用 Streams 为您执行工作。但最后,如果您已经在使用数组,我建议您使用数组列表的同步版本。以及对它的可变引用,如果有必要的话。如果在创建类后不更新对数组的引用,则不需要 volatile,如果可能,最好将其设为 final

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-03-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多