Java - ThreadLocal数据存储和传递方式的演变之路
Java - ThreadLocal数据存储和传递方式的演变之路
- 前言
- 一. InheritableThreadLocal - 父子线程数据传递
- 1.1 父子线程知识预热和 InheritableThreadLocal 实现原理
- 1.2 InheritableThreadLocal 的诟病
- 二. TransmittableThreadLocal (TTL) 横空出世
- 2.1 跨线程变量传递测试案例
- 2.2 TTL的基本原理
- 2.2.1 静态属性 holder 和 threadLocalHolder
- 2.2.2 静态内部类 Transmitter
- 2.3 总结
前言
我在 Java - ThreadLocal原理 这篇文章里面主要介绍了ThreadLocal的使用、基本原理。不过有一点需要在这里做个特殊提醒:
ThreadLocal仅仅用于单线程内的上下文数据传递。多线程情况下数据隔离。
但是现实往往并没有那么简单,我们开发过程中,往往都有多线程的场景。有时候更需要一些变量在多线程中传递。那ThreadLocal显然并不满足这样的场景,那么我们来看看它的一个 “演变之路”。
一. InheritableThreadLocal - 父子线程数据传递
我们先用ThreadLocal来演示一个小Demo,我们先准备一个实体类User:
@Data
@ToString
public class User {private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();private String name;private Integer age;public static void setAge(Integer age) {THREAD_LOCAL.set(age);System.out.println("当前线程: " + Thread.currentThread().getName() + ", 赋值: " + age);}public static Integer getAge() {Integer res = Optional.ofNullable(THREAD_LOCAL.get()).orElse(0);System.out.println("当前线程: " + Thread.currentThread().getName() + ", 获取到的值: " + res);return res;}
}
然后编写测试用例:
public static void main(String[] args) throws InterruptedException {User.setAge(100);// 异步获取Thread t = new Thread(() -> User.getAge());t.start();t.join();// 同步获取User.getAge();System.out.println("**************Finish****************");
}
执行结果如下:

我们得出以下结论:
- 我们在主线程中往
ThreadLocal塞的值,只有主线程才能看到,子线程看不到。 - 父子线程之间,使用
ThreadLocal无法进行数据传递。线程隔离。
1.1 父子线程知识预热和 InheritableThreadLocal 实现原理
在上面的案例中,我们在主线程中new了一个子线程。结合结果图文来看,也就是Thread-0是main的一个子线程。为啥呢?我们来看下一下线程的初始化动作:
public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);
}
↓↓↓↓↓↓↓↓ 最终都会走到下面的代码 ↓↓↓↓↓↓↓↓// 这里有个参数:inheritThreadLocals,代表是否继承线程的本地变量们(默认是true)
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {// 当前线程作为父线程Thread parent = currentThread();// 如果需要继承变量,并且父线程中的变量非空,即拷贝一份变量(浅拷贝)if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);// 赋值一个线程ID(自增)tid = nextThreadID();
}
- 创建线程的时候,当前线程总是会作为父线程。
- 若父线程绑定了变量并且允许继承,那么就会把变量拷贝到子线程里面。(浅拷贝,若需要深拷贝则需要重写
childValue()函数)
我们再来看下InheritableThreadLocal 的源码:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {// 重写childValue函数,返回父线程绑定的变量引用。也是浅拷贝protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}// 如果父线程当中绑定的变量不为null,就可以在子线程中创建一份拷贝void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}
针对这一点,我们如果将上述案例Demo中的这行代码修改为:
private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();
↓↓↓↓↓↓↓↓
private static final ThreadLocal<Integer> THREAD_LOCAL = new InheritableThreadLocal<>();
再次运行程序可得:

我们Debug下走下流程:
- 主线程赋值,调用到了
ThreadLocal的set函数。由于我们使用的是InheritableThreadLocal,重写了getMap,返回的是主线程的inheritableThreadLocals引用(默认为null)。

- 发现为
null,就会调用createMap函数,同样,InheritableThreadLocal重写了它:

因此这一步执行完毕之后,主线程的inheritableThreadLocals属性是有值的。 - 然后创建子线程的时候(调用
new Thread构造函数):

发现主线程已经绑定了相关变量,因此会将该变量传递给子线程,同样赋值于inheritableThreadLocals变量上。 - 后续以此类推,我们只关心
inheritableThreadLocals变量上是否有值。从而实现父子线程的变量传递。
1.2 InheritableThreadLocal 的诟病
InheritableThreadLocal 虽然能解决父子之间的变量传递问题。但是大家从源码的角度来看,传递变量的关键步骤,它实现于线程的创建过程。那么如果说我有个单例线程池,复用同一个线程,会有什么问题?
private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();public static void main(String[] args) throws InterruptedException {User.setAge(100);THREAD_POOL.execute(() -> User.getAge());TimeUnit.SECONDS.sleep(2);User.setAge(200);THREAD_POOL.execute(() -> User.getAge());TimeUnit.SECONDS.sleep(2);// 同步获取User.getAge();System.out.println("**************Finish****************");
}
执行结果如下:

如果我们
结果虽意料之内但是又是给人惊喜:
- 虽然父子线程间的变量传递成功了,但是当值发生变更的时候。子线程拿到的值依旧是老的。
- 结合上面的源码来看,也就是说通过
InheritableThreadLocal拿到的变量,永远是第一次创建子线程时,父线程中存储的变量值。后续不再更改。 - 因为我们本案例中使用的是单例线程池,线程对象只会
new一次。
总结就是:
InheritableThreadLocal中拷贝的数据始终来自于第一个提交任务的父线程,这样非常容易造成线程本地变量混乱。 由于是浅拷贝,一旦传递链路上变量值被改变,那么整个链路线程上的所有变量都会随之更改。- 还有个很重要的点就是,
InheritableThreadLocal只支持父子线程间的数据传递,而不支持跨线程之间的数据传递!
二. TransmittableThreadLocal (TTL) 横空出世
TransmittableThreadLocal 简称TTL,是阿里写的一个专门用于解决InheritableThreadLocal诟病的一个功能类。可以在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
针对上述Demo(单例线程池),我们做出如下更改:添加Pom依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.11.4</version>
</dependency>
InheritableThreadLocal替换成TTL:
private static final ThreadLocal<Integer> THREAD_LOCAL = new InheritableThreadLocal<>();
↓↓↓↓↓↓↓↓
private static final ThreadLocal<Integer> THREAD_LOCAL = new TransmittableThreadLocal<>();
以及线程池做一下装饰:
private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();
↓↓↓↓↓↓↓↓
private static final ExecutorService THREAD_POOL = TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());
执行结果如下:

从这里可以发现,TTL解决了什么问题?
JDK自带的的InheritableThreadLocal虽然能在父子之间进行变量传递,但是这个变量只有在创建子线程的时候才会被创建并传递。并不会感应其更新。
我们再看另外一个案例
public static void main(String[] args) throws InterruptedException {User.setAge(100);User.getAge();THREAD_POOL.execute(() -> {User.getAge();User.setAge(200);User.getAge();});TimeUnit.SECONDS.sleep(2);THREAD_POOL.execute(() -> {User.getAge();User.setAge(300);User.getAge();});TimeUnit.SECONDS.sleep(2);User.setAge(666);THREAD_POOL.execute(() -> {User.getAge();User.setAge(400);User.getAge();});TimeUnit.SECONDS.sleep(2);// 同步获取User.getAge();System.out.println("**************Finish****************");
}
运行结果如下:

从这个结果可以得出以下结论:
- 子线程拿到的
ThreadLocal变量总是最新的。(依据的是主线程的变量)主线程在修改变量值为666之后,子线程pool-1-thread-3获取到的值是最新的666。 - 子线程之间的变量赋值操作,互相不影响,子线程哪怕更改了
ThreadLocal变量,在执行结束之后,会恢复备份,即原值。 所以上图中pool-1-thread-2线程,第一次拿到的值依旧是100。
其实上面这个案例还并不是很明显,因为我们的主线程始终只有一个。我们来看下一个彻彻底底的跨线程案例。来个高并发看看。
2.1 跨线程变量传递测试案例
需求背景:
- 我们准备一个变量,这里我们准备用
AtomicInteger。方便修改值。 - 准备一个线程池
A:用于做业务处理。 - 准备一个线程池
B:用于模拟HTTP请求层面的高并发。这样每个HTTP请求都有属于自己的主线程,也就是有个主变量。我们要验证的就是HTTP层面的并发对变量的影响。 - 业务处理要做的事情:将
AtomicInteger类型的index变量,累加至10。
我们先来看一下InheritableThreadLocal版本的:
public class TestTTL {// 定义一个线程池执行ttl,这里必须要用TTL线程池封装private static ExecutorService TTL_EXECUTOR = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));// 定义另外一个线程池循环执行,模拟业务场景下多Http请求调用的情况private static ExecutorService HTTP_EXECUTOR = Executors.newFixedThreadPool(5);private static AtomicInteger INDEX = new AtomicInteger(0);// TTL的ThreadLocalprivate static ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); //这里采用TTL的实现public static void main(String[] args) {while (true) {/*** 模拟HTTP请求的高并发*/HTTP_EXECUTOR.execute(() -> {// 累加到10我们就停止if (INDEX.get() < 10) {THREAD_LOCAL.set(INDEX.getAndAdd(1));TTL_EXECUTOR.execute(() -> {System.out.println(String.format("子线程名称-%s, 变量值=%s", Thread.currentThread().getName(), THREAD_LOCAL.get()));});}});}}
}
运行结果如下:

很直观的是,我们打印了10条记录,这一块是由AtomicInteger控制的,但是我们存入到InheritableThreadLocal中的变量,却没有累加到10,说明啥,无法跨线程传递变量。别急,我们再看下TTL版本的:
public class TestTTL {// 定义一个线程池执行ttl,这里必须要用TTL线程池封装private static ExecutorService TTL_EXECUTOR = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));// 定义另外一个线程池循环执行,模拟业务场景下多Http请求调用的情况private static ExecutorService HTTP_EXECUTOR = Executors.newFixedThreadPool(5);private static AtomicInteger INDEX = new AtomicInteger(0);// TTL的ThreadLocalprivate static ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); //这里采用TTL的实现public static void main(String[] args) {while (true) {/*** 模拟HTTP请求的高并发*/HTTP_EXECUTOR.execute(() -> {// 累加到10我们就停止if (INDEX.get() < 10) {THREAD_LOCAL.set(INDEX.getAndAdd(1));TTL_EXECUTOR.execute(() -> {System.out.println(String.format("子线程名称-%s, 变量值=%s", Thread.currentThread().getName(), THREAD_LOCAL.get()));});}});}}
}
结果如下:

这个结果和上面的结果一对比,就可以发现,使用TTL进行封装,在跨线程的变量传递下,它是生效的。
2.2 TTL的基本原理
首先,TTL是在InheritableThreadLocal的基础上开发的,也就是说TTL继承于InheritableThreadLocal类。我们来看下官网给出的一个执行时序图:

在讲这个流程之前,我们先来看下TTL中几个比较重要的成员类和属性。
2.2.1 静态属性 holder 和 threadLocalHolder
TTL中有个静态属性holder,它的特点和作用有以下几点:
- 存储所有使用了
TTL的引用类。用于复制值的时候,可以通过这个holder去遍历到所有的TTL。 key就是当前TTL。value则固定是null。虽然使用了WeakHashMap作为存储,但是它的使用方式被用来当做Set集合。
我们来看下这个静态字段:
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();}@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);}};
我们再来看下,TTL在get/set的时候做了啥,先看set函数:
@Override
public final void set(T value) {// disableIgnoreNullValueSemantics 控制序列化TTL对象的时候,是否忽略空值if (!disableIgnoreNullValueSemantics && null == value) {// may set null to remove valueremove();} else {super.set(value);addThisToHolder();}
}
我们先说下disableIgnoreNullValueSemantics这个参数在啥场景下需要用到:当我们在线程之间传递变量,在赋值的时候,如果变量值为null,那么在新的线程中,这个变量将不会存在,因此有些时候我们需要将这个属性值设置为true,保证变量值在传递过程中不会丢失,哪怕其值为null。
根据代码来看,也就是默认情况下,如果变量传递过程中值为null,就会将它剔除。反之,如果正常的存储流程是怎样的呢?
super.set(value);
addThisToHolder();
先调用了父类的set函数,在InheritableThreadLocal的基础上,在调用了addThisToHolder函数。就是将当前TTL引用存储到这个静态变量holder中罢了。
@SuppressWarnings("unchecked")
private void addThisToHolder() {if (!holder.get().containsKey(this)) {holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.}
}
那总结下就是:
TTL有一个静态属性holder,用来存储所有的TTL引用。- 在
set赋值变量的时候,会触发将当前TTL存储进去。
我们再来看下另外一个字段threadLocalHolder:
private static volatile WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>> threadLocalHolder = new WeakHashMap<ThreadLocal<Object>, TtlCopier<Object>>();
它的作用是对于在项目中使用了ThreadLocal,但是却无法替换为TTL,那么这个时候就可以使用Transmitter提供的注册方法,将项目中的threadLocal注册到threadLocalHolder中。在生成快照的时候,也会对这部分变量进行处理。案例代码:
// 注册
Transmitter.registerThreadLocalWithShadowCopier(threadLocal);
// 注销
Transmitter.unregisterThreadLocal(threadLocal);
2.2.2 静态内部类 Transmitter
TTL中有一个静态核心内部类Transmitter,它主要作用于线程切换的时候,其功能如下:
ThreadLocal变量的快照保存:capture。- 重放:
replay。 - 快照恢复:
restore。
我们先看下快照的创建(主线程执行):
public static class Transmitter {public static Object capture() {return new Snapshot(captureTtlValues(), captureThreadLocalValues());}// 循环遍历当前holder记录过的所有TTL引用,将TTL取出来并保存到Map里面private static WeakHashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {WeakHashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new WeakHashMap<TransmittableThreadLocal<Object>, Object>();for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {ttl2Value.put(threadLocal, threadLocal.copyValue());}return ttl2Value;}// 循环遍历注册过的ThreadLocal。(普通的)private static WeakHashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {final WeakHashMap<ThreadLocal<Object>, Object> threadLocal2Value = new WeakHashMap<ThreadLocal<Object>, Object>();for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) {final ThreadLocal<Object> threadLocal = entry.getKey();final TtlCopier<Object> copier = entry.getValue();threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));}return threadLocal2Value;}
}
再来看下快照的重放(子线程执行):
public static Object replay(@NonNull Object captured) {final Snapshot capturedSnapshot = (Snapshot) captured;return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
跟快照创建很像,我们这里分析ttl2Value(TTL引用),就不再分析threadLocal2Value(原生ThreadLocal的注册),我们看下replayTtlValues函数:
// 入参 captured 是从其他线程(TTL)中捕获到的变量
private static WeakHashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull WeakHashMap<TransmittableThreadLocal<Object>, Object> captured) {// 创建一个备份MapWeakHashMap<TransmittableThreadLocal<Object>, Object> backup = new WeakHashMap<TransmittableThreadLocal<Object>, Object>();// 遍历当前所有的TTL引用for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {TransmittableThreadLocal<Object> threadLocal = iterator.next();// 将当前子线程的数据进行备份backup.put(threadLocal, threadLocal.get());// 如果快照中不存在当前TTL实例则要删除,因为有些TTL引用可能是在调用capture生成快照之后才创建的。if (!captured.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 将快照值赋值到当前线程中setTtlValuesTo(captured);// 执行execute之前的一些逻辑doExecuteCallback(true);return backup;
}
最后来看下快照的恢复操作:
public static void restore(@NonNull Object backup) {final Snapshot backupSnapshot = (Snapshot) backup;restoreTtlValues(backupSnapshot.ttl2Value);restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}
同样,我们关注ttlValue:
private static void restoreTtlValues(@NonNull WeakHashMap<TransmittableThreadLocal<Object>, Object> backup) {doExecuteCallback(false);// 遍历所有的TTLfor (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {TransmittableThreadLocal<Object> threadLocal = iterator.next();// 如果备份中不存在当前TTL实例则要删除,因为有些TTL引用可能是在调用capture生成快照之后才创建的。if (!backup.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 重新将备份的值设置一下setTtlValuesTo(backup);
}
一般我们会将自定义的线程池用TtlExecutors进行封装,这样里面的任务就会被TtlRunnable封装。那么我们看下TtlRunnable类:
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {private final AtomicReference<Object> capturedRef;private final Runnable runnable;private final boolean releaseTtlValueReferenceAfterRun;// 创建任务时调用的构造函数private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {// 调用capture函数,即创建一个快照。this.capturedRef = new AtomicReference<Object>(capture());// 将原生的业务代码Jobs进行保存。this.runnable = runnable;// this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}/*** wrap method {@link Runnable#run()}.*/@Overridepublic void run() {// 子线程开始执行,先取得快找数据。Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after run!");}// 将快照中的数据设置到当前线程中(因为这些数据来自于其他线程,当前线程还没有赋值呢),同时创建一个备份数据backupObject backup = replay(captured);try {// 执行真正的业务逻辑runnable.run();} finally {// 最后恢复备份快照restore(backup);}}
}
2.3 总结
总结下TTL的原理大概如下:
- 首先,我们使用的线程池,需要通过
TtlExecutors进行封装,本质上和单独的任务被TtlRunnable封装是一致的。只不过前者更方便了。 TtlRunnable封装过的任务,也就是存储了当前TTL快照的一个引用。在创建的时候就会捕捉当前父线程中的TTL以及一些注册过的ThreadLocal(就是用于兼容老版本的ThreadLocal,让他们有跨线程传递的特性)TTL中有一个静态属性holder,存储了所有的TTL引用。存储的时机在我们set的时候发生。- 子线程开始执行的时候,先调用
Transmitter.replay,获取到其他线程里面存储的变量(也就是上一步产生的快照capturedRef这里体现到了跨线程的变量传递),然后将值赋值给当前线程。同时产生一个备份backup并返回。 - 子线程执行完毕,执行
Transmitter.restore,根据backup备份将数据恢复。
FAQ:
为什么TTL中需要有一个holder静态变量用来存储所有的TTL引用呢?回答:
holder首先是一个InheritableThreadLocal,里面放的是一个WeakHashMap。用来收集线程中所有的TTL。反之,如果我们不用holder,是否可以拿到其他线程中存储的变量呢?Thread中的ThreadLocal.ThreadLocalMap inheritableThreadLocals成员变量,它的值虽然可以被子线程继承。但是我们在业务代码中无法对这个变量直接访问。因此还是需要通过自己定义的holder进行存储和访问。
为什么子线程执行完毕之后,要恢复快照?难道不是下一次线程复用的时候,对应线程存储的TTL变量吗?回答:
- 当线程池满了,并且采取的拒绝策略是
CallerRunsPolicy。那么此时原本要执行子业务逻辑的操作可能会在主线程中执行。也就是两个操作在同一个线程中执行。 - 倘若没有
restore这个操作,倘若中途对TTL的内容进行更改,就会导致最终上下文数据被污染。
下面是我从笑傲君这截取的图,这个是正常的变量拷贝图:子线程可以对TTL中的内容做任意修改(拷贝了一份)

以下则是同线程执行情况下:这种情况下若发生TTL值变更,就会发生上下文污染。

TTL存在线程安全问题吗?回答:
- 存在,因为默认是引用类型拷贝,如果子线程修改了数据,主线程可以感知的到。
相关文章:
Java - ThreadLocal数据存储和传递方式的演变之路
Java - ThreadLocal数据存储和传递方式的演变之路 前言一. InheritableThreadLocal - 父子线程数据传递1.1 父子线程知识预热和 InheritableThreadLocal 实现原理1.2 InheritableThreadLocal 的诟病 二. TransmittableThreadLocal (TTL) 横空出世2.1 跨线程变量传递测试案例2.2…...
vuex三问
文章目录 一、什么是vuex?二、为什么使用vuex?三、如何使用vuex?1.首先安装vuex2.注册vue中3.实例化vuex的store4. 挂载在vue实例上5.在组件中就可以通过this.$store对vuex进行操作。 总结 一、什么是vuex? Vuex 是一个专为 Vue.…...
Selenium自动化测试(基于Java)
目录 一. 了解Selenium ✅1.1 概念 ✅1.2 作用 ✅1.3 特点 ✅1.4 工作原理 二. Selenium Java 环境搭建 ✅2.1 下载 Chrome 浏览器 ✅2.2 查看浏览器的版本 ✅2.3 下载浏览器驱动 ✅2.4 验证环境是否搭建成功 三. Selenium 常用 API ✅3.1 定位元素 ✅3.2 操作对象…...
【网页布局形式----浮动】
网页布局形式----浮动 css浮动:一、常见的三种网页布局形式:1.1 网页布局两大准则: 二 、浮动:2.1 浮动语法:2.2 浮动特性(重难点):浮动元素通常与标准流的父级元素搭配使用…...
人力资源管理的本质
文章目录 写在前面简述用人方面 写在前面 还没写完呢 这是个人理解,本人理工科出身,喜欢直来直去,理论化的知识,苦于市面上的人力书籍资料都不说人话,遂有此文刚入门,甚至没有系统的学习管理知识…...
[NOIP2015 提高组] 运输计划
题目链接 给定一棵树以及树上的 m m m 条通路,我们可以在树上选取一条边,将其权重置为 0 0 0,目标是 min 将某条边权重置 0 max 通路权重 . \min_{将某条边权重置 0}\max 通路权重. 将某条边权重置0minmax通路权重. 20pts(m1) 当…...
【GreendDao 】RxQuery根据指定条件查询,完成后处理UI逻辑
GreenDao 和 RxJava 结合使用可以更方便地处理数据查询和 UI 逻辑的交互。RxQuery 使得一次查询结果可以直接转化成 Observable,而通过 RxJava 的操作符,可以方便地完成异步查询和 UI 逻辑的交互。以下是一个根据指定条件查询数据,查询完成后…...
【C++】unordered_set 和 unordered_map 使用 | 封装
文章目录 1. 使用1. unordered_set的使用2. unordered_map的使用 2. 封装修改结构定义针对insert参数 data的两种情况复用 哈希桶的insertKeyOfT模板参数的作用 迭代器operator()beginendunordered_set对于 begin和end的复用unordered_map对于 begin和end的复用unordered_map中…...
C++环形缓冲区设计与实现:从原理到应用的全方位解析
C环形缓冲区设计与实现:从原理到应用的全方位解析 一、环形缓冲区基础理论解析(Basic Theory of Circular Buffer)1.1 环形缓冲区的定义与作用(Definition and Function of Circular Buffer)1.2 环形缓冲区的基本原理&…...
阿里云服务器部署flask简单方法
记录如何在阿里云服务器上部署flask接口并实现公网访问。 文章目录 1. 简介2. 部署python3环境3. 生成requirement.txt4. 将项目打包上传5. 安装依赖库6. 查看防火墙7. 测试能否公网访问 1. 简介 因落地通话callback服务测试,需要我写一个测试demo,用于…...
【JavaSE】Java基础语法(二十三):递归与数组的高级操作
文章目录 1. 递归1.1 递归1.2 递归求阶乘 2. 数组的高级操作2.1 二分查找2.2 冒泡排序2.3 快速排序2.4 Arrays (应用) 1. 递归 1.1 递归 递归的介绍 以编程的角度来看,递归指的是方法定义中调用方法本身的现象把一个复杂的问题层层转化为一个与原问题相似的规模较…...
HUSTOJ使用指南
如何快速上手(了解系统的功能)? admin管理员用户登录,点击右上角管理,仔细阅读管理首页的说明。 切记:题目导入后一次只能删一题,不要导入过多你暂时用不上的题目,正确的方式是每次…...
java基础学习
一、注释 1)当行注释 // 2)多行注释 /* ... */ 3)文档注释 (java特有) /** author 张三 version v1.0 这是文档注释,需要将class用public修饰 */ 二、关键字 (1)48个关键…...
Linux——进程优先级
1.什么是优先级? 优先级和权限息息相关。权限的含义为能还是不能做这件事。而优先级则表示:你有权限去做,只不过是先去做还是后去做这件事罢了。 2.为什么会存在优先级? 优先级表明了狼多肉少的理念,举个例子ÿ…...
音频设备初始化与输出:QT与SDL策略模式的实现
音频设备初始化与输出:QT与SDL策略模式的实现 一、引言(Introduction)1.1 音频设备初始化与输出的重要性1.2 QT与SDL的音频设备处理1.3 策略模式在音频设备处理中的应用 二、深入理解音频设备初始化与输出2.1 音频设备的基本概念2.2 音频设备…...
Linux 手动部署 SpringBoot 项目
Linux 手动部署 SpringBoot 项目 1. 将项目打包成 jar 包 (1)引入插件 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></pl…...
华为OD机试真题B卷 Java 实现【内存资源分配】
一、题目描述 有一个简易内存池,内存按照大小粒度分类,每个粒度有若干个可用内存资源,用户会进行一系列内存申请,需要按需分配内存池中的资源,返回申请结果成功失败列表。 分配规则如下: 分配的内存要大于等于内存的申请量,存在满足需求的内存就必须分配,优先分配粒度…...
深入理解ChatGPT插件:competitorppcads、seoanalysis和kraftful
1. 引言 插件,作为一种扩展功能的工具,为我们的应用程序提供了无限的可能性。在ChatGPT中,我们有许多强大的插件,如competitorppcads、seoanalysis和kraftful。这篇博客将详细介绍这三个插件的功能和使用方法。 2. competitorpp…...
通过源码编译安装LAMP平台的搭建
目录 1. 编译安装Apache httpd服务2 编写mysqld服务3 编译安装PHP 解析环境安装论坛 LAMP架构是目前成熟的企业网站应用模式之一,指的是协同工作的一整套系统和相关软件,能够提供动态Web站点服务及其应用开发环境。 LAMP是一个缩写词,具体包…...
mac os 安装rz/sz
说明:使用rz sz实现终端的文件传输,该命令主要使用场景为 macos中通过堡垒机登陆后无法使用ftp工具传输文件。 工具:iTerm2、lrzsz、homebrew 以及两个脚本文件(iterm2-recv-zmodem.sh、iterm2-send-zmodem.sh) …...
练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
HDFS分布式存储 zookeeper
hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架,允许使用简单的变成模型跨计算机对大型集群进行分布式处理(1.海量的数据存储 2.海量数据的计算)Hadoop核心组件 hdfs(分布式文件存储系统)&a…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
