1、多线程的几种创建方式
常见的创建方式有四种方式:
1、继承Thread类创建
2、实现Runnable接口创建
3、实现Callable接口创建
4、通过线程池的方式创建
注意:其中 第2、3点的区别是:runable接口无返回值,不抛异常。callable接口,有返回值,抛异常。
2、线程池的方式创建线程几点知识
线程池概念
就是事先创建若干个可执行的线程放入一个池(容器) 中, 需要的时候从池中获取线程不用自行创建, 使用完毕不需 要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销
1、为什么使用线程池,优势。
线程池做的工作,主要是控制运行的线程的数量,处理过程中将任务放入对垒,然后,在线程创建后,启动这些任务。如果线程数量超过了最大数量超出数量的线排队等候,等其他线程执行完毕,再从队列中取出这些任务来执行。
主要特点为:线程复用,控制最大的并发数,管理线程。
使用线程池的好处
1、降低资源消耗。重复利用已创建线程,降低线程创建与销毁的资源消耗。
2、提高响应效率。任务到达时,不需等待创建线程就能立即执行。
3、提高线程可管理性。
4、防止服务器过载。内存溢出、CPU耗尽
优点:
第一:降低资源消耗,当通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
第二:提高响应的速度,当任务到达的时候,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统的资源还会降低系统的稳定性,使用线程池,可以进行统一的分配,调优和监控。
2、常用的三种线程池
(1) newSingleThreadExecutor: 创建一个单线程的线程池, 此线程池保证所有任务的执行顺序按照任务的 提交顺序执行。
(2) newFixedThreadPool: 创建固定大小的线程池, 每次提交一个任务就创建一个线程, 直到线程达到线程池的最大大小。
(3) newCachedThreadPool: 创建一个可缓存的线程池, 此线程池不会对线程池大小做限制, 线程池大小完全依赖于操作系统(或者说 JVM) 能够创建的最大线程大小。
不常用:
(4) newScheduledThreadPool: 创建一个大小无限的线程池, 此线程池支持定时以及周期性执行任务的需求。
3、线程池的底层原理
线程池的底层是通过**ThreadPoolExecutor**类来实现的。
4、线程池主要处理流程
1、在创建了线程池后,等待提交过来的任务请求。
2、当调用execute()方法添加一个任务请求,线程池会做如下判断:
2.1 如果正在运行的线程数小于corePoolSize,那么马上会创建线程运行这个任务;
2.2 如果正在运行的线程数大于或者等于corePoolSize,那么会将这个任务放入队列;
2.3 如果这时候队列满了并且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程运行这个任务;
2.4 如果队列满了并且线程数大于或者等于maximumPoolSize,那么会启动饱和拒绝策略来执行。
3、当一个线程完成时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,且超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程会停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
5、线程池的几个重要参数
从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
下面会对这7个参数一一解释。
一、corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
二、maximumPoolSize 线程池最大线程数量
一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
三、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
四、unit 空间线程存活时间单位
keepAliveTime的计量单位
五、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
到此,构造线程池时的七个参数,就全部介绍完毕了。
6、合理的配置线程池,你是怎么考虑的。
首先先看下要实现的业务 ,是cpu密集型,还是io密集型的业务。
如果是cpu密集型
1、Cpu密集型
意思是该任务需要大量的运算,而没有阻塞,cpu一直全速运行。
Cpu密集性任务也只有在真正的多核的cpu上才可能得到真正的加速(通过多线程)
Cpu 密集型的任务,配置尽可能少的线程数量。
一般公式:cpu核数+1个线程的线程池。
2、Io密集型
分为两种情况:
a、由于io密集型的任务线程并不是一直在执行任务,则应配置尽可能多的线程,
如cpu核数*2.
b、 即该任务需要大量的io,即大量的阻塞。
在单线程上运行io密集性的任务会导致
参考公式:cpu核数 / 1-阻塞系数 阻塞系数在0.8-0.9之间。
比如8核的cpu 8/1-0.9 =80个线程数