目录
1、实现多线程的几种方式?
- 继承thread类
- 实现Runnable接口
- 实现callable接口,使用Future Task类来包装callable对象,(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
- 线程池
2、创建线程的三种方式的对比?
1、采用实现Runnable、Callable接口的方式创建多线程
优势:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程
优势:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的
完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
3、为什么使用线程池,线程池的优势是什么?
线程池的主要工作是控制运行线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点为:线程复用;控制最大并发数;管理线程。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的调配、调优和监控。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
5、线程池有几种创建方式?
newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一工作线程来执行任务,确保所有任务按照指定顺序(FIFO,LIFO,优先级)执行。(一个任务执行的场景)
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。(执行长期的任务,性能好很多)
newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程。(适用于执行很多短期异步的小程序或者负载较轻的服务器)
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。(思该丢)
6、线程池中的参数有哪些?
- corePoolSize:线程中的常驻核心线程数
- maxmumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:空闲线程存活时间;当空闲线程超过这个时间,会销毁回收
- unit:keepAliveTime的单位
- workQueue:工作队列,被提交但尚未被执行的任务。新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于或等于线程池的最大线程数(maxmumPoolSize)
7、线程池的工作队列有哪些?
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现
8、线程池的工作原理?
9、线程池的拒绝策略有哪几种?
AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:丢弃任务,不予任何处理,也不抛出异常。如果允许任务丢失,这是最好的一种方案
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务(调用者运行一种调节机制,该策略既不会抛弃任务,也不会抛弃异常,而是将某些任务回退到调用者,从而降低新任务的流量)
都是使用ThreadPoolExecutor类
10、线程池生产实际用的哪种?
实际一个都不用,不允许使用Executors去创建,而是使用ThreadPoolExecutor的方式
FixedThreadPool和SingleThreadPool:允许的请求队列的长度Integer.MAX_VALUE,可能会堆积大量的请求,从而导致oom。
CachedThreadPool 和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致oom。
11、线程池的手写改造如何改造?
ExecutorService threadPool = new ThreadPoolExecutor(3, 5, 1L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
12、线程池如何配置合理线程数?
cpu密集型,
该任务需要大量的运算,没有阻塞,cpu一直全速运行。密集任务只有在真正的多核cpu上才能得到加速。要配置尽可能少的线程数量:一般公式为: cpu核数 + 1个线程的线程池
io密集型(仅供参考)
1、io密集型任务线程数并不是一直在执行,则应配置尽可能多的线程,如CPU核数* 2
2.、io密集型,即该任务需要大量的io,即大量的阻塞。在单线程上运行io密集型的任务会导致浪费大量的cpu运算能力,浪费在等待。
参考公式: CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间, 比如8核Cpu:8/1 -0.9 = 80个线程数
System.out.println(Runtime.getRuntime().availableProcessors()); 获取实际的线程数
13、线程池比较重要的几个类?
ExecutorService:真正的线程池接口
ThreadPoolExecutor: ExecutorService的默认实现,底层实现方法。
ScheduledExecutorService:能和Timer/Timer Task类似,解决那些需要任务重复执行的问题。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
参考资料:
https://blog.youkuaiyun.com/qq_35275233/article/details/87893337 (java 创建线程的三种方式、创建线程池的四种方式)
https://blog.youkuaiyun.com/suifeng629/article/details/98884972 (四种线程池拒绝策略)