目录
1. 线程
1.1 并发和并行
1.2 进程和线程
1.3 创建线程的方式
1.3.1 继承 Thread 的方式(无返回值)
1.3.2 实现Runnable接口(无返回值)
1.3.3 实现Callable接口(有返回值)
1.4 线程的生命周期
2. 线程池
2.1 为什么要使用线程池
2.2 使用线程池的优势
2.3 线程池的应用场景
2.4 spring封装创建线程池
2.5 jdk 创建线程池的七种方式
2.6 自定义多个参数创建线程池
2.7 线程池七个参数的设计方
学习线程之前我们需要一点知道相关的知识:
时间段: 并发是指在同一时间段发生的事情
时间点: 并行是指在同一时间点同时发生的事情
多线程情况是属于并发和并行
进程:正在执行的应用程序
线程:存在于进程中,是进程中最小的执行单元,一个进程至少有一个线程。
现实生活中应用程序绝大多数都是多线程的程序。
多个应用软件是如何执行的:
1、CPU会把所有程序的线程收集起来
2、线程主动去 “抢占式” 的夺取CPU的执行权
3、哪个线程抢到了执行权就执行哪个程序的该线程
1、创建一个类,此类继承Thread类
2、手动重写父类的run()方法
3、在run方法内编写线程需要执行的任务
4、在main方法内,创建此类对象
5、并调用 start() 方法,启动线程public static void main(String[] args){ // 开启5个线程 for (int i = 0; i < 5; i++) { TestThread testThread = new TestThread(); testThread.setName("自定义线程-" + i); testThread.start(); } } static class TestThread extends Thread{ @Override public void run() { int num = 5; for (int i = 0; i < num; i++) { System.out.println( "线程名:" + getName() + ":" + i); } } }
1、创建一个类,实现Runnable接口
2、必须重写run方法,在run方法内编写线程的任务
3、在测试类的main方法内,创建线程对象Thread类的对象
4、把你刚才实现的类对象传入到线程对象中
5、启动线程// 以内部类方式体现代码 public static void main(String[] args){ // 开启5个线程 for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println( "添加需要执行的程序。。。线程名:" + Thread.currentThread().getName()); } }); System.out.println("线程名称 = " + thread.getName()); thread.start(); } }
1、创建一个类实现Callable 2、重写call方法 3、把实现类对象传递给FutureTask对象 4、在测试类中创建Thread对象,把FutureTask对象传递给Thread对象 5、启动线程 6、获取线程结果 // 以内部类方式体现代码 public static void main(String[] args) throws ExecutionException, InterruptedException { // 开启5个线程 for (int i = 0; i < 5; i++) { FutureTaskfutureTask = new FutureTask<>(new Callable () { @Override public String call() throws Exception { System.out.println("添加需要执行的程序。。。"); return "实现Callable多线程启动。。。"; } }); Thread thread = new Thread(futureTask); // 先开启线程,在获取线程结果 thread.start(); System.out.println("获取到的返回值" + futureTask.get()); } }
线程中关于等待和睡眠的方法:
Thread类中有静态方法:
static void sleep(long millis): 让线程睡一会,拿着锁资源睡Object类中有几个方法:
void wait():让线程进入无限等待,等待的时候会释放锁资源
void wait(long millis):让线程进入限时等待,等待的时候会释放锁资源
void notify():唤醒休眠的单个线程(随机唤醒)
void notifyAll():唤醒所有休眠的线程
新建状态(New) -> 就绪状态(Runnable) -> 运行状态(Running) -> 阻塞状态(Blocked) -> 死亡状态(Terminated)
线程池(thread pool):一种线程使用模式。线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。线程池不仅能够保证内核的充分利用,还能防止过分调度。进程开始时创建一定数量的线程加到池中以等待工作。当服务器收到请求时,它会唤醒池内的一个线程,并将需要服务的请求传递给它。一旦线程完成了服务,它会返回到池中再等待工作。
java线程的创建非常昂贵,需要JVM和OS(操作系统)互相配合完成大量的工作。而java高并发频繁的创建和销毁线程的操作是非常低效的,如果需要降低java线程的创建成本,就必须要使用到线程池。
1.可以根据系统的需求和硬件环境灵活的控制线程并发数量,统一管理所有线程。
2.线程和任务分离,提升线程重用性。
3.提升系统响应速度,省去了线程的创建和销毁的时间。
4.将要执行任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务。例如,任务可以被安排在某一个时间延迟后执行,或定期执行。
1.需要任务长期执行的场景。
2.需要大量的线程来完成任务,且完成任务的时间比较短。
3.对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
4.突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。
目前很多人使用spring封装创建线程池,现在我来实现一个
@Bean("springThreadPool") public ThreadPoolTaskExecutor createSpringThreadPool() { // 创建线程池对象 ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); // 设置核心线程数 threadPoolTaskExecutor.setCorePoolSize(8); // 最大线程数 threadPoolTaskExecutor.setMaxPoolSize(32); // 线程的闲置时间 threadPoolTaskExecutor.setKeepAliveSeconds(128); // 最大队列数量 threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE); // 线程名称 threadPoolTaskExecutor.setThreadNamePrefix("springThreadPool:"); // 初始化线程 threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; }
创建方式 | 说明 |
Executors.newFixedThreadPool(int nThreads) | 创建一个大小固定的线程池,可控制并发的线程数,超出的线程会在队列中等待。 |
Executors.newCachedThreadPool() | 创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收(60s),若线程数不够,则新建线程(适合短时间内有突发大量任务的处理场景)。 |
Executors.newSingleThreadExecutor() | 创建单个线程的线程池,可以保证先进先出的执行顺序。 |
Executors.newScheduledThreadPool(int nThreads) | 创建一个可以执行延迟任务的线程池(使用schedule方法添加时间和单位)。 |
Executors.newSingleThreadScheduledExecutor() | 创建一个单线程的可以执行延迟任务的线程池。 |
Executors.newWorkStealingPool() | 创建一个抢占式执行的线程池(jdk1.8新特性)。 |
new ThreadPoolExecutor(自定义多个参数) | 手动创建线程池,可自定义相关参数(**推荐使用**)。 |
不建议使用Executors静态方法创建的线程池:
1. 自己创建能够明确线程池的运行规则,规避资源耗尽的风险
2. newFixedThreadPool 和 newSingleThreadExecutor:
允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
3. newCachedThreadPool 和 newScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
其实通过Executors静态方法创建的线程池本质也都是封装的ThreadPoolExecutor()。
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
自定义参数需要传入七个参数,下面就讲解下这七个参数。
参数名 | 参数含义 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 时间单位 |
workQueue | 线程池任务队列 |
threadFactory | 创建线程的工厂 |
handler | 拒绝策略 |
1. corePoolSize
核心线程数,是指线程池中长期存活的线程数(int 类型)。可以理解为长期存在的线程。
设计原则:
1. 核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定。例如:执行一个任务需要0.1秒,系统每秒都会产生100个任务,那么要想在1秒内处理完任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8:2原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理。
2. CPU密集型
CPU核数 + 1 (获取核数命令:Runtime.getRuntime().availableProcessors())
3. IO密集型
方式1--CPU核数 + 1(任务线程并不是一直执行任务,应该配置尽可能多的线程)
方式2--CPU核数/(1-阻塞系数) (阻塞系数一般为0.8~0.9之间)
4. 混合型
核心线程数=(线程等待时间/线程CPU时间+1)* CPU核心数
这些方式都是参考,具体需要生产环境测试和压力测试最终决定
2. maximumPoolSize
1. 最大线程数的设计,除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定;例如:如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间;既: 最大线程数=(1000-200)*0.1=80个;
2. 核心线程数 * 2
这些方式都是参考,具体需要生产环境测试和压力测试最终决定
3-4. keepAliveTime、unit
空闲时间和单位。这个设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。
5. workQueue
任务队列。长度一般设计为:核心线程数/单个任务执行时间*2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200;
阻塞队列,指线程池存放任务的队列,用来存储线程池的所有待执行的任务。BlockingQueue
参数 | 含义 |
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列 |
SynchronousQueue | 一个不存储元素的阻塞队列,即直接提交给线程,不保持它们 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。与 SynchronousQueue 类似,还含有非阻塞方法 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列 |
比较常用的是 LinkedBlockingQueue。
6. threadFactory
线程工厂。线程池创建线程调用的工厂方法,通过此方法可以设置线程的优先级、线程的命名规则以及线程的类型(用户线程还是守护线程)等。ThreadFactory 类型。
7. handler
拒绝策略。当线程池的任务超出线程池队列可以存储的最大值之后,拒绝任务的策略。RejectedExecutionHandler 类型。取值有:
参数 | 含义 |
---|---|
AbortPolicy | 拒绝并抛出异常 |
CallerRunsPolicy | 使用当前调用的线程来执行此任务 |
DiscardOldestPolicy | 抛弃队列头部(最旧)的一个任务,并执行当前任务 |
DiscardPolicy | 忽略并抛弃当前任务 |
线程池的默认拒绝策略是 AbortPolicy--拒绝并抛出异常。
最后展示个实例:
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
8, 32, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(128), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());