背景
故事的起源来自于我写的Controller层中每一个方法都有这段代码
User u = userService.getUserByUserName(userDTO.getUsername());
例如,登录我需要从数据库中查询该用户,注册我也需要从数据库中查询该用户,那么对于太重复的代码我是否可以抽离出来相应的逻辑,使得代码再简便些呢?
我想到的是这样
将这一条往上提,从局部变量改造为成员变量,这样的话那么下面直接引用这个经过数据库查询好的u实例不就可以了,数据库只查询了一次,也节省了代码
类在JVM层面带来的数据共享问题探究
那么就会带来一个问题,例如A用户登录和B用户登录之后,u到底是A还是B,它们之间会不会有干扰,另外我突然开始思考,对于一个请求路由,Controller是不是多线程的,平均的分配给每个请求,Controller类里面的成员变量,局部变量对于不同请求的修改会有什么变化
于是我写了一个Controller类测试一下
@RestController
public class Test {
boolean test = true;
@GetMapping("/test")
public void test(String user){
test = !test;
System.out.println(user+"请求");
System.out.println(Thread.currentThread().getName()+":"+test);
}
}
我在前端发送两条请求
控制台输出
证明对于请求来说,一个请求对应一个Controller以及一个被分配处理的线程,但是对于类中的成员变量来说,因为成员变量是存在堆中的,所有的实例对象共享这些数据,所以,确实如果不去做相应处理的话,这些问题可能导致程序的行为不可预测,比如产生错误的计算结果、破坏数据的一致性或者导致程序完全死锁,而对于存在于栈中的局部变量来说,每个线程都会将方法中的局部变量压入栈帧中的局部变量表,这样的话倒是不担心这种线程安全的问题,因为线程与线程之间是相互独立的,是不是这样的话,反而不去按照将u实例提到成员变量反而更好?或者说用一个Utils类去将这一部分重复代码抽离掉,虽然可以,但是我还是觉得不满意,我想要追求的是,用更少的话,去做更多的事情
觉得自己学的很好了,但是一次小小的意外仍让我察觉到自己知识的浅薄
我不小心又发送了一次A请求,这次我却又发现了更加不一样的东西
A请求的线程怎么又更换了?,即便是相同类型的请求每次执行的线程也都不一样,原来我的理解依旧仍然还是肤浅,于是我更加感兴趣的去探索原理
追寻原理,我想到了线程池的概念,在翻阅相关资料中,我发现在SpringBoot的内置web服务器中,服务器为了高效处理请求,不是为每一个请求创建一个新的线程,而是从一个预先创建的线程池中分配线程来处理请求,当然这样比较高效嘛.
至此,像类似于web服务器中的线程,线程池我突然有了一种成型的概念,其实这些东西就像线程池,线程安全,tomcat服务器线程,这些概念我仅仅停留在运用阶段,只是会写代码,会排错,会修Bug或者仅仅配置像tomcat的配置文件以用来修改tomcat的功能,而我对于原理的探究却很片面,今天我却对所有的流程有一种更加直观立体的感受了,这为我以后将项目的性能调优方法是浓墨重彩的一笔
好像以前所学的线程相关的一切一下子都通过自己实际的应用通透了....
话又说出来,既然是web服务器的线程池,我用的这个是tomcat,那么我这个线程池管理策略是什么?初始值是多少?
通过询问专业人士,我得到了答案
其实这样也就是说SpringBoot本身并不强制一个全局的线程池策略,而是灵活的让开发者自己选择,这很灵动,就像Spring一样,是开发者的春天
新概念异步的究极底层原理以及与C#的区别
但是专业人士又提出一个新概念@Async的异步方法,这是一个什么东西呢?我之前努力学过一段时间的C#,在C#中使用async关键字标记的方法称为异步方法。async
方法通常与await
关键字一起使用,以实现非阻塞的异步编程。当在异步方法中遇到await
表达式时,它确实会将运行权(控制权)“转让”给调用者或调度器,允许其他操作在等待异步操作完成时继续运行,从而提高应用程序的响应性。
但是对于Java来说是这一回事吗?异步到底是什么?
我问了我特别特别牛的哥,请他给我解答(C#方向)
按照我哥的原理解释,其实异步从底层原理来说是将CPU本该处理的IO操作交给了其他元器件去读取等操作,用来提升软件性能的吞吐量,并且在异步操作完成后通过读取上下文恢复到挂起前的状态,毕竟就如我哥说的,线程是很宝贵的资源,不能够一直被耗时的操作占用,所以也尽可能的往IO方面去优化
那么Java和C#之间的有什么区别呢?专业人士这么跟我说
现在我们通过web服务器提供的线程池将异步也都了解完了,了解完原理,我们就上实践吧,SpringBoot使用异步方式
步骤1:启用@Async
首先,你需要在你的Spring配置类中使用@EnableAsync
注解来启用异步方法的支持。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
// 这个类可以是空的,@EnableAsync是关键
}
步骤2:创建异步服务
接着,定义一个服务类,并在其中创建一个或多个使用@Async
注解的方法。这表示这些方法将异步执行。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
@Async
public void executeAsyncTask() {
// 模拟一个耗时操作
System.out.println("开始执行异步任务,线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(5000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步任务执行完毕,线程名称:" + Thread.currentThread().getName());
}
}
步骤3:调用异步方法
最后,在一个控制器或另一个服务类中,注入上面创建的服务,并调用异步方法。注意,调用异步方法看起来和调用普通方法没有区别,但实际上它的执行是异步的。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private AsyncService asyncService;
@GetMapping("/testAsync")
public String testAsync() {
System.out.println("调用异步方法之前,线程名称:" + Thread.currentThread().getName());
asyncService.executeAsyncTask();
System.out.println("调用异步方法之后,线程名称:" + Thread.currentThread().getName());
return "异步任务已经启动";
}
}
步骤4:示例输出
调用异步方法之前,线程名称:http-nio-8080-exec-1
调用异步方法之后,线程名称:http-nio-8080-exec-1
开始执行异步任务,线程名称:SimpleAsyncTaskExecutor-1
异步任务执行完毕,线程名称:SimpleAsyncTaskExecutor-1
可以看到,控制器方法在同一个线程(例如http-nio-8080-exec-1
)中开始和结束,而异步方法则在不同的线程(SimpleAsyncTaskExecutor-1
)中执行。这正是@Async
提供的异步执行功能的直观演示。
我觉得线程重用可能会带来一些线程局部变量污染的问题
好了聊完异步,wen线程池,其实我还有一个想法,先引入一个概念,ThreadLocal类为线程提供局部变量的能力,并且因为每个线程都有属于自己的ThreadLocal类,所以在线程数据上它们是相互隔离的,通过这个概念我们可以将用户登录信息例如用户id,账户名等加密后的Jwt令牌信息存入ThreadLocal类中,这样在一个线程活动中,可以通过ThreadLocal内的Jwt令牌进行各种校验工作,用户请求线程相互隔离,但是对于线程池来说,一个线程被使用完毕后回收到线程池立刻又被新的http请求使用,如果该线程里面的数据,例如上一个用户的Jwt令牌还没有被清理,下一个用户的http请求是否会访问到不该访问的数据?也就是线程重用带来的问题
我做了一个这样的实验
在application.yml中修改tomcat的线程最大数量
server:
port: 8080
servlet:
context-path: /
tomcat:
threads:
max: 2
min-spare: 2
然后利用PostMan发送A用户登录请求
B用户请求
之后通过代码再读ThreadLocal中的变量,我发现果然,再未清理的情况下,线程残留着Jwt令牌的相关信息,这可以被称为线程局部变量污染,可能是开发过程中会遇到的一个坑,那么如何避免呢?
我们在过滤器中定义,当请求完成之后,我们可以对ThreadLocal进行清理操作,当然,这确实也是这个方法定义时应该做的事情
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ThreadLocalUtils.remove();
}
话又说回来,之前我想到的提升局部变量成为成员变量这个问题确实不太好,因为对于现在的计算机来说,已经不需要那么扣死细节疯狂克扣每一份内存了,并且如果提升了,那么前端请求映射根据用户名调用的方法总不能直接写在类上吧,而且参数传递总得有映射,所以并不可行,即便想方设法去运行这种思路,还要为线程安全考虑,实在是没必要,但是通过这个问题引发的思考以及学习,却是我的一大进步