您上面的代码不是线程安全的。想象一下:
add()
线程 A 在之后被搁置store.get()
- 线程 B 在 中
processAndClear()
,替换列表,处理旧列表的所有元素,然后返回。
- 线程 A 恢复并将新项目添加到现在已过时的列表中,该列表将永远不会被处理。
这里可能最简单的解决方案是使用LinkedBlockingQueue,这也可以大大简化任务:
class Store{
final LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void add(final Object o){
queue.put(o); // blocks until there is free space in the optionally bounded queue
}
void processAndClear(){
Object element;
while ((element = queue.poll()) != null) { // does not block on empty list but returns null instead
doSomething(element);
}
}
}
编辑:如何做到这一点synchronized
:
class Store{
final LinkedList<Object> queue = new LinkedList<>(); // has to be final for synchronized to work
void add(final Object o){
synchronized(queue) { // on the queue as this is the shared object in question
queue.add(o);
}
}
void processAndClear() {
final LinkedList<Object> elements = new LinkedList<>(); // temporary local list
synchronized(queue) { // here as well, as every access needs to be properly synchronized
elements.addAll(queue);
queue.clear();
}
for (Object e : elements) {
doSomething(e); // this is thread-safe as only this thread can access these now local elements
}
}
}
为什么这不是一个好主意
尽管这是线程安全的,但与并发版本相比,它要慢得多。假设您有一个系统有 100 个经常调用的线程add
,而一个线程调用processAndClear
。那么就会出现以下性能瓶颈:
- 如果一个线程调用
add
其他 99 个线程同时被搁置。
- 在
processAndClear
所有 100 个线程的第一部分被搁置。
如果您假设这 100 个添加线程无事可做,您可以很容易地证明,应用程序的运行速度与单线程应用程序的运行速度相同,但减去了同步成本。这意味着:100 个线程的添加实际上会比 1 个线程慢。如果您使用第一个示例中的并发列表,则情况并非如此。
然而,处理线程的性能会略有提升,因为doSomething
可以在添加新元素时在旧元素上运行。但是,并发示例可能会更快,因为您可以让多个线程同时进行处理。
Effectivelysynchronized
也可以使用,但是你会自动引入性能瓶颈,可能导致应用程序作为单线程运行速度较慢,迫使你进行复杂的性能测试。此外,扩展功能总是存在引入线程问题的风险,因为锁定需要手动完成。
相比之下,并发列表无需额外代码即可解决所有这些问题,并且代码可以在以后轻松更改或扩展。