1、背景
在日常SpringBoot应用或者Java应用开发中,使用多线程编程有很多好处,比如可以同时处理多个任务,提高程序的并发性;可以充分利用计算机的多核处理器,使得程序能够更好地利用计算机的资源,提高程序的性能;还可以使用多线程将耗时的任务放在后台执行,使得用户不会感觉到程序的卡顿或者阻塞,从而改善用户体验;也可以用多线程使程序能够实现更复杂的功能等等。
多线程编程开发,经常会遇到数据共享的问题,不同的环境或场景,会有不同的解决方案,下面介绍几种常见的在线程间共享数据的方式。
2、常见的线程创建方式
根据不同代码实现方式,JDK提供了4种常见创建线程的方式,分别是继承Thread类、实现Runnable接口、实现Callable接口、线程池等方式。
2.1、继承Thread类
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.thread;
/**
* 自定义线程类,继承Thread
*
* @author Bruce.CH
* @since 2023年08月26日
*/
public class MyThread extends Thread {
// 将共享数据定义为线程类的内部属性
private final StringBuffer data;
public MyThread(StringBuffer data) {
this.data = data;
}
/**
* 覆盖run方法:自定义线程要执行的任务
*/
@Override
public void run() {
if (data.length() > 1) {
// 移除最后一个数字
data.deleteCharAt(data.length() - 1);
}
System.out.println("继承Thread类创建的线程[" + Thread.currentThread().getName() + "]从data移除了一个数字.");
}
public static void main(String[] args) throws InterruptedException {
// 定义共享数据对象
StringBuffer data = new StringBuffer("123");
// 创建MyThread线程对象
MyThread newThread = new MyThread(data);
// 启动线程
newThread.start();
Thread.sleep(100L);
System.out.println("data: " + data);
}
}
2.2、实现Runnable接口
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.thread;
/**
* 无返回值的Runnable线程任务
*
* @author Bruce.CH
* @since 2023年08月26日
*/
public class RunnableTaskThread implements Runnable {
// 将共享数据定义为Runnable线程任务类的内部属性
private final StringBuffer data;
public RunnableTaskThread(StringBuffer data) {
this.data = data;
}
/**
* 实现run方法
*/
@Override
public void run() {
data.append(data.length() + 1);
System.out.println("实现Runnable接口创建的线程[" + Thread.currentThread().getName() + "]往data中追加了一个数字.");
}
public static void main(String[] args) throws InterruptedException {
// 定义共享数据对象
StringBuffer data = new StringBuffer();
Thread runnableTaskThread = new Thread(new RunnableTaskThread(data));
runnableTaskThread.start();
Thread.sleep(100L);
System.out.println("data: " + data);
}
}
2.3、实现Callable接口
在主线程执行多个子任务并汇总子任务执行结果的场景下,可以通向线程池中提交实现Callable接口的线程任务创建线程。
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Stream;
/**
* 有返回值的Callable线程任务
*
* @author Bruce.CH
* @since 2023年08月27日
*/
public class CallableTaskThread implements Callable {
// 将共享数据定义为Callable线程任务类的内部属性
private final StringBuffer data;
public CallableTaskThread(StringBuffer data) {
this.data = data;
}
/**
* 实现call方法
*
* @return 线程类型与线程名称
*/
@Override
public String call() {
data.append(data.length() + 1);
return "实现Callable接口创建的线程[" + Thread.currentThread().getName() + "]往data中追加了一个数字.";
}
public static void main(String[] args) {
// 在主线程中定义一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(4);
// 保存子线程结果Future
List<Future> results = new ArrayList<>();
// 定义共享数据对象
StringBuffer data = new StringBuffer();
// 提交实现了Callable接口的线程任务到线程池,线程池会创建线程执行任务
Stream.of(1, 2, 3, 4).forEach(nThread -> {
CallableTaskThread taskThread = new CallableTaskThread(data);
Future result = threadPool.submit(taskThread);
// 保存持有线程执行结果的Future对象
results.add(result);
});
// 关闭线程池,等待4子线程的执行结果
threadPool.shutdown();
// 遍历打印子线程返回结果
results.forEach(CallableTaskThread::print);
System.out.println("data: " + data);
}
public static void print(Future taskResult) {
try {
// 获取子线程返回结果并打印
System.out.println(taskResult.get());
} catch (InterruptedException | ExecutionException e) {
System.err.println("error:" + e.getMessage());
}
}
}
2.4、线程池
包括JDK中Executors类方法创建的5个不同场景的线程池以及自定义的线程池。自定义线程池创建线程示例如下:
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.thread;
import org.springframework.lang.NonNull;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义线程池
*
* @author Bruce.CH
* @since 2023年08月27日
*/
public class MyThreadPoolExecutor {
/**
* 创建自定义线程池
*
* @return 自定义线程池对象
*/
public static ThreadPoolExecutor newThreadPool() {
// 核心线程数4
int corePoolSize = 4;
// 最大线程数6
int maximumPoolSize = 6;
// 空闲线程最大空闲实践2分钟
long keepAliveTime = 2;
TimeUnit unit = TimeUnit.MINUTES;
// 任务队列长度为100:使用链表实现的阻塞队列,并发性能较高。
BlockingQueue workQueue = new LinkedBlockingQueue<>(100);
// 自定义线程池名称的线程池
MyThreadFactory myThreadFactory = new MyThreadFactory("自定义线程池");
return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, myThreadFactory);
}
/**
* 自定义线程工厂
*/
static class MyThreadFactory implements ThreadFactory {
// 线程工程示例计数
private static final AtomicInteger poolNumber = new AtomicInteger(1);
// 线程组
private final ThreadGroup group;
// 线程计数
private final AtomicInteger threadNumber = new AtomicInteger(1);
// 线程名称前缀
private final String namePrefix;
public MyThreadFactory(String poolNamePrefix) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "[" + poolNamePrefix + "-" + poolNumber.getAndIncrement() + "]创建的线程-";
}
/**
* 实现newThread方法
*
* @param runnable runnable实现对象
* @return 线程对象
*/
@Override
public Thread newThread(@NonNull Runnable runnable) {
Thread thread = new Thread(group, runnable,
namePrefix + threadNumber.getAndIncrement(),
0);
if (thread.isDaemon())
thread.setDaemon(false);
if (thread.getPriority() != Thread.NORM_PRIORITY)
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
public static void main(String[] args) throws InterruptedException {
// 1、在主线程即外部类定义共享数据
final StringBuffer data = new StringBuffer();
ThreadPoolExecutor executor = MyThreadPoolExecutor.newThreadPool();
// 2、使用匿名内部类创建线程任务
executor.execute(() -> {
System.out.println("任务1 " + Thread.currentThread().getName() + " 往data追加了一个数字.");
// 3、在匿名内部类中引用外部类的共享数据
data.append(1);
});
executor.execute(() -> {
System.out.println("任务2 " + Thread.currentThread().getName() + " 往data追加了一个数字.");
data.append(2);
}
);
executor.execute(() -> {
System.out.println("任务3 " + Thread.currentThread().getName() + " 往data追加了一个数字.");
data.append(3);
});
executor.execute(() -> {
System.out.println("任务4 " + Thread.currentThread().getName() + " 往data追加了一个数字.");
data.append(4);
});
Thread.sleep(100L);
System.out.println("data: " + data);
executor.shutdown();
}
}
3、进程内线程间共享数据
3.1、将共享数据定义为静态类变量
适合全局共享数据的场景,即进程内所有线程可访问。
将数据对象定义为静态类变量,进程内的所有线程即可在数据对象允许的范围内实现访问数据对象。
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Stream;
/**
* 将共享数据定义为静态类变量实现线程间数据共享
*
* @author Bruce.CH
* @since 2023年08月27日
*/
public class StaticDataThreadDemo {
// 1、定义静态变量data,当前类下创建的线程均可访问
private static final StringBuffer data = new StringBuffer();
// 声明一个线程池,用于创建任务线程
private static final ExecutorService threadPool = Executors.newFixedThreadPool(5);
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 启动4个Callable任务线程往data追加数据
testCallableTaskThread();
// 启动1个Runnable任务线程往data追加数据
testRunnableTaskThread();
// 关闭线程池
threadPool.shutdown();
}
private static void testCallableTaskThread() {
List<Future> results = new ArrayList<>();
Stream.of(1, 2, 3, 4).forEach(nThread -> {
// 2、在需要的地方引用或传递共享数据
CallableTaskThread taskThread = new CallableTaskThread(data);
Future result = threadPool.submit(taskThread);
results.add(result);
});
results.forEach(CallableTaskThread::print);
System.out.println("CallableTaskThread data: " + data);
}
private static void testRunnableTaskThread() throws InterruptedException, ExecutionException {
// 2、在需要的地方引用或传递共享数据
RunnableTaskThread taskThread = new RunnableTaskThread(data);
Future> submit = threadPool.submit(taskThread);
submit.get(); // 等待子线程结束再打印
System.out.println("testRunnableTaskThread data: " + data);
}
}
3.2、将数据封装为线程或线程任务内部属性
适用运行时创建的对象数据需要在当前上下文内的线程中共享的场景。
- 首先,将数据抽象为类,在自定义线程类或线程任务类中定义一个共享数据类属性,用于持有共享的数据对象
- 然后,在主线程类中初始化一个需要共享的数据对象
- 最后,在创建自定义线程对象或自定义线程任务对象时,通过构造方法入参,将需要共享的数据对象赋值给自定义的线程对象或自定义的线程任务对象
可参考创建线程的2.1到2.3案例,均为此种方式。
3.3、匿名线程类或匿名线程任务对象持有外部共享数据对象
适用运行时创建的对象数据需要在当前上下文内的线程中共享的场景。与3.2中不同,此种方式不需要在匿名线程类或匿名线程任务类中定义数据对象属性,而是直接方法外部共享数据对象。类似JS中的闭包概念,解决延时或访问外部上下文数据的经典手法。
- 首先,将数据抽象为类
- 然后,在主线程即外部类初始化一个需要共享的数据对象
- 同时,在主线程中定义或创建匿名线程对象或线程任务对象时,直接访问外部类中的共享数据对象
可参考创建线程的2.4案例,即为此种方式。
3.4、ThreadLocal + 任务装饰器
适用自定义线程池,且异步线程执行任务的共享数据场景,比如多租户(动态数据源)应用中执行异步任务的场景,需要传递主线程获取的当前租户的数据源信息给任务线程。
在自定义线程池时,需要提供一个线程任务执行方法的装饰器接口,在实现任务装饰器接口方法中,从当前父线程的ThreadLocal中取出数据,放入子线程ThreadLocal中,从而实现数据的共享传递。Spring
中有这类现成的线程池,具体参考
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor,可以直使用,只需要在创建线程池的时候指定任务装饰器的实现类即可。
3.4.1、定义线程本地变量ThreadLocal
这里定义了TenantDataSourceContext类,封装了一个线程本地变量ThreadLocal对象,用于保存需要在当前线程或父子/异步线程中传递的共享数据即数据源名称
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.datasource;
import org.springframework.util.StringUtils;
/**
* 当前线程中的租户数据源上下文
*
* @author Bruce.CH
* @since 2023年08月19日
*/
public class TenantDataSourceContext {
private static final ThreadLocal DATA_SOURCE_NAME_HOLDER = new ThreadLocal<>();
/**
* 缺省租户数据源名称
*/
public static final String DEFAULT_TENANT_DATA_SOURCE = "defaultTenantDataSource";
/**
* 获取当前请求中的租户数据源
*
* @return 租户数据源名称
*/
public static String get() {
String dataSourceName = DATA_SOURCE_NAME_HOLDER.get();
if (!StringUtils.hasText(dataSourceName)) {
dataSourceName = DEFAULT_TENANT_DATA_SOURCE;
}
return dataSourceName;
}
/**
* 设置当前请求将使用的租户数据源:拦截器获取租户请求头并计算后调用此方法进行设置
*
* @param dataSourceName 数据源名称
*/
public static void set(String dataSourceName) {
DATA_SOURCE_NAME_HOLDER.set(dataSourceName);
}
/**
* 请求结束后,移除本地线程中的租户数据源名称
*/
public static void clear() {
DATA_SOURCE_NAME_HOLDER.remove();
}
}
3.4.2、定义线程任务装饰器类
线程任务装饰器类
TenantDataSourceContextDecorator,从主线程中获取ThreadLocal对象中的数据源名称,通过匿名线程任务类,直接将主线程中的数据源设置到子线程/异步线程的本地线程变量中。
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.fandou.component.datasource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.task.TaskDecorator;
import org.springframework.lang.NonNull;
/**
* 租户数据源上下文Decorator,用于异步线程/跨线程间传递租户id信息
*
* @author Bruce.CH
* @since 2023年08月20日
*/
@Slf4j
public class TenantDataSourceContextDecorator implements TaskDecorator {
@Override
public Runnable decorate(@NonNull Runnable runnable) {
// 1、获取主线程的dataSourceName:TenantDataSourceContext内部封装了ThreadLocal对象
String dataSourceName = TenantDataSourceContext.get();
return () -> {
try {
// 2、将主线程的dataSourceName,设置到子线程的本地线程变量中
TenantDataSourceContext.set(dataSourceName);
// 执行子线程
runnable.run();
} finally {
// 3、子线程结束,清空子线程的本地线程变量中dataSourceName
TenantDataSourceContext.clear();
log.info("TenantDataSourceContextDecorator clear dataSourceName.");
}
};
}
}
3.4.3、配置线程池
线程池配置类TaskExecutorConfig中,初始线程池对象时,设置步骤2中的线程装饰器对象实例,完成运行时的当前租户数据源名称的传递。
/*
* Copyright (c) Bruce.CH 2022-2023. All rights reserved.
*/
package com.example.demo.config;
import com.fandou.component.datasource.TenantDataSourceContext;
import com.fandou.component.datasource.TenantDataSourceContextDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置类
*
* @author Bruce.CH
* @since 2023年08月13日
*/
@Configuration
public class TaskExecutorConfig {
/**
* 配置Spring计划任务线程池
*
* @return 线程池
*/
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
// 线程名称前缀
poolTaskExecutor.setThreadNamePrefix("tenant-task-executor-");
// 核心线程数
poolTaskExecutor.setCorePoolSize(4);
// 最大线程数
poolTaskExecutor.setMaxPoolSize(8);
// 设置线程保活时间(秒)
poolTaskExecutor.setKeepAliveSeconds(120);
// 设置任务队列容量
poolTaskExecutor.setQueueCapacity(100);
// 设置线程任务装饰器,完成异步线程或跨线程/父子线程间的租户数据源上下文值的传递
poolTaskExecutor.setTaskDecorator(new TenantDataSourceContextDecorator());
// 拒绝策略
poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return poolTaskExecutor;
}
}
4、跨进程线程间共享数据
4.1 分布式存储中间件
支持分布式的存储中间件如redis,天然就支持多线程间的数据共享访问。只需要在主线程中将共享数据放入到redis中,有变更后回写即可。
具体中间件的使用方式,自行参考相关数据或网络文章。