多线程和线程池的理解运用
引言:这是一篇关于线程的博客,会基于这个开辟出一个关于多线程和线程池的专题,包括了使用,底层源码解析以及一些相关的面试题的理解。
可能对于初学者来说,线程池和多线程还是理解的不是很清楚。
多线程
(1)多线程是一种并发的执行机制,指的是在程序中一个进程运行多个线程,来提高资源的利用率和执行的效率。
(2)每个线程有独立的执行路径,但是使用的内存是同一个进程的内存空间,比如堆、方法区。
(3)多线程的目的是让程序能够并发执行多个任务。
创建多线程的方法
(1)通过类extends thread,重写run方法,创建对象,.start()方法启动线程。
优缺点:继承thread类,使用难度低,简单方便,但是Java属于单继承,若创建多线程通过继承的方法来实现,无法继承其他类,线程任务和线程进行了耦合,灵活性低。
public class NewThread extends Thread {@Overridepublic void run() {for (int i=0;i<5;i++){System.out.println("子线程执行:"+ i);}}public static void main(String[] args) {NewThread t=new NewThread();t.start();for (int i=0;i<5;i++){System.out.println("主线程执行:"+i);}}
}
(2)通过implements Runnable来实现run() 方法,同时创建实例,将实例传入thread对象中。
优缺点:通过implements 解决了无法多继承的弊病,实现了功能的解耦,但是run方法没有返回值,无法直接获得线程的返回值,同时无法知晓线程的抛出的问题信息。
public class NewThread implements Runnable{@Overridepublic void run() {for (int i=0;i<5;i++){System.out.println("这是子线程:"+i);}}public static void main(String[] args) {NewThread t=new NewThread();Thread tt=new Thread(t);tt.start();for (int i=0;i<5;i++){System.out.println("这是主线程:"+i);}}(3)通过Callable接口+FutureTask类,与Runnable相似,但是有返回值,且可以抛出异常。
优缺点:首先同样避免单继承,同时可以获取返回值,抛出异常,但是在获取返回结果的时候,有可能会将线程阻塞。
public class NewThread implements Callable {@Overridepublic Integer call() throws Exception {int sum=0;for (int i=0;i<=5;i++){sum+=i;}return sum;}public static void main(String[] args) throws Exception {NewThread t=new NewThread();FutureTask<Integer> task=new FutureTask<>(t);Thread th=new Thread(task);th.start();int sum=task.get();System.out.println("子线程的计算结果是:"+sum);}
}
(4)通过线程池的技术来实现,其可以高效的管理多个线程,创建,删除,复用等。通过Executors这个工具类
public class NewThread{public static void main(String[] args) {ExecutorService ex= Executors.newFixedThreadPool(3);for (int i=0;i<5;i++){ex.submit(new Runnable(){@Overridepublic void run() {System.out.println("线程"+Thread.currentThread().getId()+"执行任务");}});}ex.shutdown();}
}优缺点:高效管理线程,减少资源消耗;适合大量并发任务场景(如服务器开发)。需要手动管理线程池的关闭,否则可能导致程序无法退出。
线程池
线程池的原理
线程池是一种池化技术,用于预先创建并且管一组线程,避免频繁的创建和销毁线程的开销,提高性能和相应速度。他几个关键的配置有:核心线程数,最大线程数,空闲存活时间,工作队列,拒绝策略。
通过看线程池的各个重要参数,以及源码中代表的含义:
public ThreadPoolExecutor(int corePoolSize, //这是核心线程数,一直保持N个线程int maximumPoolSize,//最大线程数,最多创建N个线程long keepAliveTime, //非核心线程空闲N秒后销毁。TimeUnit unit, //时间单位(秒,分,时,日等)BlockingQueue<Runnable> workQueue // 任务队列,最多缓存N个任务) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);}主要的工作原理如下:
1.在默认情况下,线程不会预先创建,一般任务提交的时候才会创建,但是可以通过设置prestartAllCoreThreads可以预先创建核心线程。
2.创建线程,在当前的线程数小于核心线程的情况下,会继续使用核心线程来执行任务。
3.当核心线程数都在工作是,还有任务提交,就会进入,任务队列中等待。
4.现在如果任务队列满了同时核心线程数都在工作,就会创建非核心线程,来安排任务工作,直到创建到的线程数量到达最多线程数量。
5.当任务队列满了,线程数量也到达了最大的线程数量之后,就会产生拒绝策略。
6.如果任务稀少,和线程的空闲时间到达存活时间,那么线程就会销毁,直到线程数到达核心线程数。
在面试鸭看到一个有趣的解释:
线程池可以理解为,去银行办理业务:默认有6个柜台,当没有人去银行办理业务时候,柜台小姐姐都是拉呱玩耍的,有人去办理业务,先开三个柜台来办理业务,若三个柜台都有人在办理业务,那来的先去等候区,若此时等候区也满了,就去新开另外三个柜台,随着办理业务人员增多,新开的柜台也都有人在办理业务,排队等候区也满了,那你还要来办理业务??想搞事呢??明天再来,或者去前面找个人插队,把第一个等候区的人赶走……,这就 是经理的拒绝策略。
工作队列的不同类型
SysnchronousQusus:不存储任务,直将任务提交给任务。
LinkedBlockingQueue :链表结构的阻塞队列,大小无限。
ArrayBlockingQueue:数组结构的有界阻塞队列。
PriorityBlockingQueue:优先级的误解阻塞队列。
不同线程池的类型
java 并发库中提供了5种常见的线程池实现,只要通过executors工具来创建。
(1)FixedThreadPool: 创建一个固定熟练过的线程池。线程池中的线程数量是固定的,空闲的线程会被复用,线程都在忙则会将新任务放入队列中等待。是适合负载稳定的场景,任务数量确定,且不需要动态调整线程数。
(2)CachedThreadPool:一个可以根据需要创建线程的线程池。线程数的线程没有上线,空闲线程会在60秒后被回收,如果有新任务没有可用线程,会创建新线程,适合短期大量并发任务场景任务执行时间短且线程需求变化较大。
(3)SingleThreadExecutor:创建一个只有单线程的线程池。只有一个线程处理任务,任务会按照提交的顺序执行,适用于需要保证任务按顺序执行的场景,或者不需要处理并发任务的情况。
(4)ScheduledThreadPool:支持定时任务和周期性任务的线程池,可以定时或以固定的频率执行任务。线程池大小也可以有用户指定,使用与需求周期性任务执行的场景,如定时任务的调度。
(5)WorkStealingPool:基于任务窃取算法的线程池。线程池中的每个线程维护一个双端队列,线程可以从自己的队列中取任务执行,如果线程的任务队列为空,他可以从其他队列中拿到任务来执行,形成负载均衡的效果。适合大量小型任务并执行,特别是递归算法或大人物分解成小任务的场景。
4种拒绝策略
1)AbortPolicy,当任务队列满且没有线程空闲时,此时添加任务会直接抛出 RejectedExecutionException 错误,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
2)CallerRunsPolicy,当任务队列满且没有线程空闲时,此时添加的任务由即刻调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
3)DiscardOldestPolicy,当任务队列满且没有线程空闲时,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
4)DiscardPolicy,直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。
这四种拒绝策略是 Java 线程池(ThreadPoolExecutor)中 RejectedExecutionHandler 接口的实现方式,分别对应不同的业务需求:
| 策略 | 特点 | 使用场景 |
|---|---|---|
| AbortPolicy | 抛异常,强制反馈 | 需要立即感知任务失败 |
| CallerRunsPolicy | 调用线程自己执行 | 控制任务提交速率,避免雪崩 |
| DiscardOldestPolicy | 丢弃最老任务 | 优先保障新任务处理 |
| DiscardPolicy | 无声丢弃 | 可容忍任务丢失,如日志、监控等 |
为什么线程池要先使用阻塞队列,而不是直接增加线程?
因为每创建一个线程都会占用一定的系统资源(如栈空间、线程调度开销等),直接增加线程会迅速消耗系统资源,导致性能下降。
使用阻塞队列可以将任务暂存,避免线程数量无限增长,确保资源利用率更高。
如果阻塞队列都满了,说明此时系统负载很大,再去增加线程到最大线程数去消化任务即可。
举个例子:老板现在手下有 10 个人在干活(核心线程数),突然活变多了,每个人干不过来了,此时老板不会立马招人,它会让这些活积累一下(放到阻塞队列中),看看过段时间能不能消化掉。如果老板发现这个活积累的实在太多了(队列满了),他才会继续招人(达到最大线程数)。这就是所谓的人员(线程)有成本。
这些大概就是这一篇的全部内容,不想再一篇种写太长,太多的内容所有就大致再这里结尾,但是之后肯定会有其他的相关多线程和线程池的内容,写出来的,敬请期待!!
