搞懂线程池原理,才有底气去面试

先了解一下线程池的经典面试题?
在这里插入图片描述
以上就是我们学习的目标

为什么使用线程池?优势是什么?

大部分人的回答都是,降低资源消耗,提高响应速度,提高线程的可管理性。那么为什么会提高呢?

从根源上开始将,首先我们谈一下对java线程概念的理解

线程:是调度CPU资源的最小单位,也叫轻量级进程

线程模型的分类:

用户级线程
用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度、和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。

内核级线程:系统内核管理线程(KLT) ,内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器.上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。

在这里插入图片描述java中的JVM使用的是内核级线程模型,所以线程的创建和销毁,需要在用户态和内核态进行来回的切换,来回切换的过程中占用大量的CPU资源。

java线程与内核线程的模型图java的多线程并发执行时的过程
在这里插入图片描述
进程切换的时候需要保存当前进程的信息,保存在寄存器或者缓存中,然后再暂存在内核空间的TEE任务状态段,当线程重新获取到CPU时间段,继续执行。

线程池的意义

线程是稀缺资源,它的创建与销毁是比较重且耗资源的操作。而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过渡消耗需要设法重用线程执行多个任务。线程池就是个线程缓存,负责对线程进行统一分配、调优与监控。

线程池优势:
➢重用存在的线程,减少线程创建,消亡的开销,提高性能
➢提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
➢提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。使用线程池可以进行统一的分配,调优和监控。

回答到这里相信面试官已经非常满意了,我们接着往下看

线程池如何使用?

创建线程池的方法有6种:

1 . Executors.newCachedThreadPool();

创建一个可缓存线程池,应用中存在的线程数可以无限大

2 . Executors.newFixedThreadPool(2)

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3 . Executors.newScheduledThreadPool(2)

创建一个定长线程池,支持定时及周期性任务执行。

4 . Executors.newSingleThreadExecutor();

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,

保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

5 . Executors.newSingleThreadScheduledExecutor();

创建一个单例线程池,定期或延时执行任务。

6 Executors.newWorkStealingPool(3);
创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不穿如并行级别参数,将默认为当前系统的CPU个数。

《阿里巴巴Java开发手册》 中强制线程池不允许使用Executors 创建,而是通过ThreadPoolExexutor的方式创建,这样的方式让写的同学更加明确线程池的规则,规避资源耗尽的风险。

Executor返回线程池对象的弊端
FixedThreadPool 和 SingleThreadExector : 允许队列的长度为Integer。MAX_VALUE ,可能堆积大量的请求,从而导致OOM
CachedThreadPool 和 ScheduledThreadPool : 允许线程的数量为Integer.MAX_VALUE ,可能会创建大量的线程,从而导致OOM

线程池中重要的参数

源码 (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
该策略下,抛弃进入队列最早的那个未处理的任务,然后尝试把这次拒绝的任务放入队列。

线程池的工作原理

了解线程池的状态

Running 状态: 可以接收新任务,处理已经添加到队列的任务
shutDown 状态 : 不接受新任务,处理已经添加带队列的任务
stop状态 : 不接受新任务,不处理已经添加到队列的任务,中断正在处理的任务。
Tidying状态 : 所有任务已经终止,ctl纪录的任务数量为0
Terminted 状态 : 线程池终止。

1.当创建线程池后,初始时,线程池处于RUNNING状态;

2.如果调用了**shutdown()**方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

3.如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

4.当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后(ctr纪录的任务数量为0),线程池被设置为TERMINATED状态。

源码

 if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //判断当前活跃线程数是否小于corePoolSize
        if (workerCountOf(c) < corePoolSize) {
            //如果小于,则调用addWorker创建线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果大于等于corePoolSize,则将任务添加到workQueue队列。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果放入workQueue队列失败,则创建非核心线程执行任务    
        else if (!addWorker(command, false))
            //(如果这时创建线程失败(当前线程数大于等于maximumPoolSize时))
            调用reject拒绝接受任务
            reject(command);


在这里插入图片描述线程池的执行过程:
1 .当新任务来时,execute执行,如果池中的线程数低于核心线程,此时就会创建新的线程来处理当前任务。在添加工作线程时,会检查运行状态和工作线程的数量,在addWorker返回false时,不会添加工作线程.。
2 .即使一个任务可以成功的进入队列,我们仍然需要一个双重检查以便判断是否要新增一个线程(因为在上次检查之后,可能存在线程的死亡)或者线程池已经关闭。所以我们双重检查状态并且在必要的时候进行回滚。
3 .如果任务不能进入队列,我们就尝试增加一个新的线程。如果失败了,我们就知道线程池已经关闭或者是线程池已经饱和(线程池队列已满且工作线程数已达最大)

关于线程池对线程的复用

public void execute(Runnable command){
        //获取线程池中线程的数量
        int c = ctl.get();
        //判断线程数量是否小于核心线程
        if (workerCountOf(c) < corePoolSize) {
            //小于核心线程就添加一个新线程,执行当前任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果线程数量大于核心线程数量,放入队列
        if (isRunning(c) && workQueue.offer(command)) {
            //再次检查线程状态
            int recheck = ctl.get();
            //如果线程池关闭,从池中移除当前线程
            if (! isRunning(recheck) && remove(command))
                //拒绝该任务
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //线程数量大于最大线程数
        else if (!addWorker(command, false))
            //拒绝任务
            reject(command);
    }

addWorker方法将当前线程任务当做参数传进Worker对象的构造器中。

private boolean addWorker(Runnable firstTask, boolean core) {
        Worker w = null;
        try {
	            w = new Worker(firstTask);
	            final Thread t = w.thread;
	            t.start();
            }
        } finally {
           
        }
}

Worker对象就是一个线程。构造器中将当前任务传递给了Worker对象中的firstTask对象然后将当前Worker对象传入线程对象,构造成线程然后启动Worker线程。此时,Worker对象中的run方法就会启动。

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
}

public void run() {
            runWorker(this);
        }
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //取出Worker对象中的任务
        Runnable task = w.firstTask;
        //清空firstTask任务
        w.firstTask = null;
        try {
            //如果task不为空,或者从队列中获取到任务
            while (task != null || (task = getTask()) != null) {
                    //运行当前线程任务
                    task.run();
            }
        } finally {
            //杀死线程
            processWorkerExit(w, completedAbruptly);
        }
    }

首先判断任务如果不为空,就执行任务的run方法。线程的主要逻辑就是run方法,run方法执行完,该条线程的使命就完成了。至此,我们传进去的当前任务就执行完成了。
但是,线程池的线程复用是咋做的呢?
答案就在于while循环中的getTask方法。

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
            //判断allowCoreThreadTimeout的值
            //判断线程的数量是否大于核心线程的数量
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            try {
                //如果timed为true,就调用poll方法,否则就调用take方法
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();
                if (r != null)
                    //从队列中取出任务就返回该任务
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
    }

当线程数量大于核心线程的数量或者我们设置了allowCoreThreadTimeOut变量为true,此时就会调用poll方法,在keepAliveTime时间内获取线程,即:有超时时间。如果timed为false,就会使用take方法获取线程,获取不到,就在该位置阻塞,直到有任务进入队列。
至此,我们就明白了。线程池的作者设计了一个循环,不停的从队列中获取任务,只要能从队列中获取到任务,就会一直获取。然后执行线程的run方法。一直使用当前的worker线程执行任务,从而达到了线程的复用。

如何合理设置线程池的参数

首先需要了解CPU密集型和IO密集型的区别
(1)、CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。

在多重程序系统中,大部分时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部分时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

(2)、IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
自己测一下自己机器的核数

System.out.println(Runtime.getRuntime().availableProcessors());

1

即CPU核数 = Runtime.getRuntime().availableProcessors()

(4)、分析下线程池处理的程序是CPU密集型还是IO密集型

CPU密集型:corePoolSize = CPU核数 + 1

IO密集型:corePoolSize = CPU核数 * 2

2、maximumPoolSize:最大线程数

当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。

当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。

3、keepAliveTime:线程空闲时间

当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。如果allowCoreThreadTimeout=true,则会直到线程数量=0。

4、queueCapacity:任务队列容量(阻塞队列)

当核心线程数达到最大时,新任务会放在队列中排队等待执行

5、allowCoreThreadTimeout:允许核心线程超时

6、rejectedExecutionHandler:任务拒绝处理器

两种情况会拒绝处理任务:

当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。

线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。

ThreadPoolExecutor 采用了策略的设计模式来处理拒绝任务的几种场景。

这几种策略模式都实现了RejectedExecutionHandler 接口。

AbortPolicy 丢弃任务,抛运行时异常。
CallerRunsPolicy 执行任务。
DiscardPolicy 忽视,什么都不会发生。
DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务。

如何来设置呢?

需要根据几个值来决定

tasks :每秒的任务数,假设为500~1000

taskcost:每个任务花费时间,假设为0.1s

responsetime:系统允许容忍的最大响应时间,假设为1s

做几个计算

corePoolSize = 每秒需要多少个线程处理?

threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 个线程。

corePoolSize设置应该大于50。

根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可。

queueCapacity = (coreSizePool/taskcost)*responsetime

计算可得 queueCapacity = 80/0.1*1 = 800。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行。

切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize 最大线程数在生产环境上我们往往设置成corePoolSize一样,这样可以减少在处理过程中创建线程的开销。

rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理。

keepAliveTime和allowCoreThreadTimeout采用默认通常能满足。

以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器cpu load已经满了,则需要通过升级硬件和优化代码,降低taskcost来处理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值