实战指南:理解 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
是如何实现这一特性的呢?基本原理实现如下:
-
每个
Thread
对象中都包含一个ThreadLocal.ThreadLocalMap
类型的threadlocals
成员变量; -
该map对应的每个元素
Entry
对象中:key是ThreadLocal
对象的弱引用,value是该threadlocal
变量在当前线程中的对应的变量实体; -
当某一线程执行获取该
ThreadLocal
对象对应的变量时,首先从当前线程对象中获取对应的threadlocals
哈希表,再以该ThreadLocal
对象为key查询哈希表中对应的value; -
由于每个线程独占一个
threadlocals
哈希表,因此线程间ThreadLocal
对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。
(二)既然ThreadLocalMap
的key
是弱引用,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;
}
(四)部分核心源码回顾
ThreadLocal
的API
很少就包含了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()
方法,流程如下:
- 通过
hash
计算下标。 - 从散列表该下标开始往后查
key
相等的元素,如果找到则做清除操作,引用置为null
,GC
的时候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
替换到执行线程的上下文中,执行完成后再恢复原来的上下文。
(一)增加静态方法,用于在已有的上下文中执行任务
首先,添加静态方法 runWithExistingContext
和 supplyWithExistingContext
,用于在指定的上下文中执行任务:
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
是否能正确传递和恢复上下文信息。测试用例涵盖了两种情况:
- 在当前上下文中执行任务,并使用自定义线程池执行任务。
- 在新的上下文中执行任务,并使用自定义线程池执行任务。
这两种情况覆盖了在不同上下文环境中使用线程池的情况,确保了上下文信息能够正确传递和恢复。因此,验证内容是完备的,没有问题。
四、总结
探讨如何基于 ThreadLocal
实现一个高效的上下文管理组件,以解决多线程环境下的数据共享和上下文管理这些问题。通过具体的代码示例和实战展示 ThreadLocal
如何为多线程编程提供一种简洁而高效的上下文管理方案。
参考文章
https://www.cnblogs.com/wupeixuan/p/12638203.html
一张图看懂Java中的ThreadLocal原理_threadlocal原理图解-CSDN博客
ThreadLocal原理 · 进击的java菜鸟
一文搞懂ThreadLocal原理-51CTO.COM
滑动验证页面
基于 ThreadLocal 实现一个上下文管理组件(附源码)
相关文章:
实战指南:理解 ThreadLocal 原理并用于Java 多线程上下文管理
目录 一、ThreadLocal基本知识回顾分析 (一)ThreadLocal原理 (二)既然ThreadLocalMap的key是弱引用,GC之后key是否为null? (三)ThreadLocal中的内存泄漏问题及JDK处理方法 &…...
Spark 内存管理机制
Spark 内存管理 堆内内存和堆外内存 作为一个 JVM 进程,Executor 的内存管理建立在 JVM(最小为六十四分之一,最大为四分之一)的内存管理之上,此外spark还引入了堆外内存(不在JVM中的内存),在spark中是指不…...
【Maven】继承和聚合
5. Maven的继承和聚合 5.1 什么是继承 Maven 的依赖传递机制可以一定程度上简化 POM 的配置,但这仅限于存在依赖关系的项目或模块中。当一个项目的多个模块都依赖于相同 jar 包的相同版本,且这些模块之间不存在依赖关系,这就导致同一个依赖…...
NViST运行笔记
文章标题: NViST: In the Wild New View Synthesis from a Single Image with Transformers 1. 环境配置 创建环境 conda create -n nvist python3.9 进入环境 conda activate nvist 安装torch torchvision torchaudio pip install torch2.1.2 torchvision0…...
性能测试工具Grafana、InfluxDB和Collectd的搭建
一、性能监控组成简介 1、监控能力分工:这个系统组合能够覆盖从数据采集、存储到可视化的整个监控流程。Collectd可以收集各种系统和应用的性能指标,InfluxDB提供高效的时序数据存储,而 Grafana 则将这些数据以直观的方式呈现出来。2,实时性能监控:对于需要实时了解系统状…...
JS中的类与对象
面向对象是使用最广泛的一种编程范式,最具代表性的面向对象语言就是Java和C,在它们的理念中,面向对象的三大特性:封装,继承,多态。类,对象,公有/私有方法/属性,各种继承就…...
域名解析系统 DNS
1.域名系统概述 用户与互联网上某台主机通信时,必须要知道对方的IP地址。然而用户很难记住长达32 位的二进制主机地址。即使是点分十进制地址也并不太容易记忆。但在应用层为了便于用户记忆各种网络应用,连接在互联网上的主机不仅有P地址,而…...
Flutter 1.1:下载Flutter环境
1、在AS中下载Flutter插件 在setting的Plugins中下载Flutter,如图所示,可以直接进行搜索查找 2、下载flutter的sdk源代码 flutter中文文档学习 通过Git下载SDK源代码 git clone -b stable https://github.com/flutter/flutter.git3、配置系统变量 3…...
HTML5系列(6)-- 拖放 API 实战指南
前端技术探索系列:HTML5 拖放 API 实战指南 🎯 致读者:探索现代交互技术 👋 前端开发者们, 今天我们将深入探讨 HTML5 中一个强大而实用的特性 —— 拖放 API。这项技术能够让我们创建更加直观和交互性强的用户界面…...
windows下kafka初体验简易demo
这里提供了windows下的java1.8和kafka3.9.0版本汇总,可直接免费下载 【免费】java1.8kafka2.13版本汇总资源-CSDN文库 解压后可以得到一个文件夹 资料汇总内有一个kafka文件资料包.tgz,解压后可得到下述文件夹kafka_2.13-3.9.0,资料汇总内还…...
算法训练(leetcode)二刷第三十三天 | *322. 零钱兑换、*279. 完全平方数、*139. 单词拆分
刷题记录 *322. 零钱兑换*279. 完全平方数*139. 单词拆分 *322. 零钱兑换 leetcode题目地址 dp[j]存储amount为j时所需要的最少硬币数。当j为0时需要0个硬币,因此dp[0]赋值为0. 因为是取最少硬币数,因此初始化需要赋值一个最大值。 状态转移方程&…...
windows的pip镜像源配置
Windows 中 pip 镜像源配置 在 Windows 系统中,为了提高 pip 包的安装速度,我们可以配置 pip 的镜像源。以下是具体的配置步骤: 创建文件夹 在 C:\Users\Administrator\pip 路径下创建一个名为 pip.ini 的文件。 编辑 pip.ini 文件 使用文本…...
Django Rest Framework中嵌套关系的JSON序列化
在 Django Rest Framework (DRF) 中,处理嵌套关系的 JSON 序列化是一个常见需求。以下是如何实现嵌套关系序列化的详细说明,包括序列化器定义、模型关系以及常见用法。 1、问题背景 假设我们有以下两个模型: class Jobdtl(models.Model):jo…...
ONVIF协议网络摄像机客户端使用gsoap获取RTSP流地址GStreamer拉流播放
什么是ONVIF协议 ONVIF(开放式网络视频接口论坛)是一个全球性的开放式行业论坛,旨在促进开发和使用基于物理IP的安全产品接口的全球开放标准。 ONVIF规范的目标是建立一个网络视频框架协议,使不同厂商生产的网络视频产品完全互通。…...
40分钟学 Go 语言高并发:Go程序性能优化方法论
Go程序性能优化方法论 一、性能指标概述 指标类型关键指标重要程度优化目标CPU相关CPU使用率、线程数、上下文切换⭐⭐⭐⭐⭐降低CPU使用率,减少上下文切换内存相关内存使用量、GC频率、对象分配⭐⭐⭐⭐⭐减少内存分配,优化GC延迟指标响应时间、处理延…...
MySQL基础(语句)知识复习 (除索引和视图)
1.客户端和数据库操作 1.登录客户端界面:mysql -uroot -p 2.查看当前的数据库版本:select version(); 3.显示所有数据库:show databases;, 4.创建数据库:create [IF NOT EXISTS] database 库名 character set 字符…...
【sqlcipher】pc端sqflite使用过程中遇到的问题
在flutter中使用sqlcipher时 Mac上如果通过flutter带的文件管理api(即File的delete()方法)删除数据库文件,再创建同名的数据文件的话,必现readonly问题, 这里需要注意的一点是 DatabaseFactory 在Mac上直接使用全局的…...
Vue 实现无线滚动效果
目录 1.Element-plus官网中的Infinite Scroll组件说明 2.滚动条设置 3.滚动到底部的函数调用 1.Element-plus官网中的Infinite Scroll组件说明 官网链接如下所示: Infinite Scroll 无限滚动 | Element Plus 首先查看该代码,发现这个组件使用了一个…...
【CSS in Depth 2 精译_062】第 10 章 CSS 中的容器查询(@container)概述 + 10.1 容器查询的一个简单示例
当前内容所在位置(可进入专栏查看其他译好的章节内容) 【第十章 CSS 容器查询】 ✔️ 10.1 容器查询的一个简单示例 ✔️ 10.1.1 容器尺寸查询的用法 ✔️ 10.2 深入理解容器10.3 与容器相关的单位10.4 容器样式查询的用法10.5 本章小结 文章目录 第 10…...
conda手动初始化
问题:环境中存在conda但是conda无法使用 方法: 进入到anaconda目录下, 进入bin目录, 然后执行 source activate要想启动时自动进入conda环境, 需要在 ~/.bashrc中添加如下命令 # >>> conda initialize >>> # !! Contents within this block are managed by …...
hhdb数据库介绍(10-28)
管理 管理菜单主要囊括对业务数据进行管理的功能,例如对数据的备份恢复或执行业务表的DDL语句等操作。 数据对象 数据对象功能可以帮助用户通过列表实时查看当前已存在的数据对象,了解业务数据的整体情况。提供了对数据对象的筛选、统计、关联、详情等…...
Spring Boot自定义启动banner
在启动 Springboot 应用时,默认情况下会在控制台打印出 Springboot 相关的banner信息。 自定义banner 如果你想自定义一个独特的启动banner,该怎么做呢?Springboot 允许我们通过自定义启动banner来替换默认的banner。只需要在 resources 目…...
c语言——数组名该如何理解呢?
一般情况下,数组名表示首元素地址,以下2种除外: ①、sizeof(数组名) 表示整个数组 ※只有数组名的情况 sizeof(数组名i) 就不能表示整个数组 ②、&数组名 表示整个数组,取的是整个数…...
前端 如何用 div 标签实现 步骤审批
在前端实现一个步骤审批流程,通常是通过 div 标签和 CSS 来构建一个可视化的流程图,结合 JavaScript 控制审批的状态变化。你可以使用 div 标签创建每一个步骤节点,通过不同的样式(如颜色、边框等)表示审批的不同状态&…...
QT工程,它该怎么学?
在现代软件开发中,QT因其强大的跨平台能力和友好的用户界面设计工具,成为开发者学习和应用的热门选择。特别是在Linux系统下,如何安装、配置QT开发环境,以及创建和管理QT工程是入门QT开发的关键环节。本文将从安装QT开发环境开始&…...
第426场周赛:仅含置位位的最小整数、识别数组中的最大异常值、连接两棵树后最大目标节点数目 Ⅰ、连接两棵树后最大目标节点数目 Ⅱ
Q1、仅含置位位的最小整数 1、题目描述 给你一个正整数 n。 返回 大于等于 n 且二进制表示仅包含 置位 位的 最小 整数 x 。 置位 位指的是二进制表示中值为 1 的位。 2、解题思路 我们需要找到一个整数 x,使得: x ≥ nx 的二进制表示中仅包含置位…...
23种设计模式之外观模式
目录 1. 简介2. 代码2.1 SelectFoodService (选择食品)2.2 PayService (支付服务)2.3 TakeService (制作服务)2.4 OrderService (下单服务)2.5 Food (食品)2.6 TackingSystem (外观类)2.7 Test (测试类) 3. 优缺点3. 总结 1. 简介…...
【智商检测——DP】
题目 代码 #include <bits/stdc.h> using namespace std; const int N 1e510, M 110; int f[N][M]; int main() {int n, k;cin >> n >> k;for(int i 1; i < n; i){int x;cin >> x;f[i][0] __gcd(f[i-1][0], x);for(int j 1; j < min(i, k)…...
LeetCode-430. 扁平化多级双向链表-题解
题目链接 430. 扁平化多级双向链表 - 力扣(LeetCode) 题目介绍 你将得到一个双链表,节点包含一个“下一个”指针、一个“前一个”指针和一个额外的“子指针”。这个子指针可能指向一个单独的双向链表,并且这些链表也包含类似的特殊…...
【CSS】一篇掌握CSS
不是因为有了希望才去坚持,而是坚持了才有了希望 目录 一.导入方式 1.行内样式 2.内部样式 3.外部样式(常用) 二.选择器 1.基本选择器(常用) 1.1标签选择器 1.2类选择器 1.3id选择器 2.层次选择器 2.1后代选择器 2.2子选择器 2.3相邻兄弟选择器 2.4通用兄弟选择器…...
华为仓颉编程环境搭建
1、仓颉介绍 摘自华为官方:仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。 其具体特性表现为&am…...
手机实时提取SIM卡打电话的信令声音-蓝牙电话如何适配eSIM卡的手机
手机实时提取SIM卡打电话的信令声音 --蓝牙电话如何适配eSIM卡的手机 一、前言 蓝牙电话的海外战略中,由于海外智能手机市场中政策的差异性,对内置eSIM卡的手机进行支持是非常合理的需求。Android系列手机中,无论是更换通信运营商…...
三种方式(oss、本地、minio)图片的上传下载
一、OSS 1、前期准备 1.1 注册阿里云账号,开启对象存储oss功能,创建一个bucket(百度教程多的是,跟着创建一个就行,创建时注意存储类型是标准存储,读写权限是公共读) 有的在创建桶时读写属性是…...
使用pyQT完成简单登录界面
import sysfrom PyQt6.QtGui import QMovie,QPixmap from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton,QLineEdit#封装我的窗口类 class MyWidget(QWidget):#构造函数def __init__(self):#初始化父类super().__init__()# 设置窗口大小self.resize(330,…...
Postgres数据库自动化分区
一.创建自动化分区配置表并插入数据 -- Table: managerdb.par_info-- DROP TABLE IF EXISTS managerdb.par_info;CREATE TABLE IF NOT EXISTS managerdb.par_info (table_schema character varying(255) COLLATE pg_catalog."default" NOT NULL,table_name characte…...
【技术介绍】C++编程语言中的瑰宝
C,这门源于C语言并在其基础上进行大幅增强的编程语言,自诞生以来便以其独特的魅力和强大的功能吸引了无数编程者的目光。它不仅是计算机科学领域的一颗璀璨明珠,更是现代软件开发中不可或缺的重要工具。 解析【前言】 C的命名,寓…...
nginx反向代理
目录 环境准备 启动HTTP服务 配置Nginx 访问 部署 1.配置nginx 2.自动化脚本 3.执行脚本 4.使用ansible 什么是反向代理呢,参考nginx反向代理,业务部署过长中,常遇到的场景如下,通过访问域名/ip地址,后面接入网…...
分层图最短路
常见情形: 对于边有k次操作的题。。 整体思想: 分层图最短路可以视作是dijkstra的一个扩展,通常用于处理N小于10000,或者是k不大的情形。整体有点类似于拆点。将一个点拆成k个点处理。层与层之间互不影响。 好了我就说这么多&…...
FRU文件
FRU(Field Replaceable Unit)源文件的格式通常遵循IPMI FRU Information Storage Definition标准。在实际应用中,FRU源文件可以是JSON格式的,这种格式允许用户指定所有的FRU信息字段。以下是FRU源文件的JSON格式的一些关键点&…...
兔子繁衍问题
7-2 兔子繁衍问题 分数 15 全屏浏览 切换布局 作者 徐镜春 单位 浙江大学 一对兔子,从出生后第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子。假如兔子都不死,请问第1个月出生的一对兔子,至少需要繁衍到第几个月时兔…...
飞凌嵌入式受邀亮相OpenHarmony人才生态大会2024
2024年11月27日,OpenHarmony人才生态大会2024在武汉洲际酒店举行。在这场汇聚了行业精英、技术大咖及生态伙伴的年度盛会上,飞凌嵌入式作为OpenHarmony社区的重要成员受邀出席,并展示了其在OpenHarmony 4.1系统适配方面的最新成果。 在大会的…...
Resrful控制器
Linux Debian 包管理器 apt DebianUbuntuKali红帽子 包管理器dnf或者yum RHELFedroaCentos Stream RHEL上游版本,就是什么新的内容、特性会在这个上面进行测试 运行 运行页面--dotnet blog.dll配置管理 server{listen 80;server_name m.域名;location / {proxy_p…...
Python练习(2)
重复元素判定续。利用集合的无重复性来编写一个程序如果有一个元素出现了不止一次则返回true但不要改变原来列表的值: 一: def has_duplicates(lst): # 使用集合来存储已经见过的元素 seen set() for item in lst: if item in seen: # 如果元素已经在…...
Qt清空文件夹下的内容
Qt清空文件夹下的内容 你可以使用 QDir 类来清空文件夹下的所有内容。以下是一个示例,展示了如何删除指定文件夹中的所有文件和子文件夹: #include <QCoreApplication> #include <QDir> #include <QFileInfoList> #include <QDeb…...
如何手动设置ubuntu服务器的ip、子网掩码、网关、DNS
在 Ubuntu 服务器上手动设置 IP 地址、子网掩码、网关和 DNS,通常有两种方式:使用传统的 ifconfig 命令和配置文件,或者使用现代的 netplan 配置方式(对于 Ubuntu 17.10 及以后版本,netplan 是默认的网络配置工具&…...
单片机状态机实现多个按键同时检测单击、多击、长按等操作
1.背景 在之前有个项目需要一个或多个按键检测:单击、双击、长按等操作 于是写了一份基于状态机的按键检测,分享一下思路 2.实现效果 单击翻转绿灯电平 双击翻转红灯电平 长按反转红绿灯电平 实现状态机检测按键单击,双击,长…...
graph rag都能做哪些事情
从提供的项目目录结构看,系统具备高复杂度和模块化的设计,可能用于大规模数据处理、知识图谱构建、自然语言处理等方面。以下是一些推理出的核心能力和应用场景: 1. 核心模块能力: API 层 (api) 主要用于对外接口的定义和服务调…...
Linux 用户和用户组管理
Linux 用户和用户组管理 Linux系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。 用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪&…...
Python酷库之旅-第三方库Pandas(251)
目录 一、用法精讲 1186、pandas.tseries.offsets.BusinessMonthEnd.is_year_start方法 1186-1、语法 1186-2、参数 1186-3、功能 1186-4、返回值 1186-5、说明 1186-6、用法 1186-6-1、数据准备 1186-6-2、代码示例 1186-6-3、结果输出 1187、pandas.tseries.offs…...
利用Ubuntu批量下载modis图像(New)
由于最近modis原来批量下载的代码不再直接给出,因此,再次梳理如何利用Ubuntu下载modis数据。 之前的下载代码为十分长,现在只给出一部分,需要自己再补充另一部分。之前的为: 感谢郭师兄的指导(https://blo…...