在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
线程池的优势:
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等待,等其它线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用、控制最大并发数、管理线程。
1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3.提高线程的课管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java中的线程池是通过Executor框架来实现的,而我们创建时,一般使用它的子类:ThreadPoolExecutor。
java中提供了一个静态工厂方法来创建不同的线程池:Executors
通过静态方法创建出的线程都实现了ExecutorService接口
常用的方法包括:
FixedThreadPool(int threads):定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
SingleThreadPool():只有一条线程来执行任务,适用于有顺序的任务的应用场景。
CachedThreadPool():可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
WorkStealingPool:java8新增,使用目前机器上可用的处理器作为它的并行级别。
常用的线程池创建方法示例如下:
/** * 第四种获取线程的方式-线程池 */ public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService threadPool=Executors.newFixedThreadPool(5);//一池5个线程 //ExecutorService threadPool=Executors.newSingleThreadExecutor();//一池一个线程 //ExecutorService threadPool=Executors.newCachedThreadPool();//一池N线程,根据情况来创建 try{ //模拟十个用户来办理业务 for(int i=1;i<=10;i++){ //submit有返回值 Future future=threadPool.submit( () -> { System.out.println(Thread.currentThread().getName()+" 办理业务"); return "办理业务成功"; }); System.out.println("获取结果"+future.get()); //execute无返回值 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 办理业务"); }); } }catch (Exception e){ e.printStackTrace(); }finally { threadPool.shutdown(); } } }
线程池的底层就是ThreadPoolExecutor类。
1.Executors.FixedThreadPool(int),执行长期的任务,性能好很多
主要特点如下:
1).创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
2).newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue。
2.Executors.SingleThreadPool( ),一个任务一个任务执行的场景
主要特点如下:
1).创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
2).newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。
3.Executors.CachedThreadPool( ),适用:执行很多短期异步的小程序或者负载较轻的服务器。
主要特点如下:
1).创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2).newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
七大参数
1.corePoolSize:线程池中的常驻核心线程数
在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程;
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3.keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交单尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略。
拒绝策略是当等待队列也已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了了,无法继续为新任务服务。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略
1)AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
2)CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
3)DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
4)DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口
线程池的底层工作原理
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
3)如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
4)如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
实际开发中一般手动写线程池,这样合理利用线程资源,避免资源耗尽的。
手写线程池示例:
public class MyThreadPoolDemo { public static void main(String[] args) { //自定义线程池 ExecutorService threadPool= new ThreadPoolExecutor( 2, 5, 1L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3),//自定义阻塞队列长度为3 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()//自定义拒绝策略,默认为AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy ); try{ //模拟十个用户来办理业务,每个用户都是一个来自外部的请求线程 for(int i=1;i<=10;i++){ //无返回值 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+" 办理业务"); }); } }catch (Exception e){ e.printStackTrace(); }finally { //关闭线程池 threadPool.shutdown(); } }
使用默认的拒绝策略,能够访问的上限数=当最大线程数(maximumPoolSize) + 任务队列长度,当超过就会报java.util.concurrent.RejectedExecutionException异常。
死锁编码及定位分析
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
死锁示例:
/** * 死锁是指两个或者两个以上的进程在执行过程中, * 因争夺资源而造成的一种相互等待的现象, * 若无外力干涉,那它们就无法推进下去,从而造成死锁现象。 */ public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new DeadLockResource(lockA, lockB), "ThreadA").start(); new Thread(new DeadLockResource(lockB, lockA), "ThreadB").start(); } } /** * 线程操作资源类 */ class DeadLockResource implements Runnable { private String lockA; private String lockB; public DeadLockResource(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + " 拥有" + lockA + ",试图获取" + lockB); System.out.println("----------*****************----------"); synchronized (lockB) { System.out.println(Thread.currentThread().getName() + " 拥有" + lockB + ",试图获取" + lockA); } } } }
ThreadA持有lockA,试图获取lockB,而ThreadB持有lockB,试图获取lockA。造成两个线程相互等待,又没有外力干涉,无法推进下去,最终导致死锁现象。
产生死锁的主要原因
1.系统资源不足
2.进程运行推进的顺序不合适
3.资源分配不当
死锁解决
Linux环境下查看进程:ps -ef | grep java
Windows环境下查看java进程:jps -l
Windows环境下查看死锁栈信息:jstack +进程号
1.jps命令定位进程号
2.jstack找到死锁查看
只有停下程序,找到对应业务代码进行修改。
合理配置线程池:
首先得知道服务器cpu核心数
获取cpu核心数
Runtime.getRuntime().availableProcessors()
业务分为cpu密集型还是IO密集型,根据实际业务来配置
1.CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池
2.IO密集型
1) 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
2) IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数(阻塞系数在0.8~0.9之间),比如8核CPU:8/1-0.9=90个线程数