当前位置: 首页 > news >正文

Java—ThreadLocal底层实现原理

首先,ThreadLocal 本身并不提供存储数据的功能,当我们操作 ThreadLocal 的时候,实际上操作线程对象的一个名为 threadLocals 成员变量。这个成员变量的类型是 ThreadLocal 的一个内部类 ThreadLocalMap,它是真正用来存储数据的容器。因此,不同线程间的数据从物理上就是隔离的,所以 ThreadLocal 不需要任何同步机制也天然是线程安全的。

ThreadLocalMap 底层基于一个长度为 2 的次方的数组实现,所有的数据都会被封装为以 ThreadLocal 作为 Key 的键值对对象 Entry 存放在数组中。底层数组默认大小为 16,扩容阈值为当前容量的三分之二,每次扩容容量都翻倍。

为了提高散列效率,ThreadLocalMap 采用斐波那契散列法作为哈希算法。具体而言,当在根据 ThreadLocal 计算下标的时候,不像 HashMap 那样直接取 hashCode 方法的返回值作为哈希值,而是使用通过一个全局计数器,保证每个 ThreadLocal 实例创建的时候都采用一个特殊的魔数 0x61c88647 的倍数作为哈希值,比如第一个创建的 ThreadLocal 的哈希值为 1 * 0x61c88647,第二个则为 2 * 0x61c88647……以此类推。并且,由于 ThreadLocalMap 底层存放键值对的槽位数量总是 2 的次方,根据斐波那契散列法的特性,在这种情况下,可以大幅度降低计算得到相同下标的可能性,换而言之,就是可以减少哈希冲突发生的概率。

不过哈希冲突总是存在的,对此 ThreadLocalMap 使用线性探测的方式来解决,简单的说,就是如果发生哈希冲突,它就检查下一个槽位是否未被使用,如果未被使用就将值设置到该槽位,否则就继续向后探测,直到找到一个可用槽位为止。

最后,由于数据是直接绑定到线程上的,为了防止用户因为未及时清理数据而导致内存泄露,ThreadLocalMap 底层使用的键值对对象将其的 Key —— 也就是 ThreadLocal 本身 —— 设置为了弱引用,如此一来,当外界没有对 ThreadLocal 的强引用时,键值对的 Key 将会随着 GC 被回收,此时该数据相当于被自动标记为失效。在后续的增删改查操作时,ThreadLocalMap 将会顺带检查并清理这些失效数据

问题详解​

1. 数据结构​

1.1. ThreadLocal 与 ThreadLocalMap​

与通常的 Map 或 List 这类数据结构不同,ThreadLocal 本身并不直接存储数据,真正的数据其实直接存储在线程对象 Thread 中

public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

我们可以看到,每个 Thread 都通过 threadLocals 和 inheritableThreadLocals 两个成员变量各持有一个特殊的 ThreadLocalMap 集合,它就是实际存储数据的地方:

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private static final int INITIAL_CAPACITY = 16;// 数组大小总是2的倍数private Entry[] table;private int size = 0;private int threshold; // Default to 0
}

由于每个线程只操作其独有的数据,每个线程的数据都是彼此隔离的,因此不需要任何同步机制,ThreadLocal 也天然就是线程安全的

inheritableThreadLocals 这个变量是专门为 InheritableThreadLocal 准备的,具体可参见:✅ ThreadLocal 有哪些扩展实现?

1.2. 键值对对象 Entry​

ThreadLocalMap 和我们熟悉的 HashMap 一样,它使用数组最为最底层的数据结构,数组中的每个槽位都对应一个键值对对象 Entry ,其中 Key 就是对应的 ThreadLocal 本身,而 Value 则是要“存储”到 ThreadLocal 里的数据。

static class Entry extends WeakReference<ThreadLocal<?>> {Object value; // 存储的数据Entry(ThreadLocal<?> k, Object v) {super(k); // 对 ThreadLocal 的引用为弱引用value = v;}
}

此外,值得注意的是,Entry 继承了 WeakReference,并且将 ThreadLocal 作为弱引用,这意味着当外界对 ThreadLocal 的强引用消失后,即使该 Entry 依然在槽中存在,但是它的 Key 却已经变为了 null,这种键值对实际上是已经失效的。

在后文,我们会在 ThreadLocalMap 的增删改方法中看到对槽位中失效的键值进行清理的操作。

1.3. 为什么需将 Key 设置为弱引用?​

在理解这个问题之前,我们不妨想一下,如果 Entry 不设置为弱引用会怎么样?

以下面的代码为例:

public class static run() {ThreadLocal tl = new ThreadLocal();Object value = new Object();tl.set(value);
}

结合之前的例子,我们知道,当执行完上述代码后,当前线程将会把 tl 和 value 作为一个 Entry 对象存储在自己拥有的 ThreadLocalMap 中。

由于 tl 和 value 都间接的被当前线程对象强引用,也就是说,在当前线程对象的生命周期结束前, tl 和 value 一直都不会被回收

并且,由于我们也没有调用 remove 方法主动的让线程对象把 tl 从它拥有的 ThreadLocalMap 中移除,这样等于实质上的发生了内存泄露

而当 Entry 里面的 Key —— 也就是 ThreadLocal —— 被设置为弱引用后,哪怕用户没有及时清空数据,在 GC 的时候 JVM 也会自动回收 ThreadLocal,这等于主动标记 Entry 为失效数据,如此一来,当后续进行增删改等操作的时候,ThreadLocalMap 将会自动清除失效数据,实现内存的自动释放,减小内存泄露的可能性。

关于 ThreadLocal 与内存泄露的问题,具体可以参见:✅ ThreadLocal 什么场景内存泄露?

1.4. 为什么不选择把 Value 设置为弱引用?​

从原理来说,要确认一个 Entry 是失效的,只要有办法让 Key 或者 Value 失效就行,从这个角度上来看,把 Key 或者 Value 设置为弱引用都可以实现自动回收的效果。

不过,把 Value 而不是 Key 作为弱引用,最大的问题在于 Value 的生命周期是不确定的。比如,如果缓存的值对象恰好是 String 或者 Integer 类型,由于值本身具备缓存机制导致很难被回收,会进而导致数据迟迟无法失效,进而导致内存泄露。因此,为了避免用户使用常量或长生命周期的对象作为弱引用导致数据迟迟无法被回收,需要把 Key 而不是 Value 设置为弱引用。

2. 哈希算法​

键值对集合要实现高效的访问,就需要一个合理的哈希算法,而要理解其哈希算法的运作过程,就要理解一个值是如何添加到集合中的。

我们查看 ThreadLocalMap 的 set方法:

private void set(ThreadLocal<?> key, Object value) {// 根据桶容量对哈希取模确定下标Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);// 从指定下标开始遍历槽,如果槽位不为空:for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 1、如果槽中的 ThreadLocal 就是当前要操作的,则更新值if (k == key) {e.value = value;return;}// 2、如果槽中的 ThreadLocal 已经被回收,则更新整个键值对if (k == null) {replaceStaleEntry(key, value, i);return;}}// 如果目标槽位仍然未被使用,则直接设置一个键值对tab[i] = new Entry(key, value);int sz = ++size;// 清空一些必要的槽位,如果已用槽位仍然大于扩容阈值,则进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

2.1. 斐波那契散列法​

根据上文的代码,我们知道 ThreadLocalMap 通过 key.threadLocalHashCode & (len-1) 这段代码计算下标。这段看似平平无奇的代码其实暗藏玄机。

我们先从 ThreadLocal 哈希值的生成看起:

public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode =new AtomicInteger();// 每次创建对象时,其哈希值都比上一次递增 0x61c88647private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}}

简单的来说,ThreadLocal 的哈希值并不像 HashMap 那样,使用 Key 的 hashCode() 方法的返回值进行高低位混淆后作为哈希值,而是直接声明了一个全局的静态计数器 nextHashCode,以该计数器按特定规律生成的固定值作为哈希值。

每当创建一个 ThreadLocal 实例时,就获取当前计数值并累加 0x61c88647,简单的来说:第一个 ThreadLocal 的哈希值是 0x61c88647 * 1,而第二个是 0x61c88647 * 2…… 以此类推。

这里每次递增的魔数 0x61c88647 转为十进制是 1640531527,而 1640531527 则是整数位数(即 2^32)乘以黄金分割比例 0.68 得到的近似结果。当 ThreadLocal 底层槽位的大小 n 为 2 的次方时,key.threadLocalHashCode & (n-1) 计算将得到是 key.threadLocalHashCode 的低 n 位,换算成十进制数后得到的恰好是一个小于 n 且大概率不重合的数。

ThreadLocal 使用的这种哈希算法被称为斐波那契散列,它是一种神奇而高效的哈希算法。

有的同学看到这里可能会感觉很懵,关于为当数组长度为 2 的次方的时候,哈希值每次递增 0x61c88647 在计算下标的时候就可以得到很好的散列效果?这就是一个有意思的数学 & 计算机科学问题了,三言两语很难讲清楚,因此这里推荐直接阅读文章,虽然是英文的,不过简单机翻一下也可以看懂,感兴趣的可以了解一下 斐波那契散列 sourl.cn/8Ucdag

2.2. 如何解决哈希冲突?​

不过,即使再强大的哈希算法,要把无限的数据映射到有限的空间里,总归要面临哈希冲突问题。目前主流解决哈希冲突的方案有两种:

  • 拉链法:发生哈希冲突的元素,在同一槽位中形成链表。
  • 开放定址法:发生哈希冲突的元素,通过其他的方式转移到另一个空闲槽位。

其中,ThreadLocalMap 选择的使用开放定址法作为解决方案,而开放定址法根据二次定位的方式,又分为线性探测、随机探测与平方探测等多种具体方案,而 ThreadLocalMap 使用了其中最为直观的一种,也就是线性探测

简单的来说,当计算出下标后,如果下标对应的槽位已经被占用,ThreadLocalMap 会尝试访问下一个下标,直到找到一个可用的槽位位置

相比起 HashMap 使用的拉链法,这种解决方式实现起来更加简单,并且更加节约内存,不过当频繁发生哈希冲突时也会带来额外的性能开销。不过,考虑 ThreadLocal 本身的哈希算法十分高效,并且一个线程往往不会拥有太多的 ThreadLocal,哈希冲突的概率非常小,因此这个缺点也就不那么明显了。

3. 无效数据的清理​

在上文,我们知道,ThreadLocalMap 通过将 Key —— 也就是 ThreadLocal 本身 —— 设置为弱引用,从而实现了让数据自动失效的效果。

不过,失效不代表数据已经被移除,当 Entry 中的 Key 被回收后,Entry 实际上依然存在于槽位中。因此,ThreadLocalMap 会一些情况下被动的清理失效数据

  • 当进行增删改查操作时,会清空指定范围内的失效数据。
  • 当进行扩容操作时,会清空所有失效数据。
3.1. expungeStaleEntry​

所有的数据清理操作,最终都会调用 expungeStaleEntry来清理指定的槽位:

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 移除指定槽位上的数据tab[staleSlot].value = null;tab[staleSlot] = null;size--;// 一并向后清理,直到遇到空槽位为止Entry e;int i;for (i = nextIndex(staleSlot, len); // 下一个槽位(e = tab[i]) != null; // 如果该槽位不为空i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果数据已失效,则将其移除if (k == null) {e.value = null;tab[i] = null;size--;} else {// 如果数据未失效,则对其重新哈希调整位置int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}

我们需要注意的是,删除数据并不是直接清空指定的槽位就可以了,由于 ThreadLocalMap 使用线性探测解决哈希冲突,因此连续的不为空的槽位中的数据有可能在最开始计算得到的是同一个下标,只是因为哈希冲突才挪到了这里。

因此,在清除指定槽位后,还需要会向后遍历,在这个过程中:

  1. 如果遇到的槽位中的数据已经失效,则将其移除。
  2. 如果遇到的槽位中的数据还未失效,则对其重新哈希,并进行迁移
  3. 如果已经没有下一个槽位了,或者下一个槽位为空,则终止遍历。

在后面,我们还会在查找和更新数据的操作里面看到类似的做法,它们是思路基本都是一样的。

3.2. cleanSomeSlots​

expungeStaleEntry 方法每次只能清理一段相连的槽位,因此基于它, ThreadLocalMap 还提供了批量清理的方法 cleanSomeSlots,它通常在增删改查等常规操作中调用:

private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.get() == null) {n = len;removed = true;// 清空一段连续的槽位i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0); // 清理范围为 log(n)return removed;
}

�相比起 cleanSomeSlots,它的清理范围是从指定下标开始向后延伸 log(n)长度。

3.3. expungeStaleEntries​

�在进行扩容的时候,会调用 expungeStaleEntries 方法清空全局的无效数据:

private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];// 循环调用 expungeStaleEntry 方法if (e != null && e.get() == null)expungeStaleEntry(j);}
}

这个清理方法是最重的,因此一般只在扩容的时候调用。

4. 设置值​

在了解了 ThreadLocalMap 的数据结构,与哈希算法,还有失效数据的清理机制后,我们可以正式开始了解一个值是如何添加到 ThreadLocalMap 里面的了:

private void set(ThreadLocal<?> key, Object value) {// 确定下标Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);// 从指定下标开始遍历槽,如果槽位不为空:for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 1、如果槽中的 ThreadLocal 就是当前要操作的,则更新值if (k == key) {e.value = value;return;}// 2、如果槽中的 ThreadLocal 已经被回收,则更新整个键值对if (k == null) {replaceStaleEntry(key, value, i);return;}}// 如果目标槽位仍然未被使用,则直接设置一个键值对tab[i] = new Entry(key, value);int sz = ++size;// 清空一些必要的槽位,如果已用槽位仍然大于扩容阈值,则进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

在方法的最开始,自然是获取 ThreadLocal 的哈希值,并根据哈希算法计算下标,然后又因为线性探测的特殊性,在得到下标后,我们还需要从这个下标开始依次向后遍历每个槽位:

  1. 如果该槽位已被当前操作的 ThreadLocal 使用,则更新槽位中键值对的值;
  2. 如果该槽位已被使用,但是对应的 ThreadLocal 已经被回收,则替换该槽位中的键值对,并清空一些槽位;
  3. 如果该槽位尚未被使用,则直接创建并设置一个键值对,并终止遍历。此外,如果有必要,清理一些槽位,并视情况决定是否要扩容。

5. 扩容​

在 ThreadLocalMap 的构造函数中,我们可以知道它的默认大小是 16,扩容阈值为当前容量的 2/3,且不可更改:

// 初始容量
private static final int INITIAL_CAPACITY = 16;// 扩容阈值为容量的三分之二
private void setThreshold(int len) {threshold = len * 2 / 3;
}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}

而在真正用于扩容的则是 resize 方法中,每次扩容容量都翻倍,且每个槽位中的数据都要进行重哈希:

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新长度为旧长度的两倍int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();// 如果 Key 已经被回收,则将 Value 也置空if (k == null) {e.value = null; // Help the GC} else {// 对每个 Key 进行重哈希int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 更新扩容阈值 setThreshold(newLen);size = count;table = newTab;
}

为什么选择 2/3 作为负载系数?

这里有个额外的问题,为什么扩容阈值要是 2/3 这么一个奇怪的数值?关于这方面,笔者目前没有找到比较权威的解释,不过我们可以大致推测一下:

首先,我们都知道,由于哈希函数的散列度直接受槽位数量的影响,槽位可用率较低的时候会导致哈希冲突比较严重。

因此,哈希函数的扩容阈值必定不能过大,否则扩容前的空闲槽位较少的那段时间哈希冲突会比较严重,并且 ThreadLocalMap 采用线性探测的方式解决哈希冲突,此时相比起 HashMap 使用的拉链法会更加消耗性能,所以 ThreadLocalMap 的负载系数起码要小于 HashMap 的 0.75。 不过,如果设置的过小,又会导致槽位闲置率过高,浪费内存,因此起码得大于 0.5。综合考虑一下,2/3 就是一个比较合适的值。

6. 获取值​

我们看看 ThreadLocal 的 get 方法,整个流程大概分为三步:

  1. 先确认线程里面的 ThreadLocalMap 是否初始化,如果未初始化则进行初始化,如果已初始化则开始进行查找;
  2. 通过哈希值计算得到下标,如果下标对应的槽位为空,或者直接找到了目标数据,则直接返回,否则说明存在哈希冲突,需要进行线性探测;
  3. 从指定下标开始向后探测:
    1. 如果找到了目标数据,则中断探测,直接返回数据;
    2. 如果槽位不为空,且数据已经失效,则进行清理;
    3. 如果槽位为空或者已没有下一个可遍历的槽位,说明没有要查找的数据,直接返回空。
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果已经初始化,则获取值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 如果尚未初始化,则进行初始化return setInitialValue();
}private Entry getEntry(ThreadLocal<?> key) {// 计算下标,获取值int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// 如果能直接获取到值,或者确认没有值,直接返回if (e != null && e.get() == key)return e;// 否则说明存在哈希冲突,需要进行线性探测elsereturn getEntryAfterMiss(key, i, e);
}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// 线性探测,从指定下标开始遍历槽位,直到找到为止while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;if (k == null)// 如果发现槽位中的数据失效,则进行清理expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}

7. 数据的初始化​

这里我们关注一下 ThreadLocalMap 中数据的初始化。在最开始的时候,由于每个线程 threadLocals 和 inheritableThreadLocals 两个变量都未初始化,此时就会在 setInitialValue 方法里面创建 ThreadLocalMap 实例,并对数据进行初始化:

private T setInitialValue() {// 为 ThreadLocal 设置初始值,// 不重写 initialValue 方法的话默认都是 nullT value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}// 注册到 TerminatingThreadLocal 注册表if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;
}

简单的来说,这里先确认线程的 threadLocals 是否已经初始化,若没有则初始化一个 ThreadLocalMap,并调用 initialValue 方法获取并添加一个初始值。这里的 initialValue 方法是一个留给子类重写的钩子方法,默认返回的是一个 null。

相关文章:

Java—ThreadLocal底层实现原理

首先&#xff0c;ThreadLocal 本身并不提供存储数据的功能&#xff0c;当我们操作 ThreadLocal 的时候&#xff0c;实际上操作线程对象的一个名为 threadLocals 成员变量。这个成员变量的类型是 ThreadLocal 的一个内部类 ThreadLocalMap&#xff0c;它是真正用来存储数据的容器…...

GTSRB德国交通标志数据集下载以及训练集划分

GTSRB德国交通标志数据集下载以及训练集划分 一、数据集下载二、数据集划分 一、数据集下载 官网地址&#xff1a;附含数据集说明文档点击下载&#xff1a;训练数据集点击下载&#xff1a;测试数据集 二、数据集划分 在模型训练时&#xff0c;将训练数据集分成训练集和验证集&…...

python 实现客户端软件许可证书签名授权 cryptography

目录 1.需求 2.cryptography介绍 3.实际代码 4.结束语 1.需求 采用pyside6开发了一款客户端软件, 为保护核心算法源码, 采用Nuitka打包python代码&#xff0c;这仅仅保护了核心算法代码&#xff0c;不能限制用户使用软件&#xff0c;因此需要软件许可授权签名证书&#xff…...

明远智睿SD2351核心板:以48元撬动AI视觉产业革命的“硬核引擎”

在人工智能浪潮席卷全球的今天&#xff0c;AI视觉作为连接虚拟与现实的“智慧之眼”&#xff0c;正以惊人的速度重塑着产业格局。从智慧城市中的安防监控到自动驾驶汽车的“视觉神经”&#xff0c;从工业产线的缺陷检测到家庭场景的智能管家&#xff0c;AI视觉技术的每一次突破…...

【C语言】全局变量、静态本地变量

在C语言中&#xff0c;变量是存储数据的基本单元。 不同类型的变量有着不同的特性和用途&#xff0c;其中全局变量和本地变量是比较特殊且重要的两类变量。 一、全部变量 1.1 全局变量的作用域和生存期 全局变量是在函数外部定义的变量&#xff0c;其作用域从定义的位置开始&…...

32.768kHz晶振详解:作用、特性及与其他晶振的区别

一、32.768kHz晶振的核心作用 实时时钟&#xff08;RTC&#xff09;驱动&#xff1a; 提供精确的1Hz时钟信号&#xff0c;用于计时功能&#xff08;如电子表、计算机CMOS时钟&#xff09;。 分频公式&#xff1a; 1Hz 32.768kHz / 2^15&#xff08;通过15级二分频实现&#x…...

classfinal 修改过源码,支持jdk17 + spring boot 3.2.8

先贴图 使用 classfinal 修改过源码 支持jdk17 spring boot 3.3.0 使用方式&#xff1a; 1、springboot的jar加密 java -jar classfinal-fatjar-1.2.1.jar -file MySpringBoot.jar -libjars my-common.jar -packages cn.com.cmd -pwd 123456 -Y 得到&#xff1a; MySpri…...

算法训练营 Day1

努力追上那个曾经被寄予厚望的自己 —— 25.4.25 一、LeetCode_26 删除有序数组中的重复项 给你⼀个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现⼀次 &#xff0c;返回删除后数组的 新⻓度。元素的 相对顺序 应该保持 ⼀致 …...

4/25 研0学习日志

Python学习 python 4个常用的数据容器 list dict tuple set list 列表中数据类型可以不一样 构造方式 mylist["xxx","xxxx"] 获取数据方式 mylist[1] mylist[:4] mylist[-1:] 添加数据 mylist.append() mylist.extern(["aaa","aaaa&…...

手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段

手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段 --本地AI电话机器人 前言 书接上一篇&#xff0c;《手机打电话通话时如何向对方播放录制的IVR引导词声音》中介绍了【蓝牙电话SDK示例App】可以实现手机app在电话通话过程中插播预先录制的开场白等语音片段的功能。…...

汽车零配件供应商如何通过EDI与主机厂生产采购流程结合

当前&#xff0c;全球汽车产业正经历深刻的数字化转型&#xff0c;供应链协同模式迎来全新变革。作为产业链核心环节&#xff0c;汽车零部件供应商与主机厂的高效对接已成为企业发展的战略要务。然而&#xff0c;面对主机厂日益严格的数字化采购要求&#xff0c;许多供应商在ED…...

sql server 开启cdc报事务正在执行

今天开启数据库cdc 功能的时候提示&#xff1a;一个dbrole 的存储过程&#xff0c;rolemember cdc db_ower, &#xff0c;有事务正在进行&#xff0c;执行失败。 执行多次仍然如此&#xff0c;开启cdc的存储过程是sys.sp_cdc_enable_db;查询了一下网络&#xff0c;给出的方…...

03实战篇Redis02(优惠卷秒杀、分布式锁)

3、优惠卷秒杀 3.1 -全局唯一ID 每个店铺都可以发布优惠券&#xff1a; 当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题&#xff1a; id的规律性太明显 受单表数据量的限制 场景分析&…...

ECharts 地图开发入门

一、准备工作&#xff1a;环境搭建与数据准备​ 1. 引入 ECharts 库​ ​ TypeScript 取消自动换行复制 <!-- 引入 ECharts 核心库 -->​ <script src"https://cdn.jsdelivr.net/npm/echarts5.4.0/dist/echarts.min.js"></script>​ <!-…...

机器学习基础 - 回归模型之线性回归

机器学习: 线性回归 文章目录 机器学习: 线性回归1. 线性回归1. 简介2. 线性回归如何训练?1. 损失函数2. 正规方程3. 梯度下降法4. 两种方法的比较2. 岭回归岭回归与线性回归3. Lasso 回归4. ElasticNet 回归LWR - 局部加权回归QA1. 最小二乘法估计2. 最小二乘法的几何解释3…...

《解锁LLMs from scratch:开启大语言模型的探索之旅》

《解锁LLMs from scratch:开启大语言模型的探索之旅》 GitHub - datawhalechina/llms-from-scratch-cn: 仅需Python基础,从0构建大语言模型;从0逐步构建GLM4\Llama3\RWKV6, 深入理解大模型原理 项目首页 - LLMs-from-scratch:从零开始逐步指导开发者构建自己的大型语言模型…...

嵌入式 C 语言面试核心知识点全面解析:基础语法、运算符与实战技巧

在嵌入式面试中&#xff0c;C 语言基础是重中之重。本文针对经典面试题进行详细解析&#xff0c;帮助新手系统掌握知识点&#xff0c;提升面试应对能力。 一、数据结构逻辑分类 题目 在数据结构中&#xff0c;从逻辑上可以把数据结构分为&#xff08; &#xff09;。 A、动态…...

pyqt中以鼠标所在位置为锚点缩放图片

在编写涉及到图片缩放的pyqt程序时&#xff0c;如果以鼠标为锚点缩放图片&#xff0c;图片上处于鼠标所在位置的点&#xff08;通常也是用户关注的图片上的点&#xff09;不会移动&#xff0c;更不会消失在图片显示区域之外&#xff0c;可以提高用户体验&#xff0c;是一个值得…...

登高架设作业证考试的实操项目有哪些?

登高架设作业证考试的实操项目分为 4 个科目&#xff0c;包括安全防护用品使用、作业现场安全隐患排除、安全操作技术、作业现场应急处置&#xff0c;具体内容如下&#xff1a; 科目一&#xff1a;安全防护用品使用&#xff08;K1&#xff09; 考试项目&#xff1a;安全帽、安全…...

闻性与空性:从耳根圆通到究竟解脱的禅修路径

一、闻性之不动&#xff1a;超越动静的觉性本质 在《楞严经》中&#xff0c;佛陀以钟声为喻揭示闻性的奥秘&#xff1a;钟声起时&#xff0c;闻性显现&#xff1b;钟声歇时&#xff0c;闻性不灭。此“不动”并非如磐石般凝固&#xff0c;而是指觉性本身超越生灭、来去的绝对性…...

404了怎么办快把路由给我断掉(React配置路由)

路由基础概念 什么是前端路由&#xff1f; 核心作用&#xff1a;管理单页面应用的页面切换主要功能&#xff1a; 根据URL显示对应组件 保持浏览器历史记录 实现页面间导航不刷新 React Router 包含三个主要包&#xff1a; react-router&#xff1a;核心逻辑react-router-d…...

React类组件与React Hooks写法对比

React 类组件 vs Hooks 写法对比 分类类组件&#xff08;Class Components&#xff09;函数组件 Hooks组件定义class Component extends React.Componentconst Component () > {}状态管理this.state this.setState()useState()生命周期componentDidMount, componentDidU…...

Tailwind CSS实战:快速构建定制化UI的新思路

引言 在当今快节奏的前端开发环境中&#xff0c;开发者不断寻找能够提高效率并保持灵活性的工具。Tailwind CSS作为一个功能型优先的CSS框架&#xff0c;正在改变开发者构建用户界面的方式。与Bootstrap和Material UI等传统组件库不同&#xff0c;Tailwind不提供预设组件&…...

告别手动映射:在 Spring Boot 3 中优雅集成 MapStruct

在日常的后端开发中&#xff0c;我们经常需要在不同的对象之间进行数据转换&#xff0c;例如将数据库实体&#xff08;Entity&#xff09;转换为数据传输对象&#xff08;DTO&#xff09;发送给前端&#xff0c;或者将接收到的 DTO 转换为实体进行业务处理或持久化。手动进行这…...

JavaScript学习教程,从入门到精通,Ajax数据交换格式与跨域处理(26)

Ajax数据交换格式与跨域处理 一、Ajax数据交换格式 1. XML (eXtensible Markup Language) XML是一种标记语言&#xff0c;类似于HTML但更加灵活&#xff0c;允许用户自定义标签。 特点&#xff1a; 可扩展性强结构清晰数据与表现分离文件体积相对较大 示例代码&#xff1…...

抖音IP属地跟无线网有关系吗?一文解析

在抖音等社交平台上&#xff0c;IP属地显示功能让许多用户感到好奇——为什么自己的位置信息有时准确&#xff0c;有时却显示在其他城市&#xff1f;这时&#xff0c;用户会疑惑&#xff1a;抖音IP属地跟无线网有关系吗&#xff1f;本文将详细解析‌IP属地‌和‌无线网‌的概念…...

RDK X3新玩法:超沉浸下棋机器人开发日记

一、项目介绍 产品中文名&#xff1a;超沉浸式智能移动下棋机器人 产品英文名&#xff1a;Hackathon-TTT 产品概念&#xff1a;本项目研发的下棋机器人&#xff0c;是一款能自主移动、具备语音交互并能和玩家在真实的棋盘上进行“人机博弈”的移动下棋平台&#xff0c;能够带给…...

Trae 实测:AI 助力前端开发,替代工具还远吗?

Trae 实测&#xff1a;AI 助力前端开发&#xff0c;替代工具还远吗&#xff1f; 字节上线了一款 AI 新产品&#xff0c;名叫 Trae 。这是一款 IDE 工具&#xff0c;中文意思是「集成开发环境」&#xff0c;做技术的读者对此应该很熟悉。简单理解&#xff0c;就是程序员用来写代…...

SpringCloud基于Eureka和Feign实现一个微服务系统

Spring Cloud 是基于 Spring Boot 的 ‌微服务开发全栈解决方案‌,通过集成多种开源组件,提供分布式系统构建所需的服务治理、配置管理、容错机制‌等核心能力,简化微服务架构的复杂性。其核心目标是实现服务间的高效协同与弹性伸缩,支撑企业级云原生应用开发。Spring Clou…...

nacos设置权重进行负载均衡不生效

nacos设置权重进行负载均衡不生效&#xff0c;必须在启动类下加上这个bean Beanpublic IRule nacosRule(){return new NacosRule();}如下图所示...

Flowable7.x学习笔记(十四)查看部署流程Bpmn2.0-xml

前言 Flowable 在其前端 Modeler 中&#xff0c;采用 BPMN 2.0 标准将流程中的任务、网关、事件等元素以 XML 形式存储&#xff0c;并附带图形互换&#xff08;Diagram Interchange&#xff0c;DI&#xff09;数据&#xff0c;以保证在前端与后端都能精准重建可视化流程图。 生…...

大模型应用中Agent2Agent(A2A)的应用场景,以及A2A与MCP的区别和适用场景

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下大模型应用中Agent2Agent&#xff08;A2A&#xff09;的应用场景&#xff0c;以及A2A与MCP的区别和适用场景。 文章目录 一、引言二、Agent2Agent&#xff08;A2A&#xff09;协议原理2.1 核心架构2.2 基础框架与依…...

Windows server:

企鹅裙&#xff1a;429603659 域搭建 (细节上的问题) 1.将IP地址修改为静态IP&#xff0c;搭建完后设置DNS为本身(在搭建完域服务器后设置DNS) 2.配置之前需将计算机名更改为后面题目中所要求的. 3&#xff0e;一些版本之中比如Windows Server2012之中搭建域之后重启会显示…...

Python 3.14:探索新版本的魅力与革新

在Python编程语言的不断演进中&#xff0c;Python 3.14作为又一重要里程碑&#xff0c;承载着开发者们的期待与热情&#xff0c;悄然走进了我们的视野。尽管在撰写本文时&#xff0c;Python 3.14可能尚未正式成为主流版本&#xff08;注&#xff1a;实际发布情况需根据最新信息…...

LabVIEW基于VI Server的控件引用操作

本 VI 通过展示控件引用&#xff08;Control References&#xff09;的使用&#xff0c;借助 VI Server 实现对前面板对象的编程操作。 ​ 详细说明 隐式属性节点&#xff08;Implicitly Linked Property Node&#xff09;&#xff1a;通过右键单击控件&#xff08;或其控件终…...

【不同名字的yolo的yaml文件名是什么意思】

以下是这些 YOLO 系列配置文件的详细解析&#xff0c;按版本和功能分类说明&#xff1a; 一、YOLOv3 系列 文件名核心特性适用场景yolov3.yaml原始 YOLOv3 结构&#xff0c;3 尺度预测&#xff08;13x13,26x26,52x52&#xff09;通用目标检测yolov3-spp.yaml增加 SPP&#xff…...

《100天精通Python——基础篇 2025 第3天:变量与数据类型全面解析,掌握Python核心语法》

目录 一、Python变量的定义和使用二、Python整数类型&#xff08;int&#xff09;详解三、Python小数/浮点数&#xff08;float&#xff09;类型详解四、Python复数类型(complex)详解---了解五、Python字符串详解(包含长字符串和原始字符串)5.1 处理字符串中的引号5.2 字符串的…...

精益数据分析(24/126):聚焦第一关键指标,驱动创业成功

精益数据分析&#xff08;24/126&#xff09;&#xff1a;聚焦第一关键指标&#xff0c;驱动创业成功 在创业和数据分析的探索之旅中&#xff0c;我们都在不断寻找能够助力成功的关键因素。今天&#xff0c;我依旧带着与大家共同进步的初心&#xff0c;深入解读《精益数据分析…...

【刷题Day26】Linux命令、分段分页和中断(浅)

说下你常用的 Linux 命令&#xff1f; 文件与目录操作&#xff1a; ls&#xff1a;列出当前目录的文件和子目录&#xff0c;常用参数如-l&#xff08;详细信息&#xff09;、-a&#xff08;包括隐藏文件&#xff09;cd&#xff1a;切换目录&#xff0c;用于在文件系统中导航m…...

互联网实验室的质量管控痛点 质检LIMS系统在互联网企业的应用

在互联网行业流量红利消退与用户体验至上的时代背景下&#xff0c;产品迭代速度与质量稳定性成为企业核心竞争力的分水岭。传统测试实验室依赖人工操作、碎片化工具与线下沟通的管理模式&#xff0c;已难以应对敏捷开发、多端适配、数据安全等复合型挑战。 一、互联网实验室的…...

VScode远程连接服务器(免密登录)

一、本机生成密钥对 本地终端输入ssh-keygen&#xff0c;生成公钥&#xff08;id_rsa.pub&#xff09;和私钥&#xff08;id_rsa&#xff09; 二、在远程服务器根目录的.ssh文件夹的authorized_keys中输入id_rsa的内容 三、修改vscode的config文件.ssh/config&#xff0c;加…...

【Go语言】RPC 使用指南(初学者版)

RPC&#xff08;Remote Procedure Call&#xff0c;远程过程调用&#xff09;是一种计算机通信协议&#xff0c;允许程序调用另一台计算机上的子程序&#xff0c;就像调用本地程序一样。Go 语言内置了 RPC 支持&#xff0c;下面我会详细介绍如何使用。 一、基本概念 在 Go 中&…...

安卓四大组件之ContentProvider

目录 实现步骤 代码分析 onCreate insert query ContextHolder Cursor 作用与用法 基本步骤&#xff1a; 可能的面试题&#xff1a;为什么使用Cursor&#xff1f; 为什么使用Cursor 使用Cursor的好处 静态内部类实现单例模式 AnndroidManifest.xml配置信息 注释的…...

C#中实现XML解析器

XML&#xff08;可扩展标记语言&#xff09;是一种广泛用于存储和传输数据的格式&#xff0c;因其具有良好的可读性和可扩展性&#xff0c;在许多领域都有应用。 实现思路&#xff1a; 词法分析 词法分析的目的是将输入的 XML 字符串分解为一个个的词法单元&#xff0c;例如…...

神经符号混合与跨模态对齐:Manus AI如何重构多语言手写识别的技术边界

在全球化数字浪潮下,手写识别技术长期面临"巴别塔困境"——人类书写系统的多样性(从中文象形文字到阿拉伯语连写体)与个体书写风格的随机性,构成了人工智能难以逾越的双重壁垒。传统OCR技术在处理多语言手写场景时,准确率往往不足70%,特别是在医疗处方、古代文…...

TestBrain开源程序是一款集使用AI(如deepseek)大模型自动生成测试用例、和测试用例评审、RAG知识库管理的web平台系统

一、软件介绍 文末提供程序和源码下载 TestBrain开源程序是一款集使用AI(如deepseek)大模型自动生成测试用例、和测试用例评审、RAG知识库管理的web平台系统一个基于LLM的智能测试用例生成平台(功能慢慢丰富中&#xff0c;未来可能将测试相关的所有活动集成到一起)&#xff0c…...

软件工程效率优化:一个分层解耦与熵减驱动的系统框架

软件工程效率优化&#xff1a;一个分层解耦与熵减驱动的系统框架** 摘要 (Abstract) 本报告构建了一个全面、深入、分层的软件工程效率优化框架&#xff0c;旨在超越简单的技术罗列&#xff0c;从根本的价值驱动和熵减原理出发&#xff0c;系统性地探讨提升效率的策略与实践。…...

【金仓数据库征文】- 深耕国产数据库优化,筑牢用户体验新高度

目录 引言 一、性能优化&#xff1a;突破数据处理极限&#xff0c;提升运行效率 1.1 智能查询优化器&#xff1a;精准优化数据检索路径 1.2 并行处理技术&#xff1a;充分释放多核计算潜力 1.3 智能缓存机制&#xff1a;加速数据访问速度 二、稳定性提升&#xff1a;筑牢…...

前端面试常见部分问题,及高阶部分问题

面试中也极有可能让你徒手写代码,无聊的面试问题o( ̄︶ ̄)o 一、HTML/CSS 基础与进阶 常见问题 什么是语义化标签?有哪些常用语义化标签? 答案:语义化标签是指具有明确含义的 HTML 标签,如 <header>、<footer>、<article>、<section> 等。它们有…...

使用 AutoGen 与 Elasticsearch

作者&#xff1a;来自 Elastic Jeffrey Rengifo 学习如何使用 AutoGen 为你的 agent 创建一个 Elasticsearch 工具。 Elasticsearch 拥有与行业领先的生成式 AI 工具和提供商的原生集成。查看我们的网络研讨会&#xff0c;了解如何超越 RAG 基础&#xff0c;或使用 Elastic 向量…...