每天学习一个小知识
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。其技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态是指线程因为某种原因让出了 cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(1)等待阻塞(wait->等待对列):
运行(running)的线程执行 wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。
(2)同步阻塞(lock->锁池) :
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
(3)其他阻塞(sleep/join) :
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
(1)正常结束
run()或 call()方法执行完成,线程正常结束。
(2)异常结束
线程抛出一个未捕获的 Exception 或 Error。
(3)调用 stop
直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
1、线程池刚创建时,里面是没有线程的。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:
(1)如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
(2)如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
(3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
(4)如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,则线程池会抛出异常RejectExecutionException。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
启动线程是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
如果类已经继承了父类了,那么久就不能再继承 Thread 类,我们可以实现Runnable接口。(无返回值)
执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了。(有返回值)
核心线程大小,线程池中维护了一个最小线程数量,即使这些线程处于空闲状态,也一直存在线程池中,除非设置了核心线程超时时间
线程池中允许的最大线程数量。当线程池中核心线程都处理执行状态,有新请求的任务:
(1)、工作队列未满:新请求的任务加入工作队列
(2)、工作队列已满:线程池会创建新线程,来执行这个任务。当然,创建新线程不是无限制的,因为会受到maximumPoolSize最大线程数量的限制。
空闲线程存活时间。具体说,当线程数大于核心线程数时,空闲线程在等待新任务到达的最大时间,如果超过这个时间还没有任务请求,该空闲线程就会被销毁。
空闲线程存活时间的单位。keepAliveTime的计量单位。枚举类型TimeUnit类。存活时间通常设置为 60s 左右,即当线程池中的线程空闲时间超过了 60 秒,那么这个线程就会被回收。同时,当线程池中的线程数量小于等于核心线程池大小时,存活时间将不起作用。
任务队列是存储被提交但尚未被执行的任务的阻塞队列。常用的任务队列有如下几类:
用来创建线程工厂的类,在线程池中需要创建新线程时使用 threadFactory(创建线程工厂) 进行创建线程对象,可以通过自定义 threadFactory(创建线程工厂)对线程进行个性化配置和定制,例如设置线程的名称、优先级等,可以更好的跟踪和管理线程池中的线程
拒绝策略是当任务队列满了需要执行拒绝策略来处理新提交的任务。提供了几种预定义的拒绝策略
核心线程池数量不宜过多,因为每个线程都需要占用内存和 CPU 资源,过多的核心线程池数量会导致系统资源的浪费,从而降低系统性能。但也不应过少,否则会降低系统吞吐量。
最大线程池数量一般设置:最大线程池数量 = CPU 核心数 + 网络连接数 + 其他 IO 等待时间的线程数量
设定一个适当的线程存活时间,可以有效地减少线程的创建和销毁带来的性能开销。在存活时间到达之后,多余的线程会被回收,从而释放系统资源。
任务队列是存储被提交但尚未被执行的任务的阻塞队列。在选择队列类型时,应考虑任务数量和任务类型,以及需要处理的并发请求数。常用的阻塞队列有 ArrayBlockingQueue 和 LinkedBlockingQueue。
拒绝策略通常分为四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和 DiscardPolicy。根据业务需求和系统负载情况,选择合适的拒绝策略。
在 CPU 密集型应用中,任务主要是 CPU 计算,线程池的大小应该根据 CPU 核心数来设置,以充分利用 CPU 资源,并避免过多线程间的竞争和上下文切换。通常情况下,将核心线程池大小设置为 CPU 核心数,将最大线程池大小设置为 CPU 核心数 * 2。比如,当前服务器有 8 核 CPU,那么推荐设置核心线程池大小为 8,最大线程池大小为 16。
在 IO 密集型应用中,任务主要是从事 IO 等待,而线程的 CPU 计算能力却很小,此时线程数量适当多一点,可以让 CPU 等待 IO 的数目更多,以充分利用计算机的硬件资源。通常情况下,核心线程池大小可以设置为 CPU 核心数 +1,最大线程池大小可以设置为 CPU 核心数 * 2。同时,建议使用无界的 LinkedBlockingQueue 阻塞队列,以避免丢失任务。
对于各种不同的场景,应该根据实际情况进行参数的设置。比如生产者-消费者问题,可以使用 FixedThreadPool,保证消费者线程数量少于核心线程池大小,以确保消费者线程能够及时执行。同时,阻塞队列也可以根据实际情况选择不同的存储方式。对于需要开多个线程处理的应用,可以使用 ScheduledThreadPoolExecutor,定时执行任务。总之,根据实际需求来设置线程池的参数是最重要的。