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

【Mysql】关联查询1对多处理

关联查询1对多返回 遇见的问题 审批主表&#xff0c;和审批明细表&#xff0c;一张审批对应多张明细数据&#xff0c;每条明细数据的状态是不一样的&#xff0c;现在需要根据明细的状态获取到主单子的状态&#xff0c;状态返回矩阵如下 明细状态返回总状态都是已完成已完成都…...

vue 入门案例模版

vue 入门案例1 01.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> &l…...

el-select实现懒加载

先看一个线上的演示示例&#xff1a;https://code.juejin.cn/pen/7273352811440504889 背景 我们在实际开发中经常遇到这样的需求&#xff1a; el-select实现懒加载&#xff0c;用通俗的话说&#xff0c;为了增加响应速度&#xff0c;就是初始下拉只展示50条数据&#xff0c…...

Java泛型机制

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a;每天一个知识点 ✨特色专栏&#xff1a…...

Linux CentOS安装抓包解包工具Wireshark图形化界面

1.Wireshark介绍 Wireshark 是一个开源的网络协议分析工具&#xff0c;它能够捕获和分析网络数据包&#xff0c;提供深入的网络故障排除、网络性能优化和安全审计等功能。它支持跨多个操作系统&#xff0c;包括 Windows、macOS 和 Linux。 2.Wireshark主要使用方法 捕获数据…...

虹科分享 | 温度边缘效应对冻干成品含水量的影响(下)——优化和总结

上一篇文章中介绍到借助虹科Ellab的温度记录仪观察到由于冻干机壁面温度的影响&#xff0c;形成的边缘效应导致同一隔板的不同区域冻干饼块的干燥程度不均匀&#xff0c;含水量不同。 06 初次试验结果&#xff1a; 二次干燥中的产品温度显示&#xff1a; 放置在搁板中间的产品…...

ATF(TF-A)安全通告 TFV-1 (CVE-2016-10319)

安全之安全(security)博客目录导读 ATF(TF-A)安全通告汇总 目录 一、ATF(TF-A)安全通告 TFV-1 (CVE-2016-10319) 二、CVE-2016-10319 一、ATF(TF-A)安全通告 TFV-1 (CVE-2016-10319) Title 错误的固件更新SMC可能导致意外的大数据拷贝到安全内存中 CVE ID CVE-2016-10319 …...

说说我最近筛简历和面试的感受。。

大家好&#xff0c;我是鱼皮。 都说现在行情不好、找工作难&#xff0c;但招人又谈何容易&#xff1f;&#xff01; 最近我们公司在招开发&#xff0c;实习社招都有。我收到的简历很多&#xff0c;但认真投递的、符合要求的却寥寥无几&#xff0c;而且都是我自己看简历、选人…...

Mysql /etc/my.cnf参数详解(一)

[mysqld] #server相关 server_id176452388 //每个MySQL服务器都需要具有唯一的server_id值 super_read_only 0 //不开启只读&#xff0c;在slave节点会开启即super_read_only 1 port 3306 //指定了Mysql开放的端口&#xff1b; default-storage-engine InnoDB skip-name…...

用最少数量的箭引爆气球【贪心算法】

用最少数量的箭引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可以沿着 x 轴从不同点 完全垂直 地…...