Java并发系列之七:ConcurrentHashMap

回顾HashMap

既然说到HashMap了,那么我们就先来简单总结一下HashMap的重点。

1.基本结构

HashMap存储的是存在映射关系的键值对,存储在被称为哈希表(数组+链表/红黑树)的数据结构中。通过计算key的hashCode值来确定键值对在数组中的位置,假如产生碰撞,则使用链表或红黑树。

需要注意的是,key最好使用不可变类型的对象,否则当对象本身产生变化,重新计算key的hashcode时会与之前的不一样,导致查找错误。

由这一点可知,在存储键值对时,我们希望的情况是尽量避免碰撞。那么如何尽量避免碰撞?核心在于元素的分布策略和动态扩容。

2.分布策略

分布策略方面的优化主要为三个方向:

HashMap底层数组的长度始终保持为2的次幂

将哈希值的高位参与运算

通过与操作来等价取模操作

3.动态扩容

动态扩容方面,由于底层数组的长度始终为2的次幂,也就是说每次扩容,长度值都会扩大一倍,数组长度length的二进制表示在高位会多出1bit。

而扩容时,该length值将 会参与位于操作来确定元素所在数组中的新位置。所以,原数组中的元素所在位置要么保持不动,要么就是移动2次幂个位置。

以上三点都是关于HashMap本身设计特点,不在本文的主要讨论范围内。如果还不太熟悉,建议先了解HashMap的原理。

但是,HashMap美中不足的是:它不是线程安全的。主要体现在两个方面:

扩容时出现著名的环形链表异常,此问题在JDK1 .8版本被解决。

并发下脏读脏写

所以,程序员们就想要一种与HashMap功能同样强大,但又能保证读写线程安全的集合容器,这就是本文的主角——ConcurrentHashMap

HashTable

有的读者可能会有这样的疑惑:既然HashMap有线程安全问题,那我每次进行get/put操作时,都用锁进行控制不就好了?太对了,HashTable就是这么做的,可以看到它的源码里简单粗暴,给put/get操作都加上了sychronized。

public synchronized V get(object key) {Entry<?,?> tab[] = table;int hash = key.hashCode() ;int index = (hash & 0x7FFFFFFF) % tab.length;for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {if ((e.hash == hash) & e.key.equals(key)) {return (V)e.value;}}return null;
}
public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings ("unchecked")Entry<K,V> entry = (Entry<K, V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) & entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry (hash, key, value, index);return null;
}

但这样会导致效率低下,在多线程环境下,当锁住map,进行读写操作时,其他想要操作的线程都会被阻塞。所以现在基本上都不再推荐使用HashTable。

有的读者可能又有疑惑了:ConcurrentHashMap难道就没有这个问题吗?它的内部应该也是用的锁吧?

ConcurrentHashMap

这就是本文要讲的重点,看看ConcurrentHashMap是如何在保证线程安全的情况下,并且做到高效的。

在JDK1.7和1.8中,ConcurrentHashMap的实现方式发生了很大的变化。有人可能觉得既然都已经更新了,那1.7就没有看的必要了。我认为读源码的目的是为了学习其中的思想,并尝试在今后的开发中进行运用。在1.7版本中,ConcurrentHashMap的分段锁思想比较经典,值得学习,本文主要从1.7版本说起。

HashTable之所以性能差是因为在每个方法上都加上了sychronized,这就相当于只用一把锁,锁住了整个数组资源。而ConcurrentHashMap用到了分段锁,每把锁只锁数组中的一段数据,这样就能大大减少锁的竞争。概念上很简单,那么具体是如何实现的呢?首先来看这么一张数据结构示意图:

 

可以看到,ConcurrentHashMap内部维护了一个segment数组,该数组的每个元素是HashEntry数组,看到HashEntry数组的这个结构是不是很熟悉,和HashMap中的哈希表如出一辙。

如果你读懂了这张图,基本上也就明白了ConcurrentHashMap是如何存储数据的,不过仅仅了解这些还不够,ConcurrentHashMap究竟通过哪些设计来保证其线程安全,我们需要进一步深挖。细节都藏在源码里。

这里再次声明一下,ConcurrentHashMap内部有很多和HashMap样的设计和技巧,一旦遇到,本文不再详细介绍。

源码

在JDK1.7版本中,ConcurrentHashMap的源码并不长,首先来看ConcurrentHashMap类的继承情况。

public class ConcurrentHashMap<K,V> extends AbstractMap<K, V>implements ConcurrentMap<K,V>,Serializable

继承情况

ConcurrentHashMap继承了AbstractMap抽象类,实现了ConcurrentMap接口。

AbstractMap类内部是一些Map通用方法的声明以及一些公共方法实现,本文不做深究。

ConcurrentMap接口中声明了四个方法,是对Map本身的增删改查,只不过要求实现类保证这些操作的线程安全,我们之后会看ConcurrentHashMap具体是如何实现的。

public interface ConcurrentMap<K, V> extends Map<K,V> {V putIfAbsent(K key, V value);boolean remove (object key, object value);boolean replace(K key, V oldValue, V newValue) ;V replace(K key, V value);
}

接下来就来看ConcurrentHashMap的内部实现。

静态变量

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0. 75f;
static final int DEFAULT_CONCURRENCY LEVEL = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
static final int RETRIES_BEFORE_LOCK = 2;

以上各个静态变量都被final修饰了,而且都是基本数据类型,所以是作为常量来使用的。结合这张结构图,一期理解各个变量的含义。

 

DEFAULT_INITIAL_CAPACITY:HashEntry数组长度的初始值

DEFAULT_LOAD_FACTOR:加载因子,决定扩容时机,和HashMap里一样

DE FAULT_CONCURRENCY_LEVEL:并发等级(后续详细介绍)

MAXIMUM_CAPACITY:HashEntry数组长度的最大值,这里为2的30次方

MIN_SEGMENT_TABLE _CAPACITY:Segment数组最小长度,这里为2

MAX_SEGMENTS:Segment数组最大长度,这里为2的16次方

RETRIES_BEFORE_LOCK:重试次数(后续介绍在哪里用到)

属性

在没有看源码前,根据上述结构图,大致可以猜到最核心的属性应该有支持范型的Segment数组(其元素为HashEntry数组)。核心内部类应该就是Segment和HashEntry。在源码中也确实如此。

final int segmentMask;
final int segmentShift;final Segment<K,V>[] segments; // Segment数组// HashEntry数组保存的KV相关信息
transient Set<K> keySet;
transient Set<Map. Entry<K,V>> entrySet;
transient Collection<V> values;

除此之外,segmentMask和segmentShift这两个属性我们暂时还不明白它们的作用,后续用

到时会详细讲到。

内部类

HashEntry

static final class HashEntry<K, V> {final int hash; final K key;volatile V value;volatile HashEntry<K, V> next;HashEntry(int hash, K key, V value, HashEntry<K, V> next) {this.hash = hash; this.key = key;this.value = value;this.next = next;}/*** Sets next field with volatile write semantics. (See above about use of * putOrderedobject. )*/final void setNext (HashEntry<K,V> n) {UNSAFE. putOrderedobject(this, nextOffset, n);}// Unsafe mechanicsstatic final sun.misc.Unsafe UNSAFE;static final long nextOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class k = HashEntry.class;nextOffset = UNSAFE.objectField0ffset(k. getDeclaredField("next");} catch (Exception e) {throw new Error(e);}}
}

HashEntry的结构不复杂,它拥有hash、key、value三个属性值,并且可以通过next引用来构建链表,整体上和HashMap中的内部类Node比较类似。但是不难发现26-35行有一段被static修饰的代码,这段代码是什么含义呢?

简单介绍一下Unsafe这个类,正如它的名字一样,Unsafe对象用于执行一些不安全的、较为底层的操作,比如直接访问系统资源。因为使用它的风险较高并且场景较少,所以我们在日常的业务代码中几乎看不到对Unsafe的使用,但是对于一些追求高效,并且有能力保证安全的下层组件来说,使用Unsafe是家常便饭。

第30行中objectField0ffset方法返回的是“指定成员属性在内存地址相对于此对象的内存地址的偏移量”,这句听上去比较拗口。在这里,使用该方法获取next属性的相对内存偏移量,然后方便在第10行中调用putOrderedObject来对next进行赋值,而putOrdered0bject下层是一个CAS调用。可以这么理解:这里直接使用Unsafe对象获取next的内存偏移量,是为了更方便地使用CAS对next进行赋值。如果这段话你不是很明白,忽略也不会影响后续理解。

整体上,HashEntry结构清晰,易于理解。下面我们来看看相对复杂一点的Segment。

Segment

继承关系

static final class Segment<K,V> extends ReentrantLock implements Serializable

Segment继承自ReentrantLock,这里分段锁的味道就体现出来了。每个Segment对象就是一把锁,一个Segment对象内部存在一个HashEntry数组,也就是说,HashEntry数组中的数据同步依赖同一把锁。不同HashEntry数组的读写互不干扰,这就形成了所谓的分段锁。

我们可以大胆猜想:假设Segment数组的长度为n,那么相较于Hashtable,理论上ConcurrentHashMap的性能就要提升n倍以上。有的读者可能会存在疑惑: ConcurrentHashMap用n把相互独立的锁替换Hashtable全局1把锁,那照理说性能提升最多也就是n倍,为什么要说n倍以上呢?

因为相较于Hashtable中使用的synchronized,ConcurrentHashMap对锁本身也做了优化。具体是怎么优化的,我们下文会讲到。

属性

static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 :1 ;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;

MAX_ SCAN_ RETRIES:指定的重试次数。在多线程进行put操作时,只有一个线程能够成功获得锁进行写操作,那么此时其他线程也不必死等,可以通过多次tryLock进行重试,并做一些其他的工作,这就体现出了效率的提升,后面会讲到。

table:之前提到的HashEntry数组

count:HashEntry数组中元素个数

modCount:HashEntry数组修改次数

threshold:触发扩容的阈值

loadFactor:负载因子

上述属性也比较易于理解,基本和HashMap中同名属性的意义一样。

方法

final V put(K key, int hash, V value, boolean onlyIfAbsent) {}
private void rehash(HashEntry<K,V> node) {}
private HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {}
private void scanAndLock(0bject key, int hash) {}
final V remove(0bject key, int hash, object value) {}
final boolean replace(K key, int hash, V oldValue, V newValue) {}
final V replace(K key, int hash, V value) {}
final void clear() {}

方法基本上就是增删改查还有扩容,由于篇幅原因这里没有展示方法体内容,在讲解到具体方法时,再来看方法内部的具体逻辑。

但有两个方法名比较陌生:scanAndLockForPut和scanAndLock。 之前我们说到这样一个常见场景:当A线程正在修改HashEntry数组(属性名为table)的某个桶,此时B线程也想要修改这个桶,但是A线程持有了独占锁,所以B线程只能等待或重试,若只是干等或不断重试,可能会是一种浪费,所以有一种优化思路就是让B线程在重试的过程中抽空去预先完成一些后续将会用到的准备工作。

scanAndLockForPut和scanAndLock方法的逻辑就实现了这种优化,下文我们会按照自顶向下的流程详解这两个方法。

方法

至此为止,开胃菜吃完了,我们已经介绍了如下内容:

ConcurrentHashMap的属性(其中segmentMask和segmentShift还未介绍)

核心内部类HashEntry和Segment (其中Segment的方法体还未详解)

接下来就是正餐,看一看ConcurrentHashMap究竟如何实现线程安全的put操作,至于get、

replace、remove操作,相较于put更加简单,篇幅原因本文不再赘述。相信你如果能够理解put的设计后,其他都能通过举一反三的方式理解。

构造方法

首先看一看ConcurrentHashMap的构造方法,这将帮助你对它有一个更加直观的认识。

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0) |I initialCapacity < 0 II concurrencyLevel <= 0) throw new IllegalArgumentException();if (concurrencyLevel > MAX_ SEGMENTS)concurrencyLevel = MAX_ SEGMENTS;// Find power-of-two sizes best matching argumentsint 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;int C = initialCapacity / ssize;if (C * ssize < initialCapacity)++c;int cap = MIN_ SEGMENT TABLE_ CAPACITY;while (cap < c)cap <<= 1;// create segments and segments [0]Segment<K, V> s0 =new Segment<K, V> (loadFactor, (int) (cap ★loadFactor),(HashEntry<K,V>[]) new HashEntry[cap]);Segment<K, V>[] ss = (Segment<K, V>[]) new Segment[ssize];UNSAFE.putOrderedobject(ss, SBASE,s0); // ordered write of segments[0]this.segments = ss;
}

三个参数initialCapacity、loadFactor、 concurrencyLevel三个值的含义在开头介绍过,这三个值一般采用默认值16、0.75、16。

1.3-4行。对参数进行简单的校验,如果不满足条件则抛出异常

2.8-13行。出现了两个局部变量sshift、ssize。 ssize就是segment数组的长度,初始值为1,当ssize小于concurrencyLevel时,sshift自增1, ssize左移1位,相当于扩大两倍。也就是说,当concurrencyLevel为16时,ssize最终也为16,如果concurrencyLevel为17,那么ssize最终为32。为什么要这么设计呢?这是为了控制segment数组的长度始终为2的次幂,为什么要控制其为2的次幂?这是为了在计算元素索引时进行优化,和HashMap中的设计方式一样。

3.14-15行。 我们假设concurrencyLevel为16,此时,sshift为4, ssize为16。 那么segmentShift为28,segmentMask为15。ssize是segment数组长度并且总是2的次幂,segmentMask为ssize减1,二进制下为1111,这被称为掩码。在这里,segmentMask的二进制序列上每一位总是1。掩码用于与key的哈希值进行位与操作来代替取模,计算出索引值,以此定位key所在的桶。这个操作是不是很熟悉,在HashMap中也是用到了相同的设计。

4.16-23行。segment数组的长度确定了,接下俩需要确定segment数组的每个元素,即HashEntry数组的长度。变量cap即为计算后的HashEntry数组长度,相同地,cap也一定是2的次幂。

5.25-30行,确定了Segment与HashEntry的相关参数,接下来进行初始化。并且向Segment数组中加入了第一个元素。

万事俱备,只欠东风,准备阶段的内容都已经理解了的话。接下来我们就来看ConcurrentHashMap最经典的put操作。

Public put

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K, V>) UNSAFE.get0bject  // nonvolatile; recheck(segments,(j << SSHIFT) + SBASE)) == null) // in ensureSegments = ensureSegment(j) ;return s.put(key, hash, value, false) ;
}public V putIfAbsent(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>) UNSAFE.get0bject(segments,(j << SSHIFT) + SBASE)) == null)s = ensureSegment(j);return s.put(key,hash,value,true);
}

put操作主要有两个方法:put和putIfAbsent,不难发现除了最后一行,其他都一样。有的读者可能要说了:写的什么垃圾代码,懂不懂封装啊。(手动狗头)我理解这是为了屏蔽调用方的理解难度,这是闲话不表,继续看代码。主要就来讲put方法。

1.第5行。首先对key进行哈希,hash方法内部就是一系列的数学运算,细节这里就不介绍了。

2.第6行。接下来计算变量j的操作,首先移位,然后和掩码进行位与计算,其间的含义和HashMap中如出一辙。这里就不在赘述。

3.7-9行,通过索引j和ensureSegment方法来取出目标Segment对象。在介绍构造函数的篇幅中我们提过,构造函数中只为Segment数组第0个元素赋值。而ensureSegment内部会对 第j个元素是否存在进行判断,若不存在,则使用CAS进行初始化保证取出的s对象不为null。 ensureSegment内部的逻辑这里不深究,但是这种懒加载的思想值得学习。

Private put

该方法相对负复杂与核心。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab. length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index) ; for (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);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);elsesetEntryAt(tab, index, node);++modCount;count = c;oldvalue = null;break;}}} finally {unlock();}return oldValue;
}

先来看这几个形参:key、hash、value、onlylfAbsent。

前三个参数很易于理解,因为要插入新的HashEntry,那必定要构造该对象,这三个参数都是HashEntry的构造方法所需要的。最后一个onlylfAbsent指定插入条件,如果当前key已经存在,那么只有当onlylfAbsent为false时才会覆盖。

2-3行

首先就是一个三目运算符,终于见到了心心念念的tryLock()。因为插入是一个并发操作,这里通过ReentrantLock的性质来进行控制。如果当前线程获得了锁,那么将node置为null,等待后续的初始化;否则执行scanAndLockForPut方法。

这里有的读者可能会有疑问:为什么不直接调用ReentrantLock的lock方法,若第一时间没获得锁那就等待直到获取。我认为这样做也不是不行,但显而易见在这里等待的时间浪费了,作者Doug Lee提供了一种性能更高解决方法,也是精华所在。就是当某线程若没有第一时间获得锁,将会执行scanAndLockForPut方法,进行一些预处理工作,这样就减少了时间上的浪费,该方法我们下文会细讲。

4-22行

我们来看如果线程tryLock获得了锁的情况。首先计算index (length -1就是获得掩码)。我们知道,HashEntry数组table中每个元素都可能是HashEntry链表。entryAt通过index拿到HashEntry链表的头节点。接下来通过头节点去遍历链表,如果发现key已存在,则根据onlylfAbsent值判断是否应该覆盖value,然后退出;如果key不存在或头结点本身就是null,那么进入else块,执行插入新节点的逻辑。

23-35行

eles块中的逻辑比较丰富,需要仔细来看。首先判断node是否为null,这个判断有点略显奇怪,node只在最开始的三目运算中操作过,若当前线程抢到锁,那么node为null,而这里既然判断node是否为null,是不是也就是说若线程没抢到锁,执行scanAndLockForPut的结果,就是初始化node。这个可能性很大,我们暂不去证实,等到具体看scanAndLockForPut方法时再确定。

若node还是null,那么进行初始化,并插入链表,看到参数中有first,大致能猜到使用了头插法,看看HashEntry的构造方法中的逻辑,确认了一下果然。

HashEntry(int hash, K key, V value, HashEntry<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;
}

接下来判断HashEntry数组是否需要扩容,如果不需要扩容,那么将node放入HashEntry数组的相应位置,这里需要注意的是,我们刚才说将node插入链表使用的是头插法,所以这里node已经成为了头结点。最终释放锁。

该方法因为一开始就已经用锁进行了控制,所以内部不会出现并发问题,理解上应该不难。

scanAndLockForPut

接下来就是上文一直提到的scanAndLockForPut方法,在进入put方法时,如果当前线程没有拿到锁,那么将会面临什么命运呢?接下去看。

private HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {HashEntry<K,V> first = entryForHash(this,hash);HashEntry<K,V> e = first;HashEntry<K,V> node = null;int retries = -1; // negative while locating nodewhile (!tryLock()) {HashEntry<K,V> f; // to recheck first belowif (retries < 0) {if (e == null) {if (node == null) // speculatively create nodenode = new HashEntry<K, V> (hash, key, value, null);retries = 0;} else if (key.equals(e.key))retries = 0;elsee = 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 changedretries = -1;}}return node;
}

上文我们已经做出了一个大胆的猜想:scanAndLockForPut中,线程预创建了node,并插入了链表。

反应比较快的读者在这里可能会产生如下疑惑:如果scanAndLockForPut方法中根据key、value、hash预创建了node,但是后续返回的put方法中有逻辑会对当前key是否存在进行判断,如果已经存在,那么根本用不到这个预先创建的node,这样不是就浪费了,或者说根本没有预先创建的必要吗?

如果能产生这个疑问,那么说明已经完全理解了。但实际上的预创建的逻辑要更加高明一些,在创建过程中会对key是否存在进行判断。

这是一种预创建的思想,当一部分线程无事可干的时候,不要让它们干等,而是让它们去做一些可以预先完成的任务。以后在工作中遇到类似的场景时,完全可以借鉴这种思想。

在阅读该方法的逻辑时,我们需要知道其中逻辑都是处于并发状态下,因为获取锁的线程可能正在修改链表(增、删、改)。

6行

可以看到方法在第6行进入了一个while循环,当tryLock()为false,也就是说若当前线程未获得锁时,将会不断执行。

8-16行

retries值的初始值为-1,也就是说循环第一轮一定会进入这块逻辑。若e为null,也就是说链表的头结点为null,那么进入初始化node的逻辑;如果e不为null,那么遍历链表,一旦发现链表中存在相同key的node,就将retries置为0,表示不再进入这段可能初始化node的逻辑。

17-19行

若retries已经被置为>=0,说明因为存在相同key不需要创建node,或者node已经创建好了。这里就对retries开始自增,相当于自选,如果超过自选次数后还未获得锁,那么调用lock(),老老实实排队。

20-23行

如果当前retries&1== 0,这是个什么操作?也就是说retries每自增两次,将会出现一次retries&1 == 0。且此时出现了hash值一样的key的话,那么将会再次遍历链表检查是否需要创建node(因为也有可能目标key所在node已经被其他线程删除了)。以此往复,直到获取锁或retries超过阈值。

这个方法因为出现在了并发环境下,所以需要考虑的情况比较多,有兴趣的读者不妨画出流程图来仔细品一品。

扩容

最后我们再提一下扩容,ConcurrentHashMap中的扩容仅针对HashEntry数组,Segment数组在初始化后无法再扩容。

源码中我们也看到,在调用put操作时,会对是否需要rehash进行检查。扩容本身是很重要的知识点,但是由于HashEntry数组的扩容和HashMap中基本一样,所以就不赘述了。不同的是,HashEntry数组的扩容操作已经被外层put方法中获取的锁保护起来了,所以能保证线程安全。

本文讲解了JDK1.7版本的ConcurrentHashMap内部实现,相较于HashMap,它实现了线程安全的读写与扩容。相较于HashTable,它采用分段锁,通过并发等级这个参数来控制并发程度,提高了N倍的读写效率。ConcurrentHashMap的核心在于Segment和HashEntry的实现。此外,ConcurrentHashMap在进行put操作时,采用了一种“预先创建”的思想来进行优化,这是常常被人忽视,但是却很有趣的设计。

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

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

相关文章

【机器学习】西瓜书学习心得及课后习题参考答案—第5章神经网络

笔记心得 5.1神经元模型——这是神经网络中最基本的成分。 5.2感知机与多层网络——由简单的感知机循序渐进引出多层前馈神经网络。 5.3误差逆传播算法——BP算法&#xff0c;迄今最成功的神经网络学习算法。算法如下&#xff08;公式参考西瓜书&#xff09; 停止条件与缓解…

Laravel 框架路由参数.重定向.视图回退.当前路由.单行为 ②

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; THINK PHP &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

Redis压缩列表

区分一下 3.2之前 Redis中的List有两种编码格式 一个是LINKEDLIST 一个是ZIPLIST 这个ZIPLIST就是压缩列表 3.2之后来了一个QUICKLIST QUICKLIST是ZIPLIST和LINKEDLIST的结合体 也就是说Redis中没有ZIPLIST和LINKEDLIST了 然后在Redis5.0引入了LISTPACK用来替换QUiCKLIST中的…

【C++】深入浅出STL之vector类

文章篇幅较长&#xff0c;越3万余字&#xff0c;建议电脑端访问 文章目录 一、前言二、vector的介绍及使用1、vector的介绍2、常用接口细述1&#xff09;vector类对象的默认成员函数① 构造函数② 拷贝构造③ 赋值重载 2&#xff09;vector类对象的访问及遍历操作① operator[]…

学习左耳听风栏目90天——第一天 1-90(学习左耳朵耗子的工匠精神,对技术的热爱)【洞悉技术的本质,享受科技的乐趣】

洞悉技术的本质&#xff0c;享受科技的乐趣 第一篇&#xff0c;我的感受就是 耗叔是一个热爱技术&#xff0c;可以通过代码找到快乐的技术人。 作为it从业者&#xff0c;我们如何可以通过代码找到快乐呢&#xff1f;这是一个问题&#xff1f; 至少目前&#xff0c;我还没有这种…

Qt之C++

Qt之C 类的定义 C语言的灵魂是指针 C的灵魂是类&#xff0c;类可以看出C语言结构体的升级版&#xff0c;类的成员可以是变量&#xff0c;也可是函数。 class Box { public://确定类成员的访问属性double length;//长double breadth;//宽度double heigth;//高度 };定义对象 …

TestNG中实现多线程并行,提速用例的执行时间

TestNG是一个开源自动化测试工具&#xff0c;TestNG源于Junit&#xff0c;最初用来做单元测试&#xff0c;可支持异常测试&#xff0c;忽略测试&#xff0c;超时测试&#xff0c;参数化测试和依赖测试。 除了单元测试&#xff0c;TestNG的强大功能让他在接口和UI自动化中也占有…

UE4 Cesium 学习笔记

Cesium中CesiumGeoreference的原点Orgin&#xff0c;设置到新的位置上过后&#xff0c;将FloatingPawn的Translation全改为0&#xff0c;才能到对应的目标点上去 在该位置可以修改整体建筑的材质 防止刚运行的时候&#xff0c;人物就掉下场景之下&#xff0c;controller控制的…

导出LLaMA等LLM模型为onnx

通过onnx模型可以在支持onnx推理的推理引擎上进行推理&#xff0c;从而可以将LLM部署在更加广泛的平台上面。此外还可以具有避免pytorch依赖&#xff0c;获得更好的性能等优势。 这篇博客&#xff08;大模型LLaMa及周边项目&#xff08;二&#xff09; - 知乎&#xff09;进行…

堆的模板:使用一维数组实现小根堆,实现删除操作和堆顶元素输出功能

一、链接 838. 堆排序 二、题目 输入一个长度为 nn 的整数数列&#xff0c;从小到大输出前 mm 小的数。 输入格式 第一行包含整数 nn 和 mm。 第二行包含 nn 个整数&#xff0c;表示整数数列。 输出格式 共一行&#xff0c;包含 mm 个整数&#xff0c;表示整数数列中前…

外国(境外)机构在中国境内提供金融信息服务许可8家名单

6月30日&#xff0c;国家互联网信息办公室公布8家外国&#xff08;境外&#xff09;机构在中国境内提供金融信息服务许可名单&#xff0c;如下&#xff1a;

【云原生】使用kubeadm搭建K8S

目录 一、Kubeadm搭建K8S1.1环境准备1.2所有节点安装docker1.3所有节点安装kubeadm&#xff0c;kubelet和kubectl1.4部署K8S集群1.5所有节点部署网络插件flannel 二、部署 Dashboard 一、Kubeadm搭建K8S 1.1环境准备 服务器IP配置master&#xff08;2C/4G&#xff0c;cpu核心…

开源元数据管理平台Datahub最新版本0.10.5——安装部署手册(附离线安装包)

大家好&#xff0c;我是独孤风。 开源元数据管理平台Datahub近期得到了飞速的发展。已经更新到了0.10.5的版本&#xff0c;来咨询我的小伙伴也越来越多&#xff0c;特别是安装过程有很多问题。本文经过和群里大伙伴的共同讨论&#xff0c;总结出安装部署Datahub最新版本的部署手…

【Vue】Parsing error: No Babel config file detected for ... vue

报错 Parsing error: No Babel config file detected for E:\Study\Vue网站\实现防篡改的水印\demo02\src\App.vue. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files.             …

fishing之第三篇邮件服务器搭建

文章目录 EwoMail 邮件服务器搭建一、前期准备二、安装EwoMail三、添加解析四、ewomail的后台管理系统五、WebMail(web邮件系统)六、收发邮件测试免责声明EwoMail 邮件服务器搭建 一、前期准备 1、云服务-CentOS服务器 2、购买域名 CentOS服务器43.x.x.x.域名abcxxx.xxx.c…

Vue day01

Vue 1.简介&#xff1a; ​ Vue是一套用于构建用户界面的渐进式框架。与其他大型框架不同的是&#xff0c;Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层&#xff0c;不仅容易上手&#xff0c;还便于与第三方库或既有项目整合。另一方面&#xff0c;当与现代化的工…

MySQL — 存储引擎

文章目录 存储引擎存储引擎类型InnoDBMyISAMMEMORY 存储引擎是数据库的核心&#xff0c;对于mysql来说&#xff0c;存储引擎是以插件的形式运行的。虽然mysql支持种类繁多的存储引擎&#xff0c;但是常用的就那么几种。这篇文章主要是对其进行简单的介绍。 存储引擎 MySQL可插…

20天学会rust(一)和rust say hi

关注我&#xff0c;学习Rust不迷路 工欲善其事&#xff0c;必先利其器。第一节我们先来配置rust需要的环境和安装趁手的工具&#xff0c;然后写一个简单的小程序。 安装 Rust环境 Rust 官方有提供一个叫做 rustup 的工具&#xff0c;专门用于 rust 版本的管理&#xff0c;网…

xcode 的app工程与ffmpeg 4.4版本的静态库联调,ffmpeg内下的断点无法暂停。

先阐述一下我的业务场景&#xff0c;我有一个iOS的app sdk项目&#xff0c;下面简称 A &#xff0c;以及运行 A 的 app 项目&#xff0c;简称 A demo 。 引用关系为 A demo 引用了 A &#xff0c;而 A 引用了 ffmpeg 的静态库&#xff08;.a文件&#xff09;。此时业务出现了 b…

jmeter中json提取器,获取多个值,并通过beanshell组成数组

jmeter中json提取器介绍 特别说明&#xff1a;**Compute concatenation var(suffix_ALL)&#x1f617;*如果找到许多结果&#xff0c;则插件将使用’ &#xff0c; 分隔符将它们连接起来&#xff0c;并将其存储在名为 _ALL的var中 json提取器调试 在查看结果树中选择JSON Pat…