ConcurrentHashMap的实现原理与使用

24 篇文章 0 订阅

什么是ConcurrentHashMap?

ConcurrentHashMap 是java集合中map的实现,是哈希表的线程安全版本,即使是线程安全版本,

ConcurrentHashMap的性能也十分可观。但是在不同的jdk版本中,其实现也不一样,本文主要基于jdk1.8版本的实现讨论。

ConcurrentHashMap 是线程安全且高效的HashMap。

ConcurrentHashMap的红黑树实现分析

红黑树红黑树是一种特殊的二叉树,主要用它存储有序的数据,提供高效的数据检索,时间复杂度为O(lgn).

每个节点都有一个标识位表示颜色,红色或黑色.

有如下5种特性:

1、每个节点要么红色,要么是黑色;

2、根节点一定是黑色的;

3、每个空叶子节点必须是黑色的;

4、如果一个节点是红色的,那么它的子节点必须是黑色的;

5、从一个节点到该节点的子孙节点的所有路径包含相同个数的黑色节点;

     只要满足以上5个特性的二叉树都是红黑树,当有新的节点加入时,有可能会破坏其中一些特性,需要通过左旋或右旋操作调整树结构,重新着色,使之重新满足所有特性。

为什么要使用ConcurrentHashMap?

1 线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)

2 效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)

3ConcurrentHashMap 的锁 分 段技术可有效提升并发访问率。

ConcurrentHashMap 结构

  ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成。

  Segment 是一种可重入锁(ReentrantLock),在ConcurrentHashMap 里扮演锁的角色。

  一个ConCurrentHashMap里包含一个 Segment数组。

 Segment的结构和HashMap 类似,是一种数组和链表结构。

 HashEntry 则用于存储键值 对数据。

初始化

 ConcurrentHaspMap初始化方法是通过initialCapacity,loadFactor, concurrencyLevel几个参数来初始化segments数组,段偏移量segmentShift,段掩码segmentMask和每个segment里的HashEntry数组

为了能通过按位与的哈希算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个是大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度。假如concurrencyLevel等于14,15或16,ssize都会等于16,即容器里锁的个数也是16

注意concurrencyLevel的最大大小是65535,意味着segments数组的长度最大为65536,对应的二进制是16位

ConcurrentHaspMap 操作

get操作(不加锁)

    segment 的get 操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment 再通过散列算法定位到元素。

  思路:  (1)确定键值对在哪个段

                (2)确定键值对在哪个小的链表上 tab[index]

                 (3)遍历链表,找到指定的key

get方法步骤:
1、计算key的hash值,并定位table索引
2、若table索引下元素(head节点)为普通链表,则按链表的形式迭代遍历。
3、若table索引下元素为红黑树TreeBin节点,则按红黑树的方式查找(find方法)。

put操作 添加键值对.(加锁)

由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作

size操作(先尝试不加锁,再尝试加锁)

如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。

实列:

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

public class MyConcurrentMashMapTest {



    public static  void main(String[] arg)
    {
        //ConcurrentHashMap 是java集合中map的实现,是哈希表的线程安全版本,即使是线程安全版本
        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); 的这个计数器的数字减 1
            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.out.println(count);
        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

结果:{123=15}

1. ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

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冲突时才上锁,且仅影响发生冲突的那一个链表的更新操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值