Java容器(二)-CurrentHashMap详解(JDK1.8)

并发编程 1:CurrentHashMap如何实现高效地线程安全 今天我从线程安全问题开始,分析为什么要使用,进而分析了 Java 7 和 Java 8 中是如何设计实现的,从源码层面说明白了具体的实现逻辑。其实仔细认真读懂后你会发现其实也不是太难。希望本文让你对面试相关问题轻松的应对,同时作为并发编程技巧对你在日常开发可以有所帮助。 阅读详情
AI摘要

本文深入解析了ConcurrentHashMap的工作原理及其实现细节,探讨了其相较于HashMap和HashTable的优势,尤其是在多线程环境下如何保证线程安全的同时提升并发性能。

摘要

在涉及到Java多线程开发时,如果我们使用HashMap可能会导致死锁问题,使用HashTable效率又不高。而ConcurrentHashMap既可以保持同步也可以提高并发效率,所以这个时候ConcurrentHashmap是我们最好的选择。

为什么使用ConcurrentHashMap
  • 在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMap形成环形链表,即链表的一个节点的next节点永不为null,就会产生死循环。这时,CPU的利用率接近100%,所以并发情况下不能使用HashMap。

  • HashTable通过使用synchronized保证线程安全,但在线程竞争激烈的情况下效率低下。因为当一个线程访问HashTable的同步方法时,其他线程只能阻塞等待占用线程操作完毕。

  • ConcurrentHashMap使用分段锁的思想,对于不同的数据段使用不同的锁,可以支持多个线程同时访问不同的数据段,这样线程之间就不存在锁竞争,从而提高了并发效率。

简介

在阅读ConcurrentHashMap的源码时,有一段相关描述。

The primary design goal of this hash table is to maintain concurrent readability(typically method get(), but also iterators and related methods) while minimizing update contention. Secondary goals are to keep space consumption about the same or better than java.util.HashMap, and to support high initial insertion rates on an empty table by many threads.

大致意思就是: ConcurrentHashMap的主要设计目的是保持并发的可读性(通常是指的get()方法的使用,同时也包括迭代器和相关方法),同时最小化更新征用(即在进行插入操作或者扩容时也可以保持其他数据段的访问)。第二个目标就是在空间利用方面保持与HashMap一致或者更好,并且支持多线程在空表的初始插入速率。

Java7与Java8中的ConcurrentHashMap:

在ConcurrentHashMap中主要通过锁分段技术实现上述目标。

在Java7中,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。正是通过Segment分段锁,ConcurrentHashMap实现了高效率的并发。

在Java8中,ConcurrentHashMap去除了Segment分段锁的数据结构,主要是基于CAS操作保证保证数据的获取以及使用synchronized关键字对相应数据段加锁实现了主要功能,这进一步提高了并发性。同时同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。

Java8中ConcurrentHashMap的结构

在Java8中,ConcurrentHashMap弃用了Segment类,但是保留了Segment属性,用于序列化。目前ConcurrentHashMap采用Node类作为基本的存储单元,每个键值对(key-value)都存储在一个Node中。同时Node也有一些子类,TreeNodes用于树结构中(当链表长度大于8时转化为红黑树);TreeBins用于维护TreeNodes。当链表转树时,用于封装TreeNode。也就是说,ConcurrentHashMap的红黑树存放的是TreeBin,而不是treeNode;ForwordingNodes是一个重要的结构,它用于ConcurrentHashMap扩容时,是一个标志节点,内部有一个指向nextTable的属性,同时也提供了查找的方法;

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;  //使用了volatile属性
        volatile Node<K,V> next;  //使用了volatile属性

        Node(int hash, K key, V val) {
            this.hash = hash;
            this.key = key;
            this.val = val;
        }
        Node(int hash, K key, V val, Node<K,V> next) {
            this(hash, key, val);
            this.next = next;
        }
        public final K getKey()     { return key; }
        public final V getValue()   { return val; }
        public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
        public final String toString() {...}
        public final V setValue(V value) {...}

        public final boolean equals(Object o) {...}

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {...}
    }

处理Node之外,Node的一个子类ForwardingNodes也是一个重要的结构,它主要作为一个标记,在处理并发时起着关键作用,有了ForwardingNodes,也是ConcurrentHashMap有了分段的特性,提高了并发效率。

 /**
     * A node inserted at head of bins during transfer operations.
     */
    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
	        //hash值默认为MOVED(-1)
            super(MOVED, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            //在nextTable中查找,nextTable可以看做是当前hash表的一个副本
            outer: for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        else
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

ConcurrentHashMap中的原子操作

在ConcurrentHashMap中通过原子操作查找元素、替换元素和设置元素。这些原子操作起着非常关键的作用,你可以在所有ConcurrentHashMap的基本功能中看到它们的身影。

 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectRelease(tab, ((long)i << ASHIFT) + ABASE, v);
    }

ConcurrentHashMap的功能实现

1.ConcurrentHashMap初始化

在介绍初始化之前先介绍一个重要的参数sizeCtl,含义如下:

 /**
     * Table initialization and resizing control.  When negative,the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads). Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     * Hash表的初始化和调整大小的控制标志。为负数,Hash表正在初始化或者扩容;
     * (-1表示正在初始化,-N表示有N-1个线程在进行扩容)
     * 否则,当表为null时,保存创建时使用的初始化大小或者默认0;
     * 初始化以后保存下一个调整大小的尺寸。
     */
    private transient volatile int sizeCtl;

这个参数起到一个控制标志的作用,在ConcurrentHashMap初始化和扩容都有用到。 ConcurrentHashMap构造函数只是设置了一些参数,并没有对Hash表进行初始化。当在从插入元素时,才会初始化Hash表。在开始初始化的时候,首先判断sizeCtl的值,如果sizeCtl < 0,说明有线程在初始化,当前线程便放弃初始化操作。否则,将SIZECTL设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。

 /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
	        //sizeCtl小于0,正在初始化
            if ((sc = sizeCtl) < 0)
                //调用yield()函数,使线程让出CPU资源
                Thread.yield(); // lost initialization race; just spin
            //设置SIZECTL为-1,表示正在初始化
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2); //n-(1/4)n,即默认的容量(n * loadFactor)
                    }
                } finally {
                    sizeCtl = sc; //重新设置sizeCtl
                }
                break;
            }
        }
        return tab;
    }

2.确定元素在Hash表的索引

通过hash算法可以将元素分散到哈希桶中。在ConcurrentHashMap中通过如下方法确定数组索引: 第一步:

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

第二步:(length-1) & (h ^ (h >>> 16)) & HASH_BITS);

ConcurrentHashMap的put方法

  • 如果key或者value为null,则抛出空指针异常;

  • 如果table为null或者table的长度为0,则初始化table,调用initTable()方法。

  • 计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)

  • 如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。

  • 当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。

	/** Implementation for put and putIfAbsent */
	final V putVal(K key, V value, boolean onlyIfAbsent) {
		//数据不合法,抛出异常
        if (key == null || value == null) throw new NullPointerException();
        //计算索引的第一步,传入键值的hash值
        int hash = spread(key.hashCode());
        int binCount = 0; //保存当前节点的长度
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); //初始化Hash表
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
	            //利用CAS操作将元素插入到Hash表中
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;  // no lock when adding to empty bin(插入null的节点,无需加锁)
            }
            else if ((fh = f.hash) == MOVED) //f.hash == -1 
	            //正在扩容,当前线程加入扩容
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent && fh == hash &&  // check first node
                     ((fk = f.key) == key || fk != null && key.equals(fk)) &&
                     (fv = f.val) != null)
                return fv;
            else {
                V oldVal = null;
                //当前节点加锁
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //插入的元素键值的hash值有节点中元素的hash值相同,替换当前元素的值
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
	                                    //替换当前元素的值
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //没有相同的值,直接插入到节点中
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key, value);
                                    break;
                                }
                            }
                        }
                        //节点为树
                        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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
	                //如果节点长度大于8,转化为树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal; 
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

ConcurrentHashMap的扩容机制

当ConcurrentHashMap中元素的数量达到cap * loadFactor时,就需要进行扩容。扩容主要通过transfer()方法进行,当有线程进行put操作时,如果正在进行扩容,可以通过helpTransfer()方法加入扩容。也就是说,ConcurrentHashMap支持多线程扩容,多个线程处理不同的节点。

  • 开始扩容,首先计算步长,也就是每个线程分配到的扩容的节点数(默认是16)。这个值是根据当前容量和CPU的数量来计算(stride = (NCPU > 1) ? (n >>> 3) / NCPU : n),最小是16。

  • 接下来初始化临时的Hash表nextTable,如果nextTable为null,初始化nextTable长度为原来的2倍;

  • 通过计算出的步长开始遍历Hash表,其中坐标是通过一个原子操作(compareAndSetInt)记录。通过一个while循环,如果在一个线程的步长内便跳过此节点。否则转下一步;

  • 如果当前节点为空,之间将此节点在旧的Hash表中设置为一个ForwardingNodes节点,表示这个节点已经被处理过了。

  • 如果当前节点元素的hash值为MOVED(f.hash == -1),表示这是一个ForwardingNodes节点,则直接跳过。否则,开始重新处理节点;

  • 对当前节点进行加锁,在这一步的扩容操作中,重新计算元素位置的操作与HashMap中是一样的,即当前元素键值的hash与长度进行&操作,如果结果为0则保持位置不变,为1位置就是i+n。其中进行处理的元素是最后一个符合条件的元素,所以扩容后可能是一种倒序,但在Hash表中这种顺序也没有太大的影响。

  • 最后如果是链表结构直接获得高位与低位的新链表节点,如果是树结构,同样计算高位与低位的节点,但是需要根据节点的长度进行判断是否需要转化为树的结构。

 /**
     * Moves and/or copies the nodes in each bin to new table. See
     * above for explanation.
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //根据长度和CPU的数量计算步长,最小是16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                //初始化新的Hash表,长度为原来的2倍
                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;
        //初始化ForwardingNodes节点
        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.compareAndSetInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                //更新下表transferIndex,在步长的范围内都忽略
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //所有节点都被接收处理或者已经处理完毕
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //处理完毕
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    //更新sizeCtl
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //判断所有节点是否全部被处理
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //如果节点为null,直接标记为已接收处理
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //键值的hash为-1,表示这是一个ForwardingNodes节点,已经被处理
            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;
                                }
                            }
                            //runBit = 0,保持位置不变
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            //runBit = 1,位置时i+n
                            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);
                                    //构建高位(i+n)新的链表
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //将新的链表设置到新的Hash表中相应的位置
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            //将原来的Hash表中相应位置的节点设置为ForwardingNodes节点
                            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;
                            //将新的链表设置到新的Hash表中相应的位置
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            //将原来的Hash表中相应位置的节点设置为ForwardingNodes节点
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

ConcurrentHashMap的get方法

ConcurrentHashMap的get方法就是从Hash表中读取数据,而且与扩容不冲突。该方法没有同步锁。

  • 通过键值的hash计算索引位置,如果满足条件,直接返回对应的值;

  • 如果相应节点的hash值小于0 ,即该节点在进行扩容,直接在调用ForwardingNodes节点的find方法进行查找。

  • 否则,遍历当前节点直到找到对应的元素。

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        //满足条件直接返回对应的值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //e.hash<0,正在扩容
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //遍历当前节点
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
总结

ConcurrentHashMap就介绍到这里,以上主要是对ConcurrentHashMap中经常使用到的特性进行分析,如果对其他内容感兴趣,可以阅读相应的源码。

JDK1.8版本HashMap源码原理分析 HashMap是一个KV的容器对象,也是日常在开发当中非常常用的对象,比如我们可能经常做一些内存缓存的时候,很多时候就选用HashMap这种KV的数据结构。在实现原理上,是基于哈希表的Map接口实现,是常用的java集合之一,非线程安全的。 HashMap可以存储null的key和value,但是null作为键值只能有一个,做为值的话,可以是多个。这和Map的键要保持唯一性并不冲突。 一、HashMap的类图结构 二、概念、原理概述 Jdk1.8之前的HashMap是由数组+链表作为底层数据结构实现的,数 阅读详情

相关推荐

Java并发编程(22)—— CurrentHashMap源码分析

java 并发包中提供的是一个线程安全且高效的HashMap ,也是面试的高频考点。下面将围绕主题:ConcurrentHashMap 如何实现高效地线程安全?以及在Java8中它从设计实现上有哪些演进?这篇文章一开始我以为会比较简单,但是在深入源码分析时,遇到了很大的阻碍,比前面我们分析AQS以及读写锁的源码要难理解的多,断断续续也写了4天了。如果你看完还是没有理解的话,那我在这里表示深深的歉意,同时也欢迎你和我一起沟通。

shangjg3的博客 313

CurrentHashmap知识详解

为什么要使用CurrentHashmap 在多线程中,hashmap的put方法可能会造成死循环的问题,put的时候引发扩容。hashtable虽然是线程安全的,但是在多线程中使用的是synchronized,当一个线程抢到锁之后,别的线程会等待使用锁的线程释放后才会执行,相当于单线程,效率不高,而CurrentHashmap是采用的是分段锁的思想,可以支持16个线程同时执行,效果显然是比较快的。 CurrentHashmap详解 CurrentHashmap1.7 ConcurrentHashMap1.7

baidu_39170202的博客 1万+

Origin2018(汉化版)在使用科学计数法的时候如何将坐标刻度的0.0改成0

文章目录1 问题2 解决方法 1 问题 先看下面这张图,当我使用科学计数法时,纵坐标出现了0.0的字样,这样看起来很不美观。有些期刊还会有要求,不能出现0.0的格式。 图1 那么该怎么将它改成0呢?首先让我们尝试着用Origin自带的属性,看看能不能设置。 2 解决方法 解决方法一: step1: 双击坐标轴,打开坐标轴属性设置。 step2: 在刻度线标签选项中设置左轴的显示 图2 会发现,按照图2的方式改好之后,其它的小数位也全没了,如果但是有些数字就被覆盖了,这显然不是我

qq_40077565的博客 2万+

HashMap 和 currentHashMap

HashMap 和 currentHashMap

Aspiring youth的博客 2801

AI大模型开发——transformer模型超全完整版(0基础可懂)

为了更深入的了解transformer模型,我们首先从transformer的诞生聊起吧。transformer在2017年由Vaswani等人首次提出,它摒弃了之前流行的循环神经网络和卷积神经网络的架构,引入了自注意力机制( Self-Attention),显著提高了处理序列数据的能力和效率。transformer模型的这种设计使其在处理长距离依赖问题时, 相比于传统的循环神经网络和长短期记忆网络(LSTM)表现得更加出色,至此, 开启了NLP技术的新篇章。

m0_74922316的博客 2120

工业质检实战:如何用MVTec AD数据集训练你的第一个无监督异常检测模型

本文提供了一份基于MVTec AD数据集的工业质检实战指南,详细介绍了如何从零开始训练一个无监督异常检测模型。内容涵盖数据预处理、模型架构选型(如CAE、GAN与特征字典方法)、特征提取、损失函数设计以及关键的阈值确定与后处理流程,旨在帮助工程师快速掌握将AI技术落地于实际产线质检的核心步骤与调参经验。

weixin_29182481的博客 458

Java容器(一)—CurrentHashMap详解(JDK1.8)

摘要 在涉及到Java多线程开发时,如果我们使用HashMap可能会导致死锁问题,使用HashTable效率又不高。而ConcurrentHashMap既可以保持同步也可以提高并发效率,所以这个时候ConcurrentHashmap是我们最好的选择。 为什么使用ConcurrentHashMap 在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMa...

weixin_30549175的博客 649

NMS非极大值抑制算法原理

NMS非极大值抑制算法

pywin 3500

currenthashmap实现原理_面试官再问currentHashMap,就将这篇文章甩给他

currentHashMap的介绍currentHashMap是线程安全并且高效的一种容器,我们就需要研究一下currentHashMap为什么既能够保证线程安全,又可以保证高效的操作。为什么使用currentHashMap,我们就需要和HashMap以及HashTable进行比较?HashMap是线程不安全的,在多线程的情况下,HashMap的操作会引起死循环,导致CPU的占有量达到100%,所...

weixin_39629269的博客 4831

中兴U30air随身WiFi刷机全攻略:从ABD开启到系统调试(附工具包)

本文提供中兴U30air随身WiFi刷机全攻略,从ADB开启到系统调试的详细步骤,包括必备工具清单、ADB调试模式解锁、Bootloader解锁及自定义ROM刷入与调优。适合技术爱好者深度定制设备,提升网络性能和功能扩展。

weixin_29235923的博客 1933

OpenClaw实战四|内容创作全流程AI助理:自动撰稿+智能剪辑+多平台发布,一人顶一个内容团队

AI替代的是「写稿、剪辑、发布」这种流水线工作,真正让你解放出来,去做选题、创意、人设、变现这些高价值的事。OpenClaw这套内容创作全流程AI助理,最大的价值不是「帮你发内容」,而是让一个人,拥有一个内容团队的产能。不管是个人博主、小团队,还是企业新媒体,都能用零代码的方式,快速搭建属于自己的自动化内容矩阵。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 588

java容器之 ConcurrentHashMap jdk1.8 思路整理

下面就来写一下关于jdk1.8的的ConcurrentHashMap的整体逻辑 本文不纠结于源码,因为源码太过于复杂,不想深究,暂时也没有必要深究,源码虽好,但暂时无须贪杯 本文将ConcurrentHashMap简称为chm 本文目的在于,快速理清chm逻辑,便于理解 chm的结构 相比jdk1.7的改进 sizeCtl变量 put操作的大致逻辑 addCount方法 如何扩容,扩容协助 注释源码地址 ...

或左的博客 337

【时时三省】tessy 集成测试:小白入门指导手册

有图有文字详细介绍 tessy 集成测试 操作方法。

时时三省的博客 2514

浅析currentHashmap的理解

1,currentHashMap的介绍 currentHashMap是线程安全并且高效的一种容器,我们就需要研究一下currentHashMap为什么既能够保证线程安全,又可以保证高效的操作 currentHashMap使用的原因 为什么使用currentHashMap,这时候我们就需要和HashMap以及HashTable进行比较 HashMap线程不安全的原因? 在多线程的情况下,HashMa...

qq2212573001的博客 3175

AI应用工程师面试指南与参考答案

摘要:AnkerAI应用工程师职位面向2024/25届毕业生,要求计算机相关专业背景,强调工程实现与技术探索能力。岗位职责包括全栈开发、AI技术整合(如大模型、图像识别)、团队AI赋能及系统优化。职位更看重编程实践能力(Python/Java等)和学习潜力,而非单纯算法知识。成功候选人将参与智能化产品开发,获得导师指导和技术成长机会。面试准备应聚焦项目经验展示、业务需求理解和技术解决方案能力,通过结构化回答和实操案例体现工程思维与创新潜力。

郑伟强dev的专栏 407

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

高性能编程——并发容器类CurrentHashMap

文章目录JDK1.7版本带注释的源码1.8添加操作1.8对比1.7的优点 JDK1.7版本 带注释的源码 /* * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ /* * * ...

weixin_41746577的博客 417

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

面试官再问currentHashMap,就将这篇文章甩给他

点击上方"IT牧场",选择"设为星标"技术干货每日送达!currentHashMap的介绍currentHashMap是线程安全并且高效的一种容器,我们就...

ITMuch的专栏 1662

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Java并发容器和框架

目录 ConcurrentHashMap的实现原理与使用 为什么要使用ConcurrentHashMap 线程不安全的HashMap 效率低下的Hash Table ConcurrentHashMap的锁分段技术可有效提升并发访问率 ConcurrentHashMap的结构 ConcurrentHashMap的初始化 初始化segments数组 初始化segmentShift和s...

今天吃了吗的博客 503

ConcurrentHashMap的JDK1.7和JDK1.8的实现

JDK1.7 ConcurrentHashMap(支持并发)ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。ConcurrentHashMap 有 16 个 Segments,所以理论上,最...

HxxxxxxU的博客 971

ConcurrentHashMap底层结构和原理详解

ConcurrentHashMap ConcurrentHashMap是线程安全的数组,是HashTable的替代品,同为线程安全,其性能要比HashTable更好 HashMap不是线程安全: 在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的 HashTable是线程安全的: HashTable和HashMap的实现原理几乎一样, 与HashMap的差别: HashTa

cV展示的学习园 4万+

jdk1.8中ConcurrentHashMap的实现原理

并发环境下为什么使用ConcurrentHashMap 1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终不为空,因此产生死循环获取Entry 2. HashTable虽然是线程安全的,但是效率低下,当一个线程访问HashTable的同步方法时,其他线程如果也访问HashTable的同步方法,那

Summer_YuXia的博客 1406

Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么?

真实面试题之:Hashmap的结构,1.7和1.8有哪些区别 不同点: (1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。 (2)扩容后数据存储位置的计算方式也不一样:1....

云祁QI 1299

HashMap和currentHashMap原理解析

Java、大数据开发学习要点(持续更新中…) 文章目录一、前言二、HashMapBase 1.7put方法get方法Base 1.8put方法get方法三、CurrentHashMapBase 1.7put方法get方法Base 1.8put方法get方法 参考自HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你! 一、前言 Map 这样的Key,Value在软件开发中是非常经典的结构,常用于在内存中存放数据。 本篇主要想讨论 ConcurrentHashMap 这样一个.

weixin_38836273的博客 349
上一篇: Java容器(一)-HashMap详解(JDK1.8)
下一篇: Python基础概览
Coding-ls
博客等级 码龄11年 41粉丝 61原创
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值