数组的易变阵列

时间:2015-05-24 06:37:45

标签: java arrays multithreading volatile

我有一个

的课程
private volatile long[][] data = new long[SIZE][];

最初只包含空值和访问它的方法。当它命中null元素时,它会创建一个long[]并存储以备将来使用。这个操作是幂等的,多个线程在同一个元素上浪费时间不是问题。

没有线程必须看到未完全填充的元素。我希望以下代码做得对:

long[] getOrMakeNewElement(int index) {
    long[] element = data[index]; // volatile read
    if (element==null) {
        element = makeNewElement(index); // local operation
        data = data; // ugliness 1
        data[index] = element;
        data = data; // ugliness 2
    }
    return element;
}

第一个丑陋是确保其他线程理论能够看到对element所做的更改。它们实际上无法访问它,因为它还没有存储。但是,在下一个类似element被存储而另一个线程可能会或可能不会看到这个商店,所以AFAIK第一个丑陋是必要的。

第二个丑陋然后只是确保其他线程看到包含data的新element。这里奇怪的是两次使用丑陋。

这是必要足以安全发布吗?

注意:虽然这个问题类似于this one,但它没有重复,因为它处理修改现有的1D阵列而不是创建一个。这使答案清楚。

更新

注意:这不是生产代码,我知道并且不关心替代方案(AtomicReferenceArraysynchronized,无论如何......)。我写了这个问题是为了更多地了解JMM。这是一个真实的代码,但仅用于我对项目Euler的愚弄,并且没有动物在这个过程中受到伤害。

我想,一个安全而实用的解决方案是

class Element {
    Element(int index) {
        value = makeNewElementValue(index);
    }
    final long[] value;
}

private volatile Element[] data = new Element[SIZE];

Element通过Semantics of final Fields确保可见性。

正如user2357112所指出的,当多个线程写入相同的data[index]时,还存在(IMHO无害)数据竞争,这实际上很容易避免。虽然读取速度必须很快,但制作新元素的速度足够慢,无法进行任何同步。它还可以更有效地初始化数据。

4 个答案:

答案 0 :(得分:2)

This is probably safe in practice, according to roach-motel model, or JSR-133 Cookbook.

Let's expand all volatile reads/writes in the following code

    element[0] = something;
    data = data; // ugliness 1
    data[index] = element;

it becomes

    element[0] = something;             [0]
    tmp1 = data;  // volatile read      [1]
    data = tmp1;  // volatile write     [2]
    tmp2 = data;  // volatile read      [3]
    tmp2[index] = element;              [4]

The critical barrier here is [2]+[3]. According to roach motel model:

  nothing before a volatile write can be reordered after  it.
  nothing after  a volatile read  can be reordered before it.

Therefore [2]+[3] combo prevents instructions from being reordered across it. Therefore [0] and [4] cannot be reordered.

Note that [1]+[2] combo is not enough; without [3], the code can be reordered as [1] [4][0] [2].


That's cool. But is it safe in JMM which is weaker than roach-motel? I think it's not. This code

    long[] element = data[index];
    x = element[0];

is expanded to

    tmp0 = data; // volatile read   [a]
                                    [b]  
    element = tmp0[index];          [c]
    x = element[0]                  [d]

Now imagine the earliest thread does [a], then gets paused in [b] for a few seconds, then does [c]. Now [a] is before any other volatile writes, therefore it does not establish any ordering per JMM, therefore there's no telling what [c] and [d] could read. [c] is not a serious problem, but [d] is - [d] could read the default value 0 instead of something written by [0]; or even gibberish because it's long.

As far as I know, if you want to publish an object through a volatile field, either it must be a new object (immutable or not), or it must use volatile members.

JMM is strong enough to guarantee that common concurrency patterns work; but if you want to do something unconventional, it is often too weak; and the worse thing is that JMM is too complicated as a reasoning framework. So if you want your code strictly JMM compliant, the best practice is to follow boring ordinary patterns:)


A better solution that favors reads would be copy-on-write.

答案 1 :(得分:2)

Java语言规范定义volatile的语义如下:

  

对易失性变量v的写入与任何线程的所有后续v读取同步(其中"后续"根据同步顺序定义)。

规范保证每当某个动作发生在另一个动作之前时,该动作就会被其他动作看到。

对于要安全发布的对象,对象的初始化必须对获取对该对象的引用的任何线程可见。由于我们感兴趣的字段不是final,所以只有在任何其他线程获得对该对象的引用之前保证对象的初始化发生时才能实现。

要验证是否属于这种情况,请查看所涉及操作的先前图表:

  makeNewElement        
        |               
        v               
   read of data         
        |               
        v               ?
  write to data --------------> read of data
        |                             |
        v                             v
write to array element       read of array element
        |                             |
        v                             V
  read of data                   useElement
        |
        v
  write to data

显然,当且仅当"写入数据"时,才会有一条从makeNewElement到useElement的路径。同步 - "读取数据",当且仅当读取后续时才会执行此操作。但是,它不需要是后续的:对于后续读取的每个执行,我们可以创建另一个不执行的执行,只需通过以同步顺序向后移动读取:

  makeNewElement                   
        |                                
        v                                
   read of data                    read of data
        |                                |
        v                                |
  write to data                          |
        |                                |
        v                                v
write to array element          read of array element            
        |                                |
        v                                v
  read of data                      useElement
        |                                
        v                                
  write to data                     

通常,我们不能这样做,因为这会改变读取的值,但由于写入不会改变data的值,我们无法从读取的值中判断读取是在之前还是写完之后。

在这样的执行中,对新对象的引用是通过数据争用发布的,因为在读取之前不会发生数组元素的写入。在这种情况下,规范写道:

  

更具体地说,如果两个动作共享一个发生在之前的关系,那么它们不一定必须按照那个顺序发生在它们不与之共享的任何代码中。例如,在具有另一个线程中的读取的数据争用中的一个线程中的写入可能看起来不按顺序发生到那些读取。

也就是说,读取线程可能会看到对新对象的引用,但不会看到其初始化的效果,这将是相当糟糕的。

那么,我们怎样才能确保随后的读取?如果从volatile字段读取的值证明已发生必要的写入。在您的情况下,我们需要区分写入不同的元素。我们可以为每个数组元素使用一个单独的volatile变量:

Element[] data = ...;

class Element {
    volatile long[] value;
}

long[] getOrMakeNewElement(int index) {
    long[] element = data[index].value; // volatile read
    if (element==null) {
        element = makeNewElement(index); // local operation
        data[index].value = element;
    }
    return element;
}

或为每次写入更改单个volatile字段的值:

volatile long[][] data;

long[] getOrMakeNewElement(int index) {
    long[] element = data[index]; // volatile read
    if (element==null) {
        long[][] newData = Arrays.copyOf(data);
        newData[index] = element = makeNewElement(index);
        data = newData;
    }
    return element;
}    

当然,后一种解决方案的缺点是即使对于不同的数组元素,并发写入也会发生冲突。

答案 2 :(得分:1)

我会确定你的任务的两个方面。 第一个一个是由makeNewElement准备的新数组的“可见性”(两者都是对该数组的引用及其由该方法准备的初始内容)。这正是JMM和Happens之前的关系(与内存障碍)相关的问题。您可能知道,有很多方法可以设置之前发生的边缘(以及有关JMM / HB的材料)

根据JMM,volatile写入来自同一var的共享var HB volatile读取。这意味着volatile读取应该看到最后一次对变量的volatile写入所做的所有更改。并且由于易失性写入之前的所有写入都不能在它之后重新排序,因此它们也应该在易失性读取之后可见。因此,您的代码可能如下:

long[] getOrMakeNewElement(int index) {
    long[][] data = this.data; // volatile read (to a local var) produces memory barriers to get all changes happened before this read (see volatile write below). Acquire semantic
    long[] element = data[index]; // work with local
    if (element == null) {
        element = makeNewElement(index); // local operation
        data[index] = element;
        this.data = data; // volatile write (from the local var) produces memory barriers to make all changes, including just created array and all writes to it, visible to all volatile reads which happen after this write (see volatile read above). Release semantic
    }
    return element;
}

如果你不担心关于“元素”数组应该只为索引创建一次,并且所有读者都应该引用一个数组实例。那是第二个问题 - “关键部分”。您可以使用CAS或任何类型的锁定来处理它(synchronized,java.util.concurrent.locks等)。例如:

long[] getOrMakeNewElement(int index) {
    long[][] data = this.data; // volatile read
    long[] element = data[index];
    if (element == null) {
        synchronized (this) { // critical section
            data = this.data; // volatile read again 
            element = data[index];
            if (element == null) { // yes, that's a double checking
                element = makeNewElement(index);
                data[index] = element;
                this.data = data; // volatile write
            }
         }
    }
    return element;
}

现在,当您处理由makeNewElement准备的数组时,除了对数组的引用及其初始状态的可见性之外,您可能还必须关心其内容更改的可见性。如果某些线程向数组写入一些long并且其他一些读取写入的值,则需要HB边缘/内存屏障,例如使用MonitorEnter / MonitorExit(仅作为具有HB边缘的其他方式的示例),如下所示: / p>

public class MyArayWriter extends Thread {
    public void run() {
        final long[] myarray = getOrMakeNewElement(1);
        synchronyzed (myarray) { // MonitorEnter produces memory barriers to make all changes made before visible below. Acquire semantic 
            myarray[0] = 1;
        } // MonitorExit produces memory barriers to make all changes made before in this thread visible for all other threads.  Release semantic
    }
}

public class MyArayReader extends Thread {
    public void run() {
        final long[] myarray = getOrMakeNewElement(1);
        synchronyzed (myarray) { // MonitorEnter... Acquire semantic
            System.out.println(myarray[0]);
        } // MonitorExit... Release semantic
    }
}

如果您愿意,可以使用AtomicReferenceArray / AtomicLongArray:

private final AtomicReferenceArray<AtomicLongArray> data = new AtomicReferenceArray<>(SIZE);

至于你的第一个版本

long[] getOrMakeNewElement(int index) {
    long[] element = data[index]; // volatile read
    if (element==null) {
        element = makeNewElement(index); // local operation
        data = data; // ugliness 1 - DEFINITELY THERE IS NO NEED TO HAVE THIS ONE
        data[index] = element;
        data = data; // ugliness 2
    }
    return element;
}

它几乎等于我的第一个版本(没有关于新数组准备的关键部分)但你绝对可以摆脱 ugliness 1 ,如下所示:

long[] getOrMakeNewElement(int index) {
    long[] element = data[index]; // volatile read (for acquire semantic with appropriate memory barriers)
    if (element==null) {
        element = makeNewElement(index); // local operation
        data[index] = element; // here is a volatile read, again
        data = data; // volatile read (again? but OK) and write, at last (for release semantic with appropriate memory barriers)
    }
    return element;
}

但即使在此之后,您的版本仍有许多易失性操作,而不是安全发布所需的两个操作 - 获取易失性读取和释放易失性写入。

更新更新

使用“冻结动作HB读取刚刚创建的对象引用”将“value”数组发布为最终数组:

class Element {
    Element(int index) {
        value = makeNewElementValue(index);
    }
    final long[] value;
}

private volatile Element[] data = new Element[SIZE];

保证任何读者都可以看到对正确实例化和最初填充的数组的引用,并且只有在发布常量内容时才能使用(在数组作为final字段发布后,没有任何一个线程更改该数组的元素)。

答案 3 :(得分:0)

您的代码有数据竞争。如果线程A和B都执行

        data = data; // ugliness 1

然后每次执行

        data[index] = element;

然后每次执行

        data = data; // ugliness 2

data[index]的两次写入之间没有发生在之前的关系。我不完全确定JMM在数据竞争中有什么保证,但是没有多少。使用此代码不是一个好主意。