java - изменчивая семантика в ConcurrentHashMap

В ConcurrentHashMap JDK 8 методы tabAt и setTabAt используются для обеспечения энергозависимого чтения / записи первого элемента бинов в Node<K,V>[] table. Однако авторы отмечают, что:

Обратите внимание, что вызовы setTabAt всегда происходят в заблокированных областях, и поэтому в принципе требуется только порядок выпуска, а не полная семантика изменчивости, но в настоящее время они кодируются как изменчивые записи, чтобы быть консервативными.

Интересно, означает ли здесь порядок выпуска связь «происходит до» (разблокировка монитора происходит до каждой последующей блокировки того же монитора), гарантированную synchronized. И если да, то почему setTabAt считается консервативным, но не обязательным, учитывая, что вызовы tabAt существуют не только внутри, но и за пределами synchronized блоков? Например:

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //...
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // -> tabAt called here, outside the synchronized block
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    // -> tabAt called here, inside the synchronized block
                    if (tabAt(tab, i) == f) {
                        // do insertion...
                    }
                }
            }        
        }
    }

Другой вопрос, что в приведенном выше коде необходим вызов tabAt внутри блока synchronized? Насколько я понимаю, блокировка монитора уже заботится о видимости памяти между потоками, например:

  1. Предположим, что в корзине только один элемент, скажем, Узел 0.
  2. Поток A хочет вставить узел 1 в корзину сразу после узла 0, который он находит, вызывая tabAt вне блока synchronized.
  3. Но прежде чем Thread-A сможет заблокировать узел 0, Thread-B блокирует узел 0 и удаляет его (вызывая setTabAt)
  4. Поток-A получает блокировку узла-0 после того, как поток-B освободил блокировку
  5. Поскольку связь между потоком-A и поток-B "происходит раньше", гарантируется блокировкой монитора, в этом случае мне кажется, что нет необходимости вызывать tabAt (который, в свою очередь, вызывает Unsafe.getObjectVolatile) для доступа и повторной проверки элемент.

Любая помощь будет принята с благодарностью.


person yangty89    schedule 07.07.2020    source источник


Ответы (1)


В java-8 этот метод, который вы упомянули, определяется как:

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

Например, в jdk-13 это уже release:

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}

И, насколько я понимаю, должен работать в связке с:

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

Итак, вы можете думать как setTabAt как set release и tabAt как get acquire.

release здесь означает семантику выпуска / получения, о которой я как бы говорил в этот ответ. Дело в том, что volatile операций записи слишком много для некоторых случаев (последовательная согласованность), как здесь.

В исходном коде (jdk-13) есть комментарий, в котором говорится (включая putReferenceRelease), что это слабый (er) изменчивый:

Версии putReferenceVolatile ... которые не гарантируют немедленную видимость хранилища для других потоков ...

Часть synchronized дает гарантии видимости памяти только тогда, когда поток чтения также использует ту же блокировку; в противном случае все ставки отменены. Кажется, это та часть, которую вам не хватает. Вот более подробный ответ, объясняющий, как synchronized часть может быть сильно сломана.

person Eugene    schedule 07.07.2020
comment
Большое спасибо за ответ. Я потратил много дней, пытаясь полностью понять разницу между изменчивой семантикой и семантикой выпуска / получения в соответствии с предоставленными вами ссылками на статьи ... Наконец, я пришел к эта статья, которая, на мой взгляд, наиболее понятна для меня. - person yangty89; 11.07.2020
comment
Согласно упомянутой выше статье, Последовательно-согласованное упорядочивание не только упорядочивает память так же, как упорядочивание выпуска / получения, но также устанавливает единый общий порядок модификации всех атомарных операций, которые помечены таким образом ... Последовательные упорядочивание может быть необходимо для ситуаций несколько производителей - несколько потребителей, когда все потребители должны наблюдать за действиями всех производителей, происходящими в одном и том же порядке. Но мне интересно, гарантирует ли volatile в Java такой порядок модификации? Спасибо! - person yangty89; 11.07.2020
comment
@ yangty89 да, java volatile действительно обеспечивает последовательную согласованность. Подробнее об этом - person Eugene; 11.07.2020