HashBiMap блокируется - PullRequest
       62

HashBiMap блокируется

1 голос
/ 06 мая 2020

Мы используем HashBiMap в нашем приложении, и мы создали его вот так

HashBiMap<String, String> map = HashBiMap.create();

, и у нас есть версия guava-26.0-jre для guava.

Недавно заметили, что наше приложение зависает и не может обрабатывать другие запросы. Получил дамп потока и заметил такие вещи, как эти

Thread.State: RUNNABLE
native=false, suspended=false, blockCount=1, waitCount=4, cpu=9h, 12m, 45s, 434ms, user=9h, 10m, 59s, 990ms
    com.google.common.collect.HashBiMap.seekByKey(HashBiMap.java:223)
    com.google.common.collect.HashBiMap.get(HashBiMap.java:254)

Thread.State: RUNNABLE
native=false, suspended=false, blockCount=1, waitCount=6, cpu=9h, 11m, 49s, 453ms, user=9h, 10m, 3s, 760ms
    com.google.common.collect.HashBiMap.seekByKey(HashBiMap.java:223)
    com.google.common.collect.HashBiMap.get(HashBiMap.java:254)

Thread.State: RUNNABLE
native=false, suspended=false, blockCount=274, waitCount=6615, cpu=22h, 31m, 29s, 966ms, user=22h, 27m, 30s, 540ms
    com.google.common.collect.HashBiMap.seekByKey(HashBiMap.java:223)
    com.google.common.collect.HashBiMap.get(HashBiMap.java:254)

Thread.State: RUNNABLE
native=false, suspended=false, blockCount=91, waitCount=2443, cpu=22h, 29m, 51s, 541ms, user=22h, 25m, 54s, 140ms
    com.google.common.collect.HashBiMap.seekByKey(HashBiMap.java:223)
    com.google.common.collect.HashBiMap.get(HashBiMap.java:254)

Было несколько других потоков, подобных выше, которые были заблокированы при вызове для получения, но самое долгое ожидание было с cpu = 22h, 31m, 29s , 966 мс

Thread.State: RUNNABLE
native=false, suspended=false, blockCount=3, waitCount=32, cpu=5h, 46m, 7s, 733ms, user=5h, 45m, 500ms
    com.google.common.collect.HashBiMap.seekByValue(HashBiMap.java:234)
    com.google.common.collect.HashBiMap.put(HashBiMap.java:274)
    com.google.common.collect.HashBiMap.forcePut(HashBiMap.java:301)

Был только один поток, ожидающий на forcePut, как указано выше.

Будет ли какая-то причина, по которой HashBiMap.get будет go в al oop для найти значение ключа и никогда не возвращается.

1 Ответ

2 голосов
/ 06 мая 2020

TL; DR

Согласно предложению Xaerxess , использует Maps # synchronizedBiMap в случае, если к карте обращаются несколько потоков. Никогда не знаешь, что может случиться, когда есть несколько потоков.

Для тех, кому интересно, что произошло

Будет ли какая-то причина, по которой HashBiMap.get будет go в цикл для поиска значения ключа, который никогда не возвращается.


Это интересный пример того, как несколько потоков создают «неожиданный» результат.
Давайте посмотрим на строку HashBiMap. java: 223 метода seekByKey

private BiEntry<K, V> seekByKey(@Nullable Object key, int keyHash) {
  for (BiEntry<K, V> entry = hashTableKToV[keyHash & mask];
      entry != null;
      entry = entry.nextInKToVBucket) {
    if (keyHash == entry.keyHash && Objects.equal(key, entry.key)) {
      return entry;
    }
  }
  return null;
}

И строка 223:

      entry = entry.nextInKToVBucket) {

Блокировка в этой строке означает, что существует бесконечное l oop, что связано с циклическими ссылками entry и entry.nextInKToVBucket.

Один из возможных случаев: в методе put,

private V put(@Nullable K key, @Nullable V value, boolean force) {
  ...

  BiEntry<K, V> newEntry = new BiEntry<>(key, keyHash, value, valueHash);
  if (oldEntryForKey != null) {
    ...
  } else {
    insert(newEntry, null);
    rehashIfNecessary();
    return null;
  }
}

предположим, к сожалению есть два вызова с одним и тем же ключом и значением из двух потоков одновременно, созданы две новые записи A и B. Затем в методе insert,

private void insert(BiEntry<K, V> entry, @Nullable BiEntry<K, V> oldEntryForKey) {
  int keyBucket = entry.keyHash & mask; // 1
  entry.nextInKToVBucket = hashTableKToV[keyBucket]; // Step 2
  hashTableKToV[keyBucket] = entry; // 3
  ...
}

Предположим, что A завершается первым, hashTableKToV[keyBucket] = A, A.nextInKToVBucket = ноль. когда B приходит и завершает шаг 2, B.nextInKToVBucket = A. Предположим, перед выполнением шага 3 поток A выполняет rehashIfNecessary, и, к сожалению, требуется reha sh.

private void rehashIfNecessary() {
  BiEntry<K, V>[] oldKToV = hashTableKToV;
  if (Hashing.needsResizing(size, oldKToV.length, LOAD_FACTOR)) {
    int newTableSize = oldKToV.length * 2;

    this.hashTableKToV = createTable(newTableSize);  // Step 4
    ...

    for (BiEntry<K, V> entry = firstInKeyInsertionOrder; 
        entry != null;
        entry = entry.nextInKeyInsertionOrder) {
      insert(entry, entry); // step 5
    }
    ...
  }
}

По завершении шага 4 hashTableKToV удаляется. Не повезло, что поток B выполняет шаг 3 в этот момент и hashTableKToV[keyBucket] = B. Поток A продолжается с шага 5, который снова вставляет A, и A.nextInKToVBucket = A после шага 2, вызывая циклическую ссылку. И, следовательно, бесконечное l oop в seekByKey.

Вот пример того, как воспроизвести вышеуказанный случай (не 100%, возможно, придется попробовать несколько раз):

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

public class HashBiMapConcurrentTest {
    public static void main(String[] args) throws InterruptedException {
        BiMap<String, String> biMap = HashBiMap.create();
        ExecutorService executors = Executors.newFixedThreadPool(4);
        Collection<Callable<Integer>> tasks = new ArrayList<>();
        Callable<Integer> task = () -> {
            for (int i = 0; i < 1000; i++) {
                biMap.put("A" + i, "B" + i);
                biMap.get("A" + i);
            }
            System.out.println("Done");
            return 0;
        };
        tasks.add(task);
        tasks.add(task);
        List<Future<Integer>> futures = executors.invokeAll(tasks);
        for (Future<Integer> future : futures) {
            while (!future.isDone()) {
                Thread.sleep(10);
            }
        }
        executors.shutdown();
    }
}
...