Параллелизм Java на практике. Листинг 5.6 - PullRequest
3 голосов
/ 07 марта 2019

В Java Concurrency in Practice автор приводит следующий пример не поточного безопасного класса, который за кулисами вызывает итератор для объекта set и, если задействовано несколько потоков, это может вызвать ConcurrentModificationException. Это понятно: один поток изменяет коллекцию, другой перебирает ее и, - boom!

Что я не понимаю, - автор говорит, что этот код можно исправить, добавив HashSet к Collections.synchronizedSet(). Как это решит проблему? Несмотря на то, что доступ ко всем методам будет синхронизирован и защищен одной и той же внутренней блокировкой, после получения объекта итератора нет гарантии, что другой поток не изменит коллекцию после выполнения итерации.

Цитата из книги:

Если HiddenIterator обернет HashSet синхронизированным набором, инкапсулируя синхронизацию, такого рода ошибки не возникнут.

public class HiddenIterator {

    //Solution : 
    //If HiddenIterator wrapped the HashSet with a synchronizedSet, encapsulating the synchronization, 
    //this sort of error would not occur. 
    //@GuardedBy("this") 
    private final Set<Integer> set = new HashSet<Integer>();

    public synchronized void add(Integer i) {
        set.add(i);
    }

    public synchronized void remove(Integer i) {
        set.remove(i);
    }

    public void addTenThings() {
        Random r = new Random();
        for (int i = 0; i < 10; i++)
            add(r.nextInt());
        /*The string concatenation gets turned by the compiler into a call to StringBuilder.append(Object), 
         * which in turn invokes the collection's toString method - and the implementation of toString in 
         * the standard collections iterates the collection and calls toString on each element to
         * produce a nicely formatted representation of the collection's contents. */
        System.out.println("DEBUG: added ten elements to " + set);
    }
}

Если бы кто-нибудь мог помочь мне понять это, я был бы благодарен.

Вот как я думаю, это могло быть исправлено:

public class HiddenIterator {

    private final Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());

    public void add(Integer i) {
        set.add(i);
    }

    public void remove(Integer i) {
        set.remove(i);
    }

    public void addTenThings() {
        Random r = new Random();
        for (int i = 0; i < 10; i++)
            add(r.nextInt());
        // synchronizing in set's intrinsic lock
        synchronized(set) {
            System.out.println("DEBUG: added ten elements to " + set);
        }
    }
}

Или, в качестве альтернативы, можно оставить ключевое слово synchronized для методов add() и remove(). В этом случае мы синхронизируемся на this. Кроме того, нам нужно добавить синхронизированный блок (снова синхронизированный на this) в addTenThings(), который будет содержать одну операцию - запись с неявной итерацией:

public class HiddenIterator {

    private final Set<Integer> set = new HashSet<Integer>();

    public synchronized void add(Integer i) {
        set.add(i);
    }

    public synchronized void remove(Integer i) {
        set.remove(i);
    }

    public void addTenThings() {
        Random r = new Random();
        for (int i = 0; i < 10; i++)
            add(r.nextInt());
        synchronized(this) {
            System.out.println("DEBUG: added ten elements to " + set);
        }
    }
}

Ответы [ 2 ]

3 голосов
/ 07 марта 2019

Collections.synchronizedSet() упаковывает коллекцию в экземпляр внутреннего класса с именем SynchronizedSet, расширяющий SynchronizedCollection. Теперь давайте посмотрим, как реализовано SynchronizedCollection.toString():

public String toString() {
    synchronized (mutex) {return c.toString();}
}

По сути, итерация все еще там, скрытая в вызове c.toString(), но она уже синхронизирована со всеми другими методами этой коллекции-обертки. Поэтому вам не нужно повторять синхронизацию в вашем коде.

0 голосов
/ 07 марта 2019

Отредактировано

synchronizedSet () :: toString ()

Как правильно заметил Сергей Петунин, метод toString() Collections.synchronizedSet() внутренне заботится осинхронизации, поэтому в этом случае ручная синхронизация не требуется.

внешняя итерация для synchronizedSet ()

после получения объекта итератора нет никакой гарантии, что другой поток выиграл 't изменить коллекцию после выполнения итерации.

В случаях внешней итерации, например, с использованием for-each или Iterator, подход с инкапсуляцией этой итерации вsynchronize(set) блок необходим / достаточен.

Вот почему JavaDoc Collections.synchronizedSet() утверждает, что

Крайне важно, чтобы пользователь вручную синхронизировална возвращенном отсортированном наборе при итерации по нему или любому из его subSet, headSet или tailSet представлений.

  SortedSet s = Collections.synchronizedSortedSet(new TreeSet());
      ...   
  synchronized (s) {
      Iterator i = s.iterator(); // Must be in the synchronized block
      while (i.hasNext())
          foo(i.next());   
  }

ручная синхронизация

Ваша вторая версияс помощью метода synchronized add / removeс классами HiddenIterator и synchronize(this) тоже будут работать, но это приводит к ненужным накладным расходам, поскольку добавление / удаление будет синхронизироваться дважды (HiddenIterator и Collections.synchronizedSet(..).

Однако в этом случае выможет опустить Collections.synchronizedSet(..), так как HiddenIterator позаботится обо всей синхронизации, необходимой при доступе к закрытому полю Set.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...