这个话题在入行之前就想过很多次,很多8古文或者你搜索的结果都是告诉你什么提高高并发或者是一些很高大上的话,既没有案例也没有什么公式去证明,但是面试中总是被问到,也没有实战经历,所以面试时一问到多线程的东西就无从下手,接下来我就谈谈我的理解。
1.Java中为什么要使用多线程?
多线程:其实是在主线程中再开启一个线程去执行。
为什么要用呢?
先不谈什么高并发,这三个字太荒谬。从基本的作用开始说,其实就是对于一个请求内的耗时操作另外开启一个新的异步线程,不让耗时操作阻塞主线程,这样主线程直接跳过耗时操作去往下面执行,然后返回结果给前端,提高了响应速度。
上案例,这个我们先看一下没有使用多线程的正常请求?
Controller
Service
很简单的东西,我模拟testThread这个里面的东西是一个耗时操作,让它睡了10秒,也就是相当于一个耗时操作。正常请求的话前端肯定是得等10s以后才有响应,就会显示一直加载
正常打印结果
也就是内容是自上而下依次执行的。
上面案例全是靠主线程自己执行的,假如现在使用多线程,就是对于10s耗时的testThread这个操作开启一个异步线程去执行,会有什么效果?此处暂时只加上一个注解就表示这个方法使用了多线程,下面会介绍如何使用,此处先知道就是开启了异步线程。
再来看看响应结果及其速度,
结果表明,它会先打印主线程的东西,然后再打印开启的异步线程内的东西,并且呢可能大家以为主线程提前返回了,那个10s的耗时操作怎么办?结束了吗?其实并没有它只是在后台继续执行10s结束后打印的"66666666666666",但是我们提前结束了请求响应给了前端。这样前端就不用等很长时间才能拿到结果对吧。别说10s,实际2s都太慢了,但是这个耗时操作一般是什么呢?例如发送邮件,读取文件,等等,也可能是公司的具体业务,并且这个耗时操作的结果不影响你下面继续执行的代码,即你继续执行的代码不依赖于耗时操作的结果。
所以第一个用处就可总结为:开启异步线程处理耗时操作,提高请求的响应速度。
其次再谈谈下一个作用,充分利用资源,提高了CPU的利用率。什么意思呢?我们知道CPU在同一时间内是支持多个线程同时执行的,我们如果不使用多线程,也就是主线程自己去完成所有操作,一定会因为耗时操作阻塞主线程,此时导致CPU空闲,那利用率肯定不高嘛,那我们如果使用异步线程去做,那就是两个线程同时去完成一件事情,多多少少是要比一个线程要快的,而且我们充分利用了它的特性在同一时刻开启多个线程去执行。
所以第二个用户可总结为:分利用资源,提高了CPU的利用率
下面就说说提高高并发怎么回事?上面两个我一开始就知道而且比较好理解,但是高并发这个确实抽象,但是好在下点功夫还是能理解的。高并发:最多能同时处理的请求数量。我们知道那这个肯定是我们服务器的功能,拿我们后端Tomcat来说,支持最大线程数200,最大并发量1000,我就可好奇,200个线程怎么处理1000请求的,很离谱,并且一个请求就耗费一个线程了,实际上是1秒内,有可能一个请求0.5s就结束了,此时线程让出来了处理其他请求了。所以理想情况下在1s内还是有可能的,但是为什么说使用多线程也可以呢,因为这样的话,首先Tomcat的线程可以理解为我们的主线程,如何让主线程更快的释放出来,然后让它去处理别的请求,这才是我们关心的,而我们多线程刚好就解决了这个困难,首先我们自己去创建一个异步线程去处理耗时操作,这个异步线程你想创建多少个都没关系,跟Tomcat提供的那个主线程没关系,不会占用它的那200个。这样我们就让耗时操作让我们的异步线程执行,主线程就释放出来了,这样就提高了整体的并发量。
上面所说的主线程就是main线程,是由Tomcat提供的。而异步线程是jdk提供的。
所以第三可总结为:提高系统的并发处理能力
2.多线程的创建?
1.继承Thread
2.实现Runnabe接口
3.实现Callable接口
4.Executors.newFixedThreadPool
好的上面这个是标准的八股文,但实际工作中不用,1,2,3不用我说也肯定不使用,已经有线程池了,肯定使用线程池的池化思想。好处再帮各位回忆一下
1.减少频繁创建线程和销毁线程带来的额外开销,线程池可以提前帮我们创建好这些线程。
2.避免一直创建线程造成OOM,而线程池会把使用过的线程再使用,避免重复创建线程。
.......还有一些,自己下去可以会议下。
那上面我们都不用,但是得知道,因为面试造火箭,这些先表明你都知道,后面紧接着说,实际工作中并不会使用上面的几种方式去创建多线程,而是使用spirng给我们提供的ThreadPoolTaskExecutor或者是jdk提供的ThreadPoolExecutor,一般还是用sping提供的,毕竟现在项目都是spring家族的,IOC这么好用,必须得用上。下面上实战。
3.在代码中如何使用多线程(线程池的方式)?
我就说我在项目中看到的都是使用spring提供的ThreadPoolTaskExecutor,而并没有使用jdk提供的ThreadPoolExecutor,并不是说不可以,肯定是因为spirng提供的更简单,它提供的IOC自动注入,使用相关注解直接使用,比我们自己去创建对象更方便,也更实用。但是一定不要使用Executors.newFixedThreadPool这玩意去创建,阿里开发规范中也明确禁止,为什么禁止?
- 隐藏关键配置参数:`Executors`提供的便捷方法通常会隐藏线程池的重要配置参数,比如线程池的大小、工作队列类型及容量、拒绝策略等。
这限制了开发者对线程池行为的精确控制和优化,可能导致资源使用不当或性能问题。
- 潜在的资源耗尽风险:
(1)`newFixedThreadPool`和`newSingleThreadExecutor`使用的是无界队列(通常为`LinkedBlockingQueue`),
这意味着如果生产任务的速度超过消费速度,队列会无限增长,最终可能导致内存耗尽(Out Of Memory Error)。
(2)`newCachedThreadPool`创建的是一个线程数量无界的线程池,当大量短期异步任务提交时,可能会迅速创建大量线程,消耗过多系统资源
总结一句:缺少参数配置,不可控,可能会造成OOM,消耗过多的系统资源。
如何使用?
1.创建相关的配置类,起个名字,一般叫XxxThreadPoolConfig(Xxx为相关的业务的名称)
java
@Component
@Configuration
public class TestThreadPoolConfig {
@Bean("test_thread_pool") // 设置默认线程名称
public Executor ticketing() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("test_thread_pool-");
executor.setMaxPoolSize(80); // 设置最大线程数
executor.setCorePoolSize(40);//设置核心线程数
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(300);// 设置线程活跃时间(秒)
executor.setAllowCoreThreadTimeOut(true);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 设置拒绝策略
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待所有任务结束后再关闭线程池
executor.initialize();
return executor;
}
}
2.在启动类上加上开启异步的注解@EnableAsync
java
@SpringBootApplication
@EnableAsync
public class SystemAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SystemAdminApplication.class,args);
}
}
3.在对应的耗时接口ServiceImpl上直接使用注解@Async("xxx")xxx为配置中@Bean中的名字
java
@Async("test_thread_pool")
public String testThread() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("666666666666666");
return "hello";
}
4.总结
为什么要使用多线程,哪些业务中用到了(实际开发业务中),怎么用的(多线程的创建方式)?
(1)处理耗时操作,提高响应速度,举例
(2)充分利用系统cpu的资源
(3)提高系统的并发量,拿上述的comcat来举例
(4)你怎么使用的?也就是项目如何用的,说我总结的第三点,具体Service业务,如果不知道怎么编,自己上网搜搜,比如发送邮件,读取文件等等,我的话肯定就是说自己项目中遇到的业务。
本人还是菜鸟,错误还希望大佬指点。。。。。。。