【问题标题】:Java concurrency - use which technique to achieve safety?Java并发——使用哪种技术来实现安全?
【发布时间】:2011-07-19 20:19:46
【问题描述】:

我有一个 personId 列表。有两个 API 调用来更新它(添加和删除):

public void add(String newPersonName) {
    if (personNameIdMap.get(newPersonName) != null) {
        myPersonId.add(personNameIdMap.get(newPersonName)
    } else {
        // get the id from Twitter and add to the list
    }  
    // make an API call to Twitter
 }

public void delete(String personNAme) {
    if (personNameIdMap.get(newPersonName) != null) {
        myPersonId.remove(personNameIdMap.get(newPersonName)
    } else {
        // wrong person name
    }  
    // make an API call to Twitter
}

我知道可能存在并发问题。我阅读了大约 3 个解决方案:

  1. synchronized方法
  2. 使用Collections.synchronizedlist()
  3. CopyOnWriteArrayList

我不确定应该选择哪一个来防止不一致。

【问题讨论】:

  • 我建议同步列表,而不是方法。在这种情况下,我肯定会避免使用CopyOnWriteArrayList,因为它比变异更适合遍历。
  • 从您的代码 sn-p 中,我不确定您是如何确定存在并发问题的?您不能删除一个项目,除非它是首先添加的,如果它已经在列表中,您也不能添加相同的项目。我想我想了解这里同时发生的事情。
  • myPersonId可以同时添加和删除,如果不同的线程调用上面的adddelete

标签: java concurrency thread-safety


【解决方案1】:

1) 同步方法

2) 使用 Collections.synchronizedlist

3) CopyOnWriteArrayList ..

一切都会奏效,这取决于您需要什么样的性能/功能。

方法#1 和#2 是阻塞方法。如果你同步方法,你自己处理并发。如果您在Collections.synchronizedList 中包含一个列表,它会为您处理它。 (恕我直言,#2 更安全——请务必按照文档所述使用它,并且不要让 anything 访问包装在 synchronizedList 中的原始列表。)

CopyOnWriteArrayList 是在某些应用程序中使用的那些奇怪的东西之一。它是一个非阻塞的准不可变列表,即如果线程 A 在线程 B 更改列表时迭代该列表,则线程 A 将迭代旧列表的快照。如果您需要非阻塞性能,并且您很少写入列表,但经常从中读取,那么也许这是最好的选择。


编辑:至少还有两个其他选项:

4) 使用Vector 代替ArrayListVector 实现 List 并且已经同步。然而,由于它被认为是一个老派的类(从 Java 1.0 开始就存在!),因此它通常不受欢迎,并且应该等同于 #2。

5) 仅从一个线程串行访问List。如果您这样做,则可以保证List 本身不会出现任何并发问题。一种方法是使用Executors.newSingleThreadExecutor 并一个接一个地排队任务以访问列表。这会将资源争用从您的列表移至ExecutorService;如果任务很短,可能还好,但如果有些任务很长,可能会导致其他任务的阻塞时间超出预期。

最后,您需要考虑应用程序级别的并发性:线程安全应该是一项要求,并找出如何以最简单的设计获得所需的性能。


顺便说一句,您在 add() 和 delete() 中调用了两次 personNameIdMap.get(newPersonName)。如果另一个线程在每个方法中的两个调用之间修改 personNameIdMap,这会遇到并发问题。你最好这样做

PersonId id = personNameIdMap.get(newPersonName);
if (id != null){
    myPersonId.add(id);
} 
else 
{
    // something else
}

【讨论】:

  • 因为我的操作会少读多写。我想选择 2 会更安全。
  • 请务必阅读@TheLQ 的回答,它提出了一些要点
【解决方案2】:

Collections.synchronizedList 是最容易使用并且可能是最好的选择。它只是用synchronized 包装底层列表。请注意,多步操作(例如 for 循环)仍然需要您同步。

一些快速的事情

  • 除非确实需要,否则不要同步方法 - 它只会锁定 整个 对象,直到方法完成;几乎没有理想的效果
  • CopyOnWriteArrayList 是一个非常专业的列表,您很可能不需要它,因为您有一个 add 方法。它本质上是一个普通的ArrayList,但每次添加一些东西时,整个数组都会重新构建,这是一项非常昂贵的任务。它的线程安全,但并不是真正想要的结果

【讨论】:

  • 我喜欢这个答案。它提出了两个要点:(1)多步操作需要手动同步,(2)将方法标记为synchronized(相对于运行synchronized(someObj) { }块)锁定整个对象并将与任何其他@987654326竞争@方法。
【解决方案3】:
  1. 同步是使用线程的旧方式。避免使用它以支持主要在 java.util.concurrent 包中表达的新习惯用法。
  2. 见 1。
  3. CopyOnWriteArrayList 读取速度快,写入速度慢。如果您对其进行大量更改,它可能会开始拖累您的表现。

并发并不是关于在单个方法中使用哪种机制或类型的孤立选择。您需要从更高的层面来考虑它,以了解它的所有影响。

【讨论】:

  • 我不会将 synchronized 视为使用线程的“旧方式”——它只是一种阻塞方法,您需要对并发有足够的了解才能在需要时使用它足够了解并发性以使用非阻塞数据结构。 java.util.concurrent 中唯一的List<>(至少在Java 6 中,我忘记了Java 7 是否添加了任何东西)是CopyOnWriteArrayList,它适用于某些情况。否则,您将使用阻塞方法。
  • synchronized 仍然是做并发的主要方式,非常通用。其他所有内容(volatilejava.lang.concurrent.atomic)或多或少都是一种性能优化,仅适用于某些特定情况。 CopyOnWriteArrayList 将始终有效,但不是成功的性能优化,除非写入很少。同样,它并没有实现 synchronized 无法完成的任何事情,您只能使用它来尝试优化性能。
  • @Jason:我倾向于同意这样的想法,即自己同步事物是不必要的低级,而且很难做到正确。最好转向 java.util.concurrent 包中 JDK 5 和 6 提供的更高抽象级别。当我们有这么多更好、更简单的并发方法时,几乎没有理由在新开发中转向同步。总的来说,我要强调的一点是,做正确的并发需要一个高级的、横向的视图,而不是“我应该在这个方法中使用什么样的并发?”接近。
  • @Gravity:见之前的评论
  • @Ryan:好点; +1,因为末尾有“更高级别”的评论。
【解决方案4】:

您是否正在对这些方法中的personNameIdMap 进行更改,或者任何其他也应该同步访问的数据结构?如果是这样,将方法标记为同步可能是最简单的;否则,您可能会考虑使用Collections.synchronizedList 获取myPersonId 的同步视图,然后通过该同步视图执行所有列表操作。请注意,在这种情况下,您不应直接操作 myPersonId,而应仅通过从 Collections.synchronizedList 调用返回的列表进行所有访问。

无论哪种方式,您都必须确保永远不会出现对同一个不同步数据结构同时发生读取和写入或两次写入的情况。记录为线程安全或从Collections.synchronizedListCollections.synchronizedMap 等返回的数据结构是此规则的例外,因此可以将对这些数据结构的调用放在任何地方。但是,非同步数据结构仍然可以在声明为同步的方法中安全地使用,因为 JVM 保证这些方法不会同时运行,因此不可能有并发读/写。

【讨论】:

    【解决方案5】:

    就您发布的代码而言,所有 3 种方式都是可以接受的。但是,有一些特定的特征:

    #3:这应该具有与 #2 相同的效果,但可能运行得更快或更慢,具体取决于系统和工作负载。

    #1:这种方式最灵活。只有使用 #1 才能使 add() 和 delete() 方法更复杂。例如,如果您需要读取或写入列表中的多个项目,那么您不能使用#2 或#3,因为其他一些线程仍然可以看到列表被更新了一半。

    【讨论】:

    • 不,#3 总是更快。如果你有很多写入,它会很慢。请编辑您的答案,否则将被否决。
    【解决方案6】:

    Java 并发(多线程):

    并发是并行运行多个程序或程序的几个部分的能力。如果一项耗时的任务可以异步或并行执行,这将提高程序的吞吐量和交互性。

    我们可以使用 Java 进行并发编程。通过 java 并发,我们可以进行并行编程、不变性、线程、执行器框架(线程池)、futures、callables 和 fork-join 框架编程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-17
      • 1970-01-01
      • 2015-09-13
      • 2011-05-27
      • 1970-01-01
      • 2011-03-26
      相关资源
      最近更新 更多