实战指南:理解 ThreadLocal 原理并用于Java 多线程上下文管理

目录

一、ThreadLocal基本知识回顾分析

(一)ThreadLocal原理

(二)既然ThreadLocalMap的key是弱引用,GC之后key是否为null?

(三)ThreadLocal中的内存泄漏问题及JDK处理方法

(四)部分核心源码回顾

ThreadLocal.set()方法源码详解

ThreadLocalMap.get()方法详解

ThreadLocal.remove()方法源码详解

(五)简单的直观体会

二、基于Threadlocal实现的上下文管理组件ContextManager

(一)定义 ContextManager 类

(二)使用 ContextManager 进行上下文管理

(三)扩展 ContextManager 的使用方式

三、在线程池中传递ContextManager

(一)增加静态方法,用于在已有的上下文中执行任务

(二)自定义线程池实现

(三)测试自定义线程池

四、总结


探讨如何基于 ThreadLocal 实现一个高效的上下文管理组件,以解决多线程环境下的数据共享和上下文管理这些问题。通过具体的代码示例和实战展示 ThreadLocal 如何为多线程编程提供一种简洁而高效的上下文管理方案。

一、ThreadLocal基本知识回顾分析

(一)ThreadLocal原理

ThreadLocal 是 Java 提供的一个用于线程级别数据存储的类。它为每个线程提供了独立的变量副本,使得每个线程都能独立地操作自己的变量,而不会与其他线程的变量冲突。这种机制特别适用于需要线程隔离的场景,通过 ThreadLocal,我们可以确保同一个变量在不同线程中拥有各自独立的值。

我们先来看下Thread、ThreadLocalMap、ThreadLocal结构关系:

  • 每个Thread都有一个ThreadLocalMap变量
  • ThreadLocalMap内部定义了Entry(ThreadLocal<?> k, Object v)节点类,这个节点继承了WeakReference类泛型为ThreacLocal

ThreadLocal主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争,那ThreadLocal是如何实现这一特性的呢?基本原理实现如下:

  1. 每个Thread对象中都包含一个ThreadLocal.ThreadLocalMap类型的threadlocals成员变量;

  2. 该map对应的每个元素Entry对象中:key是ThreadLocal对象的弱引用,value是该threadlocal变量在当前线程中的对应的变量实体;

  3. 当某一线程执行获取该ThreadLocal对象对应的变量时,首先从当前线程对象中获取对应的threadlocals哈希表,再以该ThreadLocal对象为key查询哈希表中对应的value;

  4. 由于每个线程独占一个threadlocals哈希表,因此线程间ThreadLocal对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。

(二)既然ThreadLocalMapkey是弱引用,GC之后key是否为null

在搞清楚这个问题之前,我们需要先搞清楚Java的四种引用类型

  • 强引用:new出来的对象就是强引用,只要强引用存在,垃圾回收器就永远不会回收被引用的对象,哪怕内存不足的时候。
  • 软引用:使用SoftReference修饰的对象被称为软引用,在内存要溢出的时候软引用指向的对象会被回收。
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,被弱引用指向的对象就会被回收。
  • 虚引用:虚引用是最弱的引用,用PhantomReference进行定。唯一的作用就是用来队列接受对象即将死亡的通知。

这个问题的答案是不为null,从上图的图示就可以直接看出。

(三)ThreadLocal中的内存泄漏问题及JDK处理方法

由图可知,ThreadLocal.ThreadLocalMap 对应的Entry中,key为ThreadLocal对象的弱引用,方法执行对应栈帧中的ThreadLocal引用为强引用。当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了ThreadLocal对象的强引用,即表示该ThreadLocal对象可以被回收了。又因为Entry中key为ThreadLocal对象的弱引用,所以当jvm执行GC操作时是能够回收该ThreadLocal对象的。

Entry中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal对象,是无法释放ThreadLocal.ThreadLocalMap中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象,这样整个threadlocals都被回收了。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。

JDK处理的方法是,在ThreadLocalMap进行set()get()remove()的时候,都会进行清理:

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)//如果key为null,对应的threadlocal对象已经被回收,清理该EntryexpungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}

(四)部分核心源码回顾

ThreadLocalAPI很少就包含了4个,分别是get()set()remove()withInitial(),源码如下:

public T get() {}public void set(T value){}public void remove(){}public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
  • get():从当前线程的 ThreadLocalMap 获取与当前 ThreadLocal 对象对应的值。如果 ThreadLocalMap 中不存在该值,则调用 setInitialValue() 方法进行初始化。
  • set(T value):将当前线程的 ThreadLocalMap 中的值设置为给定的 value。如果当前线程没有 ThreadLocalMap,则会创建一个新的 ThreadLocalMap 并将值设置进去。
  • remove():从当前线程的 ThreadLocalMap 中移除与当前 ThreadLocal 对象对应的值,帮助防止内存泄漏。
  • withInitial(Supplier<? extends T> supplier):返回一个新的 ThreadLocal 对象,其初始值由 Supplier 提供。这允许使用者在创建 ThreadLocal 时指定初始值。

针对这几个源码我们重点进行分析和体会。

ThreadLocal.set()方法源码详解

pubic void set(T value) {// 获取当前线程Thread t = Threac.currentThread();// 获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);// 如果map不为null, 调用ThreadLocalMap.set()方法设置值if (map != null)map.set(this, value);else // map为null,调用createMap()方法初始化创建mapcreateMap(t, value);
}// 返回线程的ThreadLocalMap.threadLocals
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}// 调用ThreadLocalMap构造方法创建ThreadLocalMap
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}// ThreadLocalMap构造方法,传入firstKey, firstValue
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化Entry表的容量 = 16table = new Entry[INITIAL_CAPACITY];// 获取ThreadLocal的hashCode值与运算得到数组下标int i = firsetKey.threadLocalHashCode & (INITAL_CAPACITY - 1);// 通过下标Entry表赋值table[i] = new Entry(firstKey, firstValue);// Entry表存储元素数量初始化为1size = 1;// 设置Entry表扩容阙值 默认为 len * 2 / 3setThreshold(INITIAL_CAPACITY);
}private void setThreshold(int len) {threshold = len * 2 / 3
}

ThreadLocal.set()方法还是很简单的,核心方法在ThreadLocalMap.set()方法

基本流程可总结如下:

ThreadLocalMap.get()方法详解

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;}}// 未找到的话,则调用setInitialValue()方法设置nullreturn setInitialValue();
}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// key相等直接返回if (e != null && e.get() == key)return e;else// key不相等调用getEntryAfterMiss()方法return getEntryAfterMiss(key, i, e);
}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// 迭代往后查找key相等的entrywhile (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;// 遇到key=null的entry,先进行探测式清理工作if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}

主要包含两种情况,一种是hash计算出下标,该下标对应的Entry.key和我们传入的key相等的情况,另外一种就是不相等的情况。

相等情况:相等情况处理很简单,直接返回value,如下图,比如get(ThreadLocal1)计算下标为4,且4存在Entry,且key相等,则直接返回value = 11

不相等情况:不相等情况,以get(ThreadLocal2)为例计算下标为4,且4存在Entry,但key相等,这个时候则为往后迭代寻找key相等的元素,如果寻找过程中发现了有key = null的元素则回进行探测式清理操作。如下图:

迭代到index=5的数据时,此时Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,index 5、8的数据都会被回收,而index 6、7的数据都会前移,此时继续往后迭代,到index = 6的时候即找到了key值相等的Entry数据,如下图:

ThreadLocal.remove()方法源码详解

public void remove() {// 获取当前线程的 ThreadLocalMapThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 如果当前线程有 ThreadLocalMap,则在 map 中移除当前 ThreadLocal 的值m.remove(this);
}static class ThreadLocalMap {// 内部 Entry 类,继承自 WeakReference<ThreadLocal<?>>static class Entry extends WeakReference<ThreadLocal<?>> {// ThreadLocal 对应的值Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 线程局部变量哈希表private Entry[] table;private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 计算当前 ThreadLocal 的哈希值在数组中的索引位置int i = key.threadLocalHashCode & (len - 1);// 从hash获取的下标开始,寻找key相等的entry元素清除for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();  // 清除键的引用expungeStaleEntry(i);  // 清除相应的值return;}}}// 用于计算下一个索引位置private int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}// 清除无效的 Entryprivate void expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清除给定槽位的 Entrytab[staleSlot].value = null;tab[staleSlot] = null;// Rehash until we encounter nullEntry 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;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}}
}

ThreadLocal.remove()核心是调用ThreadLocalMap.remove()方法,流程如下:

  1. 通过hash计算下标。
  2. 从散列表该下标开始往后查key相等的元素,如果找到则做清除操作,引用置为nullGC的时候key就会置为null,然后执行探测式清理处理。

(五)简单的直观体会

以下是 ThreadLocal 的基本使用示例:

package org.zyf.javabasic.thread.threadLocal;/*** @program: zyfboot-javabasic* @description: ThreadLocal 的基本使用示例* @author: zhangyanfeng* @create: 2024-06-02 13:22**/
public class ThreadLocalExample {private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);public static void main(String[] args) {Runnable task = () -> {int value = threadLocal.get();System.out.println(Thread.currentThread().getName() + " initial value: " + value);threadLocal.set(value + 1);System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocal.get());};Thread thread1 = new Thread(task, "Thread 1");Thread thread2 = new Thread(task, "Thread 2");thread1.start();thread2.start();}
}

直接结果查看可感受到其ThreadLocal主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争。

二、基于Threadlocal实现的上下文管理组件ContextManager

在实际开发中,我们经常需要维护一些上下文信息,这样可以避免在方法调用过程中传递过多的参数。例如,当 Web 服务器收到一个请求时,需要解析当前登录状态的用户,并在后续的业务处理中使用这个用户名。如果只需要维护一个上下文数据,如用户名,可以通过方法传参的方式,将用户名作为参数传递给每个业务方法。然而,如果需要维护的上下文信息较多,这种方式就显得笨拙且难以维护。

一个更加优雅的解决方案是使用 ThreadLocal 来实现请求线程的上下文管理。这样,同一线程中的所有方法都可以通过 ThreadLocal 对象直接读取和修改上下文信息,而无需在方法间传递参数。当需要维护多个上下文状态时,可以使用多个 ThreadLocal 实例来存储不同的信息。虽然这种方式在某些情况下也能接受,但在使用线程池时,问题就变得复杂了。因为线程池中的线程会被多个请求重复使用,如何将 ThreadLocal 中的上下文信息从主线程传递到线程池中的工作线程成为一个难题。

基于上述考虑,我们介绍一种基于 ThreadLocal 实现的上下文管理组件 ContextManager,它能够简化上下文信息的管理,并解决线程池环境中的上下文传递问题。

(一)定义 ContextManager

首先,定义一个 ContextManager 类用于管理上下文信息。

package org.zyf.javabasic.thread.threadLocal;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;/*** @program: zyfboot-javabasic* @description: 用于管理上下文信息* @author: zhangyanfeng* @create: 2024-06-02 13:48**/
public class ContextManager {// 静态变量,维护不同线程的上下文private static final ThreadLocal<ContextManager> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();// 实例变量,维护每个上下文中所有的状态数据private final ConcurrentMap<String, Object> values = new ConcurrentHashMap<>();// 获取当前线程的上下文public static ContextManager getCurrentContext() {return CONTEXT_THREAD_LOCAL.get();}// 在当前上下文设置一个状态数据public void set(String key, Object value) {if (value != null) {values.put(key, value);} else {values.remove(key);}}// 在当前上下文读取一个状态数据public Object get(String key) {return values.get(key);}// 开启一个新的上下文public static ContextManager beginContext() {ContextManager context = CONTEXT_THREAD_LOCAL.get();if (context != null) {throw new IllegalStateException("A context is already started in the current thread.");}context = new ContextManager();CONTEXT_THREAD_LOCAL.set(context);return context;}// 关闭当前上下文public static void endContext() {CONTEXT_THREAD_LOCAL.remove();}
}

(二)使用 ContextManager 进行上下文管理

假设我们有一个在线商城系统,用户在进行购物时需要进行身份认证,并且在用户进行购物操作时,需要记录用户的购物车信息。我们可以使用 ContextManager 类来管理用户的上下文信息。

package org.zyf.javabasic.thread.threadLocal;import org.zyf.javabasic.skills.reflection.dto.Product;/*** @program: zyfboot-javabasic* @description: 用户在进行购物时需要进行身份认证,并且在用户进行购物操作时,需要记录用户的购物车信息。* @author: zhangyanfeng* @create: 2024-06-02 14:02**/
public class ShoppingCartService {public void addToCart(Product product, int quantity) {// 开启一个新的上下文ContextManager.beginContext();try {// 将用户ID和商品信息设置到当前上下文中ContextManager.getCurrentContext().set("userId", getCurrentUserId());ContextManager.getCurrentContext().set("product", product);ContextManager.getCurrentContext().set("quantity", quantity);// 执行添加到购物车的逻辑// 这里可以调用其他方法,或者执行其他操作System.out.println("Adding product to cart...");checkout();} finally {// 关闭当前上下文ContextManager.endContext();}}public void checkout() {// 从当前上下文中读取用户ID和购物车信息String userId = (String) ContextManager.getCurrentContext().get("userId");Product product = (Product) ContextManager.getCurrentContext().get("product");int quantity = (int) ContextManager.getCurrentContext().get("quantity");// 执行结账逻辑// 这里可以根据购物车信息进行结账操作System.out.println("Checking out...");System.out.println("User ID: " + userId);System.out.println("Product: " + product.getName());System.out.println("Quantity: " + quantity);}private String getCurrentUserId() {// 模拟获取当前用户ID的方法return "user123";}public static void main(String[] args) {ShoppingCartService shoppingCartService = new ShoppingCartService();Product product = new Product();product.setName("iPhone");product.setId(1000);shoppingCartService.addToCart(product, 1);}
}

在这个示例中,ShoppingCartService 类模拟了一个购物车服务。在 addToCart() 方法中,我们开启了一个新的上下文,并将当前用户ID、商品信息和购买数量设置到上下文中。在 checkout() 方法中,我们从当前上下文中读取了用户ID、商品信息和购买数量,并执行了结账操作。

通过使用 ContextManager 类,我们可以轻松地在购物车服务中管理用户的上下文信息,而无需手动传递参数。

(三)扩展 ContextManager 的使用方式

我们可以给 ContextManager 添加类似的静态方法,以简化代码的书写。当前请视业务情况进行应用和分析。

package org.zyf.javabasic.thread.threadLocal;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;/*** @program: zyfboot-javabasic* @description: 用于管理上下文信息* @author: zhangyanfeng* @create: 2024-06-02 13:48**/
public class ContextManager {// 其他省去// 执行带有新的上下文的任务public static <X extends Throwable> void runWithNewContext(Runnable task) throws X {beginContext();try {task.run();} finally {endContext();}}// 在新的上下文中执行任务,并返回结果public static <T, X extends Throwable> T supplyWithNewContext(Supplier<T> supplier) throws X {beginContext();try {return supplier.get();} finally {endContext();}}
}

三、在线程池中传递ContextManager

我们通过 ThreadLocal 实现了一个自定义的上下文管理组件 ContextManager,并通过 ContextManager.set()ContextManager.get() 方法在同一个线程中读写上下文中的状态数据。

现在,我们需要扩展这个功能,使其在一个线程执行过程中开启了一个 ContextManager,随后使用线程池执行任务时,也能获取到当前 ContextManager 中的状态数据。这在如下场景中很常见:服务收到一个用户请求,通过 ContextManager 将登录态数据存储到当前线程的上下文中,随后使用线程池执行一些耗时操作,并希望线程池中的线程也能访问这些登录态数据。

由于线程池中的线程和请求线程不是同一个线程,按照目前的实现,线程池中的线程无法访问请求线程的上下文数据。

为了解决这个问题,我们可以在提交 Runnable 时,将当前的 ContextManager 引用存储在 Runnable 对象中。当线程池中的线程开始执行时,将 ContextManager 替换到执行线程的上下文中,执行完成后再恢复原来的上下文。

(一)增加静态方法,用于在已有的上下文中执行任务

首先,添加静态方法 runWithExistingContextsupplyWithExistingContext,用于在指定的上下文中执行任务:

package org.zyf.javabasic.thread.threadLocal;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;/*** @program: zyfboot-javabasic* @description: 用于管理上下文信息* @author: zhangyanfeng* @create: 2024-06-02 13:48**/
public class ContextManager {// 省略public static <X extends Throwable> void runWithExistingContext(ContextManager context, Runnable task) throws X {supplyWithExistingContext(context, () -> {task.run();return null;});}public static <T, X extends Throwable> T supplyWithExistingContext(ContextManager context, Supplier<T> supplier) throws X {ContextManager oldContext = CONTEXT_THREAD_LOCAL.get();CONTEXT_THREAD_LOCAL.set(context);try {return supplier.get();} finally {if (oldContext != null) {CONTEXT_THREAD_LOCAL.set(oldContext);} else {CONTEXT_THREAD_LOCAL.remove();}}}}

(二)自定义线程池实现

创建一个自定义线程池 ContextAwareThreadPoolExecutor,确保任务在执行时可以正确传递和恢复上下文信息:

package org.zyf.javabasic.thread.threadLocal;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;import static org.zyf.javabasic.thread.threadLocal.ContextManager.runWithExistingContext;/*** @program: zyfboot-javabasic* @description: 自定义线程池 ContextAwareThreadPoolExecutor* @author: zhangyanfeng* @create: 2024-06-02 20:23**/
public class ContextAwareThreadPoolExecutor extends ThreadPoolExecutor {public ContextAwareThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}public static ContextAwareThreadPoolExecutor newFixedThreadPool(int nThreads) {return new ContextAwareThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());}@Overridepublic void execute(Runnable command) {ContextManager context = ContextManager.getCurrentContext();super.execute(() -> runWithExistingContext(context, command::run));}
}

(三)测试自定义线程池

验证 ContextAwareThreadPoolExecutor 是否正确传递和恢复上下文:

package org.zyf.javabasic.thread.threadLocal;import org.junit.Test;import java.util.concurrent.ExecutorService;/*** @program: zyfboot-javabasic* @description: 验证 ContextAwareThreadPoolExecutor 是否正确传递和恢复上下文* @author: zhangyanfeng* @create: 2024-06-02 20:25**/
public class ContextManagerTest {@Testpublic void testContextAwareThreadPoolExecutor() {ContextManager.beginContext();try {ContextManager.getCurrentContext().set("key", "value out of thread pool");Runnable r = () -> {String value = (String) ContextManager.getCurrentContext().get("key");System.out.println("Value in thread pool: " + value);};ExecutorService executor = ContextAwareThreadPoolExecutor.newFixedThreadPool(10);executor.execute(r);executor.submit(r);} finally {ContextManager.endContext();}/** 执行结果* Value in thread pool: value out of thread pool* Value in thread pool: value out of thread pool*/}@Testpublic void testContextAwareThreadPoolExecutorWithNewContext() {ContextManager.runWithNewContext(() -> {ContextManager.getCurrentContext().set("key", "value out of thread pool");Runnable r = () -> {String value = (String) ContextManager.getCurrentContext().get("key");System.out.println("Value in thread pool: " + value);};ExecutorService executor = ContextAwareThreadPoolExecutor.newFixedThreadPool(10);executor.execute(r);executor.submit(r);});/** 执行结果* Value in thread pool: value out of thread pool* Value in thread pool: value out of thread pool*/}
}

验证ContextAwareThreadPoolExecutor 是否能正确传递和恢复上下文信息。测试用例涵盖了两种情况:

  1. 在当前上下文中执行任务,并使用自定义线程池执行任务。
  2. 在新的上下文中执行任务,并使用自定义线程池执行任务。

这两种情况覆盖了在不同上下文环境中使用线程池的情况,确保了上下文信息能够正确传递和恢复。因此,验证内容是完备的,没有问题。

四、总结

探讨如何基于 ThreadLocal 实现一个高效的上下文管理组件,以解决多线程环境下的数据共享和上下文管理这些问题。通过具体的代码示例和实战展示 ThreadLocal 如何为多线程编程提供一种简洁而高效的上下文管理方案。

参考文章

https://www.cnblogs.com/wupeixuan/p/12638203.html

一张图看懂Java中的ThreadLocal原理_threadlocal原理图解-CSDN博客

ThreadLocal原理 · 进击的java菜鸟

一文搞懂ThreadLocal原理-51CTO.COM

滑动验证页面

基于 ThreadLocal 实现一个上下文管理组件(附源码)

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

公司面试题总结(五)

25.谈一谈箭头函数与普通函数的区别&#xff0c;箭头函数主要解决什么问题&#xff1f; 箭头函数与普通函数的区别&#xff1a; ⚫ 语法简洁性&#xff1a; ◼ 箭头函数使用>符号定义&#xff0c;省略了 function 关键字&#xff0c;使得语法更为紧凑。 ◼ 对于单行函…

Adobe illustrator教程——超实用的三个进阶小技巧!

AI2024(64bit) Adobe illustrator 软件安装包下载地址&#xff1a; 百度网盘下载https://pan.baidu.com/s/1C10-2JVN1rxFF5VFRuV2Yw?pwdSIMS 01 进阶技巧1——曲率工具 基于之前的入门教程&#xff0c;大家肯定会快速想到“画笔工具”&#xff0c;但是画出来的曲线往往不够平…

12.实战私有数据微调ChatGLM3

实战私有数据微调ChatGLM3 实战私有数据微调ChatGLM3实战构造私有的微调数据集基于 ChatGPT 设计生成训练数据的 Prompt使用 LangChain GPT-3.5-Turbo 生成训练数据样例训练数据解析、数据增强和持久化存储自动化批量生成训练数据集流水线提示工程&#xff08;Prompt Engineer…

Linux操作系统学习路线

本文来自Qwen2大模型&#xff1a; Linux操作系统的全面学习是一个渐进的过程&#xff0c;涵盖从基础知识到高级特性的多个阶段。以下是一份详细的Linux操作系统学习路线图&#xff0c;包括各个阶段的学习目标、建议的学习资源和实践步骤。 1. Linux 基础知识与安装 学习目标&a…

《软件定义安全》之八:软件定义安全案例

第8章 软件定义安全案例 1.国外案例 1.1 Fortinet&#xff1a;传统安全公司的软件定义方案 Fortinet的软件定义安全架构强调与数据中心的结合&#xff0c;旨在将安全转型为软件定义的模式&#xff0c;使安全运维能够与数据中心的其他部分一样灵活、弹性。在Fortinet看来&…

单链表经典算法题 1

前言 学习了单链表&#xff0c;我们就做一些题来巩固一下。还有就是解题方法不唯一&#xff0c;我就只讲述为自己的方法。 目录 前言 1.移除链表元素 思路 代码 2.反转链表 思路 代码 3.链表的中间节点 思路 代码 总结 1.移除链表元素 思路 我们创建一个新的表…

GUI初步开始(matlab)

GUI初步开始&#xff08;matlab&#xff09; &#xff08;自用笔记&#xff09; 打工人艰辛速成&#xff0c;花几个小时从零到能用&#xff0c;记录下details and problems&#xff1a; 甲方要求&#xff1a;GUI界面&#xff0c;读下位机&#xff0c;找到解码后格式中所需要的…

搭建WWW服务

1.实验环境的配置 【1】设置windows虚拟机server和test网络属性 打开虚拟机的【开始】菜单->【控制面板】->【网络连接】窗口。 1. 选中【本地连接】右击鼠标&#xff0c;选中【属性】&#xff0c;打开【本地连接属性】窗口。 2. 选择【网络】页签。 3. 在【此连接使…

基于文本和图片输入的3D数字人化身生成技术解析

随着虚拟现实、增强现实和元宇宙等技术的飞速发展,对高度逼真且具有表现力的3D数字人化身的需求日益增长。传统的3D数字人生成方法往往需要依赖大量的3D数据集,这不仅增加了数据收集和处理的成本,还限制了生成的多样性和灵活性。为了克服这些挑战,我们提出了一种基于文本提…

刚刚!彬川机器人社招校招入职Verify测评素质性格测评真题原题题库更新了【含答案】

一、测评环境 温馨提示 1.本次测评包含【素质性格测评】和【Verify测评】两部分&#xff0c;预计用时60min&#xff0c;请确保作答时周围环境无干扰、网络畅通&#xff1b; 2.请使用电脑完成作答&#xff0c;建议使用以下浏览器登录&#xff1a;IE9.0及以上版本&#xff0c;火…

5. 条件和递归

5. 条件和递归 本章主要话题是if表达式, 它根据程序的状态执行不同的代码. 但首先介绍两个操作符号: 向下取整除法操作符和求模操作符.5.1 向下取整除法操作符和求模操作符 向下取整除法操作符(//)对两个数除法运算, 并向下取整得到一个整数. 假设, 一个电影的播放时长为105分…

94. 二叉树的中序遍历(Swift实现, 迭代)

题目描述 使用迭代方法解题 class TreeNode {var val: Intvar left: TreeNode?var right: TreeNode?init(_ val: Int) {self.val valself.left nilself.right nil} }func inorderTraversal(_ root: TreeNode?) -> [Int] {var result [Int]() // 用于存储中序遍历…

day37| 435. 无重叠区间 763.划分字母区间 56. 合并区间 738.单调递增的数字

文章目录 前言435. 无重叠区间思路方法一方法二 763.划分字母区间思路方法二 补充内容 重叠区间 56. 合并区间思路方法一 我自己写的方法二 教程的思路【更巧妙&#x1f636;】 738.单调递增的数字思路方法一方法二 使用list、不使用flag 总结 前言 435. 无重叠区间 注意&…

【PL理论】(22) 函数式语言:多参数 | 柯里化 (Currying) : 将多参数函数实现为返回一个函数的函数

&#x1f4ad; 写在前面&#xff1a;本章我们将继续讲解函数式语言&#xff0c;介绍多参数&#xff0c;着重讲解柯里化的概念&#xff0c;将多参数函数实现为返回一个函数的函数。 目录 0x00 多参数&#xff08;Multiple Arguments&#xff09; 0x01 柯里化&#xff08;Curr…

【车载音视频电脑】双卡式行车记录仪,带AI识别分析,支持4路AHD 1080p高清输入

一、产品外观 外观专利设计&#xff0c;铝合金材质&#xff0c;散热好、小巧、易安装&#xff1b;塑胶前面板&#xff0c;美观简洁大方&#xff0c;有独立锁。 二、产品特点 支持4路AHD高清输入1080P*30FPS、720P、D1、CIF分辨率等&#xff1b;支持接IPC&#xff0c;用网口&a…

Java | Leetcode Java题解之第149题直线上最多的点数

题目&#xff1a; 题解&#xff1a; class Solution {public int maxPoints(int[][] points) {int n points.length;if (n < 2) {return n;}int ret 0;for (int i 0; i < n; i) {if (ret > n - i || ret > n / 2) {break;}Map<Integer, Integer> map ne…

VScode中连接并使用docker容器

前提条件&#xff1a; 1.在windows下安装Docker Desktop(方法可见下面的教程) Docker Desktop 安装使用教程-CSDN博客 2.在vscode安装3个必备的插件 3.先在ubuntu中把docker构建然后运行 4.打开vscode&#xff0c;按下图顺序操作 调试好之后上传到git上&#xff0c;然后后面…

算法day29

第一题 695. 岛屿的最大面积 本题解法&#xff1a;采用bfs的算法&#xff1b; 本题使用象限数组的遍历方法和定义布尔数组vis来遍历每一个元素的上下左右元素&#xff0c;防治被遍历的元素被二次遍历&#xff1b; 本题具体分析如上题故事&#xff0c;但是由于要求区域的最大面…

5.7 Python内置函数

文章目录 1. 内置模块Aabs()all()any()ascii() Bbin()bool()bytearra()bytes() Ccallable()chr()classmethod()compile()complex() Ddelattr()dict()dir()divmod() Eenumerate()eval()exec()execfile() Ffile()filter()float()format()frozenset() Ggetattr()globals() Hhasatt…

django学习入门系列之第二点《浏览器能识别的标签3》

文章目录 列表表格往期回顾 列表 无序列表 <!-- <ul </ul> 无序列表 --> <ul><li> 内容1 </li><li> 内容2 </li><li> 内容3 </li><li> 内容4 </li> </ul>有序列表 <!-- <ol> &…