原文

IMAGE

HashMap 在Java中是一种非常强大的数据结构。我们会经常使用它并且在绝大多数的应用中都会使用到它。这里有一些我之前写的例子: 如何实现线程安全的缓存 , 如何转换 HashMap 为 ArrayList?

在上面的例子中,我们都使用了 HashMap,但这些是相当简单的 HashMap 的用法。 HashMap 是一个非同步 的集合类。

你有过以下的问题吗?

  • ConcurrentHashMapCollections.synchronizedMap(Map) 之间的区别是什么?
  • ConcurrentHashMapCollections.synchronizedMap(Map) 之间的性能差别如何?
  • ConcurrentHashMap vs Collections.synchronizedMap()
  • 常见的 HashMapConcurrentHashMap 的面试题

这本教程中,我们将覆盖以上所有的疑问以及为什么和如何同步 HashMap 的原因?

Why ?

Map 对象是一个存储元素的关联的容器,通过唯一标识的 key 和映射的 value 组成。如果你有一个非常高并发的应用,你可能想在不同的线程中修改或读取键值,那么理想的情况下是使用并发的 HashMap 。最典型的例子就是生产者和消费者的读写处理。

所以,线程安全的 Map 意味着什么?如果 多线程 并发地访问一个 hash map,并且至少有一条线程修改 map 的结构,它 必须在外部被同步,以免视图内容不一致。

How ?

有两种方式可以同步 HashMap

  1. Java 中的 Collections.synchronizedMap() 方法
  2. 使用 ConcurrentHashMap

HashMap Vs. synchronizedMap Vs. ConcurrentHashMap

//Hashtable
Map<String, String> normalMap = new Hashtable<String, String>();

//synchronizedMap
synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());

//ConcurrentHashMap
concurrentHashMap = new ConcurrentHashMap<String, String>();

ConcurrentHashMap

  • 当在你的项目中需要非常高并发的时候,你应该使用 ConcurrentHashMap
  • 它是线程安全的,并且不需要同步整个 Map
  • 读是非常快的,而写是通过锁的
  • 没有在 object 级别的锁
  • 锁在 HashMapbucket 级别具有更精细的粒度
  • 如果一个线程尝试修改 ConcurrentHashMap,而另一个线程正在迭代它,则 ConcurrentHashMap 不会抛出 ConcurrentModificationException 异常。
  • ConcurrentHashMap 使用多个锁

SynchronizedHashMap

  • Object 级别的锁
  • 每次的 读/写 操作都要求获得锁
  • 锁定整个集合是一个性能的开销
  • 这本质上只允许一条线程访问整个 Map 并且会阻塞所有其他的线程
  • 它可能会引起争议
  • SynchronizedHashMap 返回 Iterator ,它会在并发修改时失败

现在让我们看一下代码

  1. 创建类 CrunchifyConcurrentHashMapVsSynchronizedHashMap.java
  2. 为每个 HashTableSynchronizedMapCrunchifyConcurrentHashMap 创建一个对象
  3. Map 中添加和检索 500K 个条目
  4. 压测开始和结束时间并用毫秒显示
  5. 我们将使用 ExecutorService 来并行地执行5条线程

CrunchifyConcurrentHashMapVsSynchronizedMap.java

package crunchify.com.tutorials;

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author Crunchify.com
 *
 */

public class CrunchifyConcurrentHashMapVsSynchronizedMap {

    public final static int THREAD_POOL_SIZE = 5;

    public static Map<String, Integer> crunchifyHashTableObject = null;
    public static Map<String, Integer> crunchifySynchronizedMapObject = null;
    public static Map<String, Integer> crunchifyConcurrentHashMapObject = null;

    public static void main(String[] args) throws InterruptedException {

        // Test with Hashtable Object
        crunchifyHashTableObject = new Hashtable<String, Integer>();
        crunchifyPerformTest(crunchifyHashTableObject);

        // Test with synchronizedMap Object
        crunchifySynchronizedMapObject = Collections.synchronizedMap(new HashMap<String, Integer>());
        crunchifyPerformTest(crunchifySynchronizedMapObject);

        // Test with ConcurrentHashMap Object
        crunchifyConcurrentHashMapObject = new ConcurrentHashMap<String, Integer>();
        crunchifyPerformTest(crunchifyConcurrentHashMapObject);

    }

    public static void crunchifyPerformTest(final Map<String, Integer> crunchifyThreads) throws InterruptedException {

        System.out.println("Test started for: " + crunchifyThreads.getClass());
        long averageTime = 0;
        for (int i = 0; i < 5; i++) {

            long startTime = System.nanoTime();
            ExecutorService crunchifyExServer = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

            for (int j = 0; j < THREAD_POOL_SIZE; j++) {
                crunchifyExServer.execute(new Runnable() {
                    @SuppressWarnings("unused")
                    @Override
                    public void run() {

                        for (int i = 0; i < 500000; i++) {
                            Integer crunchifyRandomNumber = (int) Math.ceil(Math.random() * 550000);

                            // Retrieve value. We are not using it anywhere
                            Integer crunchifyValue = crunchifyThreads.get(String.valueOf(crunchifyRandomNumber));

                            // Put value
                            crunchifyThreads.put(String.valueOf(crunchifyRandomNumber), crunchifyRandomNumber);
                        }
                    }
                });
            }

            // Make sure executor stops
            crunchifyExServer.shutdown();

            // Blocks until all tasks have completed execution after a shutdown request
            crunchifyExServer.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);

            long entTime = System.nanoTime();
            long totalTime = (entTime - startTime) / 1000000L;
            averageTime += totalTime;
            System.out.println("2500K entried added/retrieved in " + totalTime + " ms");
        }
        System.out.println("For " + crunchifyThreads.getClass() + " the average time is " + averageTime / 5 + " ms\n");
    }
}

结果

Test started for: class java.util.Hashtable
500K entried added/retrieved in 1432 ms
500K entried added/retrieved in 1425 ms
500K entried added/retrieved in 1373 ms
500K entried added/retrieved in 1369 ms
500K entried added/retrieved in 1438 ms
For class java.util.Hashtable the average time 1407 ms

Test started for: class java.util.Collections$SynchronizedMap
500K entried added/retrieved in 1431 ms
500K entried added/retrieved in 1460 ms
500K entried added/retrieved in 1387 ms
500K entried added/retrieved in 1456 ms
500K entried added/retrieved in 1406 ms
For class java.util.Collections$SynchronizedMap the average time 1428 ms

Test started for: class java.util.concurrent.ConcurrentHashMap
500K entried added/retrieved in 413 ms
500K entried added/retrieved in 351 ms
500K entried added/retrieved in 427 ms
500K entried added/retrieved in 337 ms
500K entried added/retrieved in 339 ms
For class java.util.concurrent.ConcurrentHashMap the average time 373 ms  <== Much faster