java集合类_ConcurrentHashMap

 

ConcurrentHashMap在java1.8进行了重大的改进。
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:

java集合类_ConcurrentHashMap_第1张图片

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。


JDK1.8的实现
       JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。ConcurrentHashMap是HashMap的升级版,HashMap是非线程安全的集合,ConcurrentHashMap则可以支持并发操作。 因为ConcurrentHashMap 和HashMap都是采用相同的数据结构,因此在分析ConcurrentHashMap 之前,最好先了解HashMap,这样更容易理解。

1、基础知识

1.1、静态常量

在深入学习JDK1.8 ConcurrentHashMap之前,先看一下其静态常量:

// node数组最大容量:2^30=1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
//存储方式由链表转成红黑树 table 容量的最小阈值
static final int MIN_TREEIFY_CAPACITY = 64;
//用于hash 表扩容后,搬移数据的步长(下面几个属性都是用于扩容或者控制sizeCtl 变量)
static final int MIN_TRANSFER_STRIDE = 16;
static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1; 
// 树根节点的hash值
static final int TREEBIN   = -2; 
// ReservationNode的hash值
static final int RESERVED  = -3; 
// 可用逻辑处理器数量,如果cup是2核4线程,返回4.
static final int NCPU = Runtime.getRuntime().availableProcessors();

1.2、Node

Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,源代码如下:

 static class Node implements Map.Entry {
        final int hash;  //HashMap中该字段存放的是key.hashCode();而ConcurrentHashMap存放的是spread(key.hashCode())
        final K key;   //key值
        volatile V val;  //value值
        volatile Node next; //下一个节点

        Node(int hash, K key, V val, Node next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
}

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
}

1.3、TreeNode

TreeNode 用于构建红黑树节点,但是ConcurrentHashMap 中的TreeNode和HashMap中的TreeNode用途有点差别,HashMap中hash 表的部分位置上存储的是一颗树,具体存储的就是TreeNode型的树根节点,而ConcurrentHashMap 则不同,其hash 表是存储的被TreeBin 包装过的树,也就是存放的是TreeBin对象,而不是TreeNode对象,同时TreeBin 带有读写锁,当需要调整树时,为了保证线程的安全,必须上锁。
 

static final class TreeNode extends Node {
        TreeNode parent;  // red-black tree links
        TreeNode left;
        TreeNode right;
        TreeNode prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node next,
                 TreeNode parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }
    }

TreeBin 对象

static final class TreeBin extends Node {
   TreeNode root; // 树根
   volatile TreeNode first; // 树的链式结构
   volatile Thread waiter; // 等待者
   volatile int lockState; // 锁状态
   // values for lockState
   static final int WRITER = 1; // set while holding write lock
   static final int WAITER = 2; // set when waiting for write lock
   static final int READER = 4; // increment value for setting read lock
}


ForwardingNode
static final class ForwardingNode extends Node {
  final Node[] nextTable;
  ForwardingNode(Node[] tab) {
      super(MOVED, null, null, null); // hash 值为MOVED 进行标识 
      this.nextTable = tab;
  }

ForwardingNode 用于在hash 表扩容过程中的过渡节点,当hash 表进行扩容进行数据转移的时候,其它线程如果还不断的往原hash 表中添加数据,这个肯定是不好的,因此就引入了ForwardingNode 节点,当对原hash 表进行数据转移时,如果hash 表中的位置还没有被占据,那么就存放ForwardingNode 节点,表明现在hash 表正在进行扩容转移数据阶段,这样,其它线程在操作的时候,遇到ForwardingNode 节点,就知道hash 现在的状态了,就可以协助参与hash 表的扩容过程。到这里,ConcurrentHashMap 中的重要的数据结构基本都了解了,一个是hash 表(table),一个是链表节点Node,其实呢就是红黑树节点TreeNode。


1.4、Unsafe

       Unsafe类提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是不安全的,它所分配的内存需要手动free(不被GC回收)。java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作。

在ConcerrentHashMap中:


 // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long SIZECTL;  //sizeCtl字段的偏移量
    private static final long TRANSFERINDEX;//transferIndex字段的偏移量
    private static final long BASECOUNT;//baseCount字段的偏移量
    private static final long CELLSBUSY;//cellsBusy字段的偏移量
    private static final long CELLVALUE;//CounterCell类中value字段的偏移量
    private static final long ABASE;//Node数组的头部偏移量
    private static final int ASHIFT;//保存的是

    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class k = ConcurrentHashMap.class;
            SIZECTL = U.objectFieldOffset
                (k.getDeclaredField("sizeCtl"));
            TRANSFERINDEX = U.objectFieldOffset
                (k.getDeclaredField("transferIndex"));
            BASECOUNT = U.objectFieldOffset
                (k.getDeclaredField("baseCount"));
            CELLSBUSY = U.objectFieldOffset
                (k.getDeclaredField("cellsBusy"));
            Class ck = CounterCell.class;
            CELLVALUE = U.objectFieldOffset
                (ck.getDeclaredField("value"));
            Class ak = Node[].class;
            ABASE = U.arrayBaseOffset(ak);
            int scale = U.arrayIndexScale(ak);
            if ((scale & (scale - 1)) != 0)//检测scale是不是2的幂。
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);//童年ASHIFT =√scale 
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    /**
     * 获取tab数组中索引为i的值。
     */
    static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
    /**
     * tab数组中,如果a[i]等于c,将a[i]设置成v并返回true,否则返回false
     */
    static final  boolean casTabAt(Node[] tab, int i,
                                        Node c, Node v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
    /**
     * 取tab数组中索引为i的格子设置成v。
     */
    static final  void setTabAt(Node[] tab, int i, Node v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

Unsafe这里不多说,但是我们要理解 "偏移量"、Unsafe.getObjectVolatile方法、Unsafe.compareAndSwapObject方法 和 Unsafe.putObjectVolatile方法的意思。
举个例子:

class Person {
    private String name="无";
}


public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//        Unsafe unsafe = Unsafe.getUnsafe();  //不允许外部使用,会抛异常。

        //通过反射获取unsafe对象
        Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeInstance.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);

        //获取Person.name字段的偏移量
        long nameOffset = unsafe.objectFieldOffset
                (Person.class.getDeclaredField("name"));

        Person person = new Person();
        System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//无,获取name值。

        unsafe.compareAndSwapObject(person, nameOffset, "呜呜", "小黑");//false,”呜呜“不等于”无“,无法成功设置新值
        System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//无

        unsafe.compareAndSwapObject(person, nameOffset, "无", "小黑");//true ,成功设置新值
        System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//小黑

        unsafe.putObjectVolatile(person, nameOffset,"小强"); //这种方法强制设置新值,不关心旧值。
        System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//小强

    }
}

结果:

java集合类_ConcurrentHashMap_第2张图片

这个例子中,通过unsafe类完成了对person对象的操作。

unsafe类如何操作数组呢?

首先普及一下数组知识,数组是块连续的存储空间,如果数组中存放的是基本数据类型,是将数据直接保存在数组中;如果保存的是对象,其实只是保存的是对象的地址。

 public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

        //通过反射获取unsafe对象
        Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeInstance.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);

        long ABASE = unsafe.arrayBaseOffset(String[].class);//数组的头部偏移量
        int  scale = unsafe.arrayIndexScale(String[].class);//如果是基本数据类型的数组,返回数据类型长度,如long,返回8;否则,返回对象的地址长度,32位系统返回4,64位系统返回4或8.

        String[] stringArray = new String[10];
        String str = "哈哈";
        int index = 5;

        unsafe.putObjectVolatile(stringArray, ABASE + (index *scale), str);
        String StrNew = (String) unsafe.getObjectVolatile(stringArray, ABASE + (index *scale));

        System.out.println(StrNew);
        System.out.println(stringArray[index]);

    }

结果:

java集合类_ConcurrentHashMap_第3张图片

 

2、PUT方法

对于集合类的增删改查操作,其中最为重要的就是添加操作,理解了添加操作就掌握了这个集合。


public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//tab为空时初始化tab。
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//tab[ (n - 1) & hash)]不存在时
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))//新创建的填node加到tab中,如果填加成功,跳出循环。
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)// 如果table位置上的节点状态时MOVE,则表明table正在进行扩容搬移数据的过程中
                tab = helpTransfer(tab, f); //协助扩容
            else {// hash 表该位置上有数据,可能是链表,也可能是一颗树
                V oldVal = null;
                synchronized (f) {/将table表该位置进行上锁,保证线程安全
                    if (tabAt(tab, i) == f) {// 上锁后,只有再该位置数据和上锁前一致才进行,否则需要重新循环
                        if (fh >= 0) {// hash 值>=0 表明这是一个链表结构
                            binCount = 1;
                            for (Node e = f;; ++binCount) {// 遍历链表
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                if ((e = e.next) == null) { // 将新数据添加到链表尾
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {// 该位置是红黑树,是TreeBin对象(注意是TreeBin,而不是TreeNode)
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //添加了数据,需要进行检查
                 if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)  //if 成立,说明遍历的是链表结构,并且超过了阀值,需要将链表转换为树
                        treeifyBin(tab, i); //将table 索引i 的位置上的链表转换为红黑树
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
       // ConcurrentHashMap 容量增加1,检查是否需要扩容
        addCount(1L, binCount);
        return null;
    }

主要知识点:

1、计算key的hash 值。
HashMap中Node.hash存放的是key.hashCode(),为什么ConcurrentHashMap 中Node.hash存放 spread(key.hashCode())?因为ConcurrentHashMap 中Node.hash还被用来作标识,
当Node.hash=-1时表示这是一个forwardNode节点;
当Node.hash=-2时表示这时一个TreeBin节点;
当Node.hash=-3时表示短暂保留节点;
当Node.hash>=0时表示这时一个普通node节点。

因为key.hashCode()有可能返回负数(-1、-2),因此ConcurrentHashMap中Node.hash就不能在直接使用key.hashCode(),所以使用spread方法进行转换,spread方法的返回值一定大于等于0。

 static final int MOVED     = -1; // hash for forwarding nodes
 static final int TREEBIN   = -2; // hash for roots of trees
 static final int RESERVED  = -3; // hash for transient reservations
 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

2、initTable方法

如果table没有被初始化,则执行table的初始化过程,使用initTable方法对table进行始化。

/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义:
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
 */
private transient volatile int sizeCtl; 


public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

private final Node[] initTable() {
        Node[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)//sizeCtl为-1代表别的线程正在初始化,-N代表有N-1个线程正在 进行扩容
                Thread.yield(); // 线程让步,其实就是等待别的线程操作完成。
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//sizeCtl为0表于table还没有被初始化;设置成-1,开始初始化
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//获取table初始化大小
                        @SuppressWarnings("unchecked")
                        Node[] nt = (Node[])new Node[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);//设置下一次进行扩容的大小。
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

 

你可能感兴趣的:(java基础进阶)