достижение синхронизированного addAll к списку в Java - PullRequest
1 голос
/ 08 марта 2012

Обновлен вопрос .. пожалуйста, проверьте секодн часть вопроса

Мне нужно составить основной список идентификаторов книг.У меня есть несколько многопоточных задач, которые поднимают подмножество идентификаторов книг.Как только выполнение каждой задачи завершено, мне нужно добавить их в супер список идентификаторов книг.Следовательно, я планирую передать экземпляр класса-агрегатора всем моим задачам выполнения и заставить их вызывать метод updateBookIds ().Чтобы обеспечить безопасность потоков, я сохранил код addAll в синхронизированном блоке.

Может ли кто-нибудь посоветовать, что это то же самое, что синхронизированный список?Могу ли я просто сказать 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 ]

3 голосов
/ 08 марта 2012

Код, который вы реализовали, не создает точку синхронизации для того, кто получает доступ к списку через 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;
        }
    }
}

Как упоминалось ранее, в приведенном выше коде все еще есть потенциальная проблема с некоторым потоком, обращающимся к ArrayList после того, как он был получен getBookIds().Поскольку сам 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 (например, удалить метод получения и установки).Если вы обязуете, чтобы весь доступ к списку выполнялся с помощью методов, написанных в SynchronizedBookIdsAggregator, то SynchronizedBookIdsAggregator может обеспечить все управление параллелизмом списка.Как показывает мой ответ выше, предоставление потребителям агрегатора возможности использовать «геттер» для получения списка создает проблему для пользователя этого списка: для написания правильного кода им необходимо знать стратегию синхронизации / защитный объект, и, более того, онитакже необходимо использовать эти знания для активной внешней и правильной синхронизации.


Относительно вашего второго подхода.То, что вы показали, выглядит технически правильно (хорошо!).

Но , возможно, в какой-то момент вы тоже будете читать с masterBookIds?И вы не показываете и не описываете эту часть программы!Поэтому, когда вы начинаете думать о том, когда и как вы собираетесь читать masterBookIds (т. Е. Возвращаемое значение fetchMasterListOfBookIds()), просто не забывайте также учитывать проблемы параллелизма!:)

Если вы убедитесь, что все задачи / рабочие потоки завершены до того, как вы начнете читать masterBookIds, вам не нужно делать ничего особенного.

Но, по крайней мере, в показанном вами коде вы не гарантируете этого.

Обратите внимание, что libraryBookIdsExecutor.shutdown() возвращается немедленно.Поэтому, если вы начнете использовать список masterBookIds сразу после возврата fetchMasterListOfBookIds(), вы будете читать masterBookIds , в то время как ваши рабочие потоки активно записывают в него данные, и это влечет за собой некоторые дополнительные соображения.

Может быть, это то, что вы хотите - может быть, вы хотите прочитать коллекцию во время ее записи, чтобы показать результаты в реальном времени или что-то в этом роде.Но тогда вы должны рассмотреть возможность правильной синхронизации коллекции, если вы хотите выполнить итерацию по ней во время записи.

Если вы просто хотите убедиться, что все записи в masterBookIds рабочими потоками завершены доfetchMasterListOfBookIds() возвращает, вы можете использовать ExecutorService.awaitTermination (в сочетании с .shutdown(), который вы уже звоните).

3 голосов
/ 08 марта 2012

Могу ли я просто сказать Collections.newSynchronizedList и вызвать addAll в этот список из всех задач потока?

Если вы имеете в виду Collections.synchronizedList, то да, это будет работать нормально. Это даст вам объект, который реализует интерфейс List, где синхронизируются все методы этого интерфейса, включая addAll.

Рассмотрите возможность придерживаться того, что у вас есть, так как это возможно более чистый дизайн. Если вы передадите необработанный List своим задачам, они получат доступ ко всем методам в этом интерфейсе, в то время как все, что им действительно нужно знать, - это метод addAll. Использование вашего SynchronizedBookIdsAggregator позволяет отделить ваши задачи от проектной зависимости от интерфейса List и избавляет их от соблазна вызывать что-то отличное от addAll.

В подобных случаях я склонен искать какой-то интерфейс Sink, но, кажется, его никогда не бывает, когда он мне нужен ...

0 голосов
/ 08 марта 2012

Все методы, вызываемые с использованием оболочки, возвращаемой Collections.synchronizedList (), будут синхронизированы. Это означает, что метод addAll обычного List при вызове этой оболочкой будет выглядеть примерно так: -

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 возобновится. Таким образом, список будет иметь ["привет", "привет", "мир", "мир"] вместо ["привет", "мир", привет "," мир "], как и ожидалось. Это всего лишь пример показать, что список не является потокобезопасным между различными вызовами методов списка.Если мы хотим, чтобы приведенный выше код имел желаемый результат, он должен быть внутри синхронизированного блока с блокировкой списка (или этим).

Однако в вашем дизайне есть только один вызов метода. ТАК ЖЕ ИСПОЛЬЗУЕТСЯ Collections.synchronizedList().

Более того, как правильно заметил Майк Кларк, вам также следует синхронизировать getBookIds () и setBookIds (). И синхронизировать его с самим списком было бы более понятно, так как это похоже на блокировку списка перед его работой и разблокировку его после работы. Так что ничто между ними не может использовать Список.

0 голосов
/ 08 марта 2012

Collections.SynchronizedList (это тип обёртки, который вы получите) синхронизирует почти каждый метод либо с самим собой, либо с объектом мьютекса, который вы передаете конструктору (или Collections.synchronizedList(...)). Таким образом, это будет в основном так же, как ваш подход.

...