HashMap&&ConcurrentHashMap
JDK1.7:
HashMap
创建节点
void createEntry(int hash, K key, V value, int bucketIndex) { // 采取头插入法,1:创建新的Entry节点,next节点指向e, 2:新Entry节点赋值给数组中的位置 java.util.HashMap.Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new java.util.HashMap.Entry<>(hash, key, value, e); size++; }
public V put(K key, V value) { // 第一次put 进行初始化 if (table == EMPTY_TABLE) { // 默认数组大小 16 传进去初始化大小 如果不是2的整数次幂,会以最近的大于2的整数次幂的大小作为容量 inflateTable(threshold); } // key 可以为null if (key == null)
// 这里会把key为null的数据都放到table[0]的位置 return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); // 这里的遍历是 数组的某个索引位开始的链表,如果和当前存入的key相同,就覆盖当前的value值 然后返回被覆盖的value 结束 for (java.util.HashMap.Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 使用 Integer.highestOneBit(int num) 进行位运算 ,但是这个函数返回的是小于等于参数的2的整数次幂 /** 通过右移位运算和或运算 把原数据的最高位保留,其他为全部为0 * * public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); 执行到这里原数据 由 0000 1*** 变成 0000 1111 return i - (i >>> 1); 这里直接 0000 1111 - 0000 0111 } * */ int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new java.util.HashMap.Entry[capacity]; initHashSeedAsNeeded(capacity); }
扩容:
/** * Adds a new entry with the specified key, value and hash code to * the specified bucket. It is the responsibility of this * method to resize the table if appropriate. * * Subclass overrides this to alter the behavior of put method. */ void addEntry(int hash, K key, V value, int bucketIndex) { // 扩容 数据长度>= table.length*加载因子 并且 要放入的位置元素不为空的时候才会扩容 if ((size >= threshold) && (null != table[bucketIndex])) { // 扩容 直接两倍 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
void resize(int newCapacity) { java.util.HashMap.Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 直接创建了一个新的数组 java.util.HashMap.Entry[] newTable = new java.util.HashMap.Entry[newCapacity]; // 进行新老数组的转移 initHashSeedAsNeeded 是否使用hash种子 来进行hash算法让结果更散列一些 // 可以指定 jdk.map.althashing.threshold 环境变量为 n,当数组容量大于n的时候 可以使用hash种子 // 默认是hash种子是0,当使用hash种子的时候 扩容的时候hash肯定要重写计算的 但是通常hash种子为0 扩容的时候是不会 // 重写计算hash值的 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
/** * Transfers all entries from current table to newTable. */ void transfer(java.util.HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; // 双层遍历 一层是table 一层是链表 链表遍历的时候放到新的数组里面是采用头插法,所以造成新的存储顺序和原来不一样,顺序反了 // 这里会有并发造成闭环的问题 for (java.util.HashMap.Entry<K,V> e : table) { while(null != e) { java.util.HashMap.Entry<K,V> next = e.next; // 是否要重新hash 是不一定的 在JDK1.8中就没有这个过程了 // 和是否有hash种子有关 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
/** * Initialize the hashing mask value. We defer initialization until we * really need it. */ final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= java.util.HashMap.Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching; }
JDK1.7的闭环和丢失的问题可以参见:https://www.pianshen.com/article/42821264881/
ConcurrentHashMap
ConcurrentHashMap 采用分段锁(只是一个概念),hashmap中的每个数组都是一个Entry对象。但是在ConcurrentHashMap 中,数组中存的是 Segment对象,每个 Segment 中有都有一个 HashEntry[]的数组。
构造函数:
/** * Creates a new, empty map with a default initial capacity (16), * load factor (0.75) and concurrencyLevel (16). * DEFAULT_INITIAL_CAPACITY:默认 16 想要一共存储多少个元素,即所有Segment中的 HashEntry 数组有多少长度 * DEFAULT_CONCURRENCY_LEVEL:并发等级,默认16,决定Segment数组的大小,不小于此数据的最小2的幂次数 * 所以构造函数中会根据 DEFAULT_INITIAL_CAPACITY / DEFAULT_CONCURRENCY_LEVEL 计算每个Segment中的 HashEntry数组多大 * 但是又不是完全根据上面的 / 结果 */ public ConcurrentHashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); }
构造函数:
/** * Creates a new, empty map with the specified initial * capacity, load factor and concurrency level. * * @param initialCapacity the initial capacity. The implementation * performs internal sizing to accommodate this many elements. * @param loadFactor the load factor threshold, used to control resizing. * Resizing may be performed when the average number of elements per * bin exceeds this threshold. * @param concurrencyLevel the estimated number of concurrently * updating threads. The implementation performs internal sizing * to try to accommodate this many threads. * @throws IllegalArgumentException if the initial capacity is * negative or the load factor or concurrencyLevel are * nonpositive. */ @SuppressWarnings("unchecked") public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 用来决定每个HashEntry数组有多大 int c = initialCapacity / ssize; // 向上取整 if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; // 就算向上取整 也不一定能满足要求 因为HashEntry的数组长度也要是2的幂次数 while (cap < c) cap <<= 1; // create segments and segments[0] // 这里创建一个Segment对象 然后通过unsafe方法放到 Segment<K,V>[] ss 数组中的第一个位置 // 这样做是为了后面其他位置创建Segement对象的时候 对于阈值和HashEntry的大小就不用重复计算了,直接在s0中就可以得到 java.util.concurrent.ConcurrentHashMap.Segment<K,V> s0 = new java.util.concurrent.ConcurrentHashMap.Segment<K,V>(loadFactor, (int)(cap * loadFactor), (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[])new java.util.concurrent.ConcurrentHashMap.HashEntry[cap]); java.util.concurrent.ConcurrentHashMap.Segment<K,V>[] ss = (java.util.concurrent.ConcurrentHashMap.Segment<K,V>[])new java.util.concurrent.ConcurrentHashMap.Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
放入Segment对象的 put方法:
public V put(K key, V value) { java.util.concurrent.ConcurrentHashMap.Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); // 算出元素存到Segment数组中的那个位置 segmentMask 就是数组减一 int j = (hash >>> segmentShift) & segmentMask; // 获取 segments 数组中第j个元素 if ((s = (java.util.concurrent.ConcurrentHashMap.Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment // 如果第j个位置为空 就生成一个Segement元素 s = ensureSegment(j); // 操作Segment对象中的put方法 return s.put(key, hash, value, false); }
生成一个Segment对象,并放入到Segment数组的指定位置:
private java.util.concurrent.ConcurrentHashMap.Segment<K,V> ensureSegment(int k) { final java.util.concurrent.ConcurrentHashMap.Segment<K,V>[] ss = this.segments; long u = (k << SSHIFT) + SBASE; // raw offset java.util.concurrent.ConcurrentHashMap.Segment<K,V> seg; // 这里条件的判断 和外面一样 为了避免其他线程已经在这个位置生成了一个Segment对象了 如果其他线程已经生成了就直接返回 // if ((seg = (java.util.concurrent.ConcurrentHashMap.Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // 这里获取ss[0]作为基础数据 java.util.concurrent.ConcurrentHashMap.Segment<K,V> proto = ss[0]; // use segment 0 as prototype int cap = proto.table.length; // 获取HashEntry数组大小 float lf = proto.loadFactor; // 加载因子 int threshold = (int)(cap * lf); // 计算阈值 java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[] tab = (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[])new java.util.concurrent.ConcurrentHashMap.HashEntry[cap]; // 这里又判断了一次Segment对象是否存在 if ((seg = (java.util.concurrent.ConcurrentHashMap.Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck // 如果走到这里还为空 才创建一个新的 java.util.concurrent.ConcurrentHashMap.Segment<K,V> s = new java.util.concurrent.ConcurrentHashMap.Segment<K,V>(lf, threshold, tab); // 通过cas操作把新建的Segment放入 注意条件里面有 == null判断 保证只有一个线程能在这个位置放一个Segment对象 while ((seg = (java.util.concurrent.ConcurrentHashMap.Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } return seg; }
Segment中的put方法,把元素放到HashEntry数组中。
final V put(K key, int hash, V value, boolean onlyIfAbsent) { // 加鎖 tryLock() 是非阻塞的(获取到锁立即返回true 获取不到锁 立即返回false) // lock() 是阻塞的,获取到锁是会返回的 获取不到锁就阻塞住直到获取 // 获取到了 返回一个null 如果获取不到 scanAndLockForPut 遍历数组/使用lock进行等待 java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { // 内部table java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> first = entryAt(tab, index); for (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> e = first;;) { if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>(hash, key, value, first); int c = count + 1; if (c > threshold && tab.length < MAXIMUM_CAPACITY) // 扩容 这个node是 当前HashEntry数组的头节点 rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
private java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> first = entryForHash(this, hash); java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> e = first; java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> node = null; int retries = -1; // negative while locating node while (!tryLock()) { java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> f; // to recheck first below if (retries < 0) { if (e == null) { if (node == null) // speculatively create node node = new java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>(hash, key, value, null); retries = 0; } else if (key.equals(e.key)) retries = 0; else e = e.next; } else if (++retries > MAX_SCAN_RETRIES) { lock(); break; } else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { e = first = f; // re-traverse if entry changed retries = -1; } } return node; }
扩容,扩容只针对Segment内部的HashEntry数组:
@SuppressWarnings("unchecked") private void rehash(java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> node) { /* * Reclassify nodes in each list to new table. Because we * are using power-of-two expansion, the elements from * each bin must either stay at same index, or move with a * power of two offset. We eliminate unnecessary node * creation by catching cases where old nodes can be * reused because their next fields won't change. * Statistically, at the default threshold, only about * one-sixth of them need cloning when a table * doubles. The nodes they replace will be garbage * collectable as soon as they are no longer referenced by * any reader thread that may be in the midst of * concurrently traversing table. Entry accesses use plain * array indexing because they are followed by volatile * table write. */ java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity << 1; threshold = (int)(newCapacity * loadFactor); java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[] newTable = (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>[]) new java.util.concurrent.ConcurrentHashMap.HashEntry[newCapacity]; int sizeMask = newCapacity - 1; for (int i = 0; i < oldCapacity ; i++) { java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> e = oldTable[i]; if (e != null) { java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> next = e.next; // 这里要注意,它是用的old 数组中的hash值与上新的数组长度减1 并没有rehash 和JDK1.7中的hashmap不一样 int idx = e.hash & sizeMask; if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> lastRun = e; int lastIdx = idx; // 这里的遍历是为了查找old数组 next后面节点是否有和其前面节点在新数组中是同一个索引位的情况 // 但是这里遍历完 只记录了最后一次出现这种情况 for (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } // 这里直接转移 可能这里直接转移了两个及以上的数据 newTable[lastIdx] = lastRun; // Clone remaining nodes for (java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; java.util.concurrent.ConcurrentHashMap.HashEntry<K,V> n = newTable[k]; newTable[k] = new java.util.concurrent.ConcurrentHashMap.HashEntry<K,V>(h, p.key, v, n); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
JDK1.8 HashMap
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 采用尾插法 JDK1.7中是头插法 p.next = newNode(hash, key, value, null); // 上一步已经把新元素添加进去了 也就是说链表中已经有9个元素了 // 原来链表中有8个元素 就要转红黑树了 但是不一定真正要转 还要数组长度大于64 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 这个方法里面并不一定真正的树化 优先扩容 也可以减小链表的长度 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) // 这里的扩容 直接++size进行扩容操作 和JDK1.7 也不一样 这里没有了重新hash的操作了 resize(); afterNodeInsertion(evict); return null; }
红黑树中有6个元素会退化成链表。
JDK1.8中的ConcurrentHashMap
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // 这里要求了key和value不能为空 在JDK1.7中是可以为空的 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (java.util.concurrent.ConcurrentHashMap.Node<K,V>[] tab = table;;) { java.util.concurrent.ConcurrentHashMap.Node<K,V> f; int n, i, fh; //为空 初始化数组 可以保证只有一个线程初始化成功 通过cas拿到标识的线程进行初始化 其他线程通过Thread.yield()放弃CPU if (tab == null || (n = tab.length) == 0) tab = initTable(); //使用Unsafe中的方法直接获取主存中某个索引为的数据 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 如果某个索引位为空 通过cas操作设置新的节点 if (casTabAt(tab, i, null, new java.util.concurrent.ConcurrentHashMap.Node<K,V>(hash, key, value, null))) // 添加成功就推出循环 break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) // 如果节点的hash值等于了 MOVED -1 说明有其他线程在操作数组进行扩容 当前线程就帮助进行扩容 tab = helpTransfer(tab, f); else { // 走到这里说明 数组的索引位已经有元素了 可以进行插入 但是是插入链表还是红黑树呢 而且还有并发的问题 V oldVal = null; // 对头节点进行加锁 synchronized (f) { // 加锁再重新检查一下 头节点没有发生变化 if (tabAt(tab, i) == f) { // hash值大于零 说明是个链表 直接往链表里插入值 if (fh >= 0) { binCount = 1; // 记录链表中有多少的节点了 后面根据它判断 是否进行树化 for (java.util.concurrent.ConcurrentHashMap.Node<K,V> 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; } java.util.concurrent.ConcurrentHashMap.Node<K,V> pred = e; if ((e = e.next) == null) { // 采用尾插法 pred.next = new java.util.concurrent.ConcurrentHashMap.Node<K,V>(hash, key, value, null); break; } } } // 走到这里说明是个红黑树 这个TreeBin只是作为数组中的节点使用 它内部维护了红黑树的Root节点 else if (f instanceof java.util.concurrent.ConcurrentHashMap.TreeBin) { java.util.concurrent.ConcurrentHashMap.Node<K,V> p; binCount = 2; if ((p = ((java.util.concurrent.ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) // 大于阈值转换成红黑树 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 数组的长度加一 这里的加1 很复杂 为了让效率更好 使用一个BaseCount,CountCell数组 两个元素来进行统计数组长度, // 第一个线程想操作BaseCoount 加1 addCount(1L, binCount); return null; }
初始化的逻辑:
/** * Initializes table, using the size recorded in sizeCtl. */ private final java.util.concurrent.ConcurrentHashMap.Node<K,V>[] initTable() { java.util.concurrent.ConcurrentHashMap.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(this, SIZECTL, sc, -1)) { // 看那个线程cas 设置sizeCtl =-1 成功 就进行初始化操作 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") java.util.concurrent.ConcurrentHashMap.Node<K,V>[] nt = (java.util.concurrent.ConcurrentHashMap.Node<K,V>[])new java.util.concurrent.ConcurrentHashMap.Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); // 计算数组下次扩容的阈值 相当于 n*0.75 } } finally { sizeCtl = sc; // 初始化结束 sizeCtl就是下次扩容时的阈值 } break; } } return tab; }
链表转红黑树:
/** * Replaces all linked nodes in bin at given index unless table is * too small, in which case resizes instead. */ private final void treeifyBin(java.util.concurrent.ConcurrentHashMap.Node<K,V>[] tab, int index) { java.util.concurrent.ConcurrentHashMap.Node<K,V> b; int n, sc; if (tab != null) { if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { synchronized (b) { if (tabAt(tab, index) == b) { java.util.concurrent.ConcurrentHashMap.TreeNode<K,V> hd = null, tl = null; for (java.util.concurrent.ConcurrentHashMap.Node<K,V> e = b; e != null; e = e.next) { java.util.concurrent.ConcurrentHashMap.TreeNode<K,V> p = new java.util.concurrent.ConcurrentHashMap.TreeNode<K,V>(e.hash, e.key, e.val, null, null); if ((p.prev = tl) == null) hd = p; else tl.next = p; tl = p; } // 直接new了一个TreeBin 传进去hd 是红黑树的根节点 在TreeBin构造方法中构造红黑树 红黑树节点还是TreeNode setTabAt(tab, index, new java.util.concurrent.ConcurrentHashMap.TreeBin<K,V>(hd)); } } } } }
数组的长度加1
/** * Adds to count, and if table is too small and not already * resizing, initiates transfer. If already resizing, helps * perform transfer if work is available. Rechecks occupancy * after a transfer to see if another resize is already needed * because resizings are lagging additions. * * @param x the count to add * @param check if <0, don't check resize, if <= 1 only check if uncontended */ private final void addCount(long x, int check) { java.util.concurrent.ConcurrentHashMap.CounterCell[] as; long b, s; if ((as = counterCells) != null || // counterCells 不为空 则操作counterCells 进行计数 如果为空 尝试一次在基数上进行加 1 如果失败 走下面的逻辑 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { java.util.concurrent.ConcurrentHashMap.CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || // 每个线程生成一个随机数 ThreadLocalRandom.getProbe() 除非强制重置 否则不会变化 // 如果当前线程在 counterCells 中的位置为空 则走fullAddCount逻辑 如果不为空尝试在当前位置的元素属性value上加1 如果失败走 fullAddCount 逻辑 (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 主要操作 counterCells 进行初始化,扩容,加1 操作等 反正这个方法总能保证 加1成功的 fullAddCount(x, uncontended); return; } if (check <= 1) return; // 统计下数组的长度 就是计算下BaseCount和 counterCells 数组中每个元素的value值的和 s = sumCount(); } if (check >= 0) { // sizeCtl 默认为0 -1 表示正在初始化 sizeCtl< -1 说明多个线程正在扩容 正常情况下等于数组阈值 java.util.concurrent.ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) { 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); s = sumCount(); } } }
操作CounterCell 进行数组长度加1
// See LongAdder version for explanation private final void fullAddCount(long x, boolean wasUncontended) { int h; if ((h = ThreadLocalRandom.getProbe()) == 0) { ThreadLocalRandom.localInit(); // force initialization h = ThreadLocalRandom.getProbe(); wasUncontended = true; } boolean collide = false; // True if last slot nonempty for (;;) { java.util.concurrent.ConcurrentHashMap.CounterCell[] as; java.util.concurrent.ConcurrentHashMap.CounterCell a; int n; long v; if ((as = counterCells) != null && (n = as.length) > 0) { if ((a = as[(n - 1) & h]) == null) { if (cellsBusy == 0) { // Try to attach new Cell java.util.concurrent.ConcurrentHashMap.CounterCell r = new java.util.concurrent.ConcurrentHashMap.CounterCell(x); // Optimistic create if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { boolean created = false; try { // Recheck under lock java.util.concurrent.ConcurrentHashMap.CounterCell[] rs; int m, j; if ((rs = counterCells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { rs[j] = r; created = true; } } finally { cellsBusy = 0; } if (created) break; continue; // Slot is now non-empty } } collide = false; } else if (!wasUncontended) // CAS already known to fail wasUncontended = true; // Continue after rehash else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) break; else if (counterCells != as || n >= NCPU) collide = false; // At max size or stale else if (!collide) collide = true; else if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { try { if (counterCells == as) {// Expand table unless stale java.util.concurrent.ConcurrentHashMap.CounterCell[] rs = new java.util.concurrent.ConcurrentHashMap.CounterCell[n << 1]; for (int i = 0; i < n; ++i) rs[i] = as[i]; counterCells = rs; } } finally { cellsBusy = 0; } collide = false; continue; // Retry with expanded table } h = ThreadLocalRandom.advanceProbe(h); } else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { boolean init = false; try { // Initialize table if (counterCells == as) { java.util.concurrent.ConcurrentHashMap.CounterCell[] rs = new java.util.concurrent.ConcurrentHashMap.CounterCell[2]; rs[h & 1] = new java.util.concurrent.ConcurrentHashMap.CounterCell(x); counterCells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break; } else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x)) break; // Fall back on using base } }
计算数组长度:
final long sumCount() { java.util.concurrent.ConcurrentHashMap.CounterCell[] as = counterCells; java.util.concurrent.ConcurrentHashMap.CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
扩容:
对于一个线程来说,首先计算出一个步长,假设为2,它会从原数组的后面往前移动两位元素到新数组,如果同时还有另外一个线程来扩容,也会算一个步长假设 =1,这个线程就会把倒数第三个元素转移到新数组中,而且一次扩容完,会判断新数组的长度是否大于阈值,如果大于的
话,继续对新数组进行扩容。
/** * Adds to count, and if table is too small and not already * resizing, initiates transfer. If already resizing, helps * perform transfer if work is available. Rechecks occupancy * after a transfer to see if another resize is already needed * because resizings are lagging additions. * * @param x the count to add * @param check if <0, don't check resize, if <= 1 only check if uncontended */ private final void addCount(long x, int check) { java.util.concurrent.ConcurrentHashMap.CounterCell[] as; long b, s; if ((as = counterCells) != null || // counterCells 不为空 则操作counterCells 进行计数 如果为空 尝试一次在基数上进行加 1 如果失败 走下面的逻辑 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { java.util.concurrent.ConcurrentHashMap.CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || // 每个线程生成一个随机数 ThreadLocalRandom.getProbe() 除非强制重置 否则不会变化 // 如果当前线程在 counterCells 中的位置为空 则走fullAddCount逻辑 如果不为空尝试在当前位置的元素属性value上加1 如果失败走 fullAddCount 逻辑 (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 主要操作 counterCells 进行初始化,扩容,加1 操作等 反正这个方法总能保证 加1成功的 fullAddCount(x, uncontended); return; } if (check <= 1) return; // 统计下数组的长度 就是计算下BaseCount和 counterCells 数组中每个元素的value值的和 s = sumCount(); } if (check >= 0) { // sizeCtl 默认为0 -1 表示正在初始化 sizeCtl< -1 说明多个线程正在扩容 正常情况下等于数组阈值 java.util.concurrent.ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc; // 扩容的逻辑 // 数组的长度大于阈值 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { // 这里返回的就是一个绝对值很大的负数 后面要赋值给sizeCtl int rs = resizeStamp(n); // 如果小于零 说明 有其他线程已经走到transfer的逻辑了 if (sc < 0) { 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); } // 说明还没线程进行扩容 进行cas操作赋值sizeCtl为 负数 绝对值很大的负数 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } }
/** * Moves and/or copies the nodes in each bin to new table. See * above for explanation. */ private final void transfer(java.util.concurrent.ConcurrentHashMap.Node<K,V>[] tab, java.util.concurrent.ConcurrentHashMap.Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) // 最小步长 16 stride = MIN_TRANSFER_STRIDE; // subdivide range if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") // 生成新的数组 扩容两倍长度 java.util.concurrent.ConcurrentHashMap.Node<K,V>[] nt = (java.util.concurrent.ConcurrentHashMap.Node<K,V>[])new java.util.concurrent.ConcurrentHashMap.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; // 这个对象 其他put操作的线程如果操作的位置是fwd 对象 它的hash是 MOVED=-1 说明数组正在扩容 进行put操作的线程就来帮助进行扩容 java.util.concurrent.ConcurrentHashMap.ForwardingNode<K,V> fwd = new java.util.concurrent.ConcurrentHashMap.ForwardingNode<K,V>(nextTab); boolean advance = true; // 这个用来表示 当前这个线程转移完步长内的元素之后 还要不要继续往前走转移其他元素 boolean finishing = false; // to ensure sweep before committing nextTab // 为true的时候 表示当前这个线程的任务算是做完了 没有其他事情要去做了 for (int i = 0, bound = 0;;) { java.util.concurrent.ConcurrentHashMap.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) { java.util.concurrent.ConcurrentHashMap.Node<K,V> ln, hn; if (fh >= 0) { int runBit = fh & n; java.util.concurrent.ConcurrentHashMap.Node<K,V> lastRun = f; for (java.util.concurrent.ConcurrentHashMap.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 (java.util.concurrent.ConcurrentHashMap.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 java.util.concurrent.ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln); else hn = new java.util.concurrent.ConcurrentHashMap.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 java.util.concurrent.ConcurrentHashMap.TreeBin) { java.util.concurrent.ConcurrentHashMap.TreeBin<K,V> t = (java.util.concurrent.ConcurrentHashMap.TreeBin<K,V>)f; java.util.concurrent.ConcurrentHashMap.TreeNode<K,V> lo = null, loTail = null; java.util.concurrent.ConcurrentHashMap.TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (java.util.concurrent.ConcurrentHashMap.Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; java.util.concurrent.ConcurrentHashMap.TreeNode<K,V> p = new java.util.concurrent.ConcurrentHashMap.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 java.util.concurrent.ConcurrentHashMap.TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new java.util.concurrent.ConcurrentHashMap.TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }
总结:
JDK8中的HashMap与JDK7的HashMap有什么不一样?
- JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
- JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
- JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中的Hash算法实现的复杂度降低了
- JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。
- JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容
HashMap中PUT方法的流程?
- 通过key计算出一个hashcode
- 通过hashcode与“与操作”计算出一个数组下标
- 在把put进来的key,value封装为一个entry对象
- 判断数组下标对应的位置,是不是空,如果是空则把entry直接存在该数组位置
- 如果该下标对应的位置不为空,则需要把entry插入到链表中
- 并且还需要判断该链表中是否存在相同的key,如果存在,则更新value
- 如果是JDK7,则使用头插法
- 如果是JDK8,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把链表转变为红黑树,并且把元素插入到红黑树中
JDK8中链表转变为红黑树的条件?
- 链表中的元素的个数为8个或超过8个
- 同时,还要满足当前数组的长度大于或等于64才会把链表转变为红黑树。为什么?因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。
HashMap扩容流程是怎样的?
- HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容
- 在HashMap中也是一样,先新建一个2被数组大小的数组
- 然后遍历老数组上的没一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去
- 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实现时是有不一样的,jdk7就是简单的遍历链表上的没一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率
- 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置。
- 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到。
为什么HashMap的数组的大小是2的幂次方数?
JDK7的HashMap是数组+链表实现的
JDK8的HashMap是数组+链表+红黑树实现的
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。
在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过 hashcode & (table.length - 1) 运算得到一个数组下标index,是通过与运算计算出来一个数组下标的,而不是通过取余,与运算相比于取余运算速度更快,但是也有一个前提条件,就是数组的长度得是一个2的幂次方数。
为什么HashMap在多线程扩容时会出现循环链表的问题?
ConcurrentHashMap和HashMap的区别是什么?
ConcurrentHashMap是HashMap的升级版,HashMap是线程不安全的,而ConcurrentHashMap是线程安全。而其他功能和实现原理和HashMap类似。
JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
- JDK8中新增了红黑树
- JDK7中使用的是头插法,JDK8中使用的是尾插法
- JDK7中使用了分段锁,而JDK8中没有使用分段锁了
- JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了Synchronized
- JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程安全
ConcurrentHashMap是如何保证并发安全的?
JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,在JDK7的ConcurrentHashMap中,首先有一个Segment数组,存的是Segment对象,Segment相当于一个小HashMap,Segment内部有一个HashEntry的数组,也有扩容的阈值,同时Segment继承了ReentrantLock类,同时在Segment中还提供了put,get等方法,比如Segment的put方法在一开始就会去加锁,加到锁之后才会把key,value存到Segment中去,然后释放锁。
同时在ConcurrentHashMap的put方法中,会通过CAS的方式把一个Segment对象存到Segment数组的某个位置中。
同时因为一个Segment内部存在一个HashEntry数组,所以和HashMap对比来看,相当于分段了,每段里面是一个小的HashMap,每段公用一把锁,同时在ConcurrentHashMap的构造方法中是可以设置分段的数量的,叫做并发级别concurrencyLevel.
JDK8中ConcurrentHashMap是通过synchronized+cas来实现了。在JDK8中只有一个数组,就是Node数组,Node就是key,value,hashcode封装出来的对象,和HashMap中的Entry一样,在JDK8中通过对Node数组的某个index位置的元素进行同步,达到该index位置的并发安全。同时内部也利用了CAS对数组的某个位置进行并发安全的赋值。
JDK8中的ConcurrentHashMap为什么使用synchronized来进行加锁?
JDK8中使用synchronized加锁时,是对链表头结点和红黑树根结点来加锁的,而ConcurrentHashMap会保证,数组中某个位置的元素一定是链表的头结点或红黑树的根结点,所以JDK8中的ConcurrentHashMap在对某个桶进行并发安全控制时,只需要使用synchronized对当前那个位置的数组上的元素进行加锁即可,对于每个桶,只有获取到了第一个元素上的锁,才能操作这个桶,不管这个桶是一个链表还是红黑树。
想比于JDK7中使用ReentrantLock来加锁,因为JDK7中使用了分段锁,所以对于一个ConcurrentHashMap对象而言,分了几段就得有几个ReentrantLock对象,表示得有对应的几把锁。
而JDK8中使用synchronized关键字来加锁就会更节省内存,并且jdk也已经对synchronized的底层工作机制进行了优化,效率更好。
JDK7中的ConcurrentHashMap是如何扩容的?
JDK7中的ConcurrentHashMap和JDK7的HashMap的扩容是不太一样的,首先JDK7中也是支持多线程扩容的,原因是,JDK7中的ConcurrentHashMap分段了,每一段叫做Segment对象,每个Segment对象相当于一个HashMap,分段之后,对于ConcurrentHashMap而言,能同时支持多个线程进行操作,前提是这些操作的是不同的Segment,而ConcurrentHashMap中的扩容是仅限于本Segment,也就是对应的小型HashMap进行扩容,所以是可以多线程扩容的。
每个Segment内部的扩容逻辑和HashMap中一样。
JDK8中的ConcurrentHashMap是如何扩容的?
首先,JDK8中是支持多线程扩容的,JDK8中的ConcurrentHashMap不再是分段,或者可以理解为每个桶为一段,在需要扩容时,首先会生成一个双倍大小的数组,生成完数组后,线程就会开始转移元素,在扩容的过程中,如果有其他线程在put,那么这个put线程会帮助去进行元素的转移,虽然叫转移,但是其实是基于原数组上的Node信息去生成一个新的Node的,也就是原数组上的Node不会消失,因为在扩容的过程中,如果有其他线程在get也是可以的。
JDK8中的ConcurrentHashMap有一个CounterCell,你是如何理解的?
CounterCell是JDK8中用来统计ConcurrentHashMap中所有元素个数的,在统计ConcurentHashMap时,不能直接对ConcurrentHashMap对象进行加锁然后再去统计,因为这样会影响ConcurrentHashMap的put等操作的效率,在JDK8的实现中使用了CounterCell+baseCount来辅助进行统计,baseCount是ConcurrentHashMap中的一个属性,某个线程在调用ConcurrentHashMap对象的put操作时,会先通过CAS去修改baseCount的值,如果CAS修改成功,就计数成功,如果CAS修改失败,则会从CounterCell数组中随机选出一个CounterCell对象,然后利用CAS去修改CounterCell对象中的值,因为存在CounterCell数组,所以,当某个线程想要计数时,先尝试通过CAS去修改baseCount的值,如果没有修改成功,则从CounterCell数组中随机取出来一个CounterCell对象进行CAS计数,这样在计数时提高了效率。
所以ConcurrentHashMap在统计元素个数时,就是baseCount加上所有CountCeller中的value只,所得的和就是所有的元素个数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现