concurrentHashmap

http://www.importnew.com/23907.html
HashMap是开发时候经常使用的集合是非线程安全的,在涉及到线程并发的情况下,进行get操作会引起死循环,导致cpu的利用率很高,解决方案由Hashtable和collections.synchronizedMap(hashMap)这两种方式是读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能很低
1.7的ConcurrentHashMap采用的是分段锁的机制,实现并发的更新操作,底层采用数组加上链表的存储结构,其中包含两个静态内部类Segment和HashEntry
Segment继承ReentrantLock用来充当锁的角色,一个ConcurrentHashMap结构,包含Segment对象数组,每个Segment对象指向一个table数组,table数组中是hashEntry对象,对象在数组中的索引相同的时候,可以转变为链表
1.8的ConcurrentHashMap的实现抛弃了Segment分段锁机制,采用CAS+Synchronized保证并发更新安全,底层采用数组+链表+红黑树的存储结构
在这里插入图片描述
在这里插入图片描述其中table默认为null初始化发生在第一次插入操作的时候,默认大小为16的数组,用来存储node节点数据没扩容的时候,用来存储Node节点的数据,扩容的大小为2的幂次方
**nextTable默认为为null扩容的时候,新生成的数组,**其大小为原来数组的两倍
sizeCtl默认为0,用来空值table的初始化和扩容操作,
Node节点中保存着key value 和key的hash值,以及下一个node节点next,值value和节点next使用voaltile变量进行修饰,保证了并发的可见性
class Node<K,V> implements Map<K, V>.Entry<K, V>{
final K key;
final int hash;
volatile V val;
volatile Node<k,v> next
}

ForwardingNode:一个特殊的的Node节点,hash值为-1,forwarding内部存储了扩容后的数组nextTable,并且接收原先数组table
final class ForwardingNode<k,v> extends Node<K, V>{
final Node<K, V>[] nextTable; //
ForwardingNode(Node<k, V>[] tab){
super(MOVED,null,null,null);
this.nextTable=tabl
}
}
https://blog.csdn.net/sihai12345/article/details/79383766
https://blog.csdn.net/zguoshuaiiii/article/details/78495332
https://blog.csdn.net/tp7309/article/details/76532366
http://www.importnew.com/28263.html
https://www.cnblogs.com/banjinbaijiu/p/9147434.html
只有在table数组扩容的时候会用到forwardingNode,作为有一个占位符放在table中表示当前节点为null或者已经北移动
实例初始化,在这个构造函数中什么都不做
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));
this.sizeCtl = cap;// 初始化sizeCtl
}
ConcurrentHashMap<String,String> hashMap=new ConcurrentHashMap<>(100);
实例化ConcurrentHashMap的时候,有整数形式,当初始容量小于0的时候抛出异常,当容量大于MAXMUM_CAPACITY表的最大容量的时候初始容量的大小为表的最大容量,否则会找到最接近该容量的2的幂次方数,通过提供的初始容量计算了sizeCtl,没有初始化table只是初始化了sizeCtl,sizeCtl的值等于
0.75倍的initialCapacity+1之后向上取最接近2的n次方
table的初始化是发生在第一次put插入的时候,而put操作是可以并发执行的,那么什么时候可以并发执行,如何保证table初始化只有一次呢
sizeCtl:默认为0用来控制table的初始化和扩容操作
sizeCtl是一个控制标识符,取值不同
负数表示其他线程正在进行初始化或者是扩容操作,-1表示正在进行初始化,,-N表示有N-1个线程正在进行扩容操作,
正数或者0表示还没有进行初始化,这个数值表示初始化或者下一次扩容的大小
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {//while循环开始,如果不为空不进入循环,判断之后直接返回节点数组,表示已经初始化了也不是扩容的情况,进入循环表示正在进行初始化或者是扩容操作
if ((sc = sizeCtl) < 0) // 如果sizeCtl的值小于0,表示其他线程正在进行初始化或者是扩容操作,-1表示扩容操作,-N表示有N个线程正在进行扩容操作,让出当前线程,如果当前sizeCtl的值并不小于0,说明没有其他线程正在初始化或者是扩容操作,说明还没有初始化
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 当前线程开始初始化如果当前sizeCtl的值大于等于0,表示当前线程正在初始化,执行CAS原操作,将sc的值设置为-1,表示当前线程正在进行初始化操作,如果执行CAS操作失败将会重新进入while循环
try {
if ((tab = table) == null || tab.length == 0) { // 如果table的大小为0,为空
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 如果指定了初始容量就将节点数组的长度值n设为sc,否则设置为默认数组长度(16)
@SuppressWarnings(“unchecked”)
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 创建一个长度为n的节点数组,此时已经初始化完成
table = tab = nt;
sc = n - (n >>> 2); // 将sc的值设置为节点数组长度的0.75,初始化完成之后,sizeCtl表示的是table的容量,table大小的0.75倍
}
} finally {
sizeCtl = sc; // 将sc的值设置为节点数组长度的0.75
}
break;
}
}// while循环退出
return tab;
}
在intTable()方法中,首先会进行while循环,满足条件的就进入循环中,不满足(table的不为空,并且不是null)直接退出循环,返回Node类型的节点数组
yield()(yeaode)作用是让步,他能够让当前线程从运行状态进入就绪状态,从而让其他等待线程获取执行权,但是不能保证当前线程调用yield()之后其他线程就一定能够获得执行权,也有可能是当前线程又回到 了运行状态,继续运行。
对于table的大小会根据sizeCtl的值进行设置,如果没有设置sizeCtl的值,那么默认生成table大小为16,否则会根据sizeCtl调整table的大小
ConcurrentHashMap中定义了三个原子操作对指定位置进行原子操作,这些原子操作保证了ConcurrentHashMap线程安全
tabAt(Node[] tab,int i),tabAt函数的作用是给定一个数组和索引返回数组table中下标为i的结点,是通过Unsafe对象通过反射Unsfe.getObjectVolatile获取的,getObjectVolatile中的第一项是tab数组,第二项是下标为i的偏移地址
casTabAt(Node[] tab,int i, Node c, Node v)比较数组tab下标为i的值是否是节点c,如果是节点c就将节点c的值用节点v替换
替换的过程使用的是CAS算法,设置数组下标i上的节点Node,能实现并发是因为他指定了原来这个节点的值是多少,再CAS中会比较当前内存中的这个值和指定的这个值是否相同,如果相同才可以修改,因此当前线程的值可能并不是最新值,这种修改可能会覆盖掉其他线程的修改结果

setTabAt(Node[] int i, Node v),setTabAt函数的作用是给定一个节点数组和索引,是通过Unsafe对象通过反射Unsafe.putObjectVolatile将数组中的索引i设置为节点v
helpTransfer(Node[] tab, Node f)
如果tab不是空,并且f是ForwardingNode节点类型,并且节点f的nextTable不为空,进行扩容,判断节点的nextTable是否与nextTab相同,并且数组是否是ta数组,并且sc是否小于0,条件判断,使用cas比较并进行狡猾,将able的节点转移到nextTab数组,中之后退出nextTab中,
helpTransfer函数用于在扩容的时候将table表中的节点转移到nextTable中,用于将table表中的节点转移到nextTable中
java8中使用Node,其中节点Node中包含,key value key的hash值,以及节点next,Node只能用于链表的情况,红黑树的情况需要使用TreeNode,根据数组中第一个节点类型是Node还是TreeNode判断该位置下是链表还是红黑树,为什么要使用红黑树,当链表中的元素过长,顺着链表查找元素的时间复杂度为O(n),为了降低部分开销,当链表中元素超过8个以后将会转换成为红黑树,在这个位置进行查找的时候将会降低时间复杂度为O(logn)

final V putVal(K key, V value, boolean onlyIfAbsent) { // put操作传入的数值有key value onlyIfAbsent evict其中onlyifAbsent如果是true那么之后再不存在该key的时候才会进行put操作
    if (key == null || value == null) throw new NullPointerException();// 首先判断key value的值是否为空,如果为空的话抛出异常
    int hash = spread(key.hashCode());//计算key的hash值
    int binCount = 0; // binCount用于记录相应链表的长度
    for (Node<K,V>[] tab = table;;) {// 进入无限循环中,无限循环可以确保能够成功的插入数据到表中
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)// 判断table表是否为空或者长度是否为0,如果为空或者长度为0进行初始化操作
            tab = initTable();
        else if ((f = tabAt(tab, **i = (n - 1) & hash**)) == null) {// 如果table数组不为空并且--数组长度大于0根据key的hash值取出table表中的节点元素,如果取出的节点为空,该数组的这个位置为空,用cas操作将新的节点放入其中,执行casTabAt操作,casTabAt(Node[] node ,int i, Node c,Node v)比较数组tab下标为i的值是否为节点c,如果为节点c的话将节点c的值变为节点v,如果执行cas设置成功,将跳出循环,如果CAS操作失败,说明有并发操作,这时候进入下一个循环中
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        } 
        else if ((fh = f.hash) == MOVED)//如果tab数组不为空,并且取出的table数组中的节点元素f也不是空,如果节点f的hash值为MOVED,则将tab表中的节点f进行转移
            tab = helpTransfer(tab, f);
            //到这里根据key的hash值取出的节点f不是空hash值不为null,是table数组中的值,是该位置的头节点
        else {//如果从table表中取出的节点的hash值不为MOVED,获取table数组中该节点的监视器锁 ,桶中第一个元素是f(链表中的第一个元素,或者是红黑树中的第一个元素)进行加锁
            V oldVal = null; 
            synchronized (f) {
                if (tabAt(tab, i) == f) {   
                    **if (fh >= 0) {// 如果头节点的hash值大于0说明是链表**
                        binCount = 1; // binCount用于累加记录链表的长度
                        for (Node<K,V> e = f;; ++binCount) {// 遍历链表
                            K ek; 
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {// 如果节点的hash值和key值与给定的节点的hash值与key值相同
                                oldVal = e.val;
                                if (!onlyIfAbsent)// 根据标识判断是否进行更新操作
                                    e.val = value; 判断是否进行更新操作,用给定的值替换该节点的value值,
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {// 如果遍历完之后还是没有找到hashkey值与指定的Node节点的hash值和key值相同,那么就在创建新节点并赋值为最后一个节点的下一个节点
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 如果hash值小于0,判断这个这个头节点f是否是红黑树的节点,就向这个红黑树中添加节点   
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,// 调用红黑树的插值方法插入新节点中
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
			
            if (binCount != 0) { 
                if (binCount >= TREEIFY_THRESHOLD)//将节点添加上之后判断binCount的值为多少,如果是添加到了红黑树中那么bincount的值为2,如果添加到链表中,开始时候链表的长度为1,之后进行遍历,值也在不断的增加,如果到达了红黑树转换的阈值8之后
                    **treeifyBin(tab, i);//不一定会进行红黑树的转换,如果当前数组的长度小于64,将会对数组进行扩容,而不是转换为红黑树**
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

将链表转化为红黑树
当binCount链表的长度大于8的时候不一定会进行红黑树的转换,如果当前数组的长度小于64,将会进行扩容,而不是转换为红黑树

private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {// 如果table不是空的
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)// 如果table的数组长度小于最小红黑树扩容值64,进行数组扩容
tryPresize(n << 1);// 执行扩容操作,翻倍扩容,传递的值是原来数组的两倍

        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {// 如果数组table的长度大于64,如果table数组中索引i处(头节点)不为null并且节点的hash值大于0(是链表),获取头节点对应的监视器,对头节点进行加锁
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    // 遍历链表,创建TreeNode节点,建立一颗红黑树
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    // 将红黑树设置在相应的位置上
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

执行treeIfyBin的时候,当数组的长度小于红黑树最小扩容容量的时候,执行tryPresize数组扩容操作,这里的扩容操作也是翻倍扩容,扩容后的数组容量为原来的两倍,
private final void tryPresize(int size) {// 在执行treeIfyBin函数的时候就已经进行了size翻倍
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) { // sizeCtl大于等于0表示还没有初始化,表示初始化或者是扩容时候的大小
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {// 如果这个数组为空
n = (sc > c) ? sc : c;// 如果数组的sc大于c就将数组的长度是指为n
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// 执行cas原子操作将sizeCtl的值设置为-1表示正在进行初始化
try {
if (table == tab) {
@SuppressWarnings(“unchecked”)
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}

数据迁移
第一个发起数据迁移的线程会将transferIndex指向原数组最后的位置,然后从后往前的stride个任务属于第一个线程,然后将transferIndex指向新的位置,再往前stride个任务属于第二个线程,这里说的额第二个线程不一定真的只带了第二个线程,有可能是同一个线程,就是将一个大的迁移任务分为了一个个任务包
单线程的时候直接等于n多线程的时候最小值为16
stride可以理解为步长有n个位置是需要进行迁移的,将这n个任务分为多个任务包,每个任务包有stride个任务
如果nextTab==null先进行一次初始化操作,外围会保证第一个发起迁移的线程调用参数nextTab为之后参与迁移的线程调用此方法时候
容量翻倍,nextTab是ConcurrentHashMap中的属性,只是将容量翻倍了,transferIndex也是ConcurrentHashMap中的属性,用于控制迁移的位置
intnextn=nextTab.length;
ForwardingNode指的是被迁移过来的Node,这个方法会生成一个Node,key value next都是null 关键是hash为hash为Moved后面会看到原数组中位置i处的节点完成迁移工作之后,就会将为值i处设置为ForwardingNode,用来告诉其他线程该位置已经处理过了,forwardingNode表示在被迁移的Node用来告诉其他线程该位置已经处理过,所以他其实是相当于一个标志,
advance指的是做完了位置的迁移工作,可以准备下一个位置的了
for循环中i是位置的索引,bbound是边界,注意是从后往前进行,whileadvance为true表示可以进行下一个位置的迁移了,简单的理解是:i指向了tansferIndex,而bound指向了transferIndex-stride,将transfer的值赋给nextIndex
这里transferIndex一旦小于等于0说明原数组所有位置都有相应的线程去处理了

transfer方法实现了在并发的情况下,高效的从原始数组往新数组中移动元素
X为数组长度的2幂次方,数组长度为16,X即为4

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值