JDK 1.8 HashMap扩容机制

我们首先来看利用无参构造函数创建HashMap如何扩容。首先创建一个无参构造出来的hashmap

 HashMap hashMap = new HashMap();

该构造函数源码如下:

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}

此时,该构造函数只会设置默认的加载因子,即计算阈值threshold,默认为0.75大小。此时底层的table数组是指向null的

在这里插入图片描述
接下来调用put方法,往hashmap里面存一个键值对:

hashMap.put(1, 1);

由于底层的table是指向null的,所以需要肯定进行扩容。调用put方法,该方法的源码如下:

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

该方法首先会调用hash(key),来计算我们传进来的key的hash值,我们看一下hash方法的源码:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

该方法接收一个Object类型的key,即所有类型的参数都可以接收。key分为两种情况计算:

  1. 如果key为null:计算结构为0。所以hashmap可以接收key为null的键值对,这是没问题的,因为在这里做了处理,如果key为null,哈希值为0
  2. 如果key不为null:计算结果为 (h = key.hashCode()) ^ (h >>> 16)。这里相当于 h^(h>>>16),其中h是key的hashCode方法返回值。把h与h的右移16为后进行异或操作,高16位保持不变,低16位会进行扰动。

我们重点来看一下第二种情况,key不为null时,HashMap如何优化hashCode函数,使得Hash值散列更分散。以一个具体的数为例,假设h = 123456789 。下面我用32位二进制数来表述h:

  • 原始的哈希值的二进制数:           0000 0111 0101 1011 1100 1101 0001 0101
  • 无符号右移16位后的的二进制数:0000 0000 0000 0000 0000 0111 0101 1011
  • 上面两个二进制数异或的结果:    0000 0111 0101 1011 1100 1010 0100 1110

由于是异或,即相同为0,不同为1,所以与0异或,结果就是本身。而无符号右移16位,说明高16位全是0,与高16为进行异或运算,说明结果不变,而低16位的值有0也有1,与低16位异或,结果就不一样了。

这种二进制级别的位操作能有效地分散哈希值,从而在哈希表中产生更好的键分布,减少潜在的哈希冲突。

接着往下走,调用 putVal(hash(key), key, value, false, true),putVal方法。下面是该方法的源码:

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) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

我们首先看该方法接收的参数:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

接收key的哈希值hash,传进来的 key,value键值对,onlyIfAbsent 是false,evict是true。后面两个参数不重要,重点是前面三个参数:

  1. hash 经过计算得到kye的哈希值
  2. key
  3. value

进入函数,该方法创建四个局部变量:

Node<K,V>[] tab;
Node<K,V> p;
int n
int i;

HashMap底层的数组table就是Node<K,V>类型的,即Node数组,p就是一个node节点。
定义完局部变量,会进行该方法里面的第一个if条件判断:

if ((tab = table) == null || (n = tab.length) == 0)

该语句首先将成员变量table赋值给局部变量tab:

tab = table

然后判断tab是不是为null,或tab的长度是不是为0。这两种情况都对应一个结果:当前数组放不了传进来的键值对,需要进行扩容,所以该if条件里面的代码是:

n = (tab = resize()).length;

这里出现了扩容核心方法resize(),它在putVal方法里面出现了两次,我们总结这两次出现的条件:

  1. 第一次是if ((tab = table) == null || (n = tab.length) == 0) 判断为真,即第一次调用put方法会触发扩容。
  2. 第二次是if (++size > threshold)判断为真,即hashmap里面的元素个数大于阈值。

在这里插入图片描述

不同条件扩容结果是不一样的,但都调用了resize方法,我们看resize方法是如何控制的。
以下是resize方法的源码:

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

这部分代码较长,其实可以划分为两部分,从if (oldTab != null) 处划分,上面是关于扩容后的容量计算,下面是关于扩容后元素迁移。
我们首先看第一部分:

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;
else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

首先定义了一大堆局部变量:

  • oldTab,旧表,oldTab = table
  • oldCap,旧表容量,(oldTab == null) ? 0 : oldTab.length;,因为可能是第一次构造,所以oldTab 可能为null,这里用三元表达式计算值。
  • oldThr,旧的阈值,oldThr = threshold
  • newCap,新的容量,newCap = 0
  • newThr,新的阈值,newThr = 0

然后紧跟着是一大堆判断条件:

if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;
else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
}

我们先不关注判断语句里面的执行逻辑,我们看什么情况会执行到哪个逻辑代码块里。

if (oldCap > 0) {// 这里面执行的扩容逻辑表示不是第一次put扩容,而是后面达到阈值的扩容
}
else if (oldThr > 0) {// 运行到这里面需要oldCap = 0,表示是第一次put扩容,还需要oldThr > 0,即threshold > 0// 说明这里面是带参构造函数构造的map的扩容,因为带参构造函数才计算threshold
}
else {               // zero initial threshold signifies using defaults// 这里面是无参构造函数map,第一次调用put扩容时的处理逻辑
}
if (newThr == 0) {// 这里面是带参构造函数构造的map的扩容
}

为什么带参构造函数HashMap 的 threshold 不为0?
这需要去看源码了:

HashMap<Object, Object> hashMap = new HashMap<>(17);

这里构造了一个HashMap,使用带参构造函数,传入initCapacity

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

发现调用了另外一个构造函数,两个参数,需要传入加载因子,这里传入了DEFAULT_LOAD_FACTOR默认记载因子,0.75。继续进入该方法:

public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);
}

前面部分代码在对传入的initialCapacity 值做合法性判断,注意看最后一句代码:

this.threshold = tableSizeFor(initialCapacity);

使用tableSizeFor计算出一个不小于initialCapacity的2的幂次,然后返回给threshold ,注意看,这里threshold的值已经被计算出来了,但是table还是null,所以会出现oldTab = 0, oldThr > 0 的情况。
在这里插入图片描述
计算出了newCap之后,就需要新建一个数组了:

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

此时如果是第一次扩容,就可以直接return newTab,如果不是第一次扩容,即oldTab不为null,里面还有数据,需要迁移到newTab

在这里插入图片描述
所以我们重点看一下,迁移这部分代码:

 if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}
}

首先创建了一个for循环,全部的代码都在这段for循环里面:

for (int j = 0; j < oldCap; ++j)

这段for循环是变量oldTab所有槽位的元素。
定义一个局部变量,Node<K,V> e;,因为oldTab是Node类型的,e表示oldTabl每个槽位的元素。

if ((e = oldTab[j]) != null)

先让e = oldTab[j],第j个数组元素赋值给e,在判断e是否为空,如果为空,直接判断下一个槽位。如果不为空,将oldTab[j] 迁移到 newTab某个槽位处,所以这里是最关键的代码,我们看这个if里面的代码:

oldTab[j] = null;
if (e.next == null)newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}
}

首先让oldTab[j] = null; 此时e是执行这个位置原本指向的节点的
在这里插入图片描述
紧接着就是几个if-else将e指向的该Node节点分成了三种:

  1. e.next == null,就是单个节点,没有形成链表。
  2. e instanceof TreeNode,e不是单个节点,而是树的头节点
  3. else,e是链表的头节点

前面两种情况都很简单:

if (e.next == null)newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

对于第一种单个节点的情况,如源码所示,会用e的hash值与新容量-1的值进行与运算,即将e的hash值映射到newTab的索引上,直接然后newTab[计算出来的新索引] 指向该 Node节点。
对于第二种情况,e不是单个节点,而是树的头节点:

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

需要调用该节点的split方法,将红黑树切分为两个链表,之后进行扩容操作。
第三种情况,e是链表的头节点,处理部分的源码:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}
} while ((e = next) != null);
if (loTail != null) {loTail.next = null;newTab[j] = loHead;
}
if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;
}

首先定义了一些局部变量:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;

然后用一个do-while循环:

do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}
} while ((e = next) != null);

在这里插入图片描述

在这里插入图片描述
第三种情况看似代码多,但比较简单。第三种情况是oldTab[j]位置的元素是一条链表,迁移到新数组上怎么做的?
通过遍历这条链上的节点e,然后根据下面的条件判断:

(e.hash & oldCap) == 0

把这条链上的所有节点分成了两块,e.hash & oldCap结果等于0,e.hash & oldCap结果不等于0。
在这里插入图片描述

// 定义两个链表的头和尾节点,分别用于低位链表和高位链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next; // 用于暂存当前节点的下一个节点do {next = e.next; // 存储当前节点的下一个节点,以便迭代继续进行// 判断当前节点的哈希值与oldCap的关系,oldCap通常是数组的当前容量,用作重新哈希的分界线if ((e.hash & oldCap) == 0) {// 当前节点哈希值和oldCap按位与结果为0,表示当前节点应保持在原索引if (loTail == null)loHead = e; // 如果低位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点elseloTail.next = e; // 如果低位链表尾节点不为空,则将当前节点链接到链表尾部loTail = e; // 更新低位链表尾节点为当前节点}else {// 当前节点哈希值和oldCap按位与结果不为0,表示当前节点应移动到新索引位置if (hiTail == null)hiHead = e; // 如果高位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点elsehiTail.next = e; // 如果高位链表尾节点不为空,则将当前节点链接到链表尾部hiTail = e; // 更新高位链表尾节点为当前节点}
} while ((e = next) != null); // 迭代直到链表末尾// 处理完所有节点后,设置链表尾部节点的next指针为null,表示链表结束
if (loTail != null) {loTail.next = null; // 低位链表尾节点next设为nullnewTab[j] = loHead; // 将低位链表头节点放入新表的对应位置
}
if (hiTail != null) {hiTail.next = null; // 高位链表尾节点next设为nullnewTab[j + oldCap] = hiHead; // 将高位链表头节点放入新表的偏移位置
}

我们在计算旧表中的节点e,在新表中的索引位置时,是用e.hash & (newCap - 1)计算的,为什么这里不这样计算?
HashMap的扩容过程中,节点在新表中的索引位置通常是通过e.hash & (newCap - 1)来计算的。这种计算方法确保了节点根据其哈希值被均匀分布到新的哈希表中。然而,在你提到的代码片段中,使用的是e.hash & oldCap,这里有一个特定的原因和上下文:

背景
在Java的HashMap中,容量(cap)始终是2的幂次方(例如,16, 32, 64等)。这样设计的主要原因是利用位运算来替代模运算,从而提高计算效率。当cap是2的幂次方时,newCap就是oldCap * 2

为什么使用e.hash & oldCap

  1. 掩码计算: 当使用e.hash & oldCap时,我们实际上是在利用oldCap作为一个掩码。在扩容操作中,oldCap是原哈希表的大小,而newCap是新哈希表的大小,且newCap = 2 * oldCap。在这种情况下,oldCap的值实际上是一个比新容量少一位的值(如,如果oldCap = 16,则oldCap - 1 = 15,二进制为1111)。

  2. 决定节点的分布:

    • 低位桶: 如果e.hash & oldCap的结果是0,这意味着在oldCap位上e.hash是0,因此e.hash的二进制表示中,从最低位到oldCap位的部分没有变化。这表明,节点在新表中的索引位置与在旧表中的位置相同,即index
    • 高位桶: 如果结果不是0,意味着在oldCap的位置e.hash是1,这表明节点在新表中的索引位置是它在旧表中的位置加上oldCap,即index + oldCap

总结
通过这种方式,split操作不需要对每个节点重新计算其在新表中的完整位置,而只需要决定它是留在原位置还是移动到原位置加上旧容量的位置。这简化了计算过程,并利用了已有的哈希值和位运算的性质,达到既快速又高效的重新分配节点的目的。

这种方法不仅保证了正确性,而且也使得扩容过程中的节点重新分配更为高效。

下面用一个具体的例子进行解释:

我们来通过一个具体的例子来解释这个过程。假设我们有一个HashMap,它的原始容量(oldCap)是16,这意味着它可以有从0到15的索引。现在这个HashMap需要扩容,新的容量(newCap)将会是32(即16的两倍)。

哈希表的容量和二进制表示

  • 旧容量 (oldCap): 16,二进制表示为 0001 0000
  • 新容量 (newCap): 32,二进制表示为 0010 0000

索引位置的决定
假设我们有一个键的哈希值为34,我们想确定它在旧表和新表中的位置。

  • 哈希值: 34,二进制表示为 0010 0010

1. 旧表中的位置
在旧表中,索引是通过hash & (oldCap - 1)得到的:

  • oldCap - 1 = 15,二进制表示为 0000 1111
  • 34 & 15 = 0010 0010 & 0000 1111 = 0000 0010 = 2
  • 因此,哈希值为34的键在旧表中的索引是2。

2. 新表中的位置
我们可以通过hash & (newCap - 1)计算新索引,但现在我们关心的是如何使用hash & oldCap来决定索引位置变化:

  • oldCap本身为16,二进制表示为0001 0000
  • 34 & 16 = 0010 0010 & 0001 0000 = 0000 0000
    • 因为结果是0,表示在oldCap位置上的位是0,这意味着哈希值为34的键在新表中的索引仍然是2(即保留在原位置,低位桶)。

如果哈希值是例如50,我们来看看它的处理:

  • 哈希值: 50,二进制表示为 0011 0010
  • 50 & 16 = 0011 0010 & 0001 0000 = 0001 0000
    • 因为结果不是0,表示在oldCap位置上的位是1,这意味着哈希值为50的键在新表中的索引是它原来的索引(旧表中的索引为2)加上oldCap(16),也就是18(即移动到新位置,高位桶)。

总结
这种通过hash & oldCap来判断元素应该留在原位置还是移动到新位置的方法是高效的,因为它利用了已经计算的哈希值,并且只涉及简单的位运算。这样可以在扩容时快速重新分配元素,而无需重新计算每个元素的哈希值。
在这里插入图片描述
最后这个split函数的源码如下:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}if (loHead != null) {if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {tab[index] = loHead;if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}
}

逻辑上和链条拆分类似,不过涉及到树的退化,有时间我在继续写吧。。。。

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

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

相关文章

linux笔记5--shell命令2

文章目录 一. linux中的任务管理1. 图形界面2. 命令① top命令② grep命令③ ps命令补充&#xff1a; ④ kill命令图形界面杀死进程 二. 挂载(硬盘方面最重要的一个知识点)1. 什么是挂载2. 关于挂载目录① Windows② linux查看硬件分区情况(/dev下)&#xff1a;更改挂载目录结束…

揭秘 HTTP 代理:增强在线活动的安全性和匿名性

HTTP 代理在保护您的在线隐私、增强安全性以及允许访问受限内容方面发挥着关键作用。了解 HTTP 代理的工作原理以及如何有效地利用它们可以让您掌控自己的在线状态和浏览体验。让我们深入研究 HTTP 代理的世界&#xff0c;探索它们的优势、应用程序以及最大化其效用的最佳实践。…

【Unity Shader入门精要 第6章】基础光照(二)

1. 获取环境光 unity shader中可以通过 UNITY_LIGHTMODEL_AMBIENT获取当前环境光颜色信息。 fixed4 frag(v2f i) : SV_Target {return UNITY_LIGHTMODEL_AMBIENT; }2. 漫反射 2.1 兰伯特模型 创建Chapter_6_Diffuse_Lambert作为测试材质创建Chapter_6_Diffuse_Lambert作为测…

ollama api只能局域网访问,该怎么破?

安装ollama: ollama离线安装,在CPU运行它所支持的那些量化的模型-CSDN博客文章浏览阅读178次,点赞2次,收藏6次。ollama离线安装,在CPU运行它所支持的哪些量化的模型 总共需要修改两个点,第一:Ollama下载地址;第二:Ollama安装包存放目录。第二处修改,修改ollama安装目…

洛谷官方提单——【入门4】数组——python

洛谷官方提单——【入门4】数组 小鱼比可爱题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示代码 小鱼的数字游戏题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示数据规模与约定 代码 【深基5.例3】冰雹猜想题目描述输入格式输出格式样例 #1样例输入 …

matlab打开文件对话框

在使用matlab GUI制作时&#xff0c;为了便于用户交互使用&#xff0c;经常设置文件打开对话框&#xff0c;让用户根据实际需要选择打开的文件。下面以打开一张图片为例&#xff0c;matlab代码如下&#xff1a; [temp_filepath,temp_filename]uigetfile(*.jpg,请选择要打开的图…

从心理学角度看,GPT 对人有什么影响?

开启个性化AI体验&#xff1a;深入了解GPT的无限可能 导言 GPT 与我们日常生活的融合标志着技术进步的重大飞跃&#xff0c;为提高效率和创新提供了前所未有的机遇。然而&#xff0c;当我们与这些智能系统日益紧密地交织在一起时&#xff0c;探索它们对个人产生的细微的心理影响…

电子杂志制作攻略,轻松打造高质量数字出版物

随着数字科技的飞速发展&#xff0c;电子杂志作为一种新型的数字出版物&#xff0c;已经越来越受到人们的青睐。它不仅具有丰富的内容、多样的形式&#xff0c;还具有便捷的传播和阅读方式。如今&#xff0c;电子杂志已经逐渐成为企业、媒体和个人展示自身品牌、传播信息的重要…

下载驱动包提示 通常不会下载 未验证的下载

打开设置 添加允许站点 如果还是下载不了&#xff0c;那只能换资源了

发那科数控机床远程监控数据上云

发那科数控机床远程监控数据上云 在当今数字化、网络化的工业生产环境中&#xff0c;发那科数控机床作为全球领先的高端制造设备之一&#xff0c;其远程监控数据上云已成为实现智能制造、提升生产效率和优化资源管理的重要手段。本文将深入探讨发那科数控机床远程监控数据上云…

ACE框架学习4

目录 ACE Proactor框架 异步I/O工厂类 ACE_Handler类 前摄式Acceptor-Connector类 ACE_Proactor类 ACE Streams框架 ACE_Model类 ACE_Streams类 ACE Proactor框架 ACE Proactor框架实现了proactor模式&#xff0c;也就是异步网络模式&#xff0c;允许事件驱动…

axios异步操作第一篇

1 同步请求和异步请求 客户端给java后台程序发送请求&#xff0c;发送请求的方式有两种&#xff1a; 同步请求 同步请求发送方式&#xff1a; 1 浏览器地址栏&#xff0c;输入url&#xff1a;http://localhost:8080/web-app/xxxServlet 2 3 删除 4 javascript:location.hr…

运营商的mpls专线

在当今高速发展的数字化时代&#xff0c;网络已成为企业发展不可或缺的基础设施。作为企业网络 连接的重要组成部分&#xff0c;MPLS专线在运营商的推动下逐渐成为了企业选择的首选。 MPLS&#xff08;Multi-Protocol Label Switching&#xff09;是一种基于标签的交换技术&am…

探索渲染农场的高性能奥秘

在当今数字化的时代&#xff0c;渲染农场正逐渐成为许多行业不可或缺的强大工具。那么&#xff0c;为什么我们说渲染农场是高性能的计算机系统呢&#xff1f;让我们深入剖析其中关键要点。 “渲染农场”拥有大规模的计算资源。它由众多高性能的计算机节点组成&#xff0c;这些…

理解伽马分布

伽马分布 关键词&#xff1a;Gamma Distribution 文章目录 一、说明二、Gamma 分布的基础概念2.1 Gamma 分布的物理意义2.2 对比泊松与伽马分布2.3 伽马分布参数 三、具有伽马分布的高效牙科调度四、后记 一、说明 在本文中&#xff0c;我们将探讨统计学中的基本概率分布之一…

【C++】命名空间、缺省参数、函数重载、引用

文章目录 1.认识命名空间2.命名空间的使用3.C的输入和输出4.缺省参数4.1缺省参数的概念4.2缺省参数的分类 5.函数重载6.引用6.1引用的概念6.2引用的特性6.3常引用(重点题目)6.4引用和指针的区别 1.认识命名空间 C总计63个关键字&#xff0c;C语言32个关键字 下面让我们学习一…

周进院长受邀出席2024第四届屈光手术国际论坛获多项荣誉称号!

周进院长受邀出席2024第四届屈光手术国际论坛获“全国首批EVOICL&#xff08;V5&#xff09;新技术临床应用专家”等多项荣誉称号&#xff01; 5月10-12日&#xff0c;由爱尔眼科医院集团主办、长沙爱尔眼科医院协办的2024第四届屈光手术国际论坛&#xff08;IRSS 2024&#x…

618洗地机怎么选?热门洗地机选购指南,拒绝踩雷

洗地机是一种智能化的清洁工具&#xff0c;具有超强的清洁能力&#xff0c;能轻松应对各种地面污渍&#xff0c;无论是干污还是湿污。其一键操作设计简便易上手&#xff0c;省去了传统清洁方式的繁琐步骤&#xff0c;节省了时间和精力。高端型号更配备智能感应功能&#xff0c;…

ARP中间人

文章目录 ARP中间人ARP协议介绍使用kali进行ARP毒化使用kali进行ARP中间人使用kali进行ARP钓鱼ARP攻击防御ARP总结 ARP中间人 ARP协议介绍 维基百科ARP介绍 ARP&#xff08;地址解析协议&#xff09;在网络通信中扮演着至关重要的角色&#xff0c;它通过将网络层地址&#x…

吴恩达机器学习笔记:第 10 周-17大规模机器学习(Large Scale Machine Learning)17.1-17.2

目录 第 10 周 17、 大规模机器学习(Large Scale Machine Learning)17.1 大型数据集的学习17.2 随机梯度下降法 第 10 周 17、 大规模机器学习(Large Scale Machine Learning) 17.1 大型数据集的学习 如果我们有一个低方差的模型&#xff0c;增加数据集的规模可以帮助你获得更…