Java线程间同步列表。最佳实践

6
我正在制作一个数据库日志引擎,当发生某些更改时会将这些更改推送到队列中,并在处理程序中的线程中每50毫秒处理25个LogObject。
我考虑使用Collections.synchronizedList()来存储我仍需要在线程中处理的对象。主应用程序线程通过ThreadObjInstance.LogList.add(new LogObject("Something to log"))将LogObjects推送到列表中,在线程中我通过LogList.shift()来处理它。
然而,我觉得可能有更好的方法来做这件事,或者这是一种完全可以接受的方法?还是说我应该为此情况使用ArrayBlockingQueue?或者其他同步List对象...有很多选择。
这是我第一次使用线程,所以我正在尝试弄清楚建立作业队列的最佳方法以及维护它所需使用的对象。我能直接将事物添加到线程列表中吗?还是我需要在线程中使用同步方法?
问题基本上是: 1. 在两个线程之间存储同步的对象列表(在处理线程中还是在主线程中)? 2. 添加/删除项目到列表的最佳实践是什么?通过同步函数还是直接在List对象上进行操作? 3. 构建作业队列时我的List对象选择有哪些?
3个回答

3
  1. 你可以选择任何一个所有线程都可以访问的位置。
  2. BlockingQueue专为此而设计,您无需自行进行任何同步。
  3. 虽然有很多选项,但在这种情况下(生产者-消费者),BlockingQueue是最直接的选择。

我将BlockingQueue添加到我的线程对象中,应该通过同步函数还是直接在队列上添加项目?哪种方式最好? - Tschallacka
1
BlockingQueue已经同步了,您不需要做任何事情。 - Kayaman
我还在适应新的线程 API。我上一次使用线程是 15 年前... 那时候完全不同了... 现在有这么多选项 o_O - Tschallacka

3
你的选择主要取决于当队列因某种原因被堵塞时你想发生什么以及你想使用多少内存。
如果你不介意暂停主进程直到日志线程清除了一些队列,那么一个ArrayBlockingQueue就可以了,因为它是有界的(固定大小),在高负载下不会占用过多内存。
如果你在日志记录被阻塞时可以忽略内存问题(也许你确定日志线程总是能跟上),那么LinkedBlockingQueue可能更适合,因为它稍微更优化,而且是无界的。它也可以给出一个大小限制,但这是可选的。
如果你使用其中任何一个,你不需要添加任何同步逻辑,因为它们自己完成了所有操作。
“在两个线程之间处理对象的同步列表应该存储在哪里(在处理线程还是主线程中)?”
任何一个都可以-通常情况下,你会在主线程中创建它,并将其传递给日志线程和处理线程,因为它们都将共享它。
“向列表中添加/删除项目的最佳实践是什么?通过同步函数还是直接在列表对象上操作?” BlockingQueue提供了丰富的线程安全添加和删除项目的API。
“构建作业队列时,我有哪些列表对象可供选择?”
见上文。

好的。现在有一些选项,其中每个滴答(50毫秒)会积压2000个项目,但这些选项很短暂,应该占用大约1MB(可能夸张了,更可能是100KB或其他)的RAM,每个滴答突然增加,但是也有很长时间什么都不会发生,每个滴答只添加3-4个项目。但是它可以在负载是其20倍的繁忙环境中使用。因此,我更喜欢最低可能的配置文件。根据您的意见,我应该选择LinkedBlockingQueue吗? - Tschallacka
1
@MichaelDibbets - 对我来说,考虑到环境因素,这似乎是您最好的方法。当队列为空时,LinkedBlockingQueue 的占用空间将几乎为零。值得一提的是,在队列上设置一个上限,比如5000,并使用 add 方法,这样在异常情况下会抛出异常,或者如果您愿意在这种情况下丢失日志条目,则可以使用 offer 方法。 - OldCurmudgeon
很不幸,我不能承受除了在关键的系统故障之外失去任何条目。我的应用程序的整个重点是它提供了一个外部详细的日志记录功能,因此可以记录谁做了什么,并且更改可以回滚。比如如果有100,000个积压任务,我总是可以向系统管理员发出临界警告消息,因为这不应该发生,如果主线程中有足够的时钟周期剩余,我总是可以选择加速处理以消除积压。感谢您的详细解释。 - Tschallacka
2
在这种情况下,如果处理变得困难并且您的队列增长超出限制,那么您必须阻止处理。要么是这样,要么是那样。(BlockingQueue.put()是插入方法,如果队列已满,则会阻塞。) - biziclop
@OldCurmudgeon 是的,处理方面有许多可用的策略。但从主线程的角度来看,只有两个选项,即减速(由于完全阻塞、限流或更多的处理线程占用更多的 CPU)或丢失消息。这是另一个很好的理由,为什么队列应该“属于”处理线程,因为所有潜在的复杂逻辑都在它的一边。 - biziclop
显示剩余2条评论

1
  1. 在这种情况下,队列似乎属于处理实体,即日志线程。但这只是一个概念上的决定,在一天结束时,它们都必须持有对某种共享对象的引用。
  2. 最好只公开一个添加项目的方法(您真的不希望删除),而不是公开整个列表以供滥用。同样,这与同步无关,这只是封装的基本原则,或者如果您愿意,可以使用Demeter法则:只允许客户端尽可能少地执行操作。除了“我可以在这里放置事件”之外,主线程没有理由知道您如何实现队列(甚至是否存在队列)。
  3. 队列最好由LinkedList表示,但由于队列可能很小且操作不在关键路径上,所以选择可能不重要。使用最容易阅读和理解的内容。 (我会选择@Kayaman提出的BlockingQueue,因为它为您节省了很多工作,并且任何阅读代码的人都可以轻松地了解其中发生了什么。)

重点是,选项可能很少,但也可能会突然变得非常多。有时候会出现每秒推送2000个项目的情况,然后又长时间没有任何操作。25只是为了确保日志线程在需要处理2000个更改时不会占用CPU,而是继续前进。 - Tschallacka
@MichaelDibbets 我明白了,这确实稍微改变了情况。每秒2000次并不会对内存造成太大的压力,但你需要良好的插入性能,这可能会倾向于基于链表的解决方案。 - biziclop

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