2011-01-24 4 views
2

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

Обновление

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

ответ

0

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

2

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

@ Предложение Джеймс заставило меня подумать о том, ускоряет ли настройка un-need concurrency 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 указывает, больший уровень параллелизма может быть медленнее, так как она имеет худшие характеристики кэширования.

EDIT: Кроме того, @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 
+0

не совсем верно. например, для обновления карты может потребоваться несколько вызовов, для которых требуется внешняя синхронизация, но чтение карты все равно может быть одним вызовом метода, и в этом случае ConcurrentMap будет по-прежнему иметь смысл (только с использованием метода обновления, синхронизированного извне). – jtahlborn

+0

Вы говорите, что нужно, чтобы несколько puts выполнялись как одна операция? В этом случае «ConcurrentHashMap» не является допустимым решением, поскольку его уровень атомарности - это одна операция «put». – jdmichal

+0

@jdmichal, да, это моя точка зрения. –

1

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

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.

РЕДАКТИРОВАТЬ:

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

+0

+0: Уровень параллелизма по умолчанию - 16, чтобы увеличить его, вам нужно будет иметь более 16 ядер, активно использующих хэш-карту (вместо того, чтобы делать что-то полезное), например. скажем, у вас есть приложение, использующее 100 ядер (не только потоки), но оно тратит менее 10% времени на доступ к Карте, вам не нужно настраивать уровень параллелизма. В 99,9% настроек приложений/машин это не проблема. –

+0

@Peter: Да, но документация, конкретно упоминающая о снижении ее с 16, может повысить производительность, если вы знаете, сколько потоков будет доступно для нее. См. Ответ нанды. – James

+0

Мое предпочтение было бы иметь небольшое количество ConcurrentHashMaps, поэтому я бы не предложил вам уменьшить их размер. Они относительно дороги, но все же очень маленькие. –

0

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

+0

неправильный ... ConcurrentHashMap может также разместить более 1 автора – nanda

+0

@nanda Я не сказал, что не мог. Я сказал, что если есть только один писатель. @opl сказал, что у него только одна нить. – Endophage

+0

@ Эндофаг да, но способ, которым твоё предложение построено, предполагает, что он не работает с более чем одним писателем. – nanda

1

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

Из Java 6 API documentation:

операций Retrieval (включая ГЭТ), как правило, не блокируют, поэтому могут перекрываться с операциями обновления (в том числе на месте и удалить). Retrievals отражают результаты последних завершенных операций по обновлению с их началом. Для совокупных операций, таких как putAll и clear, одновременное извлечение может отражать вставку или удаление только некоторых записей.

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

+0

+1 Хорошее объяснение – nanda

+0

Что делать, если у меня есть геттер и сеттер для карты, используемой одновременно, оба потока, является ConcurrentHashMap, все еще хорошим решением? – opl

+1

+0 он использует ожидание, а не компрометацию данных. например точно так же, как AtomicInteger. (тот же автор). Это, по-видимому, отбрасывает процессор, но вряд ли вы его заметите. –

0

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

Из Javadoc:

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

0

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

Смежные вопросы