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。

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

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

相关文章

Elasticsearch(ES)中的脚本(Script)

文章目录 一. 脚本是什么&#xff1f;1. lang&#xff08;脚本语言&#xff09;2. source&#xff08;脚本代码&#xff09;3. params&#xff08;参数&#xff09;4. id&#xff08;存储脚本的标识符&#xff09;5. stored&#xff08;是否为存储脚本&#xff09;6. script 的…

客户联络中心能力与客户匹配方式

在数字化时代&#xff0c;客户联络中心作为企业与客户沟通的核心枢纽&#xff0c;其服务能力与客户需求的精准匹配至关重要。随着客户期望的不断提升&#xff0c;传统的“一刀切”服务模式已难以满足个性化需求&#xff0c;如何通过智能化的手段实现服务能力与客户的高效匹配&a…

深入理解网络原理:UDP协议详解

在计算机网络中&#xff0c;数据的传输是通过各种协议实现的&#xff0c;其中用户数据报协议&#xff08;UDP&#xff0c;User Datagram Protocol&#xff09;作为一种重要的传输层协议&#xff0c;广泛应用于实时通信、视频流、在线游戏等场景。本文将深入探讨UDP协议的特性、…

vscode切换Python环境

跑深度学习项目通常需要切换python环境&#xff0c;下面介绍如何在vscode切换python环境&#xff1a; 1.点击vscode界面左上角 2.在弹出框选择对应kernel

【MCP Node.js SDK 全栈进阶指南】中级篇(4):MCP错误处理与日志系统

前言 随着MCP应用的规模和复杂性增长,错误处理与日志系统的重要性也日益凸显。一个健壮的错误处理策略和高效的日志系统不仅可以帮助开发者快速定位和解决问题,还能提高应用的可靠性和可维护性。本文作为中级篇的第四篇,将深入探讨MCP TypeScript-SDK中的错误处理与日志系统…

【Qt】文件

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Qt 目录 一&#xff1a;&#x1f525; Qt 文件概述 二&#xff1a;&#x1f525; 输入输出设备类 三&#xff1a;&#x1f525; 文件读写类 四&#xff1a;&#x1f525; 文件和目录信息类 五&…

代码随想录算法训练营第五十八天 | 1.拓扑排序精讲 2.dijkstra(朴素版)精讲 卡码网117.网站构建 卡码网47.参加科学大会

1.拓扑排序精讲 题目链接&#xff1a;117. 软件构建 文章讲解&#xff1a;代码随想录 思路&#xff1a; 把有向无环图进行线性排序的算法都可以叫做拓扑排序。 实现拓扑排序的算法有两种&#xff1a;卡恩算法&#xff08;BFS&#xff09;和DFS&#xff0c;以下BFS的实现思…

Qt实现语言切换的完整方案

在Qt中实现语言动态切换需要以下几个关键步骤&#xff0c;我将提供一个完整的实现方案&#xff1a; 一、准备工作 在代码中使用tr()标记所有需要翻译的字符串 cpp button->setText(tr("Submit")); 创建翻译文件 在.pro文件中添加&#xff1a; qmake TRANSLATION…

面试中被问到mybatis与jdbc有什么区别怎么办

1. 核心区别 维度JDBCMyBatis抽象层级底层API&#xff0c;直接操作数据库高层持久层框架&#xff0c;封装JDBC细节代码量需要手动编写大量样板代码&#xff08;连接、异常处理等&#xff09;通过配置和映射减少冗余代码SQL管理SQL嵌入Java代码&#xff0c;维护困难SQL与Java代…

用于协同显著目标检测的小组协作学习 2021 GCoNet(总结)

摘要 一 介绍 问题一&#xff1a;以往的研究尝试利用相关图像之间的一致性&#xff0c;通过探索不同的共享线索[12, 13, 14]或语义连接[15, 16, 17]&#xff0c;来助力图像组内的共同显著目标检测&#xff08;CoSOD&#xff09;&#xff0c;什么意思&#xff1f; 一方面是探…

OpenCV 图形API(62)特征检测-----在图像中查找最显著的角点函数goodFeaturesToTrack()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 确定图像上的强角点。 该函数在图像或指定的图像区域内找到最显著的角点&#xff0c;如文献[240]中所述。 函数使用 cornerMinEigenVal 或 cor…

MySQL引擎分类与选择、SQL更新底层实现、分库分表、读写分离、主从复制 - 面试实战

MySQL引擎分类与选择、SQL更新底层实现、分库分表、读写分离、主从复制 - 面试实战 故事背景&#xff1a; 今天&#xff0c;我们模拟一场互联网大厂Java求职者的面试场景。面试官将针对MySQL的核心技术点进行提问&#xff0c;涵盖MySQL引擎分类与选择、SQL更新底层实现、分库…

如何确保微型导轨的质量稳定?

微型导轨在精密机械中扮演着至关重要的角色&#xff0c;它们不仅影响设备的性能&#xff0c;还决定了产品的寿命。那么&#xff0c;如何通过一些关键步骤来提高微型导轨的稳定性呢&#xff1f; 1、严格筛选供应商&#xff1a;选择具备高品质保证能力的供应商&#xff0c;确保原…

Golang编程拒绝类型不安全

简介 在 Go 中&#xff0c;标准库提供了多种容器类型&#xff0c;如 list、ring、heap、sync.Pool 和 sync.Map。然而&#xff0c;这些容器默认是类型不安全的&#xff0c;即它们可以接受任何类型的值&#xff0c;这可能导致运行时错误。为了提升代码的类型安全性和可维护性&am…

什么是 JSON?学习JSON有什么用?在springboot项目里如何实现JSON的序列化和反序列化?

作为一个学习Javaweb的新手&#xff0c;理解JSON的序列化和反序列化非常重要&#xff0c;因为它在现代Web开发&#xff0c;特别是Spring Boot中无处不在。 什么是 JSON&#xff1f; 首先&#xff0c;我们简单了解一下JSON (JavaScript Object Notation)。 JSON 是一种轻量级的…

iOS/Android 使用 C++ 跨平台模块时的内存与生命周期管理

在移动应用开发领域,跨平台开发已经成为一种不可忽视的趋势。随着智能手机市场的持续扩张,开发者需要同时满足iOS和Android两大主流平台的需求,而这往往意味着重复的工作量和高昂的维护成本。跨平台开发的目标在于通过一套代码库实现多平台的支持,从而降低开发成本、加速产…

【AAudio】A2dp sink创建音频轨道的源码流程分析

一、AAudio概述 AAudio 是 Android 8.0(API 级别 26)引入的 C/C++ 原生音频 API,专为需要低延迟、高性能音频处理的应用设计,尤其适用于实时音频应用(如音频合成器、音乐制作工具、游戏音效等)。 1.1 主要特点 低延迟:通过减少音频数据在内核与用户空间之间的拷贝,直…

Spring中配置 Bean 的两种方式:XML 配置 和 Java 配置类

在 Spring 框架中,配置 Bean 的方式主要有两种:XML 配置 和 Java 配置类。这两种方式都可以实现将对象注册到 Spring 容器中,并通过依赖注入进行管理。本文将详细介绍这两种配置方式的步骤,并提供相应的代码示例。 1. 使用 XML 配置的方式 步骤 创建 Spring 配置文件 创建…

海之淀攻略

家长要做的功课 家长可根据孩子情况&#xff0c;需要做好以下功课&#xff1a; 未读小学的家长&#xff1a;了解小学小升初派位初中校额到校在读小学的家长&#xff1a;了解小升初派位初中校额到校在读初中的家长&#xff1a;了解初中校额到校 越是高年级的家长&#xff0c;…

BUUCTF-[GWCTF 2019]re3

[GWCTF 2019]re3 查壳&#xff0c;64位无壳 然后进去发现主函数也比较简单&#xff0c;主要是一个长度校验&#xff0c;然后有一个mprotect函数&#xff0c;说明应该又是Smc&#xff0c;然后我们用脚本还原sub_402219函数处的代码 import idc addr0x00402219 size224 for …