Різні типи захищених від потоків наборів на Java


135

Здається, існує багато різних реалізацій та способів генерації безпечних наборів наборів у Java. Деякі приклади включають

1) CopyOnWriteArraySet

2) Collections.synchronizedSet (встановити набір)

3) ConcurrentSkipListSet

4) Collections.newSetFromMap (новий ConcurrentHashMap ())

5) Інші набори, сформовані аналогічно (4)

Ці приклади походять із шаблону сумісності: Реалізація паралельного набору в Java 6

Невже хтось може просто пояснити відмінності, переваги та недоліки цих прикладів та інших? У мене виникають проблеми з розумінням та прямим дотриманням всього документа від Java Std Docs.

Відповіді:


206

1) Це CopyOnWriteArraySetдосить проста реалізація - вона в основному має список елементів у масиві, а при зміні списку копіює масив. Ітерації та інші звернення, що працюють у цей час, тривають зі старим масивом, уникаючи необхідності синхронізації між читачами та письменниками (хоча саме написання потрібно синхронізувати). Нормально швидкі задані операції (особливо contains()) тут досить повільні, оскільки масиви будуть шукати в лінійний час.

Використовуйте це лише для дійсно невеликих наборів, які читатимуться (повторюються) часто та змінюються рідко. (Набори для слухачів Swings - це приклад, але це насправді не безліч, і їх слід використовувати лише з EDT.)

2) Collections.synchronizedSetпросто оберне синхронізований блок навколо кожного методу вихідного набору. Ви не повинні отримувати доступ до оригінального набору безпосередньо. Це означає, що жоден два способи набору не можуть бути виконані одночасно (один буде блокувати, поки інший не закінчиться) - це безпечно для потоків, але у вас не буде сумісності, якщо кілька потоків дійсно використовують набір. Якщо ви використовуєте ітератор, вам все одно потрібно синхронізувати зовнішню сторону, щоб уникнути ConcurrentModificationExceptions при зміні набору між викликами ітератора. Виконання буде подібне до виконання оригінального набору (але з деякими синхронізацією та блокуванням при одночасному використанні).

Використовуйте це, якщо у вас лише низька конкурентоспроможність, і хочете бути впевненими, що всі зміни одразу видно в інших потоках.

3) ConcurrentSkipListSet- це паралельна SortedSetреалізація, що має більшість основних операцій в O (log n). Це дозволяє одночасно додавати / видаляти та читати / ітерацію, де ітерація може або не може говорити про зміни з моменту створення ітератора. Масові операції - це просто кілька одноразових викликів, а не атомно - інші потоки можуть спостерігати лише деякі з них.

Очевидно, ви можете використовувати це лише в тому випадку, якщо у вас є загальний порядок на ваших елементах. Це виглядає як ідеальний кандидат для ситуацій з високою конкурентоспроможністю, для не надто великих наборів (через O (log n)).

4) Для ConcurrentHashMap(та набору, отриманого з нього): Тут найбільш основні варіанти (в середньому, якщо у вас хороший і швидкий hashCode()) в O (1) (але можуть перерости до O (n)), як для HashMap / HashSet Існує обмежена паралельність запису (таблиця розділена, і доступ для запису буде синхронізований на потрібному розділі), тоді як доступ до читання повністю відповідає собі та потокам запису (але може ще не бачити результатів змін, що зараз написано). Ітератор може або не може побачити зміни з моменту його створення, а масові операції не є атомними. Змінення розміру відбувається повільно (як для HashMap / HashSet), тому спробуйте уникнути цього, оцінивши необхідний розмір при створенні (і використовуючи приблизно на 1/3 більше цього, оскільки він змінюється при заповненні 3/4).

Використовуйте це, коли у вас є великі набори, хороша (і швидка) хеш-функція, і ви можете оцінити розмір набору та необхідну одночасність перед створенням карти.

5) Чи існують інші паралельні реалізації карт, які можна використати тут?


1
Тільки виправлення зору в 1), процес копіювання даних у новий масив повинен бути заблокований синхронізацією. Тому CopyOnWriteArraySet не повністю уникає необхідності синхронізації.
КапітанГастінгс

На ConcurrentHashMapоснові множини "таким чином намагайтеся уникнути цього, оцінюючи необхідний розмір для створення". Розмір, який ви надаєте карті, повинен бути на 33% більше, ніж ваша оцінка (або відома величина), оскільки набір змінюється на 75% завантаження. Я використовуюexpectedSize + 4 / 3 + 1
Дарен

@Daren Я думаю, перший +призначений бути *?
Paŭlo Ebermann

@ PaŭloEbermann Звичайно ... так і має бутиexpectedSize * 4 / 3 + 1
Дарен

1
Для ConcurrentMap(або HashMap) в Java 8, якщо кількість записів, які відображають в одне відро, досягає порогового значення (я вважаю, це 16), тоді список буде змінено на двійкове дерево пошуку (червоно-чорне дерево, яке потрібно точно визначити), і в цьому випадку шукайте час був би O(lg n)і ні O(n).
akhil_mittal

20

Можна комбінувати contains()продуктивність HashSetіз властивостями, пов'язаними з паралельністю CopyOnWriteArraySet, використовуючи AtomicReference<Set>та замінюючи весь набір на кожній модифікації.

Ескіз реалізації:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}

Фактично AtomicReferenceпозначає значення мінливим. Це означає, що він гарантує, що жоден потік не зчитує застарілі дані та надає happens-beforeгарантію, оскільки компілятор не може переупорядкувати код. Але якщо використовуються тільки get / set методи AtomicReference, то ми фактично маркуємо нашу змінну мінливою в химерному вигляді.
akhil_mittal

Ця відповідь не може бути сприйнята достатньою мірою, тому що (1) якщо я щось не пропустив, він буде працювати для всіх типів колекції (2) жоден з інших класів не дає можливість одразу оновити всю колекцію одразу ... Це дуже корисно .
Гілі

Я спробував привласнити цей дослівний текст, але виявив, що він позначений міткою abstract, здавалося б, щоб уникнути необхідності писати кілька методів. Я почав додавати їх, але наткнувся на блокпост із iterator(). Я не знаю, як підтримувати ітератор над цією річчю, не порушуючи модель. Здається, мені завжди доводиться проходити через ref, і кожен раз може отримувати інший базовий набір, що вимагає отримання нового ітератора на базовому наборі, який для мене марний, оскільки він починатиметься з нуля. Будь-які уявлення?
nclark

Гаразд, я думаю, гарантія полягає в тому, що кожен клієнт отримує фіксований знімок у часі, тож ітератор основної колекції буде добре працювати, якщо це все, що вам потрібно. Моє використання полягає в тому, щоб дозволити конкуруючим потокам "вимагати" в ньому окремі ресурси, і це не буде працювати, якщо вони мають різні версії набору. По-друге, все ж ... Я думаю, що моєму потоку потрібно просто отримати новий ітератор і спробувати знову, якщо CopyOnWriteSet.remove (selected_item) поверне помилкове значення ... Що це повинно робити незалежно :)
nclark

11

Якщо Javadocs не допомагає, ви, мабуть, просто знайдете книгу чи статтю, щоб прочитати про структури даних. З одного погляду:

  • CopyOnWriteArraySet створює нову копію базового масиву щоразу, коли ви мутуєте колекцію, тому записи проходять повільно, а Ітератори - швидкі та послідовні.
  • Collections.synchronizedSet () використовує виклики синхронізованого методу старої школи для встановлення безпечного потоку. Це була б малоефективна версія.
  • ConcurrentSkipListSet пропонує виконавцеві записи з непослідовними пакетними операціями (addAll, removeAll тощо) та Ітераторами.
  • Collections.newSetFromMap (новий ConcurrentHashMap ()) має семантику ConcurrentHashMap, яка, на мою думку, не обов'язково оптимізована для читання чи запису, але, як і ConcurrentSkipListSet, має непослідовні пакетні операції.

1
developer.com/java/article.php/10922_3829891_2/… <навіть краще, ніж книга)
ycomp

1

Паралельний набір слабких посилань

Ще один поворот - безпечний набір потоків слабких посилань .

Такий набір зручний для відстеження передплатників у сценарії pub-sub . Коли абонент виходить за межі сфери дії в інших місцях і, отже, прямує до того, щоб стати кандидатом на вивезення сміття, абоненту не потрібно перешкоджати граціозній підписці. Слабка довідка дозволяє абоненту завершити свій перехід до того, щоб бути кандидатом на вивезення сміття. Коли сміття в підсумку збирається, запис у наборі видаляється.

Хоча жоден такий набір не надається безпосередньо з пакетними класами, ви можете створити його з кількома викликами.

Спочатку ми почнемо із створення Setслабких посилань, використовуючи WeakHashMapклас. Це показано в документації на клас для Collections.newSetFromMap.

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

Значення карти, Booleanє тут недоречно як ключ карти становить наші Set.

У такому сценарії, як pub-sub, нам потрібна безпека потоків, якщо передплатники та видавці працюють на окремих потоках (цілком ймовірно, що це так).

Пройдіть ще крок далі, загорнувшись як синхронізований набір, щоб зробити цей набір безпечним. Перейти до дзвінка до Collections.synchronizedSet.

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

Тепер ми можемо додавати та видаляти підписників із отриманих нами результатів Set. І будь-які "зникаючі" підписники з часом будуть автоматично видалені після виконання сміттєзбірників. Коли це виконання відбудеться, залежить від реалізації сміттєзбірника вашого JVM і залежить від ситуації виконання на даний момент. Для обговорення та прикладу того, коли та як основні WeakHashMapдані очищають прострочені записи, див. Це запитання, * Чи WeakHashMap постійно зростає, або чи видаляються ключі від сміття? * .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.