一. 概述
首先,在这里有必要和大家复现一下我使用该技术的背景:
在使用若依框架的时候,由于实际开发的需要,我需要配置四个数据源,并且通过mapper轮流去查每个库的指定用户数据,从而去判断改库是否存在目标数据,这样,就会产生一个问题,我需要查找四个库,由于网络限制等各方面因素,导致速度特别慢,这个时候我就在想,能不能使用线程并发,在请求一个库的同时,其余单个库的请求也同步进行,这样就会大大节约请求时间,提高效率,不会导致网络请求超时,所以就会出现一个问题,如何去使用多线程技术?
二. 混淆解析
多线程、多进程、异步、并发是计算机中常见的概念,它们都与程序的执行方式和效率相关。
- 多线程
多线程是指一个进程中同时运行多个线程,每个线程相对独立地执行不同的任务,共享进程的资源。Java语言提供了Thread类和Runnable接口以实现多线程编程。通过多线程可以提高CPU利用率,加快程序执行速度,但也会带来线程同步等问题。
举例:在Java中,创建新的线程通常使用Thread类或者Runnable接口,如下所示:
public class MyThread extends Thread{
public void run(){
// 执行某些操作
}
}
public class MyRunnable implements Runnable{
public void run(){
// 执行某些操作
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
MyRunnable runnable = new MyRunnable();
Thread thread2 = new Thread(runnable);
thread2.start();
- 多进程
多进程是指一个程序同时运行多个进程,每个进程有自己的独立地址空间,并与其他进程互相独立。多进程在保证数据隔离的同时,能够充分利用系统的资源,但是进程之间的通信也需要一定的开销和时间代价。
举例:在Java中,可以使用Runtime类和ProcessBuilder类创建新的进程。如下所示:
Runtime.getRuntime().exec("myCommand");
ProcessBuilder pb = new ProcessBuilder("myCommand");
Process process = pb.start();
- 异步
异步是指程序的执行方式,即某个操作不会阻塞代码的进程或线程,而是通过回调函数等方式在后台执行。异步通常使用事件驱动或者回调函数来实现。
举例:在Java中,可以使用Future和CompletableFuture类实现异步调用。如下所示:
Future<Integer> future = new CompletableFuture<>();
new Thread(() -> {
// 执行一些耗时的操作
int result = 123;
// 将结果设置到future中
future.complete(result);
}).start();
// 阻塞并等待结果
int result = future.get();
- 并发
并发是指多个任务同时执行,每个任务独立运行,并与其他任务相互交错执行。并发能够提高程序整体执行效率,但也会带来资源占用、线程同步等问题。
举例:在Java中,可以使用synchronized关键字和ReentrantLock类等机制来控制并发访问共享资源。如下所示:
public class Counter {
private int count;
public synchronized void increment() {
count++; // 线程安全的自增操作
}
public int getCount() {
return count; //非线程安全的获取操作
}
}
总之,多线程、多进程、异步、并发等概念都与程序执行方式和效率相关,并且都需要仔细考虑各种问题,如线程安全、锁竞争等等。开发者需要根据具体业务需求选择合适的技术手段来实现程序。
三. 案例重现
//辅助对象
SysUser user = null;
//培训信息用户
RancoUser rancoUser = userService.selectUserByUserNameRanco(username);
//服务信息用户
SysUser server = userService.selectUserByUserNameServer(username);
//客户信息用户
SysUser customer = userService.selectUserByUserNameCustomer(username);
//知识库用户
SysUser knowledage = userService.selectUserByUserNameKnowledage(username);
//判断那个不为空辅助对象为那个
user = StringUtils.isNotNull(server) ? server : StringUtils.isNotNull(customer) ? customer : StringUtils.isNotNull(knowledage) ? knowledage : null;
//服务,客户,知识三大模块的用户鉴权
if (StringUtils.isNotNull(user)) {
if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
} else {
passwordService.validate(user);
return createLoginUser(user);
}
}
//都为空进行输出
else {
log.info("登录用户:{} 不存在.", user.getUserName());
throw new ServiceException("登录用户:" + user.getUserName() + " 不存在");
}
在这里,我们可以清楚的看到
//培训信息用户 RancoUser rancoUser = userService.selectUserByUserNameRanco(username); //服务信息用户 SysUser server = userService.selectUserByUserNameServer(username); //客户信息用户 SysUser customer = userService.selectUserByUserNameCustomer(username); //知识库用户 SysUser knowledage = userService.selectUserByUserNameKnowledage(username);
这四个每一次都会进行一次换库查询,注意,这里是换库,不是切换数据表
21:03:01.376 [pool-2-thread-2] INFO c.r.f.d.DynamicDataSourceContextHolder - [setDataSourceType,26] - 切换到SLAVE3数据源 21:03:01.377 [pool-2-thread-3] INFO c.r.f.d.DynamicDataSourceContextHolder - [setDataSourceType,26] - 切换到SLAVE2数据源 21:03:01.376 [pool-2-thread-1] INFO c.r.f.d.DynamicDataSourceContextHolder - [setDataSourceType,26] - 切换到SLAVE1数据源
所以呢,是一个非常耗时的事情
四. 性能优化
对于代码进行分析,我们发现代码其实有很多的相似之处,所以我们对于代码进行如下优化
//辅助对象
SysUser user = null;
//服务信息用户
SysUser server = null;
//客户信息用户
SysUser customer = null;
//知识库用户
SysUser knowledage = null;
//创建一个包含3个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
//将三个任务封装成Callable对象
Callable serverCallable = new Callable() {
public SysUser call() throws Exception {
return userService.selectUserByUserNameServer(username);
}
};
Callable customerCallable = new Callable() {
public SysUser call() throws Exception {
return userService.selectUserByUserNameCustomer(username);
}
};
Callable knowledageCallable = new Callable() {
public SysUser call() throws Exception {
return userService.selectUserByUserNameKnowledage(username);
}
};
//提交Callable任务给线程池并获取Future对象
Future<SysUser> serverFuture = executorService.submit(serverCallable);
Future<SysUser> customerFuture = executorService.submit(customerCallable);
Future<SysUser> knowledageFuture = executorService.submit(knowledageCallable);
//获取任务执行结果
try {
server = serverFuture.get();
customer = customerFuture.get();
knowledage = knowledageFuture.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
//关闭线程池
executorService.shutdown();
//判断那个不为空辅助对象为那个
user = StringUtils.isNotNull(server) ? server : StringUtils.isNotNull(customer) ? customer : StringUtils.isNotNull(knowledage) ? knowledage : null;
//服务,客户,知识三大模块的用户鉴权
if (StringUtils.isNotNull(user)) {
if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
} else {
passwordService.validate(user);
return createLoginUser(user);
}
}
//都为空进行输出
else {
log.info("登录用户:{} 不存在.", user.getUserName());
throw new ServiceException("登录用户:" + user.getUserName() + " 不存在");
}
代码解读
上述代码实现了一个多线程的登录校验功能,主要流程如下:
- 定义了四个辅助对象user、server、customer、knowledage,分别表示服务信息用户、客户信息用户、知识库用户。
- 创建一个包含3个线程的线程池executorService。
- 将三个任务封装成Callable对象,分别对应服务信息用户的查询、客户信息用户的查询和知识库用户的查询操作。
- 提交Callable任务给线程池并获取Future对象。
- 通过调用Future对象的get()方法获取任务执行结果,将结果赋值给对应的辅助对象user。
- 根据辅助对象user的不同值进行不同的处理,如果存在服务信息用户的辅助对象则进行服务鉴权,如果存在客户信息用户的辅助对象则进行客户鉴权,否则进行知识库鉴权。
- 如果存在有效的用户辅助对象,则进行密码校验并返回登录成功的用户;否则抛出异常提示用户不存在或已被停用。
- 最后输出登录失败的信息。
需要注意的是,在多线程环境下,对共享变量的操作需要考虑线程安全性。对于本例中的辅助对象user,可以使用ConcurrentHashMap来代替传统的Java集合类实现,以确保多个线程同时对其进行访问时的线程安全性。
五. 场景抉择
多线程:适用于需要同时处理多个任务并且这些任务之间存在一定的关联性,例如网络编程、UI编程等。在网络编程中,每个客户端连接可以对应一个线程,处理该客户端的请求,加速服务处理的效率;在UI编程中,需要有一个主线程负责事件响应和界面更新,而耗时的操作则需要放到其他线程中执行,以保证用户体验。
多进程:适用于需要不同进程之间进行资源隔离,并能充分利用系统资源的场景,例如操作系统管理、大规模数据处理等。在操作系统管理中,每个程序可以独立运行在自己的进程中,避免了进程间互相干扰和占用资源的问题;在大规模数据处理中,可以通过将大数据分成若干个小块,在不同进程中并行处理,提高数据处理的效率。
异步:适用于需要提高程序并发度,减少等待时间的场景,例如网络通信、IO操作等。在网络通信中,如果采用同步阻塞方式,一个请求的响应需要等待服务端完成业务才能返回,会阻塞客户端的主线程,影响其它任务的执行;而异步方式,程序可以在等待的同时继续执行后续任务,当服务端响应时再通过回调等方式进行处理,提高了程序的并发度和效率。
并发:适用于需要同时执行多个任务的场景,例如服务器编程、数据库访问等。在服务器编程中,可以通过支持多线程或者多进程来实现并发处理客户请求;在数据库访问中,多个客户端可以同时访问数据库,避免了因为单一的访问入口而导致的性能瓶颈等问题。
总之,不同的技术手段都有其对应的使用场景,开发者需要根据具体业务需求选择合适的技术方案。