解析ConcurrentHashMap: 预热(内部一些小方法分析)

前面一篇文章中介绍了并发HashMap的主要成员属性,内部类和构造函数,下面在正式分析并发HashMap成员方法之前,先分析一些内部类中的字方法函数:

首先来看下ConcurrentHashMap内部类Node中的hash成员属性值的计算方法spread(int h):

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;// 该属性是通过spread(int h)方法计算得到~
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    ...
}

1、spread(int h)方法

/**
 * 计算Node节点hash值的算法:参数h为hash值
 * eg:
 * h二进制为 --> 	 		 		 1100 0011 1010 0101 0001 1100 0001 1110
 * (h >>> 16) -->  					0000 0000 0000 0000 1100 0011 1010 0101
 * (h ^ (h >>> 16)) -->				1100 0011 1010 0101 1101 1111 1011 1011
 * 注:(h ^ (h >>> 16)) 目的是让h的高16位也参与寻址计算,使得到的hash值更分散,减少hash冲突产生~
 * ------------------------------------------------------------------------------
 * HASH_BITS -->					0111 1111 1111 1111 1111 1111 1111 1111
 * (h ^ (h >>> 16)) -->				1100 0011 1010 0101 1101 1111 1011 1011
 * (h ^ (h >>> 16)) & HASH_BITS --> 0100 0011 1010 0101 1101 1111 1011 1011
 * 注: (h ^ (h >>> 16))得到的结果再& HASH_BITS,目的是为了让得到的hash值结果始终是一个正数
 */
static final int spread(int h) {
    // 让原来的hash值异或^原来hash值的右移16位,再与&上HASH_BITS(0x7fffffff: 二进制为31个1)
    return (h ^ (h >>> 16)) & HASH_BITS;
}

下面介绍tabAt(Node<K,V>[] tab, int i)方法:获取 tab(Node[]) 数组指定下标 i 的Node节点。

2、tabAt方法

/**
 * 获取 tab(Node[]) 数组指定下标 i 的Node节点
 * Node<K,V>[] tab:表示Node[]数组
 * int i:表示数组下标
 */
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    // ((long)i << ASHIFT) + ABASE 的作用:请看下面的分析描述~
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

在分析((long)i << ASHIFT) + ABASE时,先复习一下上一篇文章:ConcurrentHashMap源码解析_01 成员属性、内部类、构造方法分析中介绍到的一些属性字段的含义:

// Node数组的class对象
Class<?> ak = Node[].class;
// U.arrayBaseOffset(ak):根据as获取Node[]数组第一个元素的偏移地址ABASE
ABASE = U.arrayBaseOffset(ak);
// scale:表示数组中每一个单元所占用的空间大小,即scale表示Node[]数组中每一个单元所占用的空间
int scale = U.arrayIndexScale(ak);// scale必须为2的次幂数
// numberOfLeadingZeros(scale) 根据scale,返回当前数值转换为二进制后,从高位到地位开始统计,统计有多少个0连续在一块:eg, 8转换二进制=>1000 则 numberOfLeadingZeros(8)的结果就是28,为什么呢?因为Integer是32位,1000占4位,那么前面就有32-4个0,即连续最长的0的个数为28个
// 4转换二进制=>100 则 numberOfLeadingZeros(8)的结果就是29
// ASHIFT = 31 - Integer.numberOfLeadingZeros(4) = 2 那么ASHIFT的作用是什么呢?其实它有数组寻址的一个作用:
// 拿到下标为5的Node[]数组元素的偏移地址(存储地址):假设此时 根据scale计算得到的ASHIFT = 2
// ABASE + (5 << ASHIFT) == ABASE + (5 << 2) == ABASE + 5 * scale(乘法运算效率低),就得到了下标为5的数组元素的偏移地址
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

由上面的几个属性字段的复习介绍,不难得出:

((long)i << ASHIFT) + ABASE 就是得到当前Node[] 数组下标为i的节点对象的偏移地址。

然后再通过(Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE) 方法,根据Node[]目标节点Node的偏移地址两个参数,得到下标为i的Node节点对象。

虽然这样很绕,不如,但是直接根据偏移地址去寻找数组元素效率较高~

3、casTabAt方法

/**
 * 通过CAS的方式去向Node数组指定位置i设置节点值,设置成功返回true,否则返回false
 * Node<K,V>[] tab:表示Node[]数组
 * int i:表示数组下标
 * Node<K,V> c:期望节点值
 * Node<K,V> v:要设置的节点值
 */
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    // 调用Unsafe的比较并交换去设置Node[]数组指定位置的节点值,参数如下:
    // tab:Node[]数组
    // ((long)i << ASHIFT) + ABASE:下标为i数组桶的偏移地址
    // c:期望节点值
    // v:要设置的节点值
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

4、setTabAt方法

/**
 * 根据数组下标i,设置Node[]数组指定下标位置的节点值:
 * Node<K,V>[] tab:表示Node[]数组
 * int i:表示数组下标
 * Node<K,V> v:要设置的节点值
 */
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    // ((long)i << ASHIFT) + ABASE:下标为i数组桶的偏移地址
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

5、resizeStamp方法

/**
 * table数组扩容时,计算出一个扩容标识戳,当需要并发扩容时,当前线程必须拿到扩容标识戳才能参与扩容:
 */
static final int resizeStamp(int n) {
    // RESIZE_STAMP_BITS:固定值16,与扩容相关,计算扩容时会根据该属性值生成一个扩容标识戳
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
=

举例子分析一下:

当我们需要table容量从16 库容到32时:Integer.numberOfLeadingZeros(16) 会得到,怎么得来的呢?

  • numberOfLeadingZeros(n) 根据传入的n,返回当前数值转换为二进制后,从高位到地位开始统计,统计有多少个0连续在一块:
  • eg:16 转换二进制 => 1 0000 则 numberOfLeadingZeros(16)的结果就是 27,因为Integer是32位,1 0000占5位,那么前面就有(32 - 5)个0,即连续最长的0的个数为27个。
  • (1 << (RESIZE_STAMP_BITS - 1)):其中RESIZE_STAMP_BITS是一个固定值16,与扩容相关,计算扩容时会根据该属性值生成一个扩容标识戳。

下面就来计算一下:

// 从16扩容到32
16 -> 32
// 用A表示:
numberOfLeadingZeros(16) => 1 0000 => 27 => 0000 0000 0001 1011
// 用B表示:
(1 << (RESIZE_STAMP_BITS - 1)) => (1 << (16 - 1) => 1000 0000 0000 0000 => 32768
// A | B
Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1))
-----------------------------------------------------------------
0000 0000 0001 1011  ---> A
1000 0000 0000 0000  ---> B
-------------------  ---> | 按位或
1000 0000 0001 1011  ---> 计算得到扩容标识戳

6、tableSizeFor方法

/**
 * 根据c,计算得到大于等于c的,最小2的次幂数,该方法在HashMap源码中分析过~
 * eg:c = 28 ,则计算得到的返回结果为 32
 * c = 28 ==> n=27 => 0b 11011
 *
 * 11011 | 01101 => 11111 ---- n |= n >>> 1
 * 11111 | 00111 => 11111 ---- n |= n >>> 2
 * ....
 * => 11111 + 1 = 100000 = 32
 */
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

7、构造方法(复习)

复习一下上一篇文章中的并发HashMap的构造方法:

// 无惨构造
public ConcurrentHashMap() {
}
// 指定初始化容量
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
            MAXIMUM_CAPACITY :
            tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    /**
     * sizeCtl > 0
     * 当目前table未初始化时,sizeCtl表示初始化容量
     */
    this.sizeCtl = cap;
}
// 根据一个Map集合来初始化
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // sizeCtl设置为默认容量值
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}
// 指定初始化容量,和负载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}
// 指定初始化容量,和负载因子,并发级别
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // 当指定的初始化容量initialCapacity小于并发级别concurrencyLevel时
    if (initialCapacity < concurrencyLevel)
        // 初始化容量值设置为并发级别的值。
        // 即,JDK1.8以后并发级别由散列表长度决定
        initialCapacity = concurrencyLevel;
    // 根据初始化容量和负载因子,去计算size
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    // 根据size重新计算数组初始化容量
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
    /**
     * sizeCtl > 0
     * 当目前table未初始化时,sizeCtl表示初始化容量
     */
    this.sizeCtl = cap;
}

8、总结

预热结束后,下一篇文章就是并发Map比较重点的地方了,即put()写入操作原理~也希望大家多多关注我们的其他内容!

(0)

相关推荐

  • java面试常见问题---ConcurrentHashMap

    1.请你描述一下ConcurrentHashMap存储数据结构是什么样子呢? ConcurrentHashMap 内部的 map 结构和 HashMap 是一致的,都是由:数组 + 链表 + 红黑树 构成. ConcurrentHashMap 存储数据的单元和 HashMap 也是一致的,即,Node 结构: static class Node<K,V> implements Map.Entry<K,V> { // hash值 final int hash; // key fina

  • Java并发系列之ConcurrentHashMap源码分析

    我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

  • Java中遍历ConcurrentHashMap的四种方式详解

    这篇文章主要介绍了Java中遍历ConcurrentHashMap的四种方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 方式一:在for-each循环中使用entries来遍历 System.out.println("方式一:在for-each循环中使用entries来遍历");for (Map.Entry<String, String> entry: map.entrySet()) { System.out.pr

  • java中ConcurrentHashMap的读操作为什么不需要加锁

    前言 ConcurrentHashMap是Java 5中支持高并发.高吞吐量的线程安全HashMap实现. 我们知道,ConcurrentHashmap(1.8)这个并发集合框架是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是这篇博文讨论的问题--为什么它不需要加锁呢? 下面话不多说了,来一起看看详细的介绍吧 ConcurrentHashMap的简介 我想有基础的同学知道在jdk1.7中是采用Segment + HashEntry + ReentrantLo

  • Java源码解析ConcurrentHashMap的初始化

    首先看一下代码 private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 第一次检查 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt

  • 解析ConcurrentHashMap: 预热(内部一些小方法分析)

    前面一篇文章中介绍了并发HashMap的主要成员属性,内部类和构造函数,下面在正式分析并发HashMap成员方法之前,先分析一些内部类中的字方法函数: 首先来看下ConcurrentHashMap内部类Node中的hash成员属性值的计算方法spread(int h): static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 该属性是通过spread(int h)方法计算得到~ final K key

  • Python实现通过解析域名获取ip地址的方法分析

    本文实例讲述了Python实现通过解析域名获取ip地址的方法.分享给大家供大家参考,具体如下: 从网上查找的一些资料,特此做个笔记 案例1: def getIP(domain): myaddr = socket.getaddrinfo(domain, 'http') print(myaddr[0][4][0]) 执行函数 getIP("www.google.com") 案例2: def get_ip_list(domain): # 获取域名解析出的IP列表 ip_list = [] t

  • 解析bitmap处理海量数据及其实现方法分析

    [什么是Bit-map] 所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素.由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省. 如果说了这么多还没明白什么是Bit-map,那么我们来看一个具体的例子,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复).那么我们就可以采用Bit-map的方法来达到排序的目的.要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所

  • 解析ConcurrentHashMap: put方法源码分析

    上一章:预热(内部一些小方法分析) put()方法是并发HashMap源码分析的重点方法,这里涉及到并发扩容,桶位寻址等等- JDK1.8 ConcurrentHashMap结构图: 1.put方法源码解析 // 向并发Map中put一个数据 public V put(K key, V value) { return putVal(key, value, false); } // 向并发Map中put一个数据 // Key: 数据的键 // value:数据的值 // onlyIfAbsent:

  • 解析ConcurrentHashMap:成员属性、内部类、构造方法

    目录 1.简介 2.JDK1.8 ConcurrentHashMap结构图 3.成员属性 4.静态属性 5.静态代码块 6.内部类 6.1 Node节点 6.2 ForwardingNode节点 6.3 TreeNode节点 7.构造方法 8.总结 1.简介 ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素.相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高. 在学习ConcurrentHashMap

  • 解析ConcurrentHashMap: get、remove方法分析

    前面几篇文章分析了并发HashMap的put方法及其相关方法,transfer方法,那么接下来本篇文章相对之前几篇难度会小一些. 本篇文章介绍ConcurrentHashMap的get方法和remove方法. 1.get方法 get方法:获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写. public V get(Object key) { // tab 引用map.table // e 当前元素(用于循环遍历) // p 目标节点 //

  • 解析ConcurrentHashMap: transfer方法源码分析(难点)

    上一篇文章介绍过put方法以及其相关的方法,接下来,本篇就介绍一下transfer这个方法(比较难),最好能动手结合着源码进行分析,并仔细理解前面几篇文章的内容~ 注:代码分析的注释中的CASE0.CASE1- ,这些并没有直接关联关系,只是为了给每个if逻辑判断加一个标识,方便在其他逻辑判断的地方进行引用而已. 再复习一下并发Map的结构图: 1.transfer方法 transfer方法的作用是:迁移元素,扩容时table容量变为原来的两倍,并把部分元素迁移到其它桶nextTable中.该方

  • PHP基于DOMDocument解析和生成xml的方法分析

    本文实例讲述了PHP基于DOMDocument解析和生成xml的方法.分享给大家供大家参考,具体如下: 前面和大家分享了SimpleXML操作xml的一些知识,但是php中除了simplexml还有DOMDocument,这次就着重来看看DOMDocument的用法,还是把生成xml和解析xml分开写 1. xml的生成 DOMDocument操作xml要比先前的simplexml要复杂一点,我觉得simplexml就想Java里的dom4j,不管怎样原理都是一样的.如果把DOMDocument

  • Java中构造器内部的多态方法的行为实例分析

    本文实例讲述了Java中构造器内部的多态方法的行为操作.分享给大家供大家参考,具体如下: 这篇文章主要讨论的是,若在一个构造器中调用正在构造的对象的某个动态绑定的方法时会出现的情况.在此之前,我们需要知道构造器是如何在复杂的层次结构中运作的,尽管构造方法并不具有多态性,因为它们实际上是static方法,只不过是隐式声明的static. 复杂层次结构中构造器的调用顺序 基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用.这样做是因为,在Jav

  • JavaScript实现重力下落与弹性效果的方法分析

    本文实例讲述了JavaScript实现重力下落与弹性效果的方法.分享给大家供大家参考,具体如下: 这里利用JS语言在Html页面中实现重力作用下落地后弹起的效果,如下所示: 在此例中主要涉及以下几个问题: 1.给球体一个释放初速度,如何实现越向下运动且在接触边缘之前,竖直方向上的速度speedY越大的效果? 答案:可以在计时器中,每及时一次,竖直方向上的速度speedY自增一个固定值来实现,下面代码中speedY += 6;就是实现这个效果. 2.球体接触地面(此例中指浏览器下边缘)后,如何实现

随机推荐