当前位置: 首页 > 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…...

工程地质软件市场:发展现状、趋势与策略建议

一、引言 在工程建设领域&#xff0c;准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具&#xff0c;正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

页面渲染流程与性能优化

页面渲染流程与性能优化详解&#xff08;完整版&#xff09; 一、现代浏览器渲染流程&#xff08;详细说明&#xff09; 1. 构建DOM树 浏览器接收到HTML文档后&#xff0c;会逐步解析并构建DOM&#xff08;Document Object Model&#xff09;树。具体过程如下&#xff1a; (…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)

设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile&#xff0c;新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;使用DevEco Studio作为开发工具&#xff0c;采用Java语言实现&#xff0c;包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

安卓基础(aar)

重新设置java21的环境&#xff0c;临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的&#xff1a; MyApp/ ├── app/ …...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

Kafka入门-生产者

生产者 生产者发送流程&#xff1a; 延迟时间为0ms时&#xff0c;也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于&#xff1a;异步发送不需要等待结果&#xff0c;同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...