当前位置: 首页 > 面试题库 >

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

荆城
2023-03-14
问题内容

如果我在多线程环境中有一个未同步的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]原因是与写入相比,读取所花费的时间比例更大,并且存在引入死锁的风险。

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

  1. 我发布的代码中存在一个错误。交换后,在全局变量(命名错误的变量)上进行同步可能无法保护同步块。
  2. 您可以通过在类上同步(将synced关键字移到方法上)来解决此问题,但是可能还有其他错误。一个更安全,更可维护的解决方案是使用java.util.concurrent中的内容。
  3. 我发布的代码中没有“最终一致性保证”,一种确保读者可以一定程度地看到作者的更新的方法是使用volatile关键字。
  4. 经过反思,促使这个问题出现的一般问题是试图用Java中的锁定写入来实现无锁定读取,但是我的(已解决)问题与集合有关,这可能会给以后的读者带来不必要的困惑。因此,在不太明显的情况下,我发布的代码可以一次允许一个作者对“某个对象”进行编辑,而该对象正在不受多个读取器线程保护的情况下进行编辑。编辑的提交是通过原子操作完成的,因此读者只能获得编辑前或编辑后的“对象”。当/如果读取器线程获得更新,则该更新不能在读取中间发生,因为读取发生在“对象”的旧副本上。在Java中提供更好的并发支持之前,可能已经发现并证明以某种方式破坏了一个简单的解决方案。

问题答案:

与其尝试推出自己的解决方案,不为何使用ConcurrentHashMap作为您的集合,而只是将所有值设置为某个标准值?(像Boolean.TRUE这样的常量会很好用。)

我认为这种实现方式在许多读者很少的情况下效果很好。甚至还有一个构造函数,可让您设置预期的“并发级别”。

更新: Veer建议使用
Collections.newSetFromMap
实用程序方法将ConcurrentHashMap转换为Set。由于该方法令人Map<E,Boolean>怀疑,因此它会将所有值设置Boolean.TRUE为幕后操作。

更新:解决海报的示例

那可能是我最终要解决的问题,但是我仍然对我的极简解决方案如何失败感到好奇。– MilesHampson

通过一些调整,您的极简解决方案将可以正常工作。我担心的是,尽管现在它很小,但将来可能会变得更加复杂。很难记住在进行线程安全操作时要假设的所有条件,尤其是如果您要在几周/几个月/几年后返回代码以进行看似微不足道的调整时。如果ConcurrentHashMap具有足够的性能来满足您的所有需求,那么为什么不使用它呢?所有令人讨厌的并发详细信息都被封装了起来,甚至从现在开始的6个月,您将很难将其弄乱!

您确实需要至少一项调整才能使当前解决方案生效。正如已经指出的那样,您可能应该将volatile修饰符添加到global的声明中。我不知道您是否具有C
/ C ++背景,但是当我得知volatile
Java的语义实际上比C复杂得多时,我感到非常惊讶。如果您打算使用Java进行大量并发编程,那么熟悉Java内存模型的基础是一个好主意。如果您不引用global某个volatile引用,那么在global尝试更新它之前,没有线程会看到该值的任何更改。synchronized
块将刷新本地缓存并获取更新的参考值。

但是,即使添加了volatile,仍然存在 很大的 问题。这是一个有两个线程的问题场景:

  1. 我们从空集或开始global={}。线程AB两者在其线程本地缓存的内存中均具有此值。
  2. 线程A获取获得synchronized锁定global并通过复制副本global并将新密钥添加到集合中来开始更新。
  3. 当Thread A仍在synchronized块内时,Thread B将其本地值读取global到堆栈上并尝试进入该synchronized块。由于线程A当前位于监视器的线程B块内。
  4. 线程A通过设置参考并退出监视器来完成更新,结果为global={1}
  5. 线程B现在能够进入监视器并制作副本global={1}
  6. 线程A决定进行另一次更新,读取其本地global引用,然后尝试进入该synchronized块。 由于线程B当前保持锁定状态,{}因此没有任何锁定{1},线程A成功进入监视器!
  7. 线程A还制作的副本以{1}进行更新。

现在,线程AB都在synchronized块内,并且它们具有相同的副本global={1}集。 这意味着他们的更新之一将丢失!
这种情况是由于您正在对存储synchronized块中要更新的引用中的对象进行同步而引起的。您应该始终非常小心地使用要同步的对象。您可以通过添加新变量来充当锁来解决此问题:

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;    
  }
}

这个错误非常阴险,其他答案都没有解决。
这些疯狂的并发细节使我建议使用已经调试过的java.util.concurrent库中的某些内容,而不是尝试自己编写一些内容。
我认为上述解决方案会奏效-但是将其再次拧紧有多容易?这样会容易得多:

private final Set<Object> global = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>());

由于是引用,因此final您不必担心使用陈旧引用的线程,并且由于ConcurrentHashMap内部处理了所有讨厌的内存模型问题,因此您不必担心监视器和内存屏障的所有讨厌的细节!



 类似资料:
  • 问题内容: 我有一些代码,经常在对它们进行很小的更改之后,就经常复制一个大的内存块。 我已经实现了一个跟踪更改的系统,但是我认为,如果可能的话,告诉操作系统对内存进行“写时复制”,让它仅处理这些部分的副本,那可能会很好。哪个改变。但是,尽管Linux会进行写时复制,例如,在fork()ing时,但我找不到控制它并自己执行的方法。 问题答案: 您最好的机会可能是将原始数据保存到文件中,然后再次使用来

  • 问题内容: 我正在传递一个accountid作为XML文件的输入,如图所示,稍后将对其进行解析并将在我们的代码中使用: 问题是,如果没有传递任何内容(accoutnid中的空值)作为accountid传递,我将无法在Java代码中处理这种情况。我尝试了这个,但是没有成功: 我可以使用以下方法成功解决此问题: 我们可以依靠该方法来检查a的空条件吗?这有效吗? 问题答案: 不,绝对不是-因为如果为nu

  • 问题内容: 在Java中,我们可以将超类Object传递给子类引用吗? 我知道这是一个奇怪的问题/实际上不可行,但是我想了解这个背后的逻辑为什么在Java中不允许这样做。 如果Java允许编译第1行会发生什么?问题将在哪里出现? 欢迎任何输入/链接。 问题答案: 如果允许您的语句进行编译,那么这将破坏多态性的原理,这是该语言具有的功能之一。 另外,您应该熟悉 编译时间类型 和 运行时类型的 含义:

  • 问题内容: 我想要与的关键字类似的语义。 问题答案: Java令人困惑,因为一切都是通过值传递的。但是,对于引用类型的参数(即不是原始类型的参数),引用本身是通过值传递的,因此它似乎是按引用传递的(人们常常声称它是)。情况并非如此,如下所示: 将打印到控制台。如果要打印上面的代码,Goodbye可以使用如下所示的显式引用:

  • 问题内容: 有时我想对流执行一组操作,然后用其他操作以两种不同的方式处理结果流。 我可以不必指定两次常见的初始操作来执行此操作吗? 例如,我希望存在以下方法: 问题答案: 通常这是不可能的。 如果要复制输入流或输入迭代器,则有两个选择: 答:将所有内容都保存在集合中,例如 假设您将一个流复制为两个流,然后将。如果您的元素中包含高级元素,并且元素中包含,则必须将元素保留在内存中,以便跟上步伐。如果流

  • 是否允许使用名称调用对象的方法,如下面的示例所示? Clang和GCC在这里分叉。Clang对程序很好,而GCC打印错误: 演示:https://gcc.godbolt.org/z/pn7jehzdp 根据标准,哪个编译器就在这里?