Java ConcurrentHashMap - PullRequest
       4

Java ConcurrentHashMap

2 голосов
/ 24 января 2011

В приложении, где один поток отвечает за постоянное обновление карты, а основной поток периодически читает карту, достаточно ли использовать ConcurrentHashmap? Или я должен явно блокировать операции в блоках синхронизации? Любое объяснение было бы замечательно.

Обновление

У меня есть геттер и сеттер для карты (инкапсулированные в пользовательский тип), которые могут использоваться одновременно обоими потоками, является ли ConcurrentHashMap хорошим решением? Или, может быть, мне следует синхронизировать метод получения / установки (или, возможно, объявить переменную экземпляра как volatile)? Просто хочу убедиться, что эта дополнительная деталь не изменит решение.

Ответы [ 7 ]

2 голосов
/ 24 января 2011

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

@ Предложение Джеймса заставило меня задуматься о том, ускоряет ли настройка ненужного параллелизма ConcurrentHashMap. Это должно уменьшить память, но вам нужно иметь тысячи таких, чтобы иметь большое значение. Поэтому я написал этот тест, и не кажется очевидным, что вам всегда нужно будет настраивать уровень параллелизма.

warmup: Average access time 36 ns.
warmup2: Average access time 28 ns.
1 concurrency: Average access time 25 ns.
2 concurrency: Average access time 25 ns.
4 concurrency: Average access time 25 ns.
8 concurrency: Average access time 25 ns.
16 concurrency: Average access time 24 ns.
32 concurrency: Average access time 25 ns.
64 concurrency: Average access time 26 ns.
128 concurrency: Average access time 26 ns.
256 concurrency: Average access time 26 ns.
512 concurrency: Average access time 27 ns.
1024 concurrency: Average access time 28 ns.

Код

    public static void main(String[] args) {
    test("warmup", new ConcurrentHashMap());
    test("warmup2", new ConcurrentHashMap());
    for(int i=1;i<=1024;i+=i)
    test(i+" concurrency", new ConcurrentHashMap(16, 0.75f, i));
}

private static void test(String description, ConcurrentHashMap map) {
    Integer[] ints = new Integer[2000];
    for(int i=0;i<ints.length;i++)
        ints[i] = i;
    long start = System.nanoTime();
    for(int i=0;i<20*1000*1000;i+=ints.length) {
        for (Integer j : ints) {
            map.put(j,1);
            map.get(j);
        }
    }
    long time = System.nanoTime() - start;
    System.out.println(description+": Average access time "+(time/20/1000/1000/2)+" ns.");
}

Как указывает @bestss, больший уровень параллелизма может быть медленнее, поскольку он имеет более плохие характеристики кэширования.

РЕДАКТИРОВАТЬ: В дополнение к @betsss озабоченность по поводу того, оптимизируются ли циклы, если нет вызовов методов. Вот три цикла, все одинаковые, но повторяющиеся разное количество раз. Они печатают

10M: Time per loop 661 ps.
100K: Time per loop 26490 ps.
1M: Time per loop 19718 ps.
10M: Time per loop 4 ps.
100K: Time per loop 17 ps.
1M: Time per loop 0 ps.

.

{
    int loops = 10*1000 * 1000;
    long product = 1;
    long start = System.nanoTime();
    for(int i=0;i< loops;i++)
        product *= i;
    long time = System.nanoTime() - start;
    System.out.println("10M: Time per loop "+1000*time/loops+" ps.");
}
{
    int loops = 100 * 1000;
    long product = 1;
    long start = System.nanoTime();
    for(int i=0;i< loops;i++)
        product *= i;
    long time = System.nanoTime() - start;
    System.out.println("100K: Time per loop "+1000*time/loops+" ps.");
}
{
    int loops = 1000 * 1000;
    long product = 1;
    long start = System.nanoTime();
    for(int i=0;i< loops;i++)
        product *= i;
    long time = System.nanoTime() - start;
    System.out.println("1M: Time per loop "+1000*time/loops+" ps.");
}
// code for three loops repeated
1 голос
/ 24 января 2011

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

Из документации Java 6 API:

Операции получения (включая get) обычно не блокируются, поэтому могут перекрываться с операциями обновления (включая put и remove).Извлечения отражают результаты самых последних завершенных операций обновления, проводимых с момента их появления.Для агрегатных операций, таких как putAll и clear, одновременные извлечения могут отражать вставку или удаление только некоторых записей.

Если это неприемлемо для вашего проекта, лучшим решением будет полностью синхронная блокировка.Решения для многих операций записи с небольшим количеством операций чтения, насколько я знаю, ставят под угрозу современное чтение для достижения более быстрой, неблокируемой записи.Если вы воспользуетесь этим решением, метод Collections.synchronizedMap(...) создаст полностью синхронизированную оболочку с одним устройством чтения / записи для любого объекта карты.Проще, чем написать свой.

1 голос
/ 24 января 2011

Этого достаточно, поскольку цель ConcurrentHashMap состоит в том, чтобы разрешить операции get / put без блокировки, но убедитесь, что вы используете его с правильным уровнем параллелизма. Из документов:

Ideally, you should choose a value to accommodate as many threads as will ever concurrently modify the table. Using a significantly higher value than you need can waste space and time, and a significantly lower value can lead to thread contention. But overestimates and underestimates within an order of magnitude do not usually have much noticeable impact. A value of one is appropriate when it is known that only one thread will modify and all others will only read. Also, resizing this or any other kind of hash table is a relatively slow operation, so, when possible, it is a good idea to provide estimates of expected table sizes in constructors.

См. http://download.oracle.com/javase/6/docs/api/java/util/concurrent/ConcurrentHashMap.html.

EDIT:

Завернутый метод получения / установки не имеет значения, если он все еще читается / записывается несколькими потоками. Вы могли бы одновременно блокировать всю карту, но это побеждает цель использования ConcurrentHashMap.

0 голосов
/ 27 января 2011

Решение работает из-за эффектов согласованности памяти для ConcurrentMaps: Как и в случае других параллельных коллекций, действия в потоке перед помещением объекта в ConcurrentMap в качестве действия ключа или значения, выполняемого до, после доступа или удаления этого объекта из ConcurrentMap в другом потоке.

0 голосов
/ 24 января 2011

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

От Javadoc:

Допустимый параллелизм среди операций обновления определяется необязательным аргументом конструктора concurrencyLevel (по умолчанию 16), который используется в качестве подсказки для внутреннего определения размера. .... Значение one подходит, когда известно, что будет изменен только один поток, а все остальные будут только читать.

0 голосов
/ 24 января 2011

Если есть только один писатель, то можно просто использовать ConcurrentHashMap. Если вы чувствуете необходимость синхронизации, есть другие HashMaps, которые выполняют синхронизацию для вас и будут быстрее, чем ручная запись синхронизации.

0 голосов
/ 24 января 2011

Вам лучше использовать ConcurrentHashMap, так как его реализация обычно не блокирует чтение. Если вы синхронизируете внешне, вы в конечном итоге заблокируете большинство операций чтения, поскольку у вас нет доступа к внутренним знаниям о impl. нужно не делать этого.

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