1.聊聊线程和进程
线程是处于进程之中,每个进程都有独立的内存空间,而每个线程都共享一片内存,但是每个线程都有单独的栈内存,用于存储本地数据。
1.1创建线程的方式有哪几种?
- 通过继承Thread类
- 通过实现Runnable接口
- 通过实现Callable接口
- 通过线程池来创建
- 通过Java8新特性lambda表达式
1.2 start()和run()的区别
start():当执行start()方法的时候会启动一个线程,然后执行对应的run(),但是start()方法只会执行一次
run():该方法和普通的方法一样,能够执行多次。单独调用run()方法的话,会在当前线程中执行run(),不会启动会新线程
1.3 Calleble接口和Runable接口的区别
1.Callable规定的方法是call(),Runnable规定的方法是run().
2.Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
3.call方法可以抛出异常,run方法不可以
2. 线程池
2.1 为什么要用线程池?
线程池顾名思义就是用来管理线程的池子,线程池的主要工作就是控制运行的线程的数量,处理过程中将任务放入队列中,然后线程创建后,就启动这些任务。如果线程数量超过了最大线程数就排队等候,等其他线程执行完毕后,就从队列里面取出任务来执行。
线程池的优点:
- 降低资源消耗。重复利用已经创建的线程能够降低线程创建和销毁造成的销毁
- 提高响应速度。任务到达时,无需等待线程创建就可立即执行任务
- 提高线程的管理性。线程是稀缺资源,创建线程和销毁线程会影响系统性能。如果无休止创建,不仅消耗系统资源,还影响系统的稳定性,通过线程池进行统一的分配、调优、监控。
2.2 架构说明
Java中的线程池是通过Executor框架实现的,该框架使用了Executor,Executors(代表工具类),ExecutorService,ThreadPoolExecutor这几个类
2.3 创建线程池
- Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池。创建固定线程数量的线程池,可以控制最大并发线程数,超出的线程在队列中排队等候
- Executors.newSingleThreadExecutor:创建一个只有一个线程的单线程池。一个任务一个任务执行的场景,保证所有任务按照指定顺序执行
- Executors.newCacheThreadPool():创建一个可扩容的线程池。执行很多短期异步的小程序或者负载较轻的小程序。创建一个可缓存的线程池,如果线程长度超过处理需要,可收回空闲线程。
- Executors.newScheduledThreadPool(int corePoolSize):线程池支持定时以及周期性执行任务,创建一个corePoolSize即核心线程数作为传入参数,最大线程数为整形的最大数的线程池
具体使用,首先我们需要使用Executors工具类,进行创建线程池,这里创建了一个拥有5个线程的线程池
// 一池5个处理线程(用池化技术,一定要记得关闭)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 创建一个只有一个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个拥有N个线程的线程池,根据调度创建合适的线程
ExecutorService threadPool = Executors.newCacheThreadPool();
执行下面的业务场景:
10个用户来办理业务,每个用户就是一个来自外部请求线程
我们需要使用 threadPool.execute执行业务,execute需要传入一个实现了Runnable接口的线程:
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户办理业务");
});
使用完毕后关闭线程池:
threadPool.shutdown();
完整代码为:
public class MyThreadPoolDemo {
public static void main(String[] args) {
// Array Arrays(辅助工具类)
// Collection Collections(辅助工具类)
// Executor Executors(辅助工具类)
// 一池5个处理线程(用池化技术,一定要记得关闭)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
try {
// 循环十次,模拟业务办理,让5个线程处理这10个请求
for (int i = 0; i < 10; i++) {
final int tempInt = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1 给用户:0 办理业务
pool-1-thread-5 给用户:4 办理业务
pool-1-thread-1 给用户:5 办理业务
pool-1-thread-4 给用户:3 办理业务
pool-1-thread-2 给用户:1 办理业务
pool-1-thread-3 给用户:2 办理业务
pool-1-thread-2 给用户:9 办理业务
pool-1-thread-4 给用户:8 办理业务
pool-1-thread-1 给用户:7 办理业务
pool-1-thread-5 给用户:6 办理业务
2.4 底层实现
我们通过查看源码,点击了Executors.newSingleThreadExecutor 和 Executors.newFixedThreadPool能够发现底层都是使用了ThreadPoolExecutor
我们可以看到线程池的内部,还使用到了LinkedBlockingQueue链表阻塞队列
同时在查看Executors.newCacheThreadPool 看到底层使用的是SynchronousBlockingQueue阻塞队列
最后查看一下,完整的三个创建线程的方法
2.5 线程池的七大参数
线程池在创建的时候,有七大参数:
- corePoolSize:核心线程数,线程池的默认线程数量。
- maximumPoolSize:线程池中的最大线程数量,此值必须大于等于1
- keepAliveTime:多余的空闲线程存活时间。当线程池的数量超过corePoolSize时,当空闲时间达到corePoolSize,多余的空闲线程会被销毁,只剩下核心线程数为止。默认情况下,只有当线程池中的线程数量大于核心线程数,keepAliveTime才会起作用
- unit:keepAliveTime的单位
- workQueue:工作队列,被提交但未执行的任务(类似于银行中候客区)。
- threadFactory:表示在线程池中生产工作线程的线程工厂,用于创建线程池,一般用默认即可
- handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数时,用于拒绝线程请求的策略
2.6 拒绝策略
拒绝策略都实现了RejectedExecutionHandler接口
- AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛异常,如果允许任务丢失,是一种好的解决方案
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列尝试再次提交当前任务
- CallerRunsPolicy:既不抛异常也不丢去任务,而是将某些任务回退给调用者
2.7 线程池的底层工作原理
线程池运行架构图
文字说明:
- 在创建了线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务,线程池会做出如下判断:
- 如果正在运行的线程数量小于核心线程数,那么会马上创建线程运行任务
- 如果正在运行的线程数量大于等于核心线程数,那么就将任务放入队列
- 如果这时候队列满了,但是正在运行的线程数量还小于最大线程数,那么启动非核心线程数运行任务
- 如果这时候队列满了并且线程运行数量大于最大线程数,此时线程池会启用饱和的拒绝策略
- 当一个线程完成任务时,就会从队列中取出任务来运行
- 当线程无事可做到一定时间(keepAliveTime),线程池会做如下判断:
- 如果当前运行线程大于核心线程数,那么这个线程就会被停掉
- 如果线程池中的所有任务完成后,那么会恢复到核心线程数