Java并发 - 使用哪种技术来实现安全性?

4
我有一个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
}

我知道并发问题可能会出现。我了解了三种解决方案:

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

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


我建议同步列表,而不是方法。在这种情况下,我绝对会避免使用CopyOnWriteArrayList,因为它更适合遍历而不是变异。 - Moonbeam
从您的代码片段中,我不确定您是如何确定存在并发问题的?除非先添加了一个项目,否则无法删除该项目,如果该项目已经在列表中,则无法再次添加相同的项目。我猜我试图理解这里正在同时发生什么。 - CoolBeans
如果不同的线程调用adddelete,则可以同时向myPersonId添加和删除。 - Gravity
6个回答

4

1) 同步方法

2) 使用 Collections.synchronizedlist

3) CopyOnWriteArrayList ..

所有都可以使用,这取决于你需要哪种性能/功能。

方法 #1 和 #2 是阻塞方法。如果你同步方法,你需要自己处理并发。如果你在列表中包装 Collections.synchronizedList,它会为你处理。 (我认为 #2 更安全 - 只需按照文档指示使用它,并且不要让 任何东西 访问包装在 synchronizedList 中的原始列表。)

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


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

4)使用{{link1:Vector}}而不是ArrayListVector实现了List并且已经同步。然而,它通常被认为是老派的类(自Java 1.0以来就存在!),应该与#2等效。

5)仅从一个线程中串行访问List。如果您这样做,可以确保List本身没有任何并发问题。一种方法是使用{{link2:Executors.newSingleThreadExecutor}}逐个排队任务以访问列表。这将资源争用从列表移动到ExecutorService;如果任务很短,那么可能没问题,但如果某些任务很长,它们可能导致其他任务阻塞时间超过预期。

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

顺便提一下,在add()和delete()方法中,你调用了personNameIdMap.get(newPersonName)两次。如果另一个线程在每个方法中的两次调用之间修改了personNameIdMap,则会出现并发问题。最好的做法是

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

由于我的操作将更少地读取而更多地写入,我想选择选项2会更安全。 - harshit
请务必阅读@TheLQ的回答,它提出了一些重要观点。 - Jason S

4

Collections.synchronizedList 是使用最简单,可能也是最好的选择。它只是用 synchronized 包装底层列表。请注意,多步操作(例如 for 循环)仍需要由您进行同步。

一些快速提示

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

1
我喜欢这个答案。它提出了两个重要观点:(1)多步操作需要手动同步,(2)将方法标记为synchronized(而不是运行synchronized(someObj) { }块)会锁定整个对象,并与任何其他synchronized方法竞争。 - Jason S

3
  1. 在处理线程时,Synchronized是旧的方法,请使用新的习惯用语,大多数表达在java.util.concurrent包中。
  2. 参见1。
  3. CopyOnWriteArrayList读取快,写入慢。如果您对其进行了大量更改,则可能会影响性能。

并发不仅仅是单个方法中选择何种机制或类型的问题,您需要从更高的层面考虑它以理解所有影响。


2
我不会把synchronized视为处理线程的“老方法”——它只是一种阻塞方法,你需要了解足够的并发知识才能使用它,就像你需要了解足够的并发知识才能使用非阻塞数据结构一样。在java.util.concurrent中,唯一的List<>(至少在Java 6中,我忘记Java 7是否添加了其他内容)是适用于某些情况的CopyOnWriteArrayList。否则,你只能使用阻塞方法。 - Jason S
synchronized仍然是处理并发的主要方式,非常通用。其他所有方法(volatilejava.lang.concurrent.atomic)或多或少都是性能优化,只适用于某些特定情况。CopyOnWriteArrayList始终有效,但除非写入很少,否则不是成功的性能优化。同样,它没有实现任何不能使用synchronized完成的功能,您只会使用它来尝试优化性能。 - Gravity
@Jason:我倾向于同意这种思想,即自己同步事物是不必要的低级操作,并且极其难以正确实现。在Java JDK 5和6中提供的java.util.concurrent包中,使用更高层次的抽象级别要好得多。在新开发中,当我们有如此多更好、更容易的并发处理方式时,几乎没有理由使用synchronized。总的来说,我强调的重点是,正确地进行并发处理需要高层次的全局视野,而不是“在这个方法中应该使用什么样的并发处理?”的方法。 - Ryan Stewart
@Gravity:请查看之前的评论。 - Ryan Stewart
@Ryan:好观点;+1,因为在最后有“高级”评论。 - Jason S

2
根据您发布的代码,三种方式均可接受。然而,有一些特殊的特点:
#3:这应该与#2具有相同的效果,但在系统和工作负载不同的情况下可能会运行得更快或更慢。
#1:这种方式是最灵活的。只有使用#1才能使add()和delete()方法更加复杂。例如,如果您需要读取或写入列表中的多个项,则不能使用#2或#3,因为某些其他线程仍然可以看到列表正在半更新状态。

不,#3并不总是更快。如果有很多写操作,它会变慢。请编辑您的答案,否则将被投票降低评分。 - Jason S

2
你是否在这些方法中对personNameIdMap进行更改,或者访问其他需要同步的数据结构?如果是,最简单的方法是将这些方法标记为同步的;否则,你可以考虑使用Collections.synchronizedList获取myPersonId的同步视图,然后通过该同步视图执行所有列表操作。请注意,在这种情况下,不应直接操作myPersonId,而是仅通过从Collections.synchronizedList调用返回的列表进行所有访问。
无论哪种方式,你都必须确保永远不会出现同时读取和写入或两个写入操作同时发生在同一个未同步的数据结构上的情况。被记录为线程安全或从Collections.synchronizedListCollections.synchronizedMap等返回的数据结构是此规则的例外,因此可以在任何地方调用它们。但是,在声明为同步的方法内部仍然可以安全地使用非同步的数据结构,因为JVM保证这些方法永远不会同时运行,因此不会出现并发读/写的情况。

0

Java并发(多线程):

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

我们可以使用Java进行并发编程。通过Java并发,我们可以进行并行编程、不可变性、线程、执行器框架(线程池)、未来、可调用和分叉-合并框架编程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接