理解Java7和8里面HashMap+ConcurrentHashMap的扩容策略

并发编程学习之ConcurrentHashMap扩容机制 一、简介 在ConcurrentHashMap中,比较复杂部分就是其扩容机制,因为涉及到多个线程分工合作完成数据迁移和key的rehash操作。 二、扩容思路 ConcurrentHashMap扩容一般分为两个步骤: 【a】Node<K,V>[] table数组的扩容,一般是扩大到原来数组大小的两倍; 【b】key的rehash以及数据的迁移:因为计算key在桶中的索引ind... 阅读详情

原文:https://blog.youkuaiyun.com/u010454030/article/details/82458413

前言

理解HashMap和ConcurrentHashMap的重点在于:

(1)理解HashMap的数据结构的设计和实现思路

(2)在(1)的基础上,理解ConcurrentHashMap的并发安全的设计和实现思路

前面的文章已经介绍过Map结构的底层实现,这里我们重点放在其扩容方法, 这里分别对JDK7和JDK8版本的HashMap+ConcurrentHashMap来分析:

JDK7的HashMap扩容

这个版本的HashMap数据结构还是数组+链表的方式,扩容方法如下:

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了旧的Entry数组  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组  
        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素  
        if (e != null) {  
            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置  
                e.next = newTable[i]; //标记[1]  
                newTable[i] = e;      //将元素放在数组上  
                e = next;             //访问下一个Entry链上的元素  
            } while (e != null);  
        }  
    }  
}

上面的这段代码不并不难理解,对于扩容操作,底层实现都需要新生成一个数组,然后拷贝旧数组里面的每一个Node链表到新数组里面,这个方法在单线程下执行是没有任何问题的,但是在多线程下面却有很大问题,主要的问题在于基于头插法的数据迁移,会有几率造成链表倒置,从而引发链表闭链,导致程序死循环,并吃满CPU。据说已经有人给原来的SUN公司提过bug,但sun公司认为,这是开发者使用不当造成的,因为这个类本就不是线程安全的,你还偏在多线程下使用,这下好了吧,出了问题这能怪我咯?仔细想想,还有点道理。

JDK7的ConcurrentHashMap扩容

HashMap是线程不安全的,我们来看下线程安全的ConcurrentHashMap,在JDK7的时候,这种安全策略采用的是分段锁的机制,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个 HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。

下面看下其扩容的源码:

// 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 2 倍
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    // 创建新数组
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    // 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’
    int sizeMask = newCapacity - 1;
 
    // 遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置
    for (int i = 0; i < oldCapacity ; i++) {
        // e 是链表的第一个元素
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            // 计算应该放置在新数组中的位置,
            // 假设原数组长度为 16,e 在 oldTable[3] 处,那么 idx 只可能是 3 或者是 3 + 16 = 19
            int idx = e.hash & sizeMask;
            if (next == null)   // 该位置处只有一个元素,那比较好办
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                // e 是链表表头
                HashEntry<K,V> lastRun = e;
                // idx 是当前链表的头结点 e 的新位置
                int lastIdx = idx;
 
                // 下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的所有元素是将要放到一起的
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                // 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置
                newTable[lastIdx] = lastRun;
                // 下面的操作是处理 lastRun 之前的节点,
                //    这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    // 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

注意这里面的代码,外部已经加锁,所以这里面是安全的,我们看下具体的实现方式:先对数组的长度增加一倍,然后遍历原来的旧的table数组,把每一个数组元素也就是Node链表迁移到新的数组里面,最后迁移完毕之后,把新数组的引用直接替换旧的。此外这里这有一个小的细节优化,在迁移链表时用了两个for循环,第一个for的目的是为了,判断是否有迁移位置一样的元素并且位置还是相邻,根据HashMap的设计策略,首先table的大小必须是2的n次方,我们知道扩容后的每个链表的元素的位置,要么不变,要么是原table索引位置+原table的容量大小,举个例子假如现在有三个元素(3,5,7)要放入map里面,table的的容量是2,简单的假设元素位置=元素的值 % 2,得到如下结构:

[0]=null
[1]=3->5->7

现在将table的大小扩容成4,分布如下:

[0]=null
[1]=5->7
[2]=null
[3]=3

因为扩容必须是2的n次方,所以HashMap在put和get元素的时候直接取key的hashCode然后经过再次均衡后直接采用&位运算就能达到取模效果,这个不再细说,上面这个例子的目的是为了说明扩容后的数据分布策略,要么保留在原位置,要么会被均衡在旧的table位置,这里是1加上旧的table容量这是是2,所以是3。基于这个特点,第一个for循环,作的优化如下,假设我们现在用0表示原位置,1表示迁移到index+oldCap的位置,来代表元素:

[0]=null
[1]=0->1->1->0->0->0->0

第一个for循环的会记录lastRun,比如要迁移[1]的数据,经过这个循环之后,lastRun的位置会记录第三个0的位置,因为后面的数据都是0,代表他们要迁移到新的数组中同一个位置中,所以就可以把这个中间节点,直接插入到新的数组位置而后面附带的一串元素其实都不需要动。

接着第二个循环里面在此从第一个0的位置开始遍历到lastRun也就是第三个元素的位置就可以了,只循环处理前面的数据即可,这个循环里面根据位置0和1做不同的链表追加,后面的数据已经被优化的迁移走了,但最坏情况下可能后面一个也没优化,比如下面的结构:

[0]=null
[1]=1->1->0->0->0->0->1->0

这种情况,第一个for循环没多大作用,需要通过第二个for循环从头开始遍历到尾部,按0和1分发迁移,这里面使用的是还是头插法的方式迁移,新迁移的数据是追加在链表的头部,但这里是线程安全的所以不会出现循环链表,导致死循环问题。迁移完成之后直接将最新的元素加入,最后将新的table替换旧的table即可。

JDK8的HashMap扩容

在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构了,因为在hash冲突严重的情况下,链表的查询效率是O(n),所以JDK8做了优化对于单个链表的个数大于8的链表,会直接转为红黑树结构算是以空间换时间,这样以来查询的效率就变为O(logN),图示如下:

在这里插入图片描述
我们看下其扩容代码:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                        //重点关注区域
                        // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
 

在JDK8中,单纯的HashMap数据结构增加了红黑树是一个大的优化,此外根据上面的迁移扩容策略,我们发现JDK8里面HashMap没有采用头插法转移链表数据,而是保留了元素的顺序位置,新的代码里面采用:

                        //按原始链表顺序,过滤出来扩容后位置不变的元素(低位=0),放在一起
                        Node<K,V> loHead = null, loTail = null;
                        //按原始链表顺序,过滤出来扩容后位置改变到(index+oldCap)的元素(高位=0),放在一起
                        Node<K,V> hiHead = null, hiTail = null;

把要迁移的元素分类之后,最后在分别放到新数组对应的位置上:

                        //位置不变    
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //位置迁移(index+oldCap)
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }

JDK7里面是先判断table的存储元素的数量是否超过当前的threshold=table.length*loadFactor(默认0.75),如果超过就先扩容,在JDK8里面是先插入数据,插入之后在判断下一次++size的大小是否会超过当前的阈值,如果超过就扩容。

JDK8的ConcurrentHashMap扩容

在JDK8中彻底抛弃了JDK7的分段锁的机制,新的版本主要使用了Unsafe类的CAS自旋赋值+synchronized同步+LockSupport阻塞等手段实现的高效并发,代码可读性稍差。

ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数,在JDK7里面最大并发个数就是Segment的个数,默认值是16,可以通过构造函数改变一经创建不可更改,这个值就是并发的粒度,每一个segment下面管理一个table数组,加锁的时候其实锁住的是整个segment,这样设计的好处在于数组的扩容是不会影响其他的segment的,简化了并发设计,不足之处在于并发的粒度稍粗,所以在JDK8里面,去掉了分段锁,将锁的级别控制在了更细粒度的table元素级别,也就是说只需要锁住这个链表的head节点,并不会影响其他的table元素的读写,好处在于并发的粒度更细,影响更小,从而并发效率更好,但不足之处在于并发扩容的时候,由于操作的table都是同一个,不像JDK7中分段控制,所以这里需要等扩容完之后,所有的读写操作才能进行,所以扩容的效率就成为了整个并发的一个瓶颈点,好在Doug lea大神对扩容做了优化,本来在一个线程扩容的时候,如果影响了其他线程的数据,那么其他的线程的读写操作都应该阻塞,但Doug lea说你们闲着也是闲着,不如来一起参与扩容任务,这样人多力量大,办完事你们该干啥干啥,别浪费时间,于是在JDK8的源码里面就引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,其含义如下:

sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
-1 代表table正在初始化
-N 表示有N-1个线程正在进行扩容操作
其余情况:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍

扩容时候会判断这个值,如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd,如果f == null,则在table中的i位置放入fwd,否则采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,然后 给旧table原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。在此期间如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。

扩容源码如下:

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

在扩容时读写操作如何进行

(1)对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。

如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。

(2)对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

对于size和迭代器是弱一致性

volatile修饰的数组引用是强可见的,但是其元素却不一定,所以,这导致size的根据sumCount的方法并不准确。

同理Iteritor的迭代器也一样,并不能准确反映最新的实际情况

总结

本文主要了介绍了HashMap+ConcurrentHashMap的扩容策略,扩容的原理是新生成大于原来1倍大小的数组,然后拷贝旧数组数据到新的数组里面,在多线程情况下,这里面如果注意线程安全问题,在解决安全问题的同时,我们也要关注其效率,这才是并发容器类的最出色的地方。

ConcurrentHashMap 扩容机制(源码解析) ConcurrentHashMap 扩容机制解析,炒鸡详细 阅读详情

相关推荐

从面试暖场题到挖出JDK Bug: 谈谈你对 HashMap, ConcurrentHashMap 的理解

本文分享了java HashMap 等相关数据结构实现原理, 以及在java 8 源码阅读中挖出的一个bug

萧萧的专栏 3017

ConcurrentHashMap的扩容

ConcurrentHashMap(以下简称CHM)扩容的实现非常精妙,可以说是CHM的精髓所在。它可以在不启用额外线程的情况下,借用已有的线程实现快速扩容,同时还可以保证线程安全。下面我们不妨带着下面这几个问题一块儿来看看CHM的源码。 ...

大数据时代的狂欢 7200

ConcurrentHashMap源码解析,多线程扩容

前面一篇已经介绍过了 HashMap 的源码: HashMap并不是线程安全的,他就一个普通的容器,没有做相关的同步处理,因此线程不安全主要体现在: put、get 等等核心方法在多线程情况下,都会出现修改的覆盖,数据不一致等等问题。比如多个线程 put 先后的问题,会导致结果覆盖,如果一个 put 一个get,也可能会因为调度问题获取到错误的结果; 多线程操作有读有写的时候,可能会出现一个典型异常:ConcurrentModificationException 另外扩容的时候,hashmap1.7 的实

LifeGoesOn 2104

ConcurrentHashMap的扩容方法详解

一、ConcurrentHashMap扩容 (jdk1.7) 扩容只针对ConcurrentHashMap中的hashEntry类型的table数组进行扩容。 方法在Segment类中:rehash() , 扩容倍数为:2倍扩容 源码解析: private void rehash(HashEntry<K,V> node) { //先将需要扩容的table数组进行拷贝 ...

Jewel’s blog 9751

ConcurrentHashMap 扩容

Java 8的引入了一些改进,其中之一与在哈希表变得太满时进行扩容或扩展有关。旨在在不需要外部同步的情况下高效处理并发读写操作。在Java 8中,引入了一种新的机制来进行扩容和重新哈希,以提高性能并减少扩容过程中的争用。这个机制被称为"TreeNodes",当哈希表中的一个存储桶变得过于拥挤时会使用它。以下是Java 8中:当创建时,需要指定一个初始容量。哈希表从这个容量开始,并创建一个基于指定容量的存储桶数组。:负载因子是一个阈值,用于确定何时应该对进行扩容。

学无止境 829

Cesium创建无人机视锥,同步视锥与鹰眼地图视角,实现视锥键盘控制。解决视锥初始化位置不准确问题等

本文描述了一个基于Cesium的3D视锥体可视化系统开发过程。系统实现了主视图与鹰眼视图的双屏联动,支持通过键盘控制视锥体的空间移动(WASD前后左右,Q/E旋转,C/Z升降),并实时同步更新两个视图。关键技术包括:1)基于主视图Scene创建Camera实现视锥体精确绘制;2)通过PerspectiveFrustum设置视锥参数;3)建立双视图的相机位置及视角同步机制。目前暂未实现视锥体far/near边界在鹰眼视图中的可视化,该系统可应用于无人机视角监控等三维场景交互需求。

coder_bin的博客 1589

HashMap & ConcurrentHashMap

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 (JDK 1.8 之前)JAVA HASHMAP的死循环 谈谈ConcurrentHashMap1.7和1.8的不同实现 深入浅出ConcurrentHashMap1.8 深入分析ConcurrentHashMap1.8的扩容实现...

zlf1227的博客 229

基于微信小程序的扫码签到系统源码

大等于jdk1.8,大于mysql5.5,idea(eclipse),微信开发者工具。代码注释齐全,没有多余代码,适合学习(毕设),二次开发,包含论文技术相关文档。签到管理:新增签到,点击查看码展示二维码(有时效性)springboot mybatis 小程序。签到管理:查看用户创建的签到,可删除。用户管理:查看注册用户信息,及删除。签到记录:查看用户签到记录:可删除。个人信息:展示个人信息,可修改。统计分析:查看用户,签到数量。签到记录:查看用户签到记录。登录注册(含授权登录)我的签到:查看签到记录。

wfuhui的博客 5224

Java面试复习总结(并发篇13)——Java 中的 HashMap 和 ConcurrentHashMap 全解析

目录 前言 Java7 HashMap put 过程分析 get 过程分析 Java7 ConcurrentHashMap 初始化 put 过程分析 get 过程分析 并发问题分析 Java8 HashMap put 过程分析 get 过程分析 Java8 ConcurrentHashMap 初始化 put 过程分析 扩容:tryPresize get 过程...

Slayer_Zhao的博客 128

开发者体验度量:构建DevEx评估体系的方法论与工具

在深入讨论度量方法前,我们需要明确什么是开发者体验(DevEx)。许多人将其简化为"开发工具好不好用",这是一个危险的误解。开发者体验是指开发者在完成工作过程中的整体感受和效率,它由三大核心维度组成:工具与基础设施:开发、测试、部署工具链的效能流程与协作:工作流程的顺畅度和团队协作的效率文化与成长:支持实验、学习和专业发展的环境这三个维度相互作用,共同决定了开发者能否高效且愉悦地完成工作。开发者体验不是一个目的地,而是一段持续的旅程。

2401_88760782的博客 1429

基于STM32避障/循迹/蓝牙遥控小车的硬件与软件底层设计

1、实现2m的自动直线行驶,要求左右偏移不大于10cm。2、车载显示屏显示车辆当前行驶速度或车辆运行时间,当前方20cm内出现障碍物时显示与前方障碍物的距离,车每次转弯蜂鸣器报警,同时通过左右指示灯指示转弯方向。3、依次驶过障碍物①②③④,且不触碰障碍物。4、成功进行调头转弯,行驶过程中车轮不碾压黑实线。5、在车道指定位置停车且不越界。6、通过红外、蓝牙、WiFi等任意一种方式实现对小车的控制,使其前进、后退、左转、右转。

2301_78758161的博客 1万+

HashMap和CurrentHashMap及HashTable的区别

HashMap原理请看https://blog.youkuaiyun.com/qq_17021569/article/details/102519163 HashMap CurrentHashMap和HashMap的主要区别在数组那里 (1)ConcurrentHashMap对整个桶数组进行了分段,HashMap没有 (2)ConcurrentHashMap在每一个分段上都用锁进行保护,从而让锁的粒度...

空杯 6770

C#实战:通过ZPL指令与斑马打印机交互及状态监控全解析

本文全面解析了在C#中通过ZPL指令与斑马打印机进行交互及状态监控的实战方法。文章详细介绍了TCP/IP网络打印和Windows Raw打印两种核心连接方式,并深入讲解了ZPL指令的构建。重点阐述了通过SGD命令和斑马官方SDK实现实时打印状态监控的策略,为工业自动化场景下的可靠打印提供了完整解决方案。

weixin_29294395的博客 117

让人头大的HashMap、HashTable、ConcurrentHashMap该到此为止了!

Map涉及的集合框架体系图 什么是 Map Map:双列数据,存储key-value对的数据 —类似于高中的函数:y = f(x) 存储结构的理解: Map中的key:无序的、不可重复的,使用Set存储所的key key所在的类要重写equals()和hashCode() (以HashMap为例) Map中的value:无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals() 一个键值对:key-value构成了一个Entry对象。 Map中

应钟二十六的博客 220

十三、FLUENT蒸发/冷凝模型实战:从沸腾模拟到常温蒸发的UDF进阶

本文深入探讨FLUENT蒸发/冷凝模型的应用实践,从沸腾模拟到常温蒸发的UDF进阶。通过分析相变模型的核心逻辑、关键陷阱及UDF动态饱和温度场的实现,帮助工程师准确模拟不同场景下的蒸发/冷凝过程。结合太阳能蒸馏器等工程案例,提供优化方案和常见问题解决方案,显著提升模拟精度与效率。

weixin_42548893的博客 571

Gerrit 使用操作手册

Gerrit是一种免费、开源的代码审查工具,使用网页交互页面。这个软件的名称来自于荷兰设计师师赫里特·里特费尔德(GerritRietveld),作者为Google公司的ShawnPearce。它使用Git作为底层版本控制系统,相比于其他的代码管理工具,Gerrit明显的特色是代码审核。利用网页浏览器,同一个团队的软件程序员,可以相互审核(Review)彼此修改后的程序代码,然后决定是继续提交还是取消提交。...

九离的博客 1万+

大厂Java面试核心:HashMap、ConcurrentHashMap与线程池解析

在Java技术面试中,数据结构与并发编程是核心考察点。HashMap作为基础数据结构,其底层实现从JDK1.7的数组+链表演进到JDK1.8的红黑树优化,体现了对哈希碰撞问题的工程解决方案。ConcurrentHashMap则展示了Java并发编程的艺术,通过CAS和synchronized实现高效线程安全。线程池作为资源管理的重要工具,其参数配置和拒绝策略直接影响系统稳定性。理解这些技术点的实现原理和设计思想,不仅能帮助开发者编写高效代码,更是通过大厂技术面试的关键。本文通过真实面试案例,深入解析Hash

weixin_34228387的博客 333

Xinference新手指南(一个命令直接安装成功)

对于新手,必须得学会使用docker部署,这是基本功!这里就不过多去介绍了,如果评论要出教程的话,以后再出了。

Ven%的博客 817

聊聊HashMap和ConcurrentHashMap的扩容机制

众所周知,JDK1.8对HashMap做了优化,1.8之前HashMap的结构是数组+链表,1.8之后是数组+链表 or 红黑树。ConcurrentHashMap和HashMap一样,1.8之前是Segment+HashEntry+链表,结构和HashMap类似,1.8之后是Node节点组成的数组+链表or红黑树,跟HashMap类似。最近突然兴起,看起了HashMap(以下简称HM)和ConcurrentHashMap(以下简称CHM。

qq_36220273的博客 2431

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

轻松理解 Java HashMap 和 ConcurrentHashMap

前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。 本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。 HashMap 众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具...

Java烂猪皮 3161

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

深入理解HashMap+ConcurrrentHashMap扩容的原理

关于HashMap和ConcurrentHashMap的内容,可以参看Java基础-HashMap集合。Java并发编程-ConcurrentHashMap。这两篇文章,本章将深入探讨两者JDK1.7和JDK1.8两个版本的扩容机制。 1.JDK1.7版本的HashMap扩容机制 HashMap的初始化容量是16,默认加载因子是0.75,扩容时扩容到原来的两倍,这些特性对于其它版本的hashMap和concurrentHashMap同样满足。当hashmap中的元素个数超过数组大小加载因子loadFacto

Java硬件工程师的博客 3318

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 原文地址 Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 正文 今天发一篇"水文",可能很多读者都会表示不理解,不过我想把它作为并发序列文章中不可缺少的一块来介绍。本来以为花不了多少时间的,不过最终还是投入了挺多时间来完成这篇文章的。 网上关于 HashMap 和 Concu...

kaven 1131

HashMap和Hashtable以及ConcurrentHashMap的区别

HashMap和Hashtable的区别 何为HashMap HashMap是在JDK1.2中引入的Map的实现类。 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。 其次,HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。 如果不理解线程安全,可以看看我这篇文章:Java并发编程之多线程 HashMap实现了*.

Jan's的博客 4337
上一篇: SSM框架整合,详细整合 (IntelliJ IDEA)
下一篇: 死锁的四个必要条件以及处理策略
少年不再NianShao
博客等级 码龄8年 8粉丝 25原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值