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

Java 并发 ThreadLocal 详解

文章首发于个人博客,欢迎访问关注:https://www.lin2j.tech

简介

ThreadLocal 即线程本地变量的意思,常被用来处理线程安全问题。ThreadLocal 的作用是为多线程中的每一个线程都创建一个线程自身才能用的实例对象,通过线程隔离的方式保证了实例对象的使用安全。

在并发编程中,有以下几种方式可以用来避免线程安全问题

  • 同步方案

    • 加锁(synchronized 和 Lock)

    • 通过 CAS (原子类)

  • 无同步方案

    • 栈封闭(方法的局部变量)
    • 本地存储(ThreadLocal)

使用示例

下面用两个例子来演示 ThreadLocal 的线程隔离能力,看看它是如何规避线程安全问题的。代码从两个方面来测试

  1. 没有使用 ThreadLocal,直接将 SimpleDateFormat 暴露在多线程的环境并发使用。
  2. 使用 ThreadLocal 在多线程下使用。
import java.text.ParseException;
import java.text.SimpleDateFormat;public class ThreadLocalExample {private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");// 创建 ThreadLocal 对象,并指定 SimpleDateFormat 的创建过程private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() -> {System.out.println(Thread.currentThread().getName() + " 创建 SimpleDateFormat 对象");return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");});public static void main(String[] args) {// 下面是没有使用 ThreadLocal 的方式,直接使用 SimpleDateFormat// 可以将注释取消运行看看结果
/*for (int i = 0; i < 10; i++) {new Thread(() -> {String t = Thread.currentThread().getName();try {System.out.println(t + "\t共享变量格式化: " + sdf.parse("2023-01-01 12:00:00"));} catch (ParseException e) {e.printStackTrace();}}, "" + i).start();}*/// 下面是使用 ThreadLocal 的方式for (int i = 0; i < 10; i++) {new Thread(() -> {String t = Thread.currentThread().getName();try {System.out.println(t + "\tThreadLocal格式化: " + THREAD_LOCAL.get().parse("2023-01-01 12:00:00"));} catch (ParseException e) {e.printStackTrace();} finally {// 最好养成用完就清除的习惯,避免内存泄漏THREAD_LOCAL.remove();}}, "" + i).start();}}
}
  1. 没用使用 ThreadLocal 的方式会因为并发使用而导致异常。
Exception in thread "3" java.lang.NumberFormatException: For input string: ".E0"at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2089)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.jia.blogdemo.thread.ThreadLocalExample.lambda$main$1(ThreadLocalExample.java:28)at java.lang.Thread.run(Thread.java:748)
4	共享变量格式化: Wed Jan 01 00:00:00 CST 2200
9	共享变量格式化: Sun Jan 01 00:00:00 CST 2023
8	共享变量格式化: Sun Jan 01 00:00:00 CST 2023
6	共享变量格式化: Sun Jan 01 00:00:00 CST 2023
5	共享变量格式化: Sun Jan 01 00:00:00 CST 2023
7	共享变量格式化: Sun Jan 01 00:00:00 CST 2023
2	共享变量格式化: Wed Jan 01 00:00:00 CST 2200
1	共享变量格式化: Wed Jun 21 14:16:33 CST 190728635
0	共享变量格式化: Wed Jun 21 14:16:33 CST 190728635
  1. 使用了 ThreadLocal 的方式运行结果正常,而且结果的前几行可以看出来,每个线程都各自创建了 SimpleDateFormat 对象。
0 创建 SimpleDateFormat 对象
2 创建 SimpleDateFormat 对象
1 创建 SimpleDateFormat 对象
4 创建 SimpleDateFormat 对象
3 创建 SimpleDateFormat 对象
5 创建 SimpleDateFormat 对象
6 创建 SimpleDateFormat 对象
7 创建 SimpleDateFormat 对象
8 创建 SimpleDateFormat 对象
9 创建 SimpleDateFormat 对象
3	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
1	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
0	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
7	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
2	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
4	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
9	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
8	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
6	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023
5	ThreadLocal格式化: Sun Jan 01 00:00:00 CST 2023

源码详解

ThreadLocal 有一个内部类叫 ThreadLocalMap,它是一个映射表,将 ThreadLocal 和存储对象作为键值对存储起来。然后关键的是,每个 Thread 对象中,都会有一个叫 threadLocals 的成员变量,它的类型是 ThreadLocalMap。在 ThreadLocal 使用 threadLocals 变量时,如果发现它是 null,那么就会 new 一个新的对象。

ThreadLocal 的关键理解点是,当我们向 ThreadLocal 设置、获取、删除存储对象的时候,第一步都是先拿到当前线程的 threadLocals 变量,然后对这个 ThreadLocalMap 进行添加、获取、删除等等操作。因为每个线程有自己的 ThreadLocalMap,所以线程拿的都是自己的那一份实例对象。

在这个理解基础上,下面的代码实际讲解的是对于 ThreadLocalMap 的操作方法。

Thread-threadLocals

内部类 ThreadLocalMap

ThreadLocalMap 是一个哈希表,用于存储 ThreadLocal 和 存储对象的映射关系。ThreadLocalMap 也包含一个内部类 Entry,它是一个键值对,其中键为弱引用,值为存储对象。

当插入键值对发生冲突时,ThreadLocalMap 的做法是向后线性寻找一个第一个 entry 为 null 的位置插入。

static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object).  Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table.  Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/*** 默认的容量*/private static final int INITIAL_CAPACITY = 16;/*** 哈希表,必要时会进行扩容,且它的长度永远是 2 的 N 次方*/private Entry[] table;/*** 哈希表的元素个数*/private int size = 0;/*** 阈值,当元素个数达到 threshold 的时候,哈希表会扩容*/private int threshold; // Default to 0// 创建一个 ThreadLocalMap 对象,并接受第一对键值对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); // 阈值}// 创建一个 ThreadLocalMap 对象,接收一个父哈希表,将里面的元素放到新创建的对象中private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length; // 哈希表长度setThreshold(len); //阈值table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) { // 因为 Entry 的 key 是弱引用,所以复制过程中可能有些 key 为 nullObject value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1); // 计算位置while (table[h] != null)  // 这个 while 循环在后面经常出现,向后寻找第一个 entry 为 null 的位置h = nextIndex(h, len);table[h] = c;  // 插入size++; // 元素个数累加}}}}
}

ThreadLocalMap 还有很多方法,下面涉及到的时候再具体讲解。

属性

ThreadLocal 的属性有三个,主要是用来生成 ThreadLocal 对象的哈希值。

/*** 当前实例对象的哈希值,用来确定实例对象在哈希表中的位置*/
private final int threadLocalHashCode = nextHashCode();/*** 类静态变量,用于生成 threadLocalHashCode,起始值为 0*/
private static AtomicInteger nextHashCode =new AtomicInteger();/*** 哈希值的增量,是一个斐波那契数,它的作用就是让 hash 分布非常均匀* * 至于为什么,有兴趣的可以自己找资料学习*/
private static final int HASH_INCREMENT = 0x61c88647;/*** 返回当前的 nextHashCode 值,并生成下一个 nextHashCode 值*/
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每当创建一个 ThreadLocal 对象,nextHashCode 的值就会增加 HASH_INCREMENT。

构造函数

ThreadLocal 的构造函数只有一个。

public ThreadLocal() {
}

这里顺便讲一下它的初始化方法,所谓初始化就是创建 ThreadLocal 对应的存储对象的过程,可以通过两个方式

  1. 重写 initialValue 方法;
  2. 通过 withInitial 静态方法,传入一个 Supplier 子类,实现 Supplier 的 get 方法;
// 方式一:initialValue 默认返回 null,需要子类重写
protected T initialValue() {return null;
}// 方式二:withInitial 返回一个 SuppliedThreadLocal 对象
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {// SuppliedThreadLocal 接收一个 Supplier 对象return new SuppliedThreadLocal<>(supplier);
}// 默认的 ThreadLocal 子类实现,重写了 initialValue 方法,
// 这个方法返回的值是构造函数传入的 Supplier 对象的 get 方法返回值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected T initialValue() {return supplier.get();}
}

核心函数 set

// 保存存储对象至当前线程
public void set(T value) {Thread t = Thread.currentThread(); // 拿到当前线程ThreadLocalMap map = getMap(t); // 获取线程的 threadLocals 成员变量if (map != null)map.set(this, value);  // 调用 map 的 set 方法elsecreateMap(t, value); // 如果 map 为空,则创建一个新的 ThreadLocalMap 实例
}void createMap(Thread t, T firstValue) {// new 一个对象t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set 方法的过程如下:

  1. 先拿到当前线程的对象,并获取当前线程对象的 threadLocals 变量;
  2. 如果 threadLocals 不为空,则调用 ThredaLocalMap#set 方法进行保存;
  3. 如果 threadLocals 为空,则调用 createMap 方法为当前线程创建一个新的 ThreadLocalMap,并将 value 作为第一个保存的值。

在这个过程中,map.set(this, value) 这句代码虽然简单,但是里面做的事情有很多。

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1); // 根据当前 ThreadLocal 实例的哈希值计算插入位置// 从位置 i 开始遍历,直到遍历到某个位置为 null 则停止for (Entry e = tab[i];e != null;  // 位置 i 的对象为空,则结束循环e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();  // 拿到位置 i 的 ThreadLocal 对象 kif (k == key) {   // 二者相等,则更新位置 i 的 valuee.value = value;return;}if (k == null) {  // 位置 i 已经被回收,则用当前 key-value 替换旧的 Entry replaceStaleEntry(key, value, i);return;}}// 能到这里,说明位置 i 的地方对象为 null,直接插入 Entry 即可tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold) // 先清除过期的 Entry,若无过期的 Entry,则需要判断是否需要扩容rehash(); // 扩容
}// 这个方法清除掉过期的 Entry,
// 如果有清除 Entry 返回 true,否则返回 false
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;  // 当有 Entry 移除,则将 n 重置为 lenremoved = true; // 删除标记i = expungeStaleEntry(i); // 从位置 i 往后移除 key 为 null 的元素}} while ( (n >>>= 1) != 0);  // 以 log2 的方式遍历,注释说这种方式简单、快速return removed;
}private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;int slotToExpunge = staleSlot; // 清除过期 key 的起始位置// 先从位置 staleSlot 往前遍历看有没有过期的 Entryfor (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)) // 往前遍历if (e.get() == null) // 过期slotToExpunge = i;  // 更新 slotToExpunge(即往前推移了)for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == key) {// 如果位置 i 的 Entry 的 key 和传入的 key 相等// 则将位置 i 和位置 staleSlot 的元素交换(注意:staleSlot < i)e.value = value; // 更新为新的 valuetab[i] = tab[staleSlot];tab[staleSlot] = e;if (slotToExpunge == staleSlot) // 如果二者相等,则说明上面的向前寻找 null 元素的遍历没有找到了slotToExpunge = i; // 将 slotToExpunge 更新为 i,因为 i 之前的元素不需要清除cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); // 清除过期元素return;}if (k == null && slotToExpunge == staleSlot) // 第一次遍历到 key 为 null 的元素,且上面向前查找 key 为 null 的遍历没有找到slotToExpunge = i; // 将 slotToExpunge 更新为 i,因为 i 之前的元素不需要清除}// 如果 key 没有找到,则新建一个新的 Entry 放在 slot 位置tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);if (slotToExpunge != staleSlot) // 二者不相等,说明除了 staleSlot 之外,还有其他位置有过期元素cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); //清除过期元素
}

expungeStaleEntry 和 rehash 方法分别是过期清理和扩容的方法,下面再专门讲。

核心函数 get

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t); if (map != null) {// 调用 getEntry 方法,从当前线程的 threadLocals 中拿到当前 ThreadLocal 实例对应的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) { // entry 不为空,直接获取 value@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();  // entry 为空,则初始化当前 ThreadLocal 实例
}// 过程与 set 方法相似
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}

get 方法的过程如下:

  1. 先拿到当前线程的对象,并获取当前线程对象的 threadLocals 变量;
  2. 如果 threadLocals 不为 null,则调用 getEntry 方法获取当前 ThreadLocal 实例对应的值;
  3. 如果 threadLocals 为 null 或者获取不到当前 ThreadLocal 实例对应的值,则调用 setInitialValue 方法进行初始化。
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);  // 计算 key 对应的位置Entry e = table[i];if (e != null && e.get() == key)  // 顺利找到 keyreturn e;elsereturn getEntryAfterMiss(key, i, e); // 找不到 key,则尝试向后查找
}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)  // 找到 keyreturn e;if (k == null)expungeStaleEntry(i);   // 清除过期的 Entryelsei = nextIndex(i, len);  // 向后遍历e = tab[i];}return null;
}

核心函数 remove

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}private void remove(ThreadLocal<?> key) {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)]) { // 如果位置 i 的 key 和传入的 key 不相等,则往后寻找if (e.get() == key) { // 找到指定的 keye.clear(); // 清除expungeStaleEntry(i); // 清理过期的 keyreturn;}}
}

扩容 rehash

/*** 1. 清理所有过期的 Entry* 2. 扩大 table 的容量为原先的两倍*/
private void rehash() {expungeStaleEntries();  // 清理所有过期的 Entryif (size >= threshold - threshold / 4)  // 当前元素个数超过 3/4 thresholdresize(); // 扩容
}private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;  // 新容量为旧容量的 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();if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1); // 根据哈希值重新计算新的位置 hwhile (newTab[h] != null)  // 如果位置 h 已经有元素,则往后寻找 Entry 为 null 的位置h = nextIndex(h, newLen);newTab[h] = e; // 插入count++; // 元素个数累加}}}// 更新setThreshold(newLen);size = count;table = newTab;
}

ThreadLocalMap 的扩容思路比较简单,先将过期的元素全部清理掉之后,如果元素个数达到 threshold 的 3 / 4 3/4 3/4 以上,则扩容。

扩容的时候先 new 一个大小为原先 table 两倍的数组 newTab,然后将 table 内的元素全部根据新的容量重新计算位置,插入到 newTab 的对应位置上。如果对应位置已经有值了,则从当前位置开始向后寻找,找到第一个 Entry 为 null 的位置插入。

过期清理

前面讲过 ThreadLocalMap 的 Entry 的 key 是 WeakReference 弱引用。当发生 GC 时,弱引用指向的对象会被回收,因此会出现 Entry 的 key 为 null 的情况。这种情况下,要及时清理过期的 Entry,从而避免内存泄漏和无效数据的积累。

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 首先清除 staleSlot 位置的元素tab[staleSlot].value = null;tab[staleSlot] = null;size--;// 从 staleSlot 开始向后遍历,直到遇见 Entry 为 null 的位置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 {// 不为空的元素会根据 key 的哈希值重新插入位置int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null)  // 如果原本的位置有数据了,则向后查找合适的位置插入h = nextIndex(h, len);tab[h] = e;}}}return i;
}

set、get、remove 方法,在遍历的时候如果遇到 key 为 null 的情况,都会调用 expungeStaleEntry 方法来清除 key 为 nul l的 Entry。

拓展

内存泄漏

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

从 Entry 的定义可以知道,虽然 Entry 的 key 是弱引用,在垃圾回收时会被回收,但是 Entry 的 value 是通过强引用来引用的。因此在不进行清理的情况下会存在如下的强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value。这会导致 key 为 null 的 Entry 的 value 永远无法回收,造成内存泄漏。

因此为了避免这种情况,我们可以在使用完 ThreadLocal 后,需要手动调用 remove 方法,以避免出现内存泄漏。

参考文章

  • https://zhuanlan.zhihu.com/p/34406557
  • https://pdai.tech/md/java/thread/java-thread-x-threadlocal.html

相关文章:

Java 并发 ThreadLocal 详解

文章首发于个人博客&#xff0c;欢迎访问关注&#xff1a;https://www.lin2j.tech 简介 ThreadLocal 即线程本地变量的意思&#xff0c;常被用来处理线程安全问题。ThreadLocal 的作用是为多线程中的每一个线程都创建一个线程自身才能用的实例对象&#xff0c;通过线程隔离的…...

JWT 技术的使用

应用场景&#xff1a;访问某些页面&#xff0c;需要用户进行登录&#xff0c;那我们如何知道用户有没有登录呢&#xff0c;这时我们就可以使用jwt技术。用户输入的账号和密码正确的情况下&#xff0c;后端根据用户的唯一id生成一个独一无二的token&#xff0c;并返回给前端&…...

机器学习深度学习——NLP实战(自然语言推断——微调BERT实现)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——针对序列级和词元级应用微调BERT &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文…...

如何在windows下使用masm和link对汇编文件进行编译

前言 32位系统带有debug程序&#xff0c;可以进行汇编语言和exe的调试。但真正的汇编编程是“编辑汇编程序文件(.asm)->编译生成obj文件->链接生成exe文件”。下面&#xff0c;我就来说一下如何在windows下使用masm调试&#xff0c;使用link链接。 1、下载相应软件 下载…...

Golang字符串基本处理方法

Golang的字符串处理 字符串拼接 两种方法&#xff1a;strings.Join方法和’方法 package mainimport ("fmt""strings" )func main() {num : 20strs : make([]string, 0)for i : 0; i < num; i {strs append(strs, "fht")}//string.join拼…...

算法训练营第三十九天(8.30)| 动态规划Part09:购买股票

Leecode 123.买卖股票的最佳时机 III 123.买卖股票的最佳时机III 123.买卖股票的最佳时机III 题目地址&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目类型&#xff1a;股票问题 class Solution { public:int maxProfit(vector<…...

renren-fast-vue环境升级后,运行正常打包后,访问页面空白

网上各种环境&#xff0c;路径都找了一遍&#xff0c;也没成功。后来发现升级后打包的dist文件结构发生了变化&#xff0c; 1.最开始正常版本是这样 2.升级后是这样&#xff0c;少了日期文件夹 3.问题&#xff1a;打包后的index.html中引入的是config文件夹&#xff0c;而打…...

Uniapp笔记(三)uniapp语法2

一、本节项目预备知识 1、组件生命周期 1.1、什么是生命周期 生命周期(Life Cycle)是指一个对象从创建-->运行-->销毁的整个阶段&#xff0c;强调的是一个时间段 我们可以把每个uniapp应用运行的过程&#xff0c;也概括为生命周期 小程序的启动&#xff0c;表示生命周…...

windows【ftp-FTP】添加配置流程【iis服务】

第一步&#xff1a;自己安装iis服务和ftp服务【自己百度搜索】 第二步&#xff1a;添加ftp站点【配置主动端口默认为21】 第三方配置&#xff1a;ftp被动端口【这里设置为3000-4000】请在防火墙开放此端口【如果是阿里云请在阿里云的后天也开通此端口】【护卫神一般使用55000…...

mysql视图的创建和选项配置详解

在 MySQL 中&#xff0c;可以使用 CREATE VIEW 语句来创建视图。基本的语法如下&#xff1a; CREATE[OR REPLACE][ALGORITHM {UNDEFINED | MERGE | TEMPTABLE}][DEFINER {user | CURRENT_USER}][SQL SECURITY { DEFINER | INVOKER }]VIEW view_name [(column_list)]AS selec…...

Python正则表达式中re.sub自定义替换方法正确使用方法

大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 话不多说&#xff0c;直接开搞&#xff0c;如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 在使用正则替换时&#xff0c;有时候需要将匹配的结果做对应处理&#xff0c;便可以使用自定义替换方法。 re.sub的用法为&…...

hyperf 十五 验证器

官方文档&#xff1a;Hyperf 验证器报错需要配合多语言使用&#xff0c;创建配置自动生成对应的语言文件。 一 安装 composer require hyperf/validation:v2.2.33 composer require hyperf/translation:v2.2.33php bin/hyperf.php vendor:publish hyperf/translation php bi…...

ssh访问远程宿主机的VMWare中NAT模式下的虚拟机

1.虚拟机端配置 1.1设置虚拟机的网络为NAT模式 1.2设置虚拟网络端口映射(NAT) 点击主菜单的编辑-虚拟网络编辑器&#xff1a; 启动如下对话框&#xff0c;选中NAT模式的菜单项&#xff0c;并点击NAT设置&#xff1a; 点击添加&#xff0c;为我们的虚拟机添加一个端口映射。…...

【一等奖方案】大规模金融图数据中异常风险行为模式挖掘赛题「NUFE」解题思路

第十届CCF大数据与计算智能大赛&#xff08;2022 CCF BDCI&#xff09;已圆满结束&#xff0c;大赛官方竞赛平台DataFountain&#xff08;简称DF平台&#xff09;正在陆续释出各赛题获奖队伍的方案思路&#xff0c;欢迎广大数据科学家交流讨论。 本方案为【大规模金融图数据中…...

npm install 报错

npm install 报错 npm install 报错 npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: yudao-ui-admin1.8.0-snapshot npm ERR! Found: eslint7.15.0 npm ERR! node_modules/eslint npm ERR! dev eslint&q…...

专业人士使用的3个好用的ChatGPT提示

AI正在席卷世界。从自动化各个领域的任务到几秒钟内协助我们的日常生活&#xff0c;它有着了公众还未理解的巨大价值……除非你是专业人士。 专业人士一般都使用什么提示以及如何使用的&#xff1f;这里是经验丰富的懂ChatGPT的专业人士才知道的3个提示。好用请复制收藏。 为…...

doris系列2: doris分析英国房产数据集

1.准备数据 2.doris建表 CREATE TABLE `uk_price_paid` (`id` varchar(50) NOT NULL,`price` int(20),`date` date...

精准运营,智能决策!解锁天翼物联水利水务感知云

面向智慧水利/水务数字化转型需求&#xff0c;天翼物联基于感知云平台创新能力&#xff0c;提供涵盖水利水务泛协议接入、感知云水利/水务平台、水利/水务感知数据治理、数据看板在内的水利水务感知云服务&#xff0c;构建水利水务感知神经系统新型数字化底座&#xff0c;实现智…...

CleanMyMac最新版4.14Mac清理软件下载安装使用教程

苹果电脑是很多人喜欢使用的一种电脑&#xff0c;它有着优美的外观&#xff0c;流畅的操作系统&#xff0c;丰富的应用程序和高效的性能。但是&#xff0c;随着时间的推移&#xff0c;苹果电脑也会产生一些不必要的文件和数据&#xff0c;这些文件和数据就是我们常说的垃圾。那…...

String.Format方法详解

在Java中&#xff0c;String.format() 方法可以用于将格式化的字符串写入输出字符串中。该方法将根据指定的格式字符串生成一个新的字符串&#xff0c;并使用可选的参数填充格式字符串中的占位符。以下是有关 String.format() 方法的更详细信息&#xff1a; 语法 public stati…...

Python|GIF 解析与构建(5):手搓截屏和帧率控制

目录 Python&#xff5c;GIF 解析与构建&#xff08;5&#xff09;&#xff1a;手搓截屏和帧率控制 一、引言 二、技术实现&#xff1a;手搓截屏模块 2.1 核心原理 2.2 代码解析&#xff1a;ScreenshotData类 2.2.1 截图函数&#xff1a;capture_screen 三、技术实现&…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互

物理引擎&#xff08;Physics Engine&#xff09; 物理引擎 是一种通过计算机模拟物理规律&#xff08;如力学、碰撞、重力、流体动力学等&#xff09;的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互&#xff0c;广泛应用于 游戏开发、动画制作、虚…...

Spring Boot面试题精选汇总

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码

目录 一、&#x1f468;‍&#x1f393;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站效果 五、&#x1fa93; 代码实现 &#x1f9f1;HTML 六、&#x1f947; 如何让学习不再盲目 七、&#x1f381;更多干货 一、&#x1f468;‍&#x1f…...

springboot整合VUE之在线教育管理系统简介

可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生&#xff0c;小白用户&#xff0c;想学习知识的 有点基础&#xff0c;想要通过项…...

面向无人机海岸带生态系统监测的语义分割基准数据集

描述&#xff1a;海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而&#xff0c;目前该领域仍面临一个挑战&#xff0c;即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...

yaml读取写入常见错误 (‘cannot represent an object‘, 117)

错误一&#xff1a;yaml.representer.RepresenterError: (‘cannot represent an object’, 117) 出现这个问题一直没找到原因&#xff0c;后面把yaml.safe_dump直接替换成yaml.dump&#xff0c;确实能保存&#xff0c;但出现乱码&#xff1a; 放弃yaml.dump&#xff0c;又切…...