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

时间:2018-01-29 19:21:38

标签: java arrays multithreading object

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

假设我有一个名为document.getElementById("theId").style.backgroundColor = 'somecolor';的类,我希望在另一个类中有一个对象数组。

MyClass

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

或者我错了?如果我想改变它的价值,我该怎么办?我应该创建一个新的数组实例,用初始赋值中的new替换旧值吗?

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

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

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

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

3 个答案:

答案 0 :(得分:0)

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

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

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

如果你真的,必须使用和数组一样:new MyClass[]{ ... };

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

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

在较高级别,您可以使用Streams为您执行工作。但最后,如果您已经在使用数组,我建议您使用arraylist的同步版本。如有必要,可以对其进行不稳定的引用。如果您在创建课程后未更新对阵列的引用,则不需要使用volatile,如果可能,最好将其设为final

答案 1 :(得分:0)

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

  1. 写/写操作
  2. 读/写操作
  3. 通过线程到同一个对象。这被称为readers/writers problem。请注意,两个线程同时从同一对象同时读取数据是完全正确的。

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

      

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

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

      

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

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

答案 2 :(得分:0)

在这种情况下,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