ConcurrentHashMap--自用,非教学

结论先行,细节在下面

jdk1.7是如何解决并发问题的以及完整流程

一.首先new一个concurrentHashMap

调用默认构造方法

二.初始化

初始化initialCapacity(默认是16,指一个segment内Entry的数量),loadFactor(默 认0.75f,负载因子),初始化concurrentLevel(默认是16,segment数量)。
1.校验传入的参数是否符合规定
2.计算concurrentLevel、segementMask(掩码)和segementShift(移位数)
3.计算每个segment中的Entry数组大小,默认且最小为2
4.此时你得到了一个segment对象,调用UNSAFE.putOrderedObject方法,利用CAS将 此segment对象放在segment数组下标为0的位置,其余15个位置为null

三.初始化完开始使用。先put一个键值对进去

1.判断value是否为空,为空直接报错
2.计算hash值。int j = (hash >>> segmentShift) & segmentMask
先用segementShift将32位的hash右移28位,剩4位,再与segmentMask(二进制码,具体数值为1111)进行与运算,得到j,此时segment[j]还是null,不像segment[0]已经初始化,那么调用ensureSegment(j)初始化segment[j]
3.上来第一步先 tryLock() ? null : scanAndLockForPut(key, hash, value);
如果tryLock失败,也就是没拿到独占锁,将调用scanAndLockForPut方法,这个方法大概是循环尝试tryLock(),尝试次数到一定后,将调用lock()进行阻塞,直到拿到锁
4.获取锁成功后,hash计算entry下标,int index = (tab.length - 1) & hash
5.遍历链表,有数据就覆盖,没数据就头插
6.判断是否需要扩容
7.释放锁

四.扩容

1.定义threshold = (int)(newCapacity * loadFactor),只要threshold小于map中实际存入的元素大小,就开始扩容;entry数组一次扩容成原来的两倍
2.用rehash方法,计算新的掩码segmentMask,然后遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置(原理HashMap那里说过)
3.最后插入新节点

五.get方法

第一次计算hash定位segment,第二次hash定位entry,然后返回。

六.并发问题的解决

注意到,get没有加锁,put和remove都加上了独占锁,需要考虑的问题就是 get 的时候在同一个 segment 中发生了 put 或 remove 操作,会发生什么
1.对于put
第一个问题是:初始化segment是用CAS将segment对象放入segment数组index为0的位置的;
第二个问题是:put进entry是头插,如果此时get操作已经遍历到链表中间,无影响。但是还需要保证put之后get要找的到刚被插入的头节点,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject;
第三个问题是:扩容也有并发。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table,get操作会在旧table上进行,不影响,如果put先行,扩容后行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。

jdk1.8是如何解决并发问题的以及完整流程

一.首先new一个concurrentHashMap

调用默认构造方法,需要注意的是,1.8摒弃了segment这个概念,引入了红黑树这个数据结构,加锁则采用CAS和synchronized实现

二.构造函数内部操作。

维护一个sizeCtl = (1.5 * initialCapacity + 1) 再向上取最近的2的倍数。比如initialCapacity = 10,则sizeCtl = 16。sizeCtl的使用场景很多。
构造函数只是计算值而已,初始化操作延迟到真正操作数据的时候。

三.put过程分析

1.key或value==null直接抛错误。
2.hash = spread(key.hashCode()),得到hash值,定义binCount记录链表长度。
3. if 数组为空,初始化数组(这里才真正初始化数组);如果已经初始化,找出该hash值对应的数组下标,得到第一个节点
else if 该位置尚未有任何节点,利用CAS将新节点放入。put逻辑基本结束。
else if hash == MOVED,说明在扩容,转而帮助其数据迁移。
else 此时节点存在,也不为空。
在这个 else 下,又有两个判断:
如果hash >= 0,说明是链表
如果节点f instanceof TreeBin,说明是红黑树
对应不同的插入逻辑
4.进行完以上判断,开始进入判断是否将链表转化成红黑树的阶段
if(binCount >= TREEIFY_THRESHOLD) 也就是第三步的第二小步定义的binCount记录着本链表的长度,大于等于8就转红黑树

四.真正对数组的初始化

initTable方法
初始化一个合适大小的数组,然后会设置 sizeCtl。
初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的
U.compareAndSwapInt(this, SIZECTL, sc, -1),将sizeCtl改成-1,代表抢到锁
接下来就是各种赋初值,比如数组长度什么的。

五.链表转红黑树

treeifyBin方法
treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容
如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容,而不是转化为红黑树。
如果需要转化,那么用synchronized加锁,将链表变成红黑树,然后返回头结点,设置 到数组相应的位置上。

六.扩容机制

tryPresize方法
这个方法的核心在于对 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)
所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚
总的来说,肯定是得把老数组的东西拷贝到新数组里面,然后引用指向新数组,这样就行了,怎么拷贝呢?用transfer方法
原理太复杂,大概意思就是将一个大数组分割成很多个小部分,可以令每个线程负责转移一部分数据,转移数据的时候,会锁头节点或者根节点,转移后一个位置,就会在那个位置放置一个特殊的节点,该节点hash值为-1,表示该位置已经转移

七.get 过程分析

计算hash,利用hash定位。
如果为null,返回null;
如果刚好是需要的,那就返回;
如果hash < 0,说明正扩容,用find方法找;
如果上面都不满足,说明是链表,直接往后遍历即可。

八.并发问题的解决

1.初始化时:在initTable方法内可以看到,通过CAS判断当前是否有其他线程在初始化,如果有,那么当前线程会被阻塞,一直CAS自旋等到数组初始化成功。
2.扩容时:将数组分割成若干份,允许多个线程一起扩容,一起转移数据,每个线程在负责自己那一part的数据转移时,会对头结点加锁。
3.插入时:位置为空时,CAS插入;不为空时,对头结点加锁,再插入。

上面是总结,速度过一遍;下面是细节,仔细看一遍

正式绪论

JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap;本文将分别介绍这两种方式的实现方案及其区别。
在这里插入图片描述
请带着这些问题学习。

为什么HashTable慢

Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。

JDK1.7版本

在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap. 简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。接下来分析JDK1.7版本中ConcurrentHashMap的实现原理。

segment

整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
在这里插入图片描述
在这里插入图片描述
concurrentLevel:是一个int数值,命名为并发数,默认是16。也就是说,一个map中有16个segment,于是map支持16个线程并发写,只要他们分别操作这16个segment。
可以人为在初始化时设置成其他值,一旦指定,不可扩容。
在这里插入图片描述

segment的内部

在使用之前先初始化map,调用上图的方法,initialCapacity是初始容量,loadFactor是负载因子,concurrentLevel是并发数,也是segment的数量。
如果调用无参构造方法,那么我将得到:
在这里插入图片描述
segmentMask要等于数组长度减一,比如16 - 1 = 15,二进制码是1111,可以更好地保证散列的均匀性;
segmentShift是移位数,由于hash是32位的,它设为28的话,可以使hash无符号右移28位,剩下4个高位数,而这四位再和1111(也就是segmentMask)做一次与运算就可以转换为segment数组的下标,因为4位二进制数可以表示数字0~15,segment数组下标也是从0到15。

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();// 1. 计算 key 的 hash 值int hash = hash(key);// 2. 根据 hash 值找到 Segment 数组中的位置 j//    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位,//    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标int j = (hash >>> segmentShift) & segmentMask;// 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,// ensureSegment(j) 对 segment[j] 进行初始化if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegments = ensureSegment(j);// 3. 插入新值到 槽 s 中return s.put(key, hash, value, false);
}

这里主要是为了计算出segment的下标,也就是该存到哪个segment下。
之后会进入segment内部获取锁,然后正式插入数据。

PUT方法的细节

初始化槽: ensureSegment(int k)方法

ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。
这里需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。

初始化第一个槽的原因

在这里插入图片描述
拿segment[0]这个最先被初始化且被操作的当做榜样,利用[0]去初始化[k]。
总的来说,ensureSegment(int k) 比较简单,对于并发操作使用 CAS 进行控制。

获取写入锁方法scanAndLockForPut(K key, int hash, V value)

在往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。
在这里插入图片描述
这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。 这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。

扩容:rehash

扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍。
注意到,在put方法里,会判断该值插入后是否会导致超出阈值,超了就先扩容再插。

get方法

计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
槽中也是一个数组,根据 hash 找到数组中具体的位置
到这里是链表了,顺着链表进行查找即可

并发问题分析

在这里插入图片描述

JDK1.8版本

写在前面

在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

数据结构

在这里插入图片描述

构造函数

// 这构造函数里,什么都不干
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {if (initialCapacity < 0)throw new IllegalArgumentException();int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));this.sizeCtl = cap;
}

sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。

PUT方法

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.htmlpublic V put(K key, V value) {return putVal(key, value, false);
}
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;// 如果数组"空",进行数组初始化if (tab == null || (n = tab.length) == 0)// 初始化数组,后面会详细介绍tab = initTable();// 找该 hash 值对应的数组下标,得到第一个节点 felse if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 如果数组该位置为空,//    用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了//          如果 CAS 失败,那就是有并发操作,进到下一个循环就好了if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}// hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容else if ((fh = f.hash) == MOVED)// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了tab = helpTransfer(tab, f);else { // 到这里就是说,f 是该位置的头节点,而且不为空V oldVal = null;// 获取数组该位置的头节点的监视器锁synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) { // 头节点的 hash 值大于 0,说明是链表// 用于累加,记录链表的长度binCount = 1;// 遍历链表for (Node<K,V> e = f;; ++binCount) {K ek;// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了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) {// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8if (binCount >= TREEIFY_THRESHOLD)// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树//    具体源码我们就不看了,扩容部分后面说treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}// addCount(1L, binCount);return null;
}

初始化数组: initTable

这个比较简单,主要就是初始化一个合适大小的数组,然后会设置 sizeCtl。

初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的。

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {// 初始化的"功劳"被其他线程"抢去"了if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {// DEFAULT_CAPACITY 默认初始容量是 16int n = (sc > 0) ? sc : DEFAULT_CAPACITY;// 初始化数组,长度为 16 或初始化时提供的长度Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];// 将这个数组赋值给 table,table 是 volatile 的table = tab = nt;// 如果 n 为 16 的话,那么这里 sc = 12// 其实就是 0.75 * nsc = n - (n >>> 2);}} finally {// 设置 sizeCtl 为 sc,我们就当是 12 吧sizeCtl = sc;}break;}}return tab;
}

数组转红黑树

前面我们在 put 源码分析也说过,treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容。我们还是进行源码分析吧。

private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {// MIN_TREEIFY_CAPACITY 为 64// 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容if ((n = tab.length) < MIN_TREEIFY_CAPACITY)// 后面我们再详细分析这个方法tryPresize(n << 1);// b 是头节点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<K,V> p =new TreeNode<K,V>(e.hash, e.key, e.val,null, null);if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}// 将红黑树设置到数组相应位置中setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}

扩容: tryPresize

如果说 Java8 ConcurrentHashMap 的源码不简单,那么说的就是扩容操作和迁移操作。 这个方法要完完全全看懂还需要看之后的 transfer 方法,读者应该提前知道这点。 这里的扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍。

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {// c: size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :tableSizeFor(size + (size >>> 1) + 1);int sc;while ((sc = sizeCtl) >= 0) {Node<K,V>[] tab = table; int n;// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码if (tab == null || (n = tab.length) == 0) {n = (sc > c) ? sc : c;if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if (table == tab) {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = nt;sc = n - (n >>> 2); // 0.75 * n}} finally {sizeCtl = sc;}}}else if (c <= sc || n >= MAXIMUM_CAPACITY)break;else if (tab == table) {// 我没看懂 rs 的真正含义是什么,不过也关系不大int rs = resizeStamp(n);if (sc < 0) {Node<K,V>[] nt;if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;// 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法//    此时 nextTab 不为 nullif (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}// 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)//     我是没看懂这个值真正的意义是什么? 不过可以计算出来的是,结果是一个比较大的负数//  调用 transfer 方法,此时 nextTab 参数为 nullelse if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);}}
}

这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。 所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚。

transfer数据迁移方法

太麻烦了

get方法

get 方法从来都是最简单的,这里也不例外:
计算 hash 值 根据 hash 值找到数组对应位置: (n - 1) & h
根据该位置处结点性质进行相应查找
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
如果以上 3 条都不满足,那就是链表,进行遍历比对即可

两个版本的区别

在这里插入图片描述

参考资料

https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329921.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java开发必须掌握的线上问题排查命令

转载自 Java开发必须掌握的线上问题排查命令作为一个合格的开发人员&#xff0c;不仅要能写得一手还代码&#xff0c;还有一项很重要的技能就是排查问题。这里提到的排查问题不仅仅是在coding的过程中debug等&#xff0c;还包括的就是线上问题的排查。由于在生产环境中&#x…

centos8启动docker-mysql8容器

【README】 本文记录了 centos8 安装&#xff0c;启动mysql8的docker容器的步骤&#xff1b; 【1】安装mysql8 docker容器 步骤1&#xff0c; 查看mysql8 docker镜像版本 &#xff1b; 最简单的方式是上 Docker Hubhttps://hub.docker.com/直接搜索mysql&#xff0c;查看其 …

Java命令学习系列(一)——Jps

转载自 Java命令学习系列&#xff08;一&#xff09;——Jpsjps位于jdk的bin目录下&#xff0c;其作用是显示当前系统的java进程情况&#xff0c;及其id号。 jps相当于Solaris进程工具ps。不象"pgrep java"或"ps -ef grep java"&#xff0c;jps并不使用应用…

springboot2.5.5配置druid数据源1.2.8与jdbc

【README】 本文记录了 springboot配置 druid数据源的步骤&#xff1b; 【1】新建springboot项目并配置druid 步骤1&#xff0c;新建springbt项目 步骤2&#xff0c;选择spring web&#xff0c;jdbc&#xff0c;mysql驱动依赖&#xff1b; 步骤3&#xff0c;添加 druid数据源…

tsc244标签编辑软件_能打小票的标签机,M110智能标签打印机来了!

每张被贴上的标签背后&#xff0c;都是对待梦想的认真、对待生活的用心&#xff0c;M110智能标签打印机为你标记美好&#xff0c;实现品质与效率兼得的追求。01、 产品简介M110智能标签打印机采取热敏无墨打印技术&#xff0c;无需碳带&#xff0c;便捷经济&#xff0c;配套“标…

面试官:简述实现一个线程池的设计思路

前言 二面碰到这个问题人都麻了&#xff0c;我扯了好多没用的&#xff0c;面后赶紧来补一下&#xff0c;但是找到的基本都是一堆纯代码&#xff0c;不是讲思路的。下面的思路是我参考美团技术团队文章后总结的。 具体思路 一、总体设计 线程池在内部实际上构建了一个生产者…

Java命令学习系列(四)——jstat

转载自 Java命令学习系列&#xff08;四&#xff09;——jstatjstat(JVM Statistics Monitoring Tool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据&#xff0c;在没有GUI图形的服务器上&…

修改打印机ip工具_使用富士施乐一体机因动态IP导致不能打印与扫描的解决方法...

背景在使用富士施乐的一体机中(其他厂商的一体机 也类似)&#xff0c;很多人的网络环境是动态IP的&#xff0c;即打印的IP地址是不固定的&#xff0c;随着每次开机或关机会变化&#xff0c;从而经常有人反应打印机不能打印或者扫描了。。总体思路1. 检查当前的IP设置2. 把相应的…

springboot2.5.5配置mybatis

【README】 1.本文记录了 springboot2.5.5 配置 mybatis的步骤&#xff1b; 2.配置mybatis 分为注解和配置两种方式&#xff1b; 3.引入mybatis&#xff0c;包括了 创建springbt项目&#xff1b;druid数据源配置&#xff1b;数据库表与javabean&#xff1b;mybatis配置与sq…

Java命令学习系列(三)——Jmap

转载自 Java命令学习系列&#xff08;三&#xff09;——Jmapjmap是JDK自带的工具软件&#xff0c;主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。可以使用jmap生成Heap Dump。在Java命令学习系列&#xff08;零&#xff09;——常见命…

skimage直方图如何保存_LightGBM的参数详解以及如何调优

lightGBM可以用来解决大多数表格数据问题的算法。有很多很棒的功能&#xff0c;并且在kaggle这种该数据比赛中会经常使用。但我一直对了解哪些参数对性能的影响最大以及我应该如何调优lightGBM参数以最大限度地利用它很感兴趣。我想我应该做一些研究&#xff0c;了解更多关于li…

基于springboot2.5.5自建启动器starter制品库

【README】 本文po出了自建springboot 启动器步骤&#xff1b; 【1】新建2个starter相关组件 根据 mybatis-spring-boot-starter&#xff0c;我们看到 自建starter需要两个组件&#xff0c;分别是 xxx-spring-boot-starter&#xff0c; xxx-spring-boot-starter-autoconfigu…

Java命令学习系列(二)——Jstack

转载自 Java命令学习系列&#xff08;二&#xff09;——Jstackjstack是java虚拟机自带的一种堆栈跟踪工具。功能 jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合&#xff0c;生成线程快照的主要目的是定位线程出…

python中ls是什么_使用Python代码实现Linux中的ls遍历目录命令的实例代码

一、写在前面 前几天在微信上看到这样一篇文章&#xff0c;链接为&#xff1a;https://www.jb51.net/it/692145.html&#xff0c;在这篇文章中&#xff0c;有这样一段话&#xff0c;吸引了我的注意&#xff1a;在 Linux 中 ls 是一个使用频率非常高的命令了&#xff0c;可选的参…

spring中stereotype注解Component,Repository,Service,Controller

【README】 本文介绍了 spring4.0 下 org.springframework.stereotype 的注解类型&#xff0c;俗称刻板型注解&#xff08;一成不变型&#xff09;&#xff1b; 包括 Component&#xff0c; Repository&#xff0c;Service&#xff0c; Controller &#xff1b; 目录 【REA…

[中级]Java命令学习系列(五)——jhat

转载自 [中级]Java命令学习系列&#xff08;五&#xff09;——jhatjhat(Java Heap Analysis Tool),是一个用来分析java的堆情况的命令。之前的文章讲到过&#xff0c;使用jmap可以生成Java堆的Dump文件。生成dump文件之后就可以用jhat命令&#xff0c;将dump文件转成html的形式…

转:IDEA 创建类注释模板和方法注释模板

转自&#xff1a; IDEA 创建类注释模板和方法注释模板 - 简书  在使用Idea的时候&#xff0c;它的注释模板很简单&#xff0c;不够详细&#xff1b;所有大多数开发者都想设置一个比较详细的注释模板&#xff0c;我现在把我了解的创建类注释模板和方法注释模板的操作记录下来…

mappedbytebuffer_Java NIO Buffer【MappedByteBuffer】概述与FileChannel的联系

“ NIO【Non-blocking IO非阻塞式IO】&#xff0c;可以让你非阻塞的使用IO&#xff0c;例如&#xff1a;当线程从通道读取数据到缓冲区时&#xff0c;线程还是可以进行其他事情。当数据被写入到缓冲区时&#xff0c;线程可以继续处理它。从缓冲区写入通道也类似&#xff0c;主要…

[初级]Java命令学习系列(六)——jinfo

转载自 [初级]Java命令学习系列&#xff08;六&#xff09;——jinfojinfo可以输出java进程、core文件或远程debug服务器的配置信息。这些配置信息包括JAVA系统参数及命令行参数,如果进程运行在64位虚拟机上&#xff0c;需要指明-J-d64参数&#xff0c;如&#xff1a;jinfo -J-…

idea 调整代码格式

1. 格式化代码时&#xff0c; 不想格式化注释&#xff0c; refer2 IDEA格式化代码时&#xff0c;不想格式化注释怎么办&#xff1f;_缘自天方的博客-CSDN博客_idea不格式化注释很简单&#xff0c;只需要把 Enable JavaDoc formatting 去掉选中状态即可。附图如下&#xff1a;h…