更新了问题..请检查问题的第二部分
我需要建立一个主要的图书ID列表。我有多个线程任务,它们会带来一部分书籍ID。每个任务执行完成后,我都需要将它们添加到book id的超级列表中。因此,我计划将下面的聚合器类实例传递给我的所有执行任务,并让它们调用updateBookIds()方法。为了确保它的线程安全,我将addAll代码保存在synchronized块中。
任何人都可以提出与同步列表相同的内容吗?我可以只说Collections.newSynchronizedList并从所有线程任务调用addAll到该列表吗?请澄清。
public class SynchronizedBookIdsAggregator {
private List<String> bookIds;
public SynchronizedBookIdsAggregator(){
bookIds = new ArrayList<String>();
}
public void updateBookIds(List<String> ids){
synchronized (this) {
bookIds.addAll(ids);
}
}
public List<String> getBookIds() {
return bookIds;
}
public void setBookIds(List<String> bookIds) {
this.bookIds = bookIds;
}
}
谢谢, 哈里什
第二种方法
因此,经过以下讨论,我目前正计划采用以下方法。如果我在这里做错了,请告诉我: -
public class BooksManager{
private static Logger logger = LoggerFactory.getLogger();
private List<String> fetchMasterListOfBookIds(){
List<String> masterBookIds = Collections.synchronizedList(new ArrayList<String>());
List<String> libraryCodes = getAllLibraries();
ExecutorService libraryBookIdsExecutor = Executors.newFixedThreadPool(BookManagerConstants.LIBRARY_BOOK_IDS_EXECUTOR_POOL_SIZE);
for(String libraryCode : libraryCodes){
LibraryBookIdsCollectionTask libraryTask = new LibraryBookIdsCollectionTask(libraryCode, masterBookIds);
libraryBookIdsExecutor.execute(libraryTask);
}
libraryBookIdsExecutor.shutdown();
//Now the fetching of master list is complete.
//So I will just continue my processing of the master list
}
}
public class LibraryBookIdsCollectionTask implements Runnable {
private String libraryCode;
private List<String> masterBookIds;
public LibraryBookIdsCollectionTask(String libraryCode,List<String> masterBookIds){
this.libraryCode = libraryCode;
this.masterBookIds = masterBookIds;
}
public void run(){
List<String> bookids = new ArrayList<String>();//TODO get this list from iconnect call
synchronized (masterBookIds) {
masterBookIds.addAll(bookids);
}
}
}
谢谢, 哈里什
答案 0 :(得分:3)
我可以只说
Collections.newSynchronizedList
并从所有线程任务中调用addAll
到该列表吗?
如果您指的是Collections.synchronizedList
,那么是的,这样可以正常工作。这将为您提供一个实现List
接口的对象,该接口将同步该接口中的所有方法,包括addAll
。
考虑坚持使用你拥有的东西,因为它可以说是一个更清洁的设计。如果将原始List
传递给您的任务,那么他们就可以访问该接口上的所有方法,而他们真正需要知道的是有一个addAll
方法。使用SynchronizedBookIdsAggregator
可以使您的任务与List
界面上的设计依赖性分离,并消除他们调用addAll
之外的其他内容的诱惑。
在这种情况下,我倾向于寻找某种类型的Sink
界面,但在我需要的时候似乎永远不会有... ...
答案 1 :(得分:3)
您实施的代码不会为通过getBookIds()
访问列表的人创建同步点,这意味着他们可以看到不一致的数据。此外,通过getBookIds()
检索到列表的人必须在访问列表之前执行外部同步。您的问题也没有说明您实际使用的是SynchronizedBookIdsAggregator类,这使我们没有足够的信息来完全回答您的问题。
以下是该课程的更安全版本:
public class SynchronizedBookIdsAggregator {
private List<String> bookIds;
public SynchronizedBookIdsAggregator() {
bookIds = new ArrayList<String>();
}
public void updateBookIds(List<String> ids){
synchronized (this) {
bookIds.addAll(ids);
}
}
public List<String> getBookIds() {
// synchronized here for memory visibility of the bookIds field
synchronized(this) {
return bookIds;
}
}
public void setBookIds(List<String> bookIds) {
// synchronized here for memory visibility of the bookIds field
synchronized(this) {
this.bookIds = bookIds;
}
}
}
如前所述,上面的代码仍然存在潜在的问题,一些线程在getBookIds()
检索到ArrayList之后访问它。由于ArrayList本身不是同步的,因此在检索它之后访问它应该在所选的保护对象上同步:
public class SomeOtherClass {
public void run() {
SynchronizedBookIdsAggregator aggregator = getAggregator();
List<String> bookIds = aggregator.getBookIds();
// Access to the bookIds list must happen while synchronized on the
// chosen guard object -- in this case, aggregator
synchronized(aggregator) {
<work with the bookIds list>
}
}
}
我可以想象使用Collections.newSynchronizedList
作为此聚合器设计的一部分,但它不是灵丹妙药。并发设计确实需要了解潜在的问题,而不是“为工作挑选合适的工具/集合”(尽管后者并不重要)。
另一个可能的选择是CopyOnWriteArrayList
。
正如skaffman所提到的,最好不要直接访问bookIds列表(例如,删除getter和setter)。如果强制所有对列表的访问必须通过SynchronizedBookIdsAggregator
中编写的方法运行,那么SynchronizedBookIdsAggregator
可以强制执行列表的所有并发控制。正如我上面的回答所示,允许聚合器的使用者使用“getter”来获取列表会给该列表的用户带来问题:要编写正确的代码,他们必须知道同步策略/保护对象,此外他们还必须利用这些知识积极地在外部和正确地进行同步。
关于你的第二种方法。您所展示的内容在技术上看是正确的(好!)。
但是,大概你也会在某个时候从masterBookIds
读取?并且您不会显示或描述该程序的那一部分!因此,当您开始考虑何时以及如何阅读masterBookIds
(即fetchMasterListOfBookIds()
的返回值)时,请记住同时考虑并发问题! :)
如果在开始阅读masterBookIds
之前确保所有任务/工作线程都已完成,则不必执行任何特殊操作。
但是,至少在您展示的代码中,您无法确保这一点。
请注意,libraryBookIdsExecutor.shutdown()
会立即返回。因此,如果您在masterBookIds
返回后立即开始使用fetchMasterListOfBookIds()
列表,那么当您的工作线程正在积极地向其写入数据时,您将阅读masterBookIds
,这样需要一些额外的考虑。
也许这就是你想要的 - 也许你想在写入时阅读该系列,以显示实时结果或其他内容。但是,如果要在写入时迭代它,则必须考虑在集合上正确同步。
如果您只想确保工作线程对masterBookIds
的所有写入都在fetchMasterListOfBookIds()
返回之前完成,您可以使用ExecutorService.awaitTermination
(与.shutdown()
结合使用,你已经打电话了。)
答案 2 :(得分:0)
Collections.SynchronizedList
(这是你得到的包装器类型)会同步几乎每个方法本身或你传递给构造函数(或Collections.synchronizedList(...)
)的互斥对象。因此,它基本上与您的方法相同。
答案 3 :(得分:0)
使用Collections.synchronizedList()返回的包装器调用的所有方法都将被同步。这意味着当这个包装器调用时,普通List的addAll方法将是这样的: -
synchronized public static <T> boolean addAll(Collection<? super T> c, T... elements)
因此,对列表的每个方法调用(使用返回的引用而不是原始引用)将被同步。
但是,不同方法调用之间没有同步。 请考虑以下代码段: -
List<String> l = Collections.synchronizedList(new ArrayList<String>);
l.add("Hello");
l.add("World");
当多个线程访问相同的代码时,很可能在线程A添加“Hello”之后,线程B将启动并再次将“Hello”和“World”添加到列表中,然后线程A继续。所以,list会有[“hello”,“hello”,“world”,“world”]而不是[“hello”,“world”,“hello”,“world”]正如预料的那样。这只是一个例子。表明列表在列表的不同方法调用之间不是线程安全的。如果我们希望上面的代码具有所需的结果,那么它应该在带有锁定列表的同步块内(或者这个)。
但是,根据您的设计,只有一个方法调用。这与使用Collections.synchronizedList()
相同。
此外,正如Mike Clark正确指出的那样,你还应该同步getBookIds()和setBookIds()。并且通过List本身进行同步将更加清晰,因为它类似于在操作之前锁定列表并在操作之后解锁它。因此,中间没有任何东西可以使用List。