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

ThreadLocal 详解

  1. ThreadLocal简介

JDK源码对ThreadLocal类的注释如下:

  1. ThreadLocal提供线程局部变量,使得每个线程都有自己的、独立初始化的变量副本

  1. ThreadLocal实例通常是类中的private static字段,用于将状态与线程相关联,如用户ID、事务ID

  1. 只要线程处于活动状态并且ThreadLocal实例是可访问的,每个线程都将持有对线程局部变量副本的隐式引用

  1. 当线程终止,线程所绑定的线程局部变量都将被垃圾回收

ThreadLocal的使用场景:

  • 保存线程上下文信息,在需要的地方进行获取

  • 实际开发中使用较少,框架中使用较多,如Spring的事务管理

  • 一般使用ThreadLocal管理数据库连接、Session会话等,保证每一个线程中使用的连接是同一个

  • 每个线程需要自己独立的实例且该实例需要在多个方法中被使用

  • 保证线程安全,避免同步操作带来的性能损耗

局限性:

  • ThreadLocal实现了线程隔离,自然也就无法解决多线程间共享对象的更新问题

下图可以增强理解:

2、ThreadLocal与Synchronized的区别

ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别:

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  1. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

3、使用

方法名

描述

public void set( T value)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public T remove()

移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC

protected T initialValue()

返回当前线程局部变量的初始值

public class ThreadLocalDemo {private ThreadLocal<String> threadLocal = new ThreadLocal<>();private String name;public static void main(String[] args) {ThreadLocalDemo demo = new ThreadLocalDemo();for (int i = 0; i < 5; i++) {new Thread(() ->{demo.setName(Thread.currentThread().getName() + "的数据");System.out.println(Thread.currentThread().getName() + " : " + demo.getName());},"线程_" + i).start();}}public String getName() {return threadLocal.get();}public void setName(String name) {threadLocal.set(name);}
}线程_0 : 线程_0的数据
线程_2 : 线程_2的数据
线程_3 : 线程_3的数据
线程_1 : 线程_1的数据
线程_4 : 线程_4的数据

4、ThreadLocal源码解析

4.1 set()

    public void set(T value) {// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);// 判断map是否存在if (map != null)map.set(this, value);else// 初始化 thradLocalMap 并赋值createMap(t, value);}/*** 获取当前线程Thread对应维护的ThreadLocalMap * 从这里可以看出,为什么说ThreadLocal 是线程本地变量来的了*/ThreadLocalMap getMap(Thread t) {return t.threadLocals;}/*** 创建当前线程Thread对应维护的ThreadLocalMap */void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

执行流程:

  1. 获取当前线程,并根据当前线程获取ThreadLocalMap

  1. 如果获取的ThreadLocalMap不为空,则将值 set 到ThreadLocalMap中(当前ThreadLocal的引用作为key)

  1. 如果ThreadLocalMap为空,则给该线程创建 ThreadLocalMap,并将第一个值存放进去

4.2 get()

    public T get() {// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);// 如果此 map 存在if (map != null) {// 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体eThreadLocalMap.Entry e = map.getEntry(this);// 对e进行判空 if (e != null) {@SuppressWarnings("unchecked")// 获取存储实体 e 对应的 value值T result = (T)e.value;return result;}}/*初始化 : 有两种情况执行下面代码第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry*/return setInitialValue();}/*** 初始化*/private T setInitialValue() {// 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回nullT value = initialValue();// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);// 判断map是否存在if (map != null)// 存在则调用set方法设置值map.set(this, value);else// 初始化 thradLocalMap 并赋值createMap(t, value);// 返回设置的值valuereturn value;}/*** 该方法是一个protected的方法,显然是为了让子类覆盖而设计的*/protected T initialValue() {return null;}

执行流程:

  1. 获取当前线程, 根据当前线程获取ThreadLocalMap

  1. 如果获取的ThreadLocalMap不为空,则在ThreadLocalMap中以ThreadLocal的引用作为key来在ThreadLocalMap中获取对应的Entry e,如果e不为null,则返回e.value。

  1. 如果获取的ThreadLocalMap 不为空,但是 Entry 为空,则通过initialValue函数获取初始值value,调用set方法设置值

  1. 如果获取的ThreadLocalMap 为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的ThreadLocalMap

4.3 remove()

     public void remove() {// 获取当前线程对象中维护的ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());// 如果此map存在if (m != null)// 以当前ThreadLocal为key删除对应的实体entrym.remove(this);}

5、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的,而Entry又是ThreadLocalMap的内部类,且集成弱引用(WeakReference)类。

    /*** Entry 的key 是一个弱引用,也就意味这可能会被垃圾回收器回收掉*/static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 初始容量,必须是2的整次幂private static final int INITIAL_CAPACITY = 16;// 存放数据的tableprivate Entry[] table;// 数组里面存放entrys的个数,用于判断table当前使用量是否超过阈值。private int size = 0;// 进行扩容的阈值,使用量大于它的时候进行扩容。private int threshold;

5.1 弱引用

弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收

Entry的Key为什么是弱引用?

如果key使用强引用:

  1. 业务代码中使用完ThreadLocal ,ThreadLocal Ref被回收了,

  1. 因为ThreadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收,

  1. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 Thread ref->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏

如果key使用弱引用:

  1. 业务代码中使用完ThreadLocal ,threadLocal Ref被回收了,

  1. 由于只有ThreadLocalMap的Entry这个弱引用指向ThreadLocal,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null

  1. 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry->value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

value内存泄漏的补救措施

看源码你会发行在调用get、set或者remove()操作的时候,都有机会执行回收无效entry的操作。但是,这也不是一个十全十美的方法,考虑这样的场景:

  • 线程在后续的执行中,没有ThreadLocal对象执行get、set或remove方法

  • 线程的ThreadLocalMap中的过期entry将无法被清理,value的强引用链将一直存在,内存泄漏也将随之发生

主动调用ThreadLocal的remove方法,实现 Entry --> keyEntry --> valueThreaLocalMap --> Entry三大引用链的断开,避免内存泄漏的问题

5.2 为什么ThreadLocalMap 采用开放地址法来解决哈希冲突?

JDK中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。

于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

链地址法和开放地址法的优缺点

开放地址法:

  1. 容易产生堆积问题,不适于大规模的数据存储。

  1. 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。

  1. 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

链地址法:

  1. 处理冲突简单,且无堆积现象,平均查找长度短。

  1. 链表中的结点是动态申请的,适合构造表不能确定长度的情况。

  1. 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

  1. 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。

ThreadLocalMap 采用开放地址法原因

  1. ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table

  1. ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

5.3 ThreadLoaclMap 构造器

    // 初始化ThreadLocalMap,并添加 firstValue到里面ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//计算索引// & (INITIAL_CAPACITY - 1) 相当于取模运算 hashCode % size 的一个更高效的实现int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//设置值table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size = 1;//设置阈值setThreshold(INITIAL_CAPACITY);}

构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold

    // ==> ThreadLocal类//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减private static AtomicInteger nextHashCode =  new AtomicInteger();//特殊的hash值private static final int HASH_INCREMENT = 0x61c88647; private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,,也就是Entry[] table中,这样做可以尽量避免hash冲突。

5.4 set()

private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;//计算索引int i = key.threadLocalHashCode & (len-1);// 从索引i开始,向后查找,直到遇到空slotfor (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}// key为 null,说明之前的 ThreadLocal 对象已经被回收了,if (k == null) {//用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏replaceStaleEntry(key, value, i);return;}}//key 和 value 都为null,则在空元素的位置创建一个新的Entry。tab[i] = new Entry(key, value);int sz = ++size;// 清理过期的entry,如果size超过阈值则需要扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}/*** 获取环形数组的下一个索引*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

代码执行流程:

  1. 首先还是根据key计算出索引 i,然后查找i位置上的Entry

  1. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值

  1. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  1. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1

  1. 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理

分析 : ThreadLocalMap使用线性探测法来解决哈希冲突的

  1. 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出

  1. 假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入

  1. 可以把Entry[] table看成一个环形数组

5.4.1 replaceStaleEntry()

replaceStaleEntry() 方法并非简单地使用新entry替换过期entry,而是从过期entry所在的slot(staleSlot)向前、向后查找过期entry,并通过slotToExpunge 标记过期entry最早的index,最后使用cleanSomeSlots() 方法从slotToExpunge开始清理过期entry

    private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;//表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始int slotToExpunge = staleSlot;//从staleSlot的前一个位置开始,向前查找过期entry并更新slotToExpunge,直到遇到空slotfor (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 从staleSlot的后一个位置开始,向后查找,直到遇到空slotfor (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 由于开放定址法,可能相同的key存放于预期的位置(staleSlot)之后// 如果遇到相同的key,则更新value,并交换索引staleSlot与索引i的entry// 交换的原因:让有效的entry占据预期的位置(staleSlot),避免重复key的情况if (k == key) {e.value = value;tab[i] = tab[staleSlot];tab[staleSlot] = e;// slotToExpunge == staleSlot,说明索引staleSlot处前一个entry为null // 未找到过期entry,更新slotToExpunge为iif (slotToExpunge == staleSlot)slotToExpunge = i;// 从slotToExpunge开始,清理一些过期entrycleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 向后查找,未找到过期entry,更新slotToExpunge为当前indexif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// 直到遇到空slot也未发现相同的key,则在staleSlot的位置新建一个entrytab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 存在过期entry,需要进行清理if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}/*** 获取环形数组的前一个索引*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}

总体来说,过期entry所在的staleSlot是key瞄准的位置:

  • 如果key已经存在,则需要将原entry更新、与staleSlot对应的过期entry交换,使其位于staleSlot这个位置

  • 如果遇到了空slot,都未发现key相等的entry,说明key不存在 ⇒ \Rightarrow⇒ 直接在在staleSlot这个位置新建entry

  • 不管是哪种情况,只要发现过期entry,都需要通过cleanSomeSlots() 进行清理

  • 而过期entry存在的判断条件为:slotToExpunge != staleSlot

为何需要staleSlot和key相等时的slot交换?

通过向后遍历数组,找到了相同的key ,说明发生了hash冲突 的情况,让 key 存储到了预期位置的后面,staleSlot和key相等时的slot交换,让有效的entry占据预期的位置(staleSlot),在调用get方法获取时可直接通过hash & len-1得到准确的索引,而不用向后遍历去查找。

5.4.2 expungeStaleEntry()

清除当前过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置,可能会遇到slot被占用的情况(开放地址法移位导致),需要向后遍历,找到空的slot放置,返回空slot的index

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清理过期的entrytab[staleSlot].value = null;tab[staleSlot] = null;size--;// 对后续entry进行rehash,直到遇到空slotEntry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {  // 过期entry,继续清理e.value = null;tab[i] = null;size--;} else { // 有效entry,rehash到合适的位置(补齐空slot)int h = k.threadLocalHashCode & (len - 1);// 期望的index与当前index不相等,说明是开放地址法移位导致的,需要将其放到最近的有效entry之后if (h != i) { tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i; // 返回空slot的index
}

5.4.3 cleanSomeSlots()

通过循环扫描,尽可能多的清理ThreadLocalMap中的过期entry

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) { // 遇到过期entry,需要重置nn = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0); //无符号右移动一位,可以简单理解为除以2return removed;
}

5.4.4 rehash()

rehash之前仍然先清理一次过期entry,如果size > = 3/4 threshold,也就是size >= 1/2 table.length则进行扩容操作( threshold = 2/3 * table.length)

private void rehash() {expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresisif (size >= threshold - threshold / 4)resize();
}

5.4.5 expungeStaleEntries()

对数组进行整体的遍历,清理过期的key,cleanSomeSlots()是尽可能多的清理,不一定清理的干净

private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)// 清除当前过期entry到下一个空slot之间所有过期entry,并将有效entry进行 hash & len-1,// 重新计算其在数组中的位置expungeStaleEntry(j);}
}

5.4.6 resize

将原数组扩大为原来的两倍,将旧的桶数组中的entry移动到新的桶数组中,重新计算其索引位置,遇到过期entry,直接断开entry对value的引用,方便gc。

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;// 旧的桶数组中的entry移动到新的桶数组中// 对于过期entry,直接断开entry对value的引用for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 更新threshold、size、table,旧的桶数组等待GCsetThreshold(newLen);size = count;table = newTab;
}

5.5 getEntry()

获取key对应的entry,若直接命中,则直接返回对应的entry;否则,需要通过getEntryAfterMiss() 方法往后遍历查找

private Entry getEntry(ThreadLocal<?> key) {// 计算索引位置int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i];if (e != null && e.get() == key)  // 直接命中return e;else  // 否则,往后遍历查找,说明出现hash冲突问题return getEntryAfterMiss(key, i, e);
}

5.5.1 getEntryAfterMiss()

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// 从 e 的下一个索引处向后遍历,遇到空slot结束while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;// 遇到过期的entry// 清除过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置if (k == null) expungeStaleEntry(i); elsei = nextIndex(i, len);e = tab[i];}return null;
}

5.6 remove()

private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 计算索引位置int i = key.threadLocalHashCode & (len-1);// 向后遍历,直到遇到空slotfor (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {// 找到keyif (e.get() == key) {// 清除ee.clear();//清除过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置expungeStaleEntry(i);return;}}
}

参考文章:

被大厂面试官连环炮轰炸的ThreadLocal (吃透源码的每一个细节和设计原理) - 掘金 (juejin.cn)

(1条消息) ThreadLocal学习_晓之木初的博客-CSDN博客

(1条消息) 史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客_threadlocal

相关文章:

ThreadLocal 详解

ThreadLocal简介JDK源码对ThreadLocal类的注释如下&#xff1a;ThreadLocal提供线程局部变量&#xff0c;使得每个线程都有自己的、独立初始化的变量副本ThreadLocal实例通常是类中的private static字段&#xff0c;用于将状态与线程相关联&#xff0c;如用户ID、事务ID只要线程…...

【Java 面试合集】重写以及重载有什么区别能简单说说嘛

重写以及重载有什么区别能简单说说嘛 前述 这是一道非常基础的面试题&#xff0c;我们在回答的过程中一定要逐一横向比较。 从方法的 修饰符&#xff0c;返回值&#xff0c;方法名&#xff0c;含义&#xff0c;参数等方面进行逐一分析来比较不同。 话不多话&#xff0c;看下…...

到底什么是股票委托接口?

在量化股票市场上&#xff0c;常见的股票委托接口其实有着不一样的交集&#xff0c;就拿股票交易接口&#xff0c;在量化股票跟程序化交易中&#xff0c;有共同之处就是在于直接委托执行下单&#xff0c;并且能很快的就能够将策略输出在账户持仓数据中&#xff0c;继续缓存下来…...

Linux驱动:VPU

1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 概述 VPU 是用来进行图像、视频数据进行硬件编、解码的硬件模块。内部集成了 Encoder、Decoder 功能部件进行图像、视频数据进行硬件编、解码&a…...

简介Servlet

目录 一、maven中心库 二、简介Servlet 三、实现Servlet动态页面 1、创建一个maven项目 2、引入依赖 3、创建目录结构 4、编写Servlet代码 5、打包 6、部署 7、验证程序 四、Servlet的运行原理 五、Tomcat伪代码 1、Tomcat初始化 a、让Tomcat先从指定的目录…...

Learning C++ No.7

引言&#xff1a; 北京时间&#xff1a;20223/2/9/22:20&#xff0c;距离大一下学期开学还有2天&#xff0c;昨天收到好消息&#xff0c;开学不要考试了&#xff0c;我并不是害怕考试&#xff0c;考试在我心里&#xff0c;地位不高&#xff0c;可能只有当我挂了&#xff0c;才能…...

【MyBatis】第八篇:一级,二级缓存

其实缓存字面的意思就是将一些内容缓存下来&#xff0c;等下次使用的时候可以直接调用&#xff0c;通过数据库得到数据&#xff0c;有时候会使用相同的数据&#xff0c;所以mybatis自然也支持缓存。 而mybatis按照缓存的效果可以分两大类&#xff1a;一级缓存和二级缓存。 一…...

【大唐杯备考】——5G基站开通与调测(学习笔记)

&#x1f4d6; 前言&#xff1a;本期介绍5G基站开通与调测。 目录&#x1f552; 1. 概述&#x1f552; 2. 5G基站开通与调测基础&#x1f558; 2.1 3.5GHz单模100MHz配置&#xff08;S111&#xff09;&#x1f558; 2.2 3.5GHz单模100MHz配置&#xff08;S111111&#xff09;&a…...

redhat7 忘记root密码,重置办法

来自https://www.tracymc.cn/archives/802 亲测可用&#xff0c;太感谢了&#xff0c;在此记录一下&#xff0c;原文有图 1.启动的时候,在有启动项界面,相应启动项内核名称上按“e”; 2.进入后,找到linux16开头的地方,按“end”键或者controle到最后,输入rd.break,再按ctrlx进…...

QML- 对象属性

QML- 对象属性一、概述二、id 属性三、Property 属性1. 定义属性1. 自定义属性定义中的有效类型2. 为属性属性赋值1. 初始化时的值赋值2. 命令式赋值3. 静态值和绑定表达式值4. 类型安全5. 特殊属性类型1. 对象列表属性2. 分组属性6. 属性别名1. 属性别名的注意事项2. 属性别名…...

将.js文件转成vue标签结构的样式

例如&#xff1a;下图所示&#xff1a; 依次识别获取.js文件中的tag和props&#xff0c;可以理解为字符串拼接&#xff0c;将整个vue的标签结构看作是一个字符串。 话不多说&#xff0c;先放上完整代码&#xff0c;思路看代码备注。&#xff08;自己实现的时候&#xff0c;可以…...

前端知识点复盘

组件和jsx <body><div id"root"></div><script type"text/babel">const root ReactDOM.createRoot(document.getElementById("root"))class App extends React.Component {render() {return (<div> <h1>s…...

前端JavaScript获取图片文件的真实格式

常见方式判断图片格式 当我们进行前端开发&#xff0c;需要处理图片上传功能&#xff0c;针对图片格式做判断时&#xff0c;常规的方法都是使用文件后缀名来判断&#xff0c;如下代码所示&#xff1a; input.addEventListener(change, (e) > {const file e.target.files[…...

今天面了一个来华为要求月薪25K,明显感觉他背了很多面试题...

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…...

11 Advanced CNN

文章目录GoogLeNetInception Module1x1 Conv计算效果代码实现总结ResNet (残差网络)问题引入梯度消失与传统神经网络的比较代码实现课程来源&#xff1a; 链接对于前篇中所提到问题&#xff0c;设计出的是一种类似于LeNet5的线性结构&#xff0c;而对于大多数问题&#xff0c;简…...

亿级高并发电商项目---万达商城项目搭建(二)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是小童&#xff0c;Java开发工程师&#xff0c;CSDN博客博主&#xff0c;Java领域新星创作者 &#x1f4d5;系列专栏&#xff1a;前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 &#x1f4…...

UML术语标准和分类

一、UML术语标准 1&#xff0e;中文UML术语标准 中国软件行业协会&#xff08;CSIA&#xff09;与日本UML建模推进协会&#xff08;UMTP&#xff09;共同在中国推动的UML专家认证&#xff0c;两个协会共同颁发认证证书、两国互认&#xff0c;CSIA与UMTP共同推出了UML中文术语…...

LeetCode 刷题系列 -- 151. 反转字符串中的单词

给你一个字符串 s &#xff0c;请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。注意&#xff1a;输入字符串 s中可能会存在前导空格、尾随空格或…...

二十二、Gtk4-ListView

GTK 4添加了新的列表对象GtkListView、GtkGridView和GtkColumnView。这个新特性在Gtk API参考—列表小构件概述中有描述。 GTK 4还有其他实现列表的方法。它们是GtkListBox和GtkTreeView&#xff0c;它们是从GTK 3接管的。在Gtk开发博客中有一篇关于Matthias Clasen所写的列表…...

ASP.NET Core3.1实战教程---基于Jquery单文件上传

这个必须记录一下费劲啊&#xff01;废了我2天的时间&#xff0c;昔日的net快速已经没落....就文件上传都这么费劲。 先说下要求&#xff08;在线apk文件上传实现手机端整包更新&#xff09;&#xff1a; 1、为了简化需求文件上传和数据提交分开执行 2、选完文件后按钮变成上…...

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇&#xff0c;在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下&#xff1a; 【Note】&#xff1a;如果你已经完成安装等操作&#xff0c;可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作&#xff0c;重…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法

树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作&#xff0c;无需更改相机配置。但是&#xff0c;一…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序

一、开发环境准备 ​​工具安装​​&#xff1a; 下载安装DevEco Studio 4.0&#xff08;支持HarmonyOS 5&#xff09;配置HarmonyOS SDK 5.0确保Node.js版本≥14 ​​项目初始化​​&#xff1a; ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

Caliper 配置文件解析:config.yaml

Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...