Java - ThreadLocal数据存储和传递方式的演变之路

news2024/10/2 1:41:48

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****************");
}

执行结果如下:
在这里插入图片描述
我们得出以下结论:

  1. 我们在主线程中往ThreadLocal塞的值,只有主线程才能看到,子线程看不到。
  2. 父子线程之间,使用ThreadLocal无法进行数据传递。线程隔离。

1.1 父子线程知识预热和 InheritableThreadLocal 实现原理

在上面的案例中,我们在主线程中new了一个子线程。结合结果图文来看,也就是Thread-0main的一个子线程。为啥呢?我们来看下一下线程的初始化动作:

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();
}
  1. 创建线程的时候,当前线程总是会作为父线程。
  2. 若父线程绑定了变量并且允许继承,那么就会把变量拷贝到子线程里面。(浅拷贝,若需要深拷贝则需要重写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下走下流程:

  1. 主线程赋值,调用到了ThreadLocalset函数。由于我们使用的是InheritableThreadLocal,重写了getMap,返回的是主线程的inheritableThreadLocals引用(默认为null)。
    在这里插入图片描述
  2. 发现为null,就会调用createMap函数,同样,InheritableThreadLocal重写了它:
    在这里插入图片描述
    因此这一步执行完毕之后,主线程的inheritableThreadLocals属性是有值的。
  3. 然后创建子线程的时候(调用new Thread构造函数):
    在这里插入图片描述
    发现主线程已经绑定了相关变量,因此会将该变量传递给子线程,同样赋值于inheritableThreadLocals变量上。
  4. 后续以此类推,我们只关心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****************");
}

执行结果如下:
在这里插入图片描述
如果我们

结果虽意料之内但是又是给人惊喜:

  1. 虽然父子线程间的变量传递成功了,但是当值发生变更的时候。子线程拿到的值依旧是老的。
  2. 结合上面的源码来看,也就是说通过InheritableThreadLocal 拿到的变量,永远是第一次创建子线程时,父线程中存储的变量值。后续不再更改。
  3. 因为我们本案例中使用的是单例线程池,线程对象只会new一次。

总结就是:

  1. InheritableThreadLocal中拷贝的数据始终来自于第一个提交任务的父线程,这样非常容易造成线程本地变量混乱。 由于是浅拷贝,一旦传递链路上变量值被改变,那么整个链路线程上的所有变量都会随之更改。
  2. 还有个很重要的点就是,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解决了什么问题?

  1. 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 跨线程变量传递测试案例

需求背景:

  1. 我们准备一个变量,这里我们准备用AtomicInteger。方便修改值。
  2. 准备一个线程池A:用于做业务处理。
  3. 准备一个线程池B:用于模拟HTTP请求层面的高并发。这样每个HTTP请求都有属于自己的主线程,也就是有个主变量。我们要验证的就是HTTP层面的并发对变量的影响。
  4. 业务处理要做的事情:将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的ThreadLocal
    private 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的ThreadLocal
    private 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就是当前TTLvalue则固定是null。虽然使用了WeakHashMap作为存储,但是它的使用方式被用来当做Set集合。

我们来看下这个静态字段:

private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
	new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
	    @Override
	    protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
	        return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
	    }
	
	    @Override
	    protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
	        return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
	    }
	};

我们再来看下,TTLget/set的时候做了啥,先看set函数:

@Override
public final void set(T value) {
	// disableIgnoreNullValueSemantics 控制序列化TTL对象的时候,是否忽略空值
    if (!disableIgnoreNullValueSemantics && null == value) {
        // may set null to remove value
        remove();
    } 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.
    }
}

那总结下就是:

  1. TTL有一个静态属性holder,用来存储所有的TTL引用。
  2. 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));
}

跟快照创建很像,我们这里分析ttl2ValueTTL引用),就不再分析threadLocal2Value(原生ThreadLocal的注册),我们看下replayTtlValues函数:

// 入参 captured 是从其他线程(TTL)中捕获到的变量
private static WeakHashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull WeakHashMap<TransmittableThreadLocal<Object>, Object> captured) {
	// 创建一个备份Map
    WeakHashMap<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);
	// 遍历所有的TTL
    for (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()}.
     */
    @Override
    public void run() {
    	// 子线程开始执行,先取得快找数据。
        Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
		// 将快照中的数据设置到当前线程中(因为这些数据来自于其他线程,当前线程还没有赋值呢),同时创建一个备份数据backup
        Object backup = replay(captured);
        try {
        	// 执行真正的业务逻辑
            runnable.run();
        } finally {
        	// 最后恢复备份快照
            restore(backup);
        }
    }
}

2.3 总结

总结下TTL的原理大概如下:

  1. 首先,我们使用的线程池,需要通过TtlExecutors进行封装,本质上和单独的任务被TtlRunnable封装是一致的。只不过前者更方便了。
  2. TtlRunnable封装过的任务,也就是存储了当前TTL快照的一个引用。在创建的时候就会捕捉当前父线程中的TTL以及一些注册过的ThreadLocal(就是用于兼容老版本的ThreadLocal,让他们有跨线程传递的特性)
  3. TTL中有一个静态属性holder,存储了所有的TTL引用。存储的时机在我们set的时候发生。
  4. 子线程开始执行的时候,先调用Transmitter.replay,获取到其他线程里面存储的变量(也就是上一步产生的快照capturedRef这里体现到了跨线程的变量传递),然后将值赋值给当前线程。同时产生一个备份backup并返回。
  5. 子线程执行完毕,执行Transmitter.restore,根据backup备份将数据恢复。

FAQ:

为什么TTL中需要有一个holder静态变量用来存储所有的TTL引用呢?回答:

  1. holder首先是一个InheritableThreadLocal,里面放的是一个WeakHashMap。用来收集线程中所有的TTL。反之,如果我们不用holder,是否可以拿到其他线程中存储的变量呢?
  2. Thread中的ThreadLocal.ThreadLocalMap inheritableThreadLocals 成员变量,它的值虽然可以被子线程继承。但是我们在业务代码中无法对这个变量直接访问。因此还是需要通过自己定义的holder进行存储和访问。

为什么子线程执行完毕之后,要恢复快照?难道不是下一次线程复用的时候,对应线程存储的TTL变量吗?回答:

  1. 当线程池满了,并且采取的拒绝策略是CallerRunsPolicy。那么此时原本要执行子业务逻辑的操作可能会在主线程中执行。也就是两个操作在同一个线程中执行。
  2. 倘若没有restore这个操作,倘若中途对TTL的内容进行更改,就会导致最终上下文数据被污染。

下面是我从笑傲君这截取的图,这个是正常的变量拷贝图:子线程可以对TTL中的内容做任意修改(拷贝了一份)
在这里插入图片描述
以下则是同线程执行情况下:这种情况下若发生TTL值变更,就会发生上下文污染。
在这里插入图片描述


TTL存在线程安全问题吗?回答:

  1. 存在,因为默认是引用类型拷贝,如果子线程修改了数据,主线程可以感知的到。

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

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

相关文章

代码随想录二刷 day06 | 哈希表之 242.有效的字母异位词 349. 两个数组的交集 202. 快乐数 1. 两数之和

day06 242.有效的字母异位词349. 两个数组的交集202. 快乐数1. 两数之和 哈希表能解决什么问题呢&#xff1f;一般哈希表都是用来快速判断一个元素是否出现集合里。 242.有效的字母异位词 题目链接 解题思路&#xff1a; 题目的意思就是 判断两个字符串是否由相同字母组成。 字…

【Java|基础篇】内部类

文章目录 1.什么是内部类?2.实例内部类3.静态内部类4.局部内部类5.匿名内部类6.结语 1.什么是内部类? 内部类就是在一个类中再定义一个类,内部类也是封装的体现.它可以被声明为 public、protected、private 或默认访问控制符。内部类可以访问外部类的所有成员变量和方法&…

【WebRTC】音视频通信

WebRTC对等体还需要查找并交换本地和远程音频和视频媒体信息&#xff0c;例如分辨率和编解码器功能。 交换媒体配置信息的信令通过使用被称为SDP的会话描述协议格式来交换&#xff0c;被称为提议和应答的元数据块 WebRTC 音视频通信基本流程 一方发起调用 getUserMedia 打开本…

线程池在业务中的实践-美团技术团队分享

原文地址&#xff1a;Java线程池实现原理及其在美团业务中的实践 场景1&#xff1a;快速响应用户请求 描述&#xff1a;用户发起的实时请求&#xff0c;服务追求响应时间。比如说用户要查看一个商品的信息&#xff0c;那么我们需要将商品维度的一系列信息如商品的价格、优惠、…

从小白到大神之路之学习运维第31天

第二阶段基础 时 间&#xff1a;2023年5月29日 参加人&#xff1a;全班人员 内 容&#xff1a; Rsync服务 目录 一、基本信息 二、rsync命令 三、rsyncinotfy实时同步 一、基本信息 &#xff08;一&#xff09;概述 rsync是linux 下一个远程数据同步工具 他可通过…

拼多多获取整站实时商品详情数据|商品标题|商品链接,数据采集,数据分析提取教程

拼多多是一个基于社交电商的购物平台&#xff0c;它通过通过价格和优惠吸引大量用户&#xff0c;使用户形成消费场景和消费共同体&#xff0c;最终实现规模效应。在拼多多运营中&#xff0c;API接口起到了重要的作用&#xff0c;它可以实现不同系统之间的信息共享和数据传递&am…

EasyDSS使用OBS推流成功,但不显示播放按钮是什么原因?

EasyDSS支持一站式的上传、转码、直播、回放、嵌入、分享功能&#xff0c;具有多屏播放、自由组合、接口丰富等特点。平台可以为用户提供专业、稳定的直播推流、转码、分发和播放服务&#xff0c;全面满足超低延迟、超高画质、超大并发访问量的要求。 有用户反馈&#xff0c;使…

如何找到高清照片,4K高清风景照片,建议收藏!

想找些高清的风景图来做本个性杂志给朋友或者只是想用做壁纸使用&#xff0c;结果崩溃于互联网图片的图海中。 如何找到高清照片&#xff1f; 1. 图片分享网站 如果你想查找高清的风景图片&#xff0c;那么图片分享网站是一个不错的选择。这些网站通常提供大量的高清图片供用…

VS2019 WPF制作OTA上位机(三)串口打开

先在UI上添加控件 首先&#xff0c;改变一下原来的方法&#xff0c; 原来的三个控件是没有布局的&#xff0c;添加一下布局。 布局用简单的行布局&#xff0c;也就是说从&#xff0c;上到下&#xff0c;分成一行一行的&#xff0c;如下图 将上一篇文章的代码修改 <Window …

【MYSQL】通过存储过程调用事务方法

假设有表test_1&#xff1a; BEGINDECLARE err int DEFAULT 0;declare continue handler for sqlexception set err1;#当sqlexception handler捕捉到异常时&#xff0c;设置err1START TRANSACTION;#开始事务update test_1 set value 50 where id 58;IF (err0) THENcommit;#增…

打包ios-App之使用Appuploader

appuploader教程 一.申请个人开放者账号&#xff1a;https://idmsa.apple.com 网站内申请即可 注意&#xff1a;申请付费开发者账号需要付费688&#xff0c;付费之后就直接申请证书即可 未付费 二.申请ios测试证书&#xff08;p12&#xff09; 1.打开Appuploader&#xff0c;用…

建行对接微信支付

1、获取配置信息&#xff0c;基础代码设置 1.1 建行支付、退款需要商户提供以下信息&#xff1a; 商户代码支付使用商户柜台代码支付使用分行代码支付使用公钥外联平台使用、支付使用操作员号外联平台使用操作员号交易密码外联平台使用证书外联平台使用证书密码外联平台使用 …

python-import request失败

mac电脑 vscode。 &#xff01;&#xff01;&#xff01;踩坑&#xff0c;搞了2天 烦了哦 1&#xff1a;python安装&#xff1a; 下载地址&#xff1a;https://cdn.npmmirror.com/binaries/python/3.12.0/python-3.12.0a7-macos11.pkg 2: python配置PATH terminal指令打which …

JVM学习(十二):执行引擎

目录 一、执行引擎概述 二、执行引擎的工作过程 三、Java代码编译和执行 3.1 过程概述 3.1 javac前端编译 3.2 Java字节码的执行 3.3 编译和解释概述 3.4 高级语言理解与执行过程&#xff08;机器码、指令、汇编&#xff09; 3.4.1 机器码 3.4.2 指令 3.4.3 指…

Apache的配置、应用和优化(遥不可及)

文章目录 一、构建虚拟web主机二、配置虚拟主机1.基于域名&#xff08;1&#xff09;为虚拟主机提供域名解析&#xff08;2&#xff09;为虚拟主机准备网页文档&#xff08;3&#xff09;添加虚拟主机配置&#xff08;5&#xff09;启用上一步的子配置文件&#xff08;6&#x…

Python来写一个童话故事

Python来写一个童话故事 主题&#xff1a;冒险&#xff0c; 风格&#xff1a;惊险&#xff0c; 人物&#xff1a;男孩&#xff0c; 地点&#xff1a;海底。 循环遍历鱼列表中的每一条鱼 for fish in fishes:# 获取男孩和鱼的坐标和距离boy_x, boy_y boy.position()fish_x, …

亿发生产管理信息化系统,生产制造型企业信息化建设

在不断发展的先进制造格局中&#xff0c;传统生产管理模式的固化限制了企业规模化生产能力。为适应这个充满活力的时代需求&#xff0c;实现战略目标&#xff0c;企业必须借助信息技术的力量加强生产过程管理&#xff0c;踏上企业生产信息化的征程。亿发生产管理信息化系统&…

Python3数据分析与挖掘建模(4)集中趋势与离中趋势、数据分布与抽样

分析理论是统计学和数据分析中的重要概念&#xff0c;它们用于描述和理解数据的集中趋势、离中趋势、数据分布以及抽样理论。下面是对这些概念的简要说明&#xff1a; 集中趋势&#xff1a; 均值、中位数与分位数、众数离中趋势&#xff1a;标准差、方差数据分布&#xff1a;偏…

[Nacos] Nacos Server与Nacos Client间的UDP通信 (十)

文章目录 1.Nacos Server与Nacos Client间的UDP通信1.1 Nacos Server向Nacos Client进行UDP推送1.2 Nacos Client接收Nacos Server的UDP推送 1.Nacos Server与Nacos Client间的UDP通信 Nacos Server向Nacos Client进行UDP推送Nacos Client接收Nacos Server的UDP推送 1.1 Naco…

黑客常用工具合集

首先恭喜你发现了宝藏。 本文章集成了全网优秀的开源攻防武器项目&#xff0c;包含&#xff1a; 信息收集工具&#xff08;自动化利用工具、资产发现工具、目录扫描工具、子域名收集工具、指纹识别工具、端口扫描工具、各种插件....etc...&#xff09;漏洞利用工具&#xff0…