实现同步addAll到java中的列表

时间:2012-03-08 17:25:59

标签: java collections thread-safety

  

更新了问题..请检查问题的第二部分

我需要建立一个主要的图书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);
        }
    }
}

谢谢, 哈里什

4 个答案:

答案 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。