当数组变量是volatile时,我们是否需要同步对数组的访问?

时间:2016-04-05 10:03:35

标签: java arrays multithreading volatile

我有一个包含对数组的volatile引用的类:

private volatile Object[] objects = new Object[100];

现在,我可以保证,只有一个线程(称为writer)可以写入数组。例如,

objects[10] = new Object();

所有其他线程只读取writer线程写入的值。

问题: 我是否需要同步此类读写操作以确保内存一致性?

我认为,是的,我应该。因为从性能角度来看,如果JVM在写入数组时提供某种内存一致性保证,那么它就没有用处。但我不确定。在文档中找不到任何有用的东西。

4 个答案:

答案 0 :(得分:14)

您可以使用AtomicReferenceArray

final AtomicReferenceArray<Object> objects = new AtomicReferenceArray<>(100);

// writer
objects.set(10, new Object());

// reader
Object obj = objects.get(10);

这将确保原子更新和发生 - 在读/写操作的一致性之前,就像每个数组项都是volatile一样。

答案 1 :(得分:13)

private volatile Object[] objects = new Object[100];

您只能objectsvolatile方式引用CopyOnWriteArrayList。不是关联的数组实例内容。

  

问题:我是否需要同步这样的读写以确保内存一致性?

  如果JVM在写入数组时提供某种内存一致性保证,那么从性能角度看它是没有用的

考虑使用像Lock这样的集合(或者你自己的数组包装器,在mutator和read方法中有一些Vector实现。)

Java平台也有synchronized List(设计有缺陷已废弃)和<Grid Margin="0,32,0,0"> <TextBlock Text="{Binding IDC_WiFi, Source={StaticResource Resources}}" FontFamily="Segoe UI" FontSize="20" Foreground="#4cb5ab" HorizontalAlignment="Left" /> <Button Command="{Binding HardwareWifiAccordionCommand}" BorderThickness="0" Width="16" HorizontalAlignment="Right" Height="16" > <Button.Background> <ImageBrush ImageSource="{Binding AccordionImageHardwareWifi}" /> </Button.Background> </Button> </Grid> <TextBlock Text="Klein's, Anil's" FontFamily="Segoe UI" FontSize="15" Foreground="#8fa3ad"/> <StackPanel Height="200" Visibility="{Binding IsAccordionHardwareWifi, Converter={StaticResource Bool2Visible}}"> <ScrollViewer VerticalScrollBarVisibility="Auto" Height="350"> <ItemsControl ItemsSource="{Binding WifiList,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" > <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Margin="0,32,0,0"> <Grid> <Image Source="/Images/Assets/da_wifi1_16x16.png" Height="16" Width="16" HorizontalAlignment="Left" /> <TextBlock Margin="25,0,0,0" Text="{Binding NetworkName}" FontSize="15" Foreground="#FFFFFF" /> <TextBlock Text="" FontSize="15" Foreground="#8fa3ad" HorizontalAlignment="Right" /> </Grid> <TextBlock Text="" FontSize="15" Foreground="#8fa3ad" HorizontalAlignment="Left" /> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </ScrollViewer> </StackPanel> (许多情况下都很慢),但我不建议使用它们。

来自One more good idea

PS: @SashaSalauyou

答案 2 :(得分:8)

JLS § 17.4.5 – Happens-before Order

  

发生在之前的关系可以订购两个动作。如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前是可见的并且在第二个动作之前被命令。

     

[...]

     

volatile字段的写入发生在每次后续读取该字段之前。

发生之前关系非常强大。这意味着如果线程A写入volatile变量,并且任何线程B稍后读取变量,则线程B保证看到volatile变量本身的变化,以及设置volatile变量之前的所有其他更改主题A,包括其他任何对象,无论它们是否为volatile

然而,这还不够!

元素分配objects[10] = new Object(); 不是变量objects的写入。它只是读取变量以确定它指向的数组,然后写入包含在位于内存中其他位置的数组对象中的不同变量。没有发生 - 在仅仅通过读取volatile变量建立关系之前,因此代码不安全。

正如@DimitarDimitrov指出的那样,你可以通过对objects变量进行虚拟写操作来解决这个问题。每对操作 - 作者线程的objects = objects;重新分配以及读者线程的foo = objects[x];查找 - 定义了更新的发生在之前的关系,因此将&# 34;发布&#34;作者线程对读者线程所做的所有最新更改。这可行,但它需要纪律,而且不优雅。

但是有一个更微妙的问题:即使读者线程看到数组元素的更新值仍然不能保证它正确地看到该元素引用的对象的字段,因为以下顺序是可能的:

  1. Writer会创建一些对象foo
  2. 作家设置objects[x] = foo;
  3. 读者检查objects[x]并看到对新foo的引用(可以做的引用,但由于没有发生在关系之前)。
  4. 作家objects = objects;
  5. 不幸的是,这并没有定义正式的发生在之前的关系,因为volatile变量read(3)来自volatile变量write(4)。虽然读者可以偶然发现objects[x]是对象foo,但这并不意味着foo字段已安全发布,因此读者理论上可能会看到新对象,但错误的值!要解决这个问题,您使用此技术在线程之间共享的对象需要包含所有字段finalvolatile或以其他方式同步。例如,如果对象都是String,那么你会没事的,但除此之外,很容易犯这个错误。 (感谢@Holger指出这一点。)

    以下是一些不太谨慎的选择:

    • 存在并发数组类,如AtomicReferenceArray,以提供每个元素的行为,就好像volatile一样。这更容易正确使用,因为它确保如果读者看到更新的数组元素值,它还可以正确地看到该元素引用的对象。

    • 您可以在synchronized块中包装对数组的所有访问,并在某些共享对象上进行同步:

      // writer
      synchronized (aSharedObject) {
          objects[x] = foo;
      }
      
      // reader
      synchronized (aSharedObject) {
          bar = objects[x];
      }
      

      volatile一样,使用synchronized创建发生之前的关系。 (在释放对象的同步锁之前,线程所做的一切都发生在任何其他线程获取同一对象的同步锁之前。)如果这样做,则您的数组不需要是{{ 1}}。

    • 考虑一下数组是否真的是你需要的。您还没有说出这些作者和读者线程的用途,但如果您想要某种生产者 - 消费者队列,那么您真正需要的课程是BlockingQueueExecutor。您应该查看Java并发类,看​​看其中一个是否已经满足您的需求,因为如果有的话,它肯定比volatile更容易正确使用。

答案 3 :(得分:7)

是的,您需要同步访问易失性数组的元素。

其他人已经解决了如何使用CopyOnWriteArrayListAtomicReferenceArray代替,所以我将转向略微不同的方向。我还建议由JMM的一个大贡献者Jeremy Manson阅读Volatile Arrays in Java

  

现在,我可以保证只有一个线程(称为编写器)可以写入数组,例如如下:

您是否可以提供单一作者保证与volatile关键字无关。我认为你并没有考虑到这一点,但我只是在澄清,以便其他读者不会得到错误的印象(我认为有一个数据竞争双关语可以用那个句子做成)。

  

所有其他线程只读取作者线程写入的值。

是的,但是就像你的直觉正确引导你一样,这只适用于数组引用的值。 这意味着除非您正在编写volatile变量的数组引用,否则您将无法获得volatile写 - 读合同的写入部分。

这意味着要么你想做像

这样的事情
objects[i] = newObj;
objects = objects;

在许多方面都是丑陋可怕的。 或者你想在每次作家更新时发布一个全新的数组,例如

Object[] newObjects = new Object[100];

// populate values in newObjects, make sure that newObjects IS NOT published yet

// publish newObjects through the volatile variable
objects = newObjects;

这不是一个非常常见的用例。

请注意,与设置数组元素(不提供volatile - 写语义不同,获取数组元素(使用newObj = objects[i];)确实提供了volatile - 读取语义,因为你正在取消引用数组:)

  

因为从性能角度来看,如果JVM在写入数组时提供某种内存一致性保证,那么它就没用了。但我不确定。

就像你提到的那样,确保volatile语义所需的内存防护成本非常高,如果你在混合中添加错误共享,它就会变得更糟。

  

在文档中找不到任何有用的东西。

您可以放心地假设数组引用的volatile语义与非数组引用的volatile语义完全相同,这一点都不奇怪,考虑到数组(甚至原语)仍然是对象。