带你从头到尾理解ConcurrentHashMap

249 阅读4分钟

ConcurrentHashMap它是HashMap的线程安全版本,内部使用的是(数组+链表+红黑树)这一种结构来存储元素。它相对于同样是线程安全的HashTable,它的效率都比HashTable有很大的提高。\

底层数据结构

在JDK1.7的ConcurrentHashMap底层是用分段式的数组+链表来实现的。

JDK1.8和HashMap1.8的结构一样是数组+链表/红黑树。

多线程Map

那么在Java当中,HashMap是非线程安全我们是知道的,那么如果想在多线程安全下操作map有什么方法呢?

  1. 可以使用Hashtable
  2. 可以使用Collections.synchronizedMap
  3. 可以使用ConcurrentHashMap

ConcurrentHashMap并发策略

jdk1.7之前,ConcurrentHashMap采用的是锁分段策略来优化性能,这相当来说就是把整一个数组拆分了,每次操作的话只需要把小数组来锁住即可。在不同的segment之间是相互不影响的,这样就提高了性能。

在JDK1.8就开始把整一个策略来进行重构了,这时候它锁的并不是segment了,而锁的是节点,这样就可以让锁的粒度降低,这个并发的效率也得以提升。

图片

ConcurrentHashMap添加数据

ConcurrentHashMap在添加数据的时候,它是采用了CAS+synchronize的结合这一种策略,它会先判断节点是不是为null的,如果是那么就添加节点。如果添加是失败的话,这说明了发生了冲突,然后会对节点进行上锁并且插入数据。在并发比较低的时候就不用加锁,因为会损耗性能。同时的话CAS只尝试一次,这样的操作也不会造成线程进行长时间的等待耗费性能。

步骤:

  1. 判断数组是否初始化
  2. 插入节点为null,就使用CAS插入数据
  3. 节点不为null,就判断hash值是不是-1,-1就说明在扩容
  4. 最后会进行上锁插入数组\

图片

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p> The value can be retrieved by calling the <tt>get</tt> method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);
    }

可以看到ConcurrentHashMap它是跟hashmap不一样的,它不允许key和value都为null。当向ConcurrentHashMap进行put操作的时候,它会先获得key的哈希值并且再次哈希,最后根据hash值定位到所插入的段当中,看源码。

    V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();    // 上锁
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();
                HashEntry<K,V>[] tab = table;    // table是Volatile的
                int index = hash & (tab.length - 1);    // 定位到段中特定的桶
                HashEntry<K,V> first = tab[index];   // first指向桶中链表的表头
                HashEntry<K,V> e = first;

                // 检查该桶中是否存在相同key的结点
                while (e != null && (e.hash != hash || !key.equals(e.key)))  
                    e = e.next;

                V oldValue;
                if (e != null) {        // 该桶中存在相同key的结点
                    oldValue = e.value;
                    if (!onlyIfAbsent)
                        e.value = value;        // 更新value值
                }else {         // 该桶中不存在相同key的结点
                    oldValue = null;
                    ++modCount;     // 结构性修改,modCount加1
                    tab[index] = new HashEntry<K,V>(key, hash, first, value);  // 创建HashEntry并将其链到表头
                    count = c;      //write-volatile,count值的更新一定要放在最后一步(volatile变量)
                }
                return oldValue;    // 返回旧值(该桶中不存在相同key的结点,则返回null)
            } finally {
                unlock();      // 在finally子句中解锁
            }
        }

相对于HashTable和HashMap只能够有一个线程来执行读或者是写的操作,这使得ConcurrentHashMap在并发的性能上有了质的提高。

ConcurrentHashMap的get操作

get操作与put操作很相似,当从ConcurrentHashMap查询一个指定Key的键值对的时候,会定位存在的段,然后查询请求给这个段来进行处理。

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
    }
//--------------------------------------------------------------------------------------
    V get(Object key, int hash) {
            if (count != 0) {            // read-volatile,首先读 count 变量
                HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对
                        V v = e.value;
                        if (v != null)  // 如果读到value域不为 null,直接返回
                            return v;   
                        // 如果读到value域为null,说明发生了重排序,加锁后重新读取
                        return readValueUnderLock(e); // recheck
                    }
                    e = e.next;
                }
            }
            return null;  // 如果不存在,直接返回null
        }

所以在ConcurrentHash来进行存取的时候,它首先是会定位到具体的段,然后再对整一个ConcurrentHashMap来进行存取。所以不论是读还是写ConcurrentHash具有很高的性能,在进行操作的时候是不需要加锁的,在写操作通过锁分段技术只是对所操作的段加锁,其他的客户端访问是不影响的。

ConcurrentHashMap 读操作不需要加锁的奥秘

用HashEntery对象的不变性来降低读操作对加锁的需求;

用Volatile变量协调读写线程间的内存可见性;

若读时发生指令重排序现象,则加锁重读