ConcurrentHashMap的实现原理与使用

  • Post author:
  • Post category:其他




ConcurrentHashMap的实现原理与使用



1.什么是ConcurrentHashMap?

ConcurrentHashMap是一个并发容器,ConcurrentHashMap 是java集合中map的实现,是哈希表的线程安全版本,即使是线程安全版本,ConcurrentHashMap的性能也十分可观。但是在不同的jdk版本中,其实现也不一样,本文主要基于jdk1.8版本的实现讨论。ConcurrentHashMap 是线程安全且高效的HashMap。

ConcurrentHashMap使用的锁分段技术,就是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),他们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里按照顺序是非常重要,否则极有可能出现死锁,在cunrrentHashMap内部,段数组是final的,并且其他成员变量实际也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这是需要实现上的保证。这可以确保不会出现死锁, 因为获得的顺序是固定的。

ConrrentHashMap是由Segment数组结构和HashEntry数组结构构成。Segment是一种可重入锁Renntrantlock,在ConcurrentHashMap里扮演锁的角色,HashMap则用于存储键值对数据。一个ConcurrentHashMap里面包含一个Segment数组,segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里面包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁.



2.为什么要使用ConcurrentHashMap?

1 线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)2 效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)。3ConcurrentHashMap 的锁 分 段技术可有效提升并发访问率。



3.ConcurrentHashMap 结构

ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成。 Segment 是一种可重入锁(ReentrantLock),在ConcurrentHashMap 里扮演锁的角色。 ConCurrentHashMap里包含一个 Segment数组。 Segment的结构和HashMap 类似,是一种数组和链表结构。 HashEntry 则用于存储键值 对数据

package com.zzh.test;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class MyCurrentHashMapTest {
    public static void main(String[] args) {
        Map<String, Integer> count = new ConcurrentHashMap<>();
        //CountDownLatch其实可以把它看做一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去减去这个计数器里面的值
        CountDownLatch endLatch = new CountDownLatch(3);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Integer oldValue;
                for (int i = 0; i < 5; i++) {
                    Integer value = count.get("123");
                    if (null == value) {
                        //添加键值对
                        count.put("123", 1);
                    } else {
                        count.put("123", value +1);
                    }
                }
                //countDown的时候每次调用都会state减1也就是new CountDownLatch(3)的这个计数器数字减一
                endLatch.countDown();
            }
        };
        //new CountDownLatch(3) 3 代表3个线程
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
        try {
            //此方法用来当前线程阻塞,直到count减少为0才恢复执行,await方法它会去获取同步值发现为0的话会返回成功,如果小于0的话,再次判断是否是头结点
            endLatch.await();
            System.err.println(count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:{123=15}


1. ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?


Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。

使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。


2.我们可以使用CocurrentHashMap来代替Hashtable吗?


我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。


3. ConcurrentHashMap有什么缺陷吗?


ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。

坏处是严格来说读取操作不能保证反映最近的更新

。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。


4. ConcurrentHashMap在JDK 7和8之间的区别

  • JDK1.8的实现降低锁 的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  • JDK1.8版本的数据结构变得更 加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档


总结:

1.ConcurrentHashMap的数据结构与HashMap基本相同,只是在put的过程中如果没有发生冲突,则采用CAS操作进行无锁化更新,只有发生了哈希冲突的时候才锁住在链表上添加新Node或者更新Node的操作。

2.像get一类的操作也是没有同步的。

3.ConcurrentHashMap 不允许存放null值。

4.ConcurrentHashMap 的大小是通过计算出来的,也就是说在超高的并发情况下,size是不精确的。这一点后面有空再补上。

5.和jdk1.7 相比,在jdk1.7中采用锁分段技术,更加复杂一点,jdk1.8中ConcurrentHashMap上锁仅在发生hash冲突时才上锁,且仅影响发生冲突的那一个链表的更新操作。



版权声明:本文为qq_44881914原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。