JUC并发—8.并发安全集合一
大纲
1.JDK 1.7的HashMap的死循环与数据丢失
2.ConcurrentHashMap的并发安全
3.ConcurrentHashMap的设计介绍
4.ConcurrentHashMap的put操作流程
5.ConcurrentHashMap的Node数组初始化
6.ConcurrentHashMap对Hash冲突的处理
7.ConcurrentHashMap的并发扩容机制
8.ConcurrentHashMap的分段锁统计元素数据
9.ConcurrentHashMap的查询操作是否涉及锁
10.ConcurrentHashMap中红黑树的使用
1.JDK 1.7的HashMap的死循环与数据丢失
(1)JDK 1.7的HashMap工作原理
(2)JDK 1.7的HashMap并发下导致的环形链表
(3)环形链表引发的死循环与数据丢失
(4)JDK 1.7和JDK 1.8的HashMap对比
(5)并发安全的集合
(1)JDK 1.7的HashMap工作原理
一.Hash算法
put(key, value) => 对key执行Hash算法 => 根据Hash值用类似取模的算法 => 定位数组的某一个元素 => 如果数组元素为空,则将value存放在该数组元素里。
二.Hash冲突
如果两个key的Hash值,经过取模算法定位到数组的同一个位置,此时就会用链表处理这种Hash冲突。
三.数组扩容
如果数组元素达到了:数组大小 * loadFactor(0.75),此时就会数组扩容。扩容时会按照倍数扩容,首先创建一个两倍大小的新数组。然后遍历原来的数组元素,对每个元素的key值进行Hash运算。接着将Hash运算后的Hash值对新数组大小进行取模,定位到新数组位置。
(2)JDK 1.7的HashMap并发下导致的环形链表
多线程并发操作HashMap时,可能会在扩容过程中形成一个环形链表。比如两个线程同时插入一个元素,而此时恰好两个线程同时触发了数组扩容。那么在数组扩容的过程中,就可能会形成一个环形链表。
下面是JDK1.7中HashMap扩容的核心源码。进行数组扩容时,会使用头插法来进行链表迁移。如果并发执行的两个线程同时使用头插法进行链表迁移,那么就有可能形成一个环形链表。
//JDK1.7的HashMap扩容的核心方法
void transfer(Entry[] newTable) {Entry[] src = table;//旧的数组int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;//线程1执行到这里,假设此时的链表为:newTable[i] = <k1,v1> -> <k2,v2>//那么可知:e = <k1,v1>,next = <k2,v2>//恰好此时CPU发生了上下文切换,于是切换到线程2去执行扩容//线程2扩容时处理完链表的这两个节点后,newTable[i]就变成了:<k2,v2> -> <k1,v1>//然后CPU又切换回线程1来执行,由于此时e = <k1,v1>,那么后续代码对e.next赋值后,e就成为环形链表了://也就是e = <k1,v1> -> <k2,v2> -> <k1,v1>,最后e又赋值给newTable[i]int i = indexFor(e.hash, newCapacity);//在新数组的位置//头插法:刚开始newTable[i]为null,后来newTable[i]变为<k1,v1>;//然后当e=<k2,v2>时,这里e设为<k2,v2> -> <k1,v1>,并又赋值给newTable[i]//接着遍历链表当e=<k3,v3>时,这里e设为<k3,v3> -> <k2,v2> -> <k1,v1> ...e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);}}
}
(3)环形链表引发的死循环与数据丢失
一.环形链表导致死循环
假如执行get(k3)时,k3的Hash取模算法定位到环形链表的位置。于是开始遍历环形链表,但由于环形链表里没有k3的值,所以会导致在环形链表中无法找到k3对应的值进行返回。这样就导致了一直在环形链表中进行死循环,无法退出遍历。最后导致CPU飙升,线上系统被这个get操作卡死。
二.环形链表导致丢失数据
上面例子就导致了从出发是无法找到的,因此这条数据就永久丢失了,甚至会被垃圾回收掉。
(4)JDK 1.7和JDK 1.8的HashMap对比
在JDK1.7中,HashMap采用数组 + 链表的数据结构来存储数据。在多个线程并发扩容时,可能会造成环形链表最终导致死循环和数据丢失。
在JDK1.8中,HashMap采用数组 + 链表 + 红黑树的数据结构来存储数据,并且优化了JDK1.7中的数组扩容方案,解决了死循环和数据丢失的问题。但是在并发场景下调用put()方法时,有可能会存在数据覆盖的问题。
(5)并发安全的集合
比如HashTable使用synchronized来保证线程的安全性,比如Collections.synchronizedMap可以把一个线程不安全的Map,通过synchronized的方式,将其变成安全的。
但是这些方法在线程竞争激烈的情况下,效率都比较低。因为它们都是在方法层面上使用了synchronized实现的锁机制,从而导致不管是put操作还是get操作都需要去竞争同一把锁。
ConcurrentHashMap既能保证并发安全,性能也好于HashTable等集合。
2.ConcurrentHashMap的并发安全
(1)如何理解ConcurrentHashMap的并发安全
(2)ConcurrentHashMap在复合操作中的安全问题
(3)ConcurrentMap可解决复合操作的安全问题
(4)ConcurrentMap支持lambda表达式操作
(1)如何理解ConcurrentHashMap的并发安全
只能保证多线程并发执行时,容器中的数据不会被破坏。无法保证涉及多个线程的复合操作的正确性,复合操作会有并发安全问题。
(2)ConcurrentHashMap在复合操作中的安全问题
假设需要通过一个ConcurrentHashMap来记录每个用户的访问次数。如果指定用户已经有访问次数的记录,则进行递增,否则添加新访问记录。
如下代码在多线程并发调用时,会存在并发安全问题。虽然ConcurrentHashMap对于数据操作本身是安全的,但这里是复合操作,也就是"读—修改—写",而这三个操作作为一个整体却不是原子的。所以当多个线程访问同一个用户时,很可能会覆盖相互操作的结果,从而造成该用户的访问记录次数少于实际访问次数。
public class Demo {private static final ConcurrentHashMap<String, Long> USER_ACCESS_COUNT = new ConcurrentHashMap<>(64);public static void main(String[] args) throws InterruptedException {Long accessCount = USER_ACCESS_COUNT.get("张三");if (accessCount == null) {USER_ACCESS_COUNT.put("张三", 1L);} else {USER_ACCESS_COUNT.put("张三", accessCount + 1);}}
}
(3)ConcurrentMap可解决复合操作的安全问题
虽然ConcurrentHashMap是并发安全的,但对于其复合操作需要特别关注。上述复合操作的安全问题的解决方案是,可以对复合操作加锁,也可以使用ConcurrentMap接口来解决复合操作的安全问题。
ConcurrentMap是一个支持并发访问的Map集合,相当于在原本的Map集合上新增了一些方法来扩展Map的功能。
ConcurrentMap接口定义的如下4个方法,都能满足原子性的,可以用在ConcurrentHashMap的复合操作场景中。
//A java.util.Map providing thread safety and atomicity guarantees.
public interface ConcurrentMap<K, V> extends Map<K, V> {...//向ConcurrentHashMap集合插入数据//如果插入数据的key不存在于集合中,则保存当前数据并且返回null//如果key已经存在,则返回存在的key对应的valueV putIfAbsent(K key, V value);//根据key和value来删除ConcurrentHashMap集合中的元素//该删除操作必须保证key和value完全匹配,删除成功则返回true,否则返回falseboolean remove(Object key, Object value);//根据key和oldValue来替换ConcurrentHashMap中已经存在的值,新的值是newValue//该替换操作必须保证key和oldValue玩去匹配,替换成功则返回true,否则返回falseboolean replace(K key, V oldValue, V newValue);//和replace(key, oldValue, newValue)不同之处在于,少了对oldValue的判断//如果替换成功,则返回替换之前的value,否则返回nullV replace(K key, V value);...
}
因此,可以基于ConcurrentMap提供的接口对上述Demo进行改造。将原来ConcurrentHashMap第一次的put()方法替换为putIfAbsent()方法,将原来ConcurrentHashMap修改用的put()方法替换为replace()方法。由于putIfAbsent()方法和replace()方法都能保证原子性,所以并发安全了。同时增加一个while(true)方法以实现一个类似自旋的操作,确保操作成功。
public class KeyUtil {private static final ConcurrentHashMap<String, Long> USER_ACCESS_COUNT = new ConcurrentHashMap<>(64);public static void main(String[] args) throws InterruptedException {while (true) {Long accessCount = USER_ACCESS_COUNT.get("张三");if (accessCount == null) {if (USER_ACCESS_COUNT.putIfAbsent("张三", 1L) == null) {break;}} else {if (USER_ACCESS_COUNT.replace("张三", accessCount, accessCount + 1)) {break;}}}}
}
(4)ConcurrentMap支持lambda表达式操作
一.computeIfAbsent()方法
该方法通过判断传入的key是否存在来对ConcurrentMap进行数据初始化。如果key存在,则不做任何处理。如果key不存在,则调用mappingFunction计算出value值添加到Map。
//如果张三这个用户不存在,则下面代码会初始化张三这个用户的值为1
USER_ACCESS_COUNT.computeIfAbsent("张三", k -> 1L);
二.computeIfPresent()方法
该方法对已经存在的key对应的value值进行修改。如果key不存在,则返回null。如果key存在,则调用mappingFunction计算出value值修改Map。
//如果要对张三这个已经存在的用户的value值进行修改,可以使用如下代码:
USER_ACCESS_COUNT.computeIfPresent("张三", (k,v) -> v + 1);
三.compute()方法
compute()方法是computeIfAbsent()方法和computeIfPresent()方法的结合体。不管key是否存在,都会调用mappingFunction计算出value值。如果key存在,则对value进行修改。如果key不存在,则进行初始化处理。
//如果张三这个用户不存在,则下面代码会初始化张三这个用户的值为1
USER_ACCESS_COUNT.computeIfAbsent("张三", k -> 1L);//如果要对张三这个已经存在的用户的value值进行修改,可以使用如下代码:
USER_ACCESS_COUNT.computeIfPresent("张三", (k,v) -> v + 1);//如果张三这个用户存在,则对其value加1,否则初始化其值为1
USER_ACCESS_COUNT.compute("张三", (k,v) -> (v == null) ? 1L : v + 1);
3.ConcurrentHashMap的设计介绍
(1)JDK1.8相比于JDK1.7的改进
(2)ConcurrentHashMap的设计思想
(3)ConcurrentHashMap的数据结构定义
(1)JDK1.8相比于JDK1.7的改进
一.取消了segment分段设计,直接使用Node数组来保存数据
采用Node数组元素作为锁的粒度,进一步减少并发冲突的范围和概率。
二.引入红黑树设计
红黑树降低了极端情况下查询某个结点数据的时间复杂度,从O(n)降低到了O(logn),提升了查找性能。
(2)ConcurrentHashMap的设计思想
一.通过对数组元素加锁来降低锁的粒度
二.多线程进行并发扩容
三.高低位迁移方法
四.链表转红黑树及红黑树转链表
五.分段锁来实现数据统计
(3)ConcurrentHashMap的数据结构定义
ConcurrentHashMap采用Node数组来存储数据,数组长度默认为16。Node表示数组中的一个具体的数据结点,并且实现了Map.Entry接口。Node的key和val属性,表示实际存储的key和value。Node的hash属性,表示当前key对应的hash值。Node的next属性,表示如果是链表结构,则指向下一个Node结点。
当链表长度大于等于8 + Node数组长度大于64时,链表会转为红黑树,红黑树的存储使用TreeNode来实现。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { ...//The default initial table capacity.//Must be a power of 2 (i.e., at least 1) and at most MAXIMUM_CAPACITY.private static final int DEFAULT_CAPACITY = 16;//The array of bins. Lazily initialized upon first insertion.//Size is always a power of two. Accessed directly by iterators.transient volatile Node<K,V>[] table;//用来存储ConcurrentHashMap数据的Node数组//Key-value entry. //This class is never exported out as a user-mutable Map.Entry //(i.e., one supporting setValue; see MapEntry below), //but can be used for read-only traversals used in bulk tasks.//Subclasses of Node with a negative hash field are special, //and contain null keys and values (but are never exported). //Otherwise, keys and vals are never null.static class Node<K,V> implements Map.Entry<K,V> {final int hash;//当前key对应的hash值final K key;//实际存储的keyvolatile V val;//实际存储的valuevolatile Node<K,V> next;//如果是链表结构,则指向下一个Node结点Node(int hash, K key, V val, Node<K,V> next) {this.hash = hash;this.key = key;this.val = val;this.next = next;}...}//Nodes for use in TreeBinsstatic final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;//red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;//needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent) {super(hash, key, val, next);this.parent = parent;}...}...
}
4.ConcurrentHashMap的put操作流程
(1)ConcurrentHashMap的put操作流程
(2)ConcurrentHashMap和HashMap的put操作
(3)为什么ConcurrentHashMap是并发安全的
(1)ConcurrentHashMap的put操作流程
首先通过key的hashCode的高低16位的位与操作来计算key的hash值,让32位的hashCode都参与运算以降低数组大小小于32时哈希冲突的概率。
然后判断Node数组是否为空或者Node数组的长度是否为0。如果为空或者为0,则调用initTable()方法进行初始化。如果不为空,则通过hash & (n - 1)计算当前key在Node数组中的下标位置。并通过tabAt()方法获取该位置的值f,然后判断该位置的值f是否为空。
如果该位置的值f为空,则把当前的key和value封装成Node对象。然后尝试通过casTabAt()方法使用CAS设置该位置的值f为封装好的Node对象。如果CAS设置成功,则退出for循环,否则继续进行下一次for循环。
如果该位置的值f不为空,则判断Node数组是否正处于扩容中。如果是,那么当前线程就调用helpTransfer()方法进行并发扩容。如果不是,那么说明当前的key在Node数组中出现了Hash冲突。于是通过synchronized关键字,对该位置的值f进行Hash冲突处理。其实JUC还可以继续优化,比如先用CAS尝试修改哈希冲突下的链表或红黑树。如果CAS修改失败,那么再通过使用synchronized对该数组元素加锁来进行处理。
最后,会调用addCount()方法统计Node数组中的元素个数。
public class ConcurrentHashMapDemo {public static void main(String[] args) {ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();map.put("k1", "v1");}
}public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { ...//The array of bins. Lazily initialized upon first insertion.//Size is always a power of two. Accessed directly by iterators.transient volatile Node<K,V>[] table;//Creates a new, empty map with the default initial table size (16).public ConcurrentHashMap() {}//Creates a new, empty map with an initial table size //accommodating the specified number of elements without the need to dynamically resize.//@param initialCapacity The implementation performs internal sizing to accommodate this many elements.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;}//Returns a power of two table size for the given desired capacity.private static final int tableSizeFor(int c) {int n = c - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}static final int spread(int h) {//通过hashCode的高低16位的异或运算来计算hash值,以降低数组大小比32小的时候的哈希冲突概率return (h ^ (h >>> 16)) & HASH_BITS;}//Maps the specified key to the specified value in this table.//Neither the key nor the value can be null.public V put(K key, V value) {return putVal(key, value, false);}//获取Node数组在位置i的元素,通过Unsafe类让数组中的元素具有可见性//虽然table变量使用了volatile修饰,但这只保证了table引用对于所有线程的可见性,还不能保证table数组中的元素的修改对于所有线程是可见的//因此需要通过Unsafe类的getObjectVolatile()来保证table数组中的元素的可见性static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}//CAS设置Node数组的元素为某个Node对象static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) {throw new NullPointerException();}//通过key的hashCode的高低16位的位与操作来计算hash值int hash = spread(key.hashCode());int binCount = 0;//这是一个没有结束条件的for循环,用来自旋//其中Node数组的引用赋值给了tab变量for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0) {//调用initTable()方法初始化Node数组tab = initTable();} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果通过CAS设置Node数组位置i的值为key/value封装的Node对象,则退出for循环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) {//如果发现Node数组正处于扩容中,那么就进行并发扩容tab = helpTransfer(tab, f);} else {V oldVal = null;//处理Hash冲突synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {//如果是链表binCount = 1;for (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;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);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;}}}}}if (binCount != 0) {//如果链表的元素大于等于8if (binCount >= TREEIFY_THRESHOLD) {//TREEIFY_THRESHOLD = 8treeifyBin(tab, i);//链表转红黑树}if (oldVal != null) {return oldVal;}break;}}}//调用addCount()方法统计Node数组元素的个数addCount(1L, binCount);return null;}...
}
(2)ConcurrentHashMap和HashMap的put操作
都是通过key的hashCode的高低16位的异或运算,来降低Hash冲突概率。
都是通过Hash值与数组大小-1的位与运算(取模),来定位key在数组的位置。
但ConcurrentHashMap使用了自旋 + CAS + synchronized来处理put操作,从而保证了多个线程对数组里某个key进行赋值时的效率 + 并发安全性。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {static final int TREEIFY_THRESHOLD = 8;//链表转红黑树的阈值...public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}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) {//如果通过哈希寻址算法定位到的下标为i的数组元素为空(即tab[i]为空)//那么就可以直接将一个新创建的Node对象放到数组的tab[i]这个位置;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)))) {//通过哈希寻址算法定位到的数组位置已有Node元素//那么判断是否为相同的key,如果是相同的key则进行value覆盖e = p;} else if (p instanceof TreeNode) {//通过哈希寻址算法定位到的数组位置已有Node元素,而且不是相同的key//那么通过"p instanceof TreeNode)",判断数组的tab[i]元素是否是一颗红黑树e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);} else {...}...}++modCount;//判断数组大小size,是否已经达到了扩容阈值threshold大小if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;}...
}
(3)为什么ConcurrentHashMap是并发安全的
首先在初始化Node数组时,会通过自旋 + CAS去设置sizeCtl的值来获得锁。然后在put()操作时,也会通过自旋 + CAS去设置数组某个位置的值。当出现Hash冲突时,则使用synchronized关键字来修改数组某个位置的值。
5.ConcurrentHashMap的Node数组初始化
(1)调用put()方法时才初始化Node数组
(2)initTable()方法的初始化逻辑
(3)sizeCtl的状态流转
(1)调用put()方法时才初始化Node数组
Node数组的初始化过程是被动的,当调用ConcurrentHashMap.put()方法时,如果发现Node数组还没有被初始化,才会调用initTable()方法完成初始化。
(2)initTable()方法的初始化逻辑
initTable()方法和一般的初始化方法不同,因为需要考虑多线程并发情形。
首先while循环的退出条件是Node数据即table初始化成功,否则一直循环。这其实就使用到了自旋的机制,因为多个线程调用initTable()必然会竞争。而在竞争的情况下如果不采用独占锁机制,就只能通过自旋来不断重试。
然后通过sizeCtl是否小于0来判断当前是否有其他线程正在进行初始化。如果有,则通过Thread.yield()把自己变成就绪状态,释放CPU资源。如果没有,则通过CAS修改sizeCtl变量的值为-1。
如果CAS修改sizeCtl成功,则表示当前线程获取初始化Node数组的锁成功了;
如果CAS修改sizeCtl失败,则表示当前线程获取初始化Node数组的锁失败了;
对于获取锁失败的线程,会继续进入下一次while循环进行重试,这样设计是为了避免出现多个线程同时初始化Node数组。
对于获取锁成功的线程,首先会判断Node数组是否已经初始化完成。如果Node数组已经初始化完成,则退出while循环。如果Node数组还是空,则创建一个Node数组,然后赋值给table变量。并且计算下次扩容的阈值(0.75倍当前数组容量),然后赋值给sizeCtl。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { //sizeCtl = -1,表示当前有线程抢占到了初始化Node数组的资格,正在初始化Node数组//sizeCtl < -1,用sizeCtl值的二进制低16位来记录当前参与扩容的线程数量//sizeCtl = 0,表示Node数组未初始化,并且在ConcurrentHashMap构造方法中没有指定初始容量//sizeCtl > 0,如果Node数组已经初始化,那么sizeCtl表示扩容的阈值(初始容量 * 0.75),如果未初始化,则表示数组的初始容量private transient volatile int sizeCtl;private static final long SIZECTL;static {try {U = sun.misc.Unsafe.getUnsafe();//获取UnSafe对象Class<?> k = ConcurrentHashMap.class;SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));//获取sizeCtl变量的偏移量...} catch (Exception e) {throw new Error(e);}}...//初始化Node数组//Initializes table, using the size recorded in sizeCtl.private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;//退出while循环的条件是Node数组即table初始化成功while ((tab = table) == null || tab.length == 0) {//判断当前是否有其他线程正在进行初始化if ((sc = sizeCtl) < 0) {//如果有,则通过Thread.yield()把自己变成就绪状态,释放CPU资源Thread.yield();//lost initialization race; just spin} else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//如果没有线程正在进行初始化,则通过CAS修改sizeCtl变量的值为-1//如果CAS修改成功,则表示当前线程获得了初始化数组的锁//如果CAS修改失败,则表示当前线程获取初始化数组的锁失败try {//再次判断Node数组是否为空,即Node数组是否已经初始化完成//因为执行Thread.yield()让出CPU资源的线程必然会再次执行到这里if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")//初始化大小为n的Node数组,然后赋值给tab变量和table变量Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//赋值给ConcurrentHashMap的全局Node数组tabletable = tab = nt;//计算下次扩容的阈值,阈值的计算是当前数组容量的0.75倍sc = n - (n >>> 2);}} finally {//最后将扩容的阈值赋值给sizeCtlsizeCtl = sc;}break;}}return tab;}...
}
(3)sizeCtl的状态流转
一.sizeCtl = -1
表示当前有线程抢占到了初始化Node数组的资格,正在初始化Node数组。
二.sizeCtl < -1
用sizeCtl值的二进制低16位来记录当前参与扩容的线程数量。
三.sizeCtl = 0
表示Node数组未初始化,且创建ConcurrentHashMap时没有指定初始容量。
四.sizeCtl > 0
如果Node数组已经初始化,那么sizeCtl表示扩容的阈值(初始容量 * 0.75)。如果Node数组未初始化,则表示数组的初始容量。
6.ConcurrentHashMap对Hash冲突的处理
(1)Hash冲突的几个解决方案
(2)ConcurrentHashMap对Hash冲突的处理
(3)链表长度大于8时是扩容还是转化为红黑树
(1)Hash冲突的几个解决方案
一.开放寻址法
如果位置i被占用,那么就探查i+1、i+2、i+3的位置。ThreadLocal采用的就是开放寻址法。
二.链式寻址法
Hash表的每个位置都连接一个链表。当发生Hash冲突时,冲突的元素会被加入到这个位置的链表中。ConcurrentHashMap就是基于链式寻址法解决Hash冲突的。
三.再Hash法
提供多个不同的Hash函数,当发生Hash冲突时,使用第二个、第三个等。
(2)ConcurrentHashMap对Hash冲突的处理
首先使用synchronized对当前位置的Node对象f进行加锁。由于这种锁控制在数组的单个数据元素上,所以长度为16的数组理论上就可以支持16个线程并发写入数据。
然后判断当前位置的Node对象f是链表还是红黑树。如果是链表,那么就把当前的key/value封装成Node对象插入到链表的尾部。如果是红黑树,那么就调用TreeBin的putTreeVal()方法往红黑树插入结点。
最后判断链表的长度是否大于等于8,如果链表的长度大于等于8,再调用treeifyBin()方法决定是扩容数组还是将链表转化为红黑树。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { ...//The array of bins. Lazily initialized upon first insertion.//Size is always a power of two. Accessed directly by iterators.transient volatile Node<K,V>[] table;//Maps the specified key to the specified value in this table.//Neither the key nor the value can be null.public V put(K key, V value) {return putVal(key, value, false);}//获取Node数组在位置i的元素//虽然table变量使用了volatile修饰,但这只保证了table引用对于所有线程的可见性,还不能保证table数组中的元素的修改对于所有线程是可见的//因此需要通过Unsafe类的getObjectVolatile()来保证table数组中的元素的可见性static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}//CAS设置Node数组的元素为某个Node对象static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) {throw new NullPointerException();}//通过key的hashCode的高低16位的位与操作来计算hash值int hash = spread(key.hashCode());int binCount = 0;//这是一个没有结束条件的for循环,用来自旋//其中Node数组的引用赋值给了tab变量for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0) {//调用initTable()方法初始化Node数组tab = initTable();} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果通过CAS设置Node数组位置i的值为key/value封装的Node对象,则退出for循环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) {//发现Node数组正处于扩容中,那么就进行并发扩容tab = helpTransfer(tab, f);} else {V oldVal = null;//处理Hash冲突synchronized (f) {//使用synchronized对当前数组位置的Node对象f进行加锁if (tabAt(tab, i) == f) {if (fh >= 0) {//如果是链表binCount = 1;//binCount用来记录链表的长度//从链表的头结点开始遍历每个结点for (Node<K,V> e = f;; ++binCount) {K ek;//如果存在相同的key,则修改该key的valueif (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) {//把当前的key/value封装成Node对象插入到链表的尾部pred.next = new Node<K,V>(hash, key, value, null);break;}}} else if (f instanceof TreeBin) {//如果是红黑树Node<K,V> p;binCount = 2;//调用TreeBin的putTreeVal()方法往红黑树插入结点if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {oldVal = p.val;if (!onlyIfAbsent) {p.val = value;}}}}}//最后判断链表的长度是否大于等于8if (binCount != 0) {//如果链表的长度大于等于8,再调用treeifyBin()方法决定是扩容数组还是转化为红黑树if (binCount >= TREEIFY_THRESHOLD) {//TREEIFY_THRESHOLD = 8treeifyBin(tab, i);//是扩容数组还是转化为红黑树}if (oldVal != null) {return oldVal;}break;}}}//调用addCount()方法统计Node数组元素的个数addCount(1L, binCount);return null;}...
}
(3)链表长度大于8时是扩容还是转化为红黑树
当链表长度 >= 8时,ConcurrentHashMap会对链表采用两种方式进行优化。
方式一:对数组进行扩容
当数组长度 <= 64,且链表长度 >= 8时,优先选择对数组进行扩容。
方式二:把链表转化为红黑树
当数组长度 > 64,且链表长度 >= 8时,会将链表转化为红黑树。
treeifyBin()方法的作用是根据相关阈值来决定是扩容还是把链表转为红黑树。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { static final int MIN_TREEIFY_CAPACITY = 64;...//Replaces all linked nodes in bin at given index unless table is too small, in which case resizes instead.private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {//如果当前数组的长度小于64,则调用tryPresize()方法进行数组扩容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) {TreeNode<K,V> hd = null, tl = null;for (Node<K,V> e = b; e != null; e = e.next) {//构建一个TreeNode并插入红黑树中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));}}}}}...
}
相关文章:
JUC并发—8.并发安全集合一
大纲 1.JDK 1.7的HashMap的死循环与数据丢失 2.ConcurrentHashMap的并发安全 3.ConcurrentHashMap的设计介绍 4.ConcurrentHashMap的put操作流程 5.ConcurrentHashMap的Node数组初始化 6.ConcurrentHashMap对Hash冲突的处理 7.ConcurrentHashMap的并发扩容机制 8.Concu…...
Linux 内核网络设备驱动编程:私有协议支持
一、struct net_device的通用性与私有协议的使用 struct net_device是Linux内核中用于描述网络设备的核心数据结构,它不仅限于TCP/IP协议,还可以用于支持各种类型的网络协议,包括私有协议。其原因如下: 协议无关性:struct net_device的设计是通用的,它本身并不依赖于任何…...
机器学习的数学基础(三)——概率与信息论
目录 1. 随机变量2. 概率分布2.1 离散型变量和概率质量函数2.2 连续型变量和概率密度函数 3. 边缘概率4. 条件概率5. 条件概率的链式法则6. 独立性和条件独立性7. 期望、方差和协方差7.1 期望7.2 方差7.3 协方差 8. 常用概率分布8.1 均匀分布 U ( a , b ) U(a, b) U(a,b)8.2 Be…...
使用Docker Desktop部署GitLab
1. 环境准备 确保Windows 10/11系统支持虚拟化技术(需在BIOS中开启Intel VT-x/AMD-V)内存建议≥8GB,存储空间≥100GB 2. 安装Docker Desktop 访问Docker官网下载安装包安装时勾选"Use WSL 2 instead of Hyper-V"(推荐…...
推理模型时代:大语言模型如何从对话走向深度思考?
一、对话模型和推理模型的区别概述 对话模型是专门用于问答交互的语言模型,符合人类的聊天方式,返回的内容可能仅仅只是一个简短的答案,一般模型名称后面会带有「chat」字样。 推理模型是比较新的产物,没有明确的定义,一般是指输出过程中带有<think>和</think&…...
GESP2024年3月认证C++七级( 第三部分编程题(1)交流问题)
参考程序: #include <iostream> #include <vector> #include <unordered_map> using namespace std;// 深度优先搜索,给每个节点染色,交替染色以模拟两校同学的划分 void dfs(vector<vector<int>>& graph…...
DeepSeek:AI商业化的新引擎与未来蓝图
摘要 在人工智能迅猛发展的浪潮中,DeepSeek以其卓越的技术实力和高超的商业化能力崭露头角。作为一款现象级AI产品,它不仅在算法性能上位居行业前列,还通过灵活的定制解决方案渗透到金融、医疗、零售等多个领域。DeepSeek以创新的商业模式和场…...
2025年度福建省职业院校技能大赛中职组“网络建设与运维”赛项规程模块三
模块三:服务搭建与运维 任务描述: 随着信息技术的快速发展,集团计划把部分业务由原有的 X86 服 务器上迁移到ARM 架构服务器上,同时根据目前的部分业务需求进行 了部分调整和优化。 一、X86 架构计算机操作系统安装与管理 1&…...
Python----数据结构(队列,顺序队列,链式队列,双端队列)
一、队列 1.1、概念 队列(Queue):也是一种基本的数据结构,在队列中的插入和删除都遵循先进先出(First in First out,FIFO)的原则。元素可以在任何时刻从队尾插入,但是只有在队列最前面 的元素才能被取出或…...
【自学笔记】Spring Boot框架技术基础知识点总览-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 Spring Boot框架技术基础知识点总览一、Spring Boot简介1.1 什么是Spring Boot?1.2 Spring Boot的主要特性 二、Spring Boot快速入门2.1 搭建Spring Boo…...
神经网络剪枝技术的重大突破:sGLP-IB与sTLP-IB
神经网络剪枝技术的重大突破:sGLP-IB与sTLP-IB 在人工智能飞速发展的今天,深度学习技术已经成为推动计算机视觉、自然语言处理等领域的核心力量。然而,随着模型规模的不断膨胀,如何在有限的计算资源和存储条件下高效部署这些复杂的神经网络模型,成为了研究者们亟待解决的…...
Django-Vue 学习-VUE
主组件中有多个Vue组件 是指在Vue.js框架中,主组件是一个父组件,它包含了多个子组件(Vue组件)。这种组件嵌套的方式可以用于构建复杂的前端应用程序,通过拆分功能和视图,使代码更加模块化、可复用和易于维…...
【Gin】2:快速上手Gin框架(模版、cookie、session)
本文目录 一、模版渲染二、自定义模版函数三、cookie四、Session五、cookie、session区别六、会话攻击 一、模版渲染 在 Gin 框架中,模板主要用于动态生成 HTML 页面,结合 Go 语言的模板引擎功能,实现数据与视图的分离。 模板渲染是一种动态…...
Linux修改主机名称
hostnamectl set-hostname 主机名称 exit 退出登录重新进入即可...
亲测Windows部署Ollama+WebUI可视化
一. Ollama下载 登录Ollama官网(Ollama)点击Download进行下载 如果下载很慢可用以下地址下载: https://github.com/ollama/ollama/releases/download/v0.5.7/OllamaSetup.exe 在DeepSeek官网上,你可以直接点击【model】 到达这个界面之后,…...
Java四大框架深度剖析:MyBatis、Spring、SpringMVC与SpringBoot
目录 前言: 一、MyBatis框架 1. 概述 2. 核心特性 3. 应用场景 4. 示例代码 二、Spring框架 1. 概述 2. 核心模块 3. 应用场景 4. 示例代码 三、SpringMVC框架 1. 概述 2. 核心特性 3. 应用场景 4. 示例代码 四、SpringBoot框架 1. 概述 2. 核心…...
ubuntu部署小笔记-采坑
ubuntu部署小笔记 搭建前端控制端后端前端nginx反向代理使用ubuntu部署nextjs项目问题一 如何访问端口号配置后台运行该进程pm2 问题二 包体过大生产环境下所需文件 问题三 部署在vercel时出现的问题需要魔法访问后端api时,必须使用https协议电脑端访问正常…...
23. AI-大语言模型-DeepSeek简介
文章目录 前言一、DeepSeek是什么1. 简介2. 产品版本1. 类型2. 版本3. 参数规模与模型能力 3. 特征4. 三种访问方式1. 网页端和APP2. DeepSeek API 二、DeepSeek可以做什么1. 应用场景2. 文本生成1. 文本创作2. 摘要与改写3. 结构化生成 3. 自然语言理解与分析1. 语义分析2. 文…...
基于SpringBoot的智慧家政服务平台系统设计与实现的设计与实现(源码+SQL脚本+LW+部署讲解等)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
Java NIO与传统IO性能对比分析
Java NIO与传统IO性能对比分析 在Java中,I/O(输入输出)操作是开发中最常见的任务之一。传统的I/O方式基于阻塞模型,而Java NIO(New I/O)引入了非阻塞和基于通道(Channel)和缓冲区&a…...
基于YOLO11深度学习的果园苹果检测与计数系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...
基于SpringBoot畅购行汽车购票系统
作者简介:✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流。✌ 主要内容:🌟Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能…...
基于 Spring Boot + 微信小程序的短文写作竞赛管理系统设计与实现(源码+文档)
大家好,今天要和大家聊的是一款基于 Spring Boot 微信小程序的“短文写作竞赛管理系统”的设计与实现。项目源码以及部署相关事宜请联系我,文末附上联系方式。 项目简介 基于 Spring Boot 微信小程序的“短文写作竞赛管理系统”设计与实现的主要使用…...
pytest运行用例的常见方式及参数
标题pytest运行用例方式及参数 用例结构目录 “”" 在最外层目录下执行所有的用例 参数说明: -s:显示用例的打印信息 -v:显示用例执行的详细信息 –alluredir:指定allure报告的路径 –clean-alluredir:清除allure报告的路径 -n:指定并发的进程数 -x:出现一条用…...
Miniconda + VSCode 的Python环境搭建
目录: 安装 VScode 安装 miniconda 在VScode 使用conda虚拟环境 运行Python程序 1.安装 vscode 编辑器 官网链接:Visual Studio Code - Code Editing. Redefined 下载得到:,双击安装。 安装成功…...
图解MySQL【日志】——Redo Log
Redo Log(重做日志) 为什么需要 Redo Log? 1. 崩溃恢复 数据库崩溃时,系统通过 Redo Log 来恢复尚未写入磁盘的数据。Redo Log 记录了所有已提交事务的操作,系统在重启后会重做这些操作,以保证数据不会丢…...
Trae AI驱动开发实战:30分钟从0到1实现Django REST天气服务
目录 一、Trae 安装 1、Trae 介绍 2、Trae 安装 二、项目构建 1、项目背景与技术选型 2、开发环境准备 三、需求分析 1、功能模块设计 2、数据库设计 四、功能实现 1、用户系统开发 2、天气服务实现 3、测试用例编写 五、Trae 体验总结 随着人工智能技术的迅猛发…...
【Linux网络编程】IP协议格式,解包步骤
目录 解析步骤 1.版本字段(大小:4比特位) 2.首部长度(大小:4比特位)(单位:4字节) 🍜细节解释: 3.服务类型(大小:8比特…...
中诺CHINO-E G076大容量录音电话产品使用注意事项
•本机需插上随机配置的电源适配器才能正常工作,切勿插入其它的适配器,以免损坏话机; •当本机出现异常时,请按“Δ/上查”键3秒,屏幕弹出确定恢复,按“设置”键恢复出厂设置; 注:…...
2025最新智能优化算法:改进型雪雁算法(Improved Snow Geese Algorithm, ISGA)求解23个经典函数测试集,MATLAB
一、改进型雪雁算法 雪雁算法(Snow Geese Algorithm,SGA)是2024年提出的一种新型元启发式算法,其灵感来源于雪雁的迁徙行为,特别是它们在迁徙过程中形成的独特“人字形”和“直线”飞行模式。该算法通过模拟雪雁的飞行…...
✨ 索引有哪些缺点以及具体有哪些索引类型
索引的定义与原理 索引是数据库中用于提高数据检索效率的数据结构。它就像是书籍的目录,通过目录可以快速定位到所需内容的页码,而在数据库中,索引可以帮助数据库系统快速找到符合查询条件的数据行,而不必对整个表进行扫描。 其…...
Promptic:Python 中的 LLM 应用开发利器
Promptic 是一个基于 Python 的轻量级库,旨在简化与大型语言模型(LLMs)的交互。它通过提供简洁的装饰器 API 和强大的功能,帮助开发者高效地构建 LLM 应用程序。Promptic 的设计理念是提供 90% 的 LLM 应用开发所需功能,同时保持代码的简洁和易用性。 1. Promptic 的核心…...
本地部署DeepSeek R1大模型
一、安装软件 1.1 安装Ollama 你可以访问Ollama的官方网站https://ollama.com/download,选择适合你操作系统的安装包进行下载。老周这里是Mac系统,所以选择下载macOS系统。 1.2 安装cherry studio 前往官网https://cherry-ai.com/download下载对应操…...
搅局外卖,京东连出三张牌
明牌暗牌,都不如民牌。 作者|古廿 编辑|杨舟 “京东来整顿外卖了”,这一网络热梗正在成为外界对京东近期一系列动作的高度概括。 0佣金、五险一金、品质外卖,京东连出三张牌打破外卖市场的旧秩序。此前这三项分别对应着长期被社会所诟病的…...
【ELK】【Elasticsearch】数据查询方式
1. 简单查询(URI Search) 通过 URL 参数直接进行查询,适合简单的搜索场景。 示例: bash 复制 GET /index_name/_search?qfield_name:search_value 说明: index_name:索引名称。 field_name…...
基于 JavaWeb 的 Spring Boot 网上商城系统设计和实现(源码+文档+部署讲解)
技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…...
C++17中的std::scoped_lock:简化多锁管理的利器
文章目录 1. 为什么需要std::scoped_lock1.1 死锁问题1.2 异常安全性1.3 锁的管理复杂性 2. std::scoped_lock的使用方法2.1 基本语法2.2 支持多种互斥锁类型2.3 自动处理异常 3. std::scoped_lock的优势3.1 避免死锁3.2 简化代码3.3 提供异常安全保证 4. 实际应用场景4.1 数据…...
Linux内核实时机制7 - 实时改造机理 - 软中断优化下
Linux内核实时机制7 - 实时改造机理 - 软中断优化下 https://blog.csdn.net/u010971180/article/details/145722641以下分别以Linux4.19、Linux5.4、Linux5.10、Linux5.15 展开分析,深入社区实时改造机理的软中断优化过程。https://blog.csdn.net/weixin_41028621/article/det…...
计算机网络:应用层 —— 文件传送协议 FTP
文章目录 FTP 是什么?FTP 的应用FTP 的基本工作原理主动模式被动模式 总结 FTP 是什么? 将某台计算机中的文件通过网络传送到可能相很远的另一台计算机中,是一项基本的网络应用,即文件传送。 文件传送协议FTP(File T…...
[笔记.AI]如何判断模型是否通过剪枝、量化、蒸馏生成?
以下摘自与DeepSeek-R1在线联网版的对话 一、基础判断维度 技术类型核心特征验证方法剪枝模型参数减少、结构稀疏化1. 检查模型参数量是否显著小于同类标准模型1 2. 分析权重矩阵稀疏性(如非零参数占比<30%)4量化权重/激活值精度降低、推理速度提升1…...
python: SQLAlchemy (ORM) Simple example using mysql in Ubuntu 24.04
mysql sql script: create table School 表 (SchoolId char(5) NOT NULL comment主鍵primary key,學校編號,SchoolName nvarchar(500) NOT NULL DEFAULT comment 學校名稱,SchoolTelNo varchar(8) NULL DEFAULT comment電話號碼,PRIMARY KEY (SchoolId) #主…...
【前端】【nuxt】nuxt优势(MVP开发),转换SSR与SPA模式
Nuxt.js 核心优势 自动化路由系统 无需手动配置路由:在 pages/ 目录下创建 .vue 文件即可自动生成路由,支持动态路由(如 pages/user/[id].vue → /user/:id)。嵌套路由:通过 parent.vue parent/child.vue 目录结构自动…...
洛谷B3619(B3620)
B3619 10 进制转 x 进制 - 洛谷 B3620 x 进制转 10 进制 - 洛谷 代码区: #include<algorithm> #include<iostream> #include<vector> using namespace std;int main(){int n,x;cin >> n >> x;vector<char> arry;while(n){if(…...
基于springboot+vue的酒店管理系统的设计与实现
开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…...
android调用ffmpeg解析rtsp协议的视频流
文章目录 一、背景二、解析rtsp数据1、C层功能代码2、jni层的定义3、app层的调用 三、源码下载 一、背景 本demo主要介绍android调用ffmpeg中的接口解析rtsp协议的视频流(不解析音频),得到yuv数据,把yuv转bitmap在android设备上显…...
cursor使用记录
一、如何查看自己登录的是哪个账号 操作路径:Cursor -- 首选项 -- Cursor Setting (有快捷键) 二、状态修改为竖排(默认是横排) 默认如图展示,想要像vscode、idea等等在左侧竖着展示 操作路径࿱…...
Java 使用websocket
添加依赖 <!-- WebSocket 支持 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>添加配置类 Configuration public class WebSocketConfig {B…...
蓝桥杯 Java B 组之背包问题、最长递增子序列(LIS)
Day 4:背包问题、最长递增子序列(LIS) 📖 一、动态规划(Dynamic Programming)简介 动态规划是一种通过将复杂问题分解成更小的子问题来解决问题的算法设计思想。它主要用于解决具有最优子结构和重叠子问题…...
在PyTorch中使用插值法来优化卷积神经网络(CNN)所需硬件资源
插值法其实就是在已知数据点之间估计未知点的值。通过已知的离散数据点,构造一个连续的曲线函数,预测数据点之间的空缺值是什么并且自动填补上去。 适用场景: 在卷积神经网络(CNN)中的应用场景中,经常遇到计算资源有限,比如显存不够或者处理速度慢,需要用插值来降低计…...
seacmsv9 SQL注入漏洞(报错注入)
一、海洋CMS简介 海洋cms是为解决站长核心需求而设计的视频内容管理系统,一套程序自适应电脑、手机、平板、APP多个终端入口,无任何加密代码、安全有保障,是您最佳的建站工具。——来自seacms官网(简而言之就是专门搭建看片网站的…...