深入理解并发容器-ConcurrentHashMap(JDK8版本)1 概述3应用场景4 源码解析

1 概述

集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析Java5的3种并发集合类型(concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在项目开发中获益非浅,顺便斩获名企offer. 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁.这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证.这可以确保不会出现死锁,因为获得锁的顺序是固定的. ConcurrentHashMap是conccurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,开源框架Spring的底层数据结构就是使用ConcurrentHashMap实现的。与同是线程安全的老大哥HashTable相比,它更胜一筹,因为它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。 本文的分析的源码是Java8版,与Java6版有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等内部类。

3应用场景

当有一个大数组时需要在多个线程共享时就可以考虑是否把它给分层多个节点了,避免大锁.并可以考虑通过hash算法进行一些模块定位. 其实不止用于线程,当设计数据表的事务时(事务某种意义上也是同步机制的体现),可以把一个表看成一个需要同步的数组,如果操作的表数据太多时就可以考虑事务分离了(这也是为什么要避免大表的出现),比如把数据进行字段拆分,水平分表等.

4 源码解析

重要的属性

首先来看几个重要的属性,与HashMap相同的就不再介绍了,这里重点解释一下sizeCtl这个属性。可以说它是ConcurrentHashMap中出镜率很高的一个属性,因为它是一个控制标识符,在不同的地方有不同用途,而且它的取值不同,也代表不同的含义。

 /**
     * 在以前的版本中使用的辅助类的精简版
     * 由于序列化兼容的缘故而声明
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        //负载因子
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }
/**
     The array of bins. Lazily initialized upon first insertion.
     Accessed directly by iterators.
     * 盛装Node元素的数组 大小永远是2的整数次幂 
     */
    transient volatile Node<K,V>[] table;
   
    /**
    当 table 为空,sizectl 保存初始的 table 大小,用于创建新的 table,默认为 0。    在初始化后,这个变量保存的值的含义是下一次需要 resize table 的元素数量,相当于阈值。元素超过这个阈值,table 就要 resize
     * 表初始化和扩容时的控制位标识量。
     * 负数代表正在进行初始化或扩容操作 
     * -1代表正在初始化 
     * -N 表示有N-1个线程正在进行扩容操作 
     * 正数或0代表哈希表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小 
     */
    private transient volatile int sizeCtl;

   // 以下两个是用来控制扩容的时候 单线程进入的变量  
   /** 
   * The number of bits used for generation stamp in sizeCtl. 
   * Must be at least 6 for 32bit arrays. 
   */  
  private static int RESIZE_STAMP_BITS = 16;  
   
   /** 
   * The bit shift for recording size stamp in sizeCtl. 
   */  
  private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;  
    
    
  /* 
   * Encodings for Node hash fields. See above for explanation. 
   */  
  static final int MOVED     = -1; // hash值是-1,表示这是一个forwardNode节点  
  static final int TREEBIN   = -2; // hash值是-2  表示这时一个TreeBin节点  

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏zhisheng

Java研发方向如何准备BAT技术面试答案(上)

1. 面向对象和面向过程的区别 面向过程 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Un...

45640
来自专栏码洞

如履薄冰 —— Redis懒惰删除的巨大牺牲

之前我们介绍了Redis懒惰删除的特性,它是使用异步线程对已经删除的节点进行延后内存回收。但是还不够深入,所以本节我们要对异步线程逻辑处理的细节进行分析,看看A...

9410
来自专栏FreeBuf

Python黑客学习笔记:从HelloWorld到编写PoC(上)

本系列文章适合CS在读学生和万年工具党,本文会在英文原文的基础上做些修改,并适当增加些解释说明。 ? 本篇包含原文的前几部分: 0x0 – Getting St...

315100
来自专栏漏斗社区

Burp Suite API学习思路(二)

在上篇工具| Burp Suite API学习思路文章中,斗哥介绍了BurpSuite扩展开发的一些准备工作与利用IHttpListener接口监听模块接收返回...

12320
来自专栏腾讯IVWEB团队的专栏

ECMAScript 2015 (ES6) in Node.js(译)

Node.js是建立在V8引擎的基础上。通过保持对该引擎最新发布版的更新,我们可以确保能够将JavaScript ECMA-262 specification中...

18500
来自专栏小灰灰

Java 回调函数的使用

回调函数 回调函数是什么鬼, 回调函数干嘛用,回调函数可以怎么用 如果有过android开发经验,经常可以看到一些类似下面的代码 Button Btn1 = ...

40680
来自专栏Java学习之路

Java的LockSupport工具,Condition接口和ConditionObject LockSupportConditionConditionObject

在之前我们文章(关于多线程编程基础和同步器),我们就接触到了LockSupport工具和Condition接口,之前使用LockSupport工具来唤醒阻塞的线...

36550
来自专栏武培轩的专栏

Keep面经汇总

原理:泛型的实现是靠类型擦除技术,类型擦除是在编译期完成的,在编译期,编译器会将泛型的类型参数都擦除成它的限定类型,如果没有则擦除为object类型之后在获取的...

13620
来自专栏安恒网络空间安全讲武堂

WriteUp分享 | CTF-web

题目 各种绕过哦 TXT? 文件上传测试 本地包含 考细心 正则? PHP很烦人? 一道签到题 抽抽奖 never give up I have a j...

3.4K80
来自专栏芋道源码1024

Redis 从入门到起飞(上)

1. Redis 介绍1.1 NoSQL 基本概念1.2 NoSQL 分类1.3 Redis 基本概念1.4 发展历史1.5 应用场景2. Redis 安装2....

18440

扫码关注云+社区

领取腾讯云代金券