在Java中,我可以依靠引用分配作为原子来实现写时复制吗?

在Java中,我可以依靠引用分配作为原子来实现写时复制吗?,java,multithreading,synchronization,locking,copy-on-write,Java,Multithreading,Synchronization,Locking,Copy On Write,如果我在多线程环境中有一个未同步的java集合,并且我不想强制该集合的读取器同步[1],那么我同步编写器并使用原子性引用分配的解决方案是否可行?比如: private Collection global = new HashSet(); // start threading after this void allUpdatesGoThroughHere(Object exampleOperand) { // My hypothesis is that this prevents opera

如果我在多线程环境中有一个未同步的java集合,并且我不想强制该集合的读取器同步[1],那么我同步编写器并使用原子性引用分配的解决方案是否可行?比如:

private Collection global = new HashSet(); // start threading after this

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(global) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

// Do multithreaded reads here. All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact 
在这种情况下,滚动您自己的解决方案似乎经常失败,因此我想了解其他模式、集合或库,这些模式、集合或库可用于防止数据使用者创建和阻塞对象


[1] 其原因是,与写操作相比,读操作花费的时间占了很大比例,再加上引入死锁的风险


编辑:在几个答案和评论中有很多好的信息,一些要点:

  • 我发布的代码中存在一个bug。在全局上进行同步(一个名称错误的变量)可能无法在交换后保护已同步的块
  • 您可以通过在类上同步(将synchronized关键字移动到方法)来修复此问题,但可能还有其他错误。一个更安全、更可维护的解决方案是使用java.util.concurrent中的内容
  • 在我发布的代码中没有“最终一致性保证”,确保读者能够看到作者的更新的一种方法是使用volatile关键字
  • 经过反思,引发这个问题的一般问题是试图在java中用锁定写入实现无锁读取,但我(解决了)的问题是使用集合,这可能会对未来的读者造成不必要的混淆。因此,我发布的代码每次允许一个编写器对“某个对象”执行编辑,而该对象正被多个读线程不受保护地读取,这一点并不明显。编辑的提交是通过原子操作完成的,所以读者只能获得编辑前或编辑后的“对象”。当读取器线程获得更新时,它不会出现在读的中间,因为读发生在“对象”的旧副本上。在java中提供更好的并发支持之前,一个简单的解决方案可能已经被发现,并被证明在某种程度上被破坏了

  • 通过使
    全局
    易失性
    替换
    同步
    ,您就可以在写入时复制了

    虽然分配是原子的,但在其他线程中,它不随对引用对象的写入而排序。需要有一个before-before关系,您可以使用
    volatile
    或同步读写

    多个更新同时发生的问题是不同的-使用单个线程或任何您想在那里执行的操作

    如果对读写操作都使用了
    synchronized
    ,那么这是正确的,但在需要切换的读操作中,性能可能不太好。
    ReadWriteLock
    可能是合适的,但仍然会有写阻塞读

    发布问题的另一种方法是使用final字段语义创建(理论上)可以安全地发布的对象


    当然,也有并发集合可用。

    您可以使用
    collections.synchronizedSet
    方法吗?来自HashSet Javadoc

    根据,

    我们已经看到,增量表达式,例如
    c++
    ,并不描述原子操作。即使是非常简单的表达式也可以定义可以分解为其他动作的复杂动作。但是,您可以指定一些原子操作:

    • 对于参考变量和大多数基本变量(除
      long
      double
      之外的所有类型),读写都是原子的
    • 对于声明的所有变量
      volatile
      (包括
      long
      double
      变量),读写都是原子的
    这一点得到了国际社会的重申

    对引用的写入和读取始终是原子的,无论它们是作为32位值还是64位值实现的

    看来您确实可以依赖原子的引用访问;但是,请认识到,这并不能确保所有读卡器在写入后都会读取
    全局
    的更新值,也就是说,这里没有内存顺序保证

    如果您通过
    synchronized
    全局
    的所有访问使用隐式锁,那么您可以在这里建立一些内存一致性。。。但使用另一种方法可能更好

    您似乎还希望
    global
    中的集合保持不变。。。幸运的是,您可以使用它来强制执行此操作。例如,您可能应该执行以下操作

    private volatile Collection global = Collections.unmodifiableSet(new HashSet());
    
    。。。或者使用原子参考

    private AtomicReference<Collection> global = new AtomicReference<>(Collections.unmodifiableSet(new HashSet()));
    
    您应该知道,在这里进行复制是多余的,因为在内部为(Object elm:global)创建了一个
    迭代器,如下所示

    final Iterator it = global.iterator();
    while (it.hasNext()) {
      Object elm = it.next();
    }
    
    因此,在阅读过程中,对于
    global
    ,不可能切换到完全不同的值


    除此之外,我同意。。。在可能的情况下,您是否有任何理由在这里滚动您自己的数据结构?我想也许您正在处理一个较旧的Java,因为您使用的是原始类型,但问一下也无妨

    您可以找到由或其同类(使用前者实现
    )提供的写时复制集合语义


    另外,你有没有考虑过使用一种新的方法?它们保证像您在示例中所做的那样,使用
    for
    循环将是一致的

    类似地,迭代器和枚举返回反映哈希表在迭代器/枚举创建时或创建之后某个点的状态的元素

    在内部,
    迭代器
    用于在
    Iterable
    上增强
    for

    您可以通过utilizi在此基础上创建一个
    // ... All reads are done through a reference copy like:
    // Collection copy = global;
    // for (Object elm: copy) {...
    // so the global reference being updated half way through should have no impact
    
    final Iterator it = global.iterator();
    while (it.hasNext()) {
      Object elm = it.next();
    }
    
    final Set<E> safeSet = Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
    ...
    /* guaranteed to reflect the state of the set at read-time */
    for (final E elem : safeSet) {
      ...
    }
    
    private volatile Collection global = new HashSet(); // start threading after this
    private final Object globalLock = new Object(); // final reference used for synchronization
    
    void allUpdatesGoThroughHere(Object exampleOperand) {
      // My hypothesis is that this prevents operations in the block being re-ordered
      synchronized(globalLock) {
        Collection copy = new HashSet(global);
        copy.remove(exampleOperand);
        // Given my hypothesis, we should have a fully constructed object here. So a 
        // reader will either get the old or the new Collection, but never an 
        // inconsistent one.
        global = copy;    
      }
    }
    
    private final Set<Object> global = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>());