多线程 | 多线程实现方式和差异
1、为什么要使用多线程呢?
从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。
现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
提高CPU的利用率 目前大多数CPU都是多核的 可以都利用起来。
耗时的操作使用多线程,可以异步执行提高应用程序响应。
常用场景:客户端请求后可以异步执行不用返回给客户端的数据处理、后台定时任务中的异步分批执行任务、优化复杂查询(FutureTask)等等。
2、线程的生命周期和状态?

3、使用多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题。
4、线程创建方式?
继承 Thread
private static void thread() {
Thread thread = new Thread(() -> {
try {
// 业务代码 。。。
//加sleep 试一下执行顺序
Thread.sleep(100);
System.out.println("Thread方式执行 新 线程:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//调用 start 才会执行线程中的 run()方法中的业务代码
thread.start();
System.out.println("Thread方式执行 主 线程:"+Thread.currentThread().getName());
}
通过实现Thread类型创建线程(实际开发中用到的不多,直接用线程池,只是为了单开一个线程可以使用)
执行结果:可以看出没有按照顺序执行,异步的。主线程比新开线程先执行。

实现 Runnable
private static void runnable() {
Runnable runnable = ()-> {
// 业务代码 。。。
try {
//加sleep 试一下执行顺序
Thread.sleep(100);
System.out.println("Runnable 方式执行 新 线程:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
//调用 start 才会执行线程中的 run()方法中的业务代码
new Thread(runnable).start();
System.out.println("Runnable 方式执行 主 线程:"+Thread.currentThread().getName());
}
通过实现Runnable和Thread的结果是一样的。

3、实现Callable
private static void callable() throws ExecutionException, InterruptedException {
Callable callable = ()-> {
// 业务代码 。。。
//加sleep 试一下执行顺序
Thread.sleep(100);
System.out.println("Callable 方式执行 新 线程:" + Thread.currentThread().getName());
return 1;
};
//要使用Callable还不能直接像 Runnable 一样,而是要借助 FutureTask
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
new Thread(futureTask).start();
//获取返回值
System.out.println(futureTask.get() );
System.out.println("Callable 方式执行 主 线程:"+Thread.currentThread().getName());
}
通过实现Runnable ,使用callable还不能直接像 runnable 一样,而是要借助 FutureTask。可以得到新线程的结果,且异常可以抛到主线程种(这里也有个坑,就是如果新新线程种出现异常抛到主线程,可能不是很容易定位)

4、线程池创建方式

线程池的4种创建方式
Executors.newCachedThreadPool() 创建一个可缓存的线程池
这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());
Executors.newFixedThreadPool(3) 创建固定大小的线程池
每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
LinkedBlockingQueue 核心线程和最大线程是相同的
Executors.newSingleThreadExecutor() 创建一个单线程化的线程池
这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
核心线程和最大都是一个 LinkedBlockingQueue
Executors.newScheduledThreadPool(5) 创建一个定长线程池。此线程池支持定时以及周期性执行任务的需求。— 延迟执行
ThreadPoolExecutor的方式程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写更加明确线程池的运行规则,规避资源耗尽的风险。
基于ThreadPoolExecutor线程池工具类
public class ThreadPoolUtils {
//可用处理器的Java虚拟机的数量
private static int CORE_SIZE = Runtime.getRuntime().availableProcessors();
private static int CORE_MAX_SIZE = Runtime.getRuntime().availableProcessors()*2;
// volatile 保证线程的存可见性
private static volatile ExecutorService executorService ;
private static ExecutorService getInstance(){
synchronized (ThreadPoolUtils.class){
if(executorService == null){
synchronized (ThreadPoolUtils.class){
return executorService = new ThreadPoolExecutor(CORE_SIZE, CORE_MAX_SIZE , 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
return executorService;
}
}
public static void submit(Runnable runnable) {
getInstance().submit(runnable);
}
public static <T> Future<T> submitCall(Callable<T> callable) {
return getInstance().submit(callable);
}
public static void synCall(Runnable... runnableSet) {
for (Runnable runnable : runnableSet) {
getInstance().submit(runnable);
}
}
}
5、SpringBoot 中的多线程开启
在SpringBootApplication启动类上开启这个异步注解@EnableAsync。
配置 Configuration 配置线程池的参数
在需要开启异步的方法上面加注解 @Async 【异步方法不要和主线程在同一个类,不然可能会不生效哦】
@Bean("scheduledTaskExecutor")
public Executor asyncSendCouponsScheduledTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
//配置最大线程数
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
//配置队列大小
executor.setQueueCapacity(500000);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("scheduled-task-executor-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
5、为什么要⽤线程池?
降低资源消耗:通过重复利用已创建的线程减少线程的创建和销毁造成的消耗。
提高响应速度:当任务到达时,可以不用等待线程创建就可以直接执行。
提⾼线程的可管理性:线程时稀缺资源,不断地无限制创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优、监管。
总结
1、Thread和Runnable的异同:
Thread 和 Runnable本质上没有什么区别,看源码可以看见Thread是 类,Runnable是 接口,Thread实现了Runnable接口(只有一个run()方法)。
Thread作为实现类扩展了很多实现方法。

在使用上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现Runnable 。
2、Runnable与Callable的异同
相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
不同点
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
要使用Callable还不能直接像 Runnable 一样,而是要借助 FutureTask
3、相比new Thread,Java提供的四种线程池的好处在于:
每次new Thread新建对象性能差。
线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
缺乏更多功能,如定时执行、定期执行、线程中断。
线程池重用存在的线程,减少对象创建、消亡的开销,性能佳。
线程池可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
线程池提供定时执行、定期执行、单线程、并发数控制等功能。