线程池

2019/12/01

线程

进程是资源分配的最小单位,线程是CPU调度的最小单位 ,是程序运行的载体

线程模型

用户级线程(ULT)内核对ULT无感知->应用管理

内核级线程(KLT)->操作系统管理

Java虚拟机的内存模型 ->内核级线程

Java线程创建 是依赖于系统内核,通过JVM调用系统库创建内核线程。

image-20200516160132491

线程创建消费比较消耗资源,因为Java内核依赖于操作系统的内核线程,创建线程需要进行操作系统的切换,为避免资源过度消耗 而需要重用线程。

线程池是一个线程缓存 负责对线程进行统一的分配、调优和监控。

什么时候使用线程池?

单个线程处理时间比较短 需要处理的任务数量比较大

线程池的优势?

重用线程 减少线程的创建 销毁,提高性能

提高响应速度

对线程统一管理 分配 调度 和 监控

线程池

Executors //工具类 提供常见的线程创建方法 底层还是ThreadPoolExecutor 参数不一样

image-20200516172008949

workQueue阻塞队列 FIFO

1.在任何时刻,不管并发有多高,永远只有一个线程能进行队列的入队或者出队操作!线程安全

有界   无界

队列满,只能进行出队操作,所有入队操作必须等待,也就是被阻塞

队列空,只能进行入队操作,所有入队操作必须等待,也就是被阻塞

ThreadPoolExecutor管理的线程数量是有界的

然而对于多用户、高并发的应用来说,提交的任务数量非常巨大,一定会比允许的最大线程数多很多。J.U.C提供的ThreadPoolExecutor只支持任务在内存中排队,通过BlockingQueue暂存还没有来得及执行的任务。

corePoolSize:线程池的基本大小,即在没有任务需要执行的时候线程池的大小并且只有在工作队列满了的情况下才会创建超出这个数量的线程

需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize

maximumPoolSize

线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。

image-20200516181747839

 1、corePoolSize:核心线程数
        * 核心线程会一直存活,及时没有任务需要执行
        * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
        * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

    2、queueCapacity:任务队列容量(阻塞队列)
        * 当核心线程数达到最大时,新任务会放在队列中排队等待执行

    3、maxPoolSize:最大线程数
        * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
        * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

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

    5、allowCoreThreadTimeout:允许核心线程超时
    6、rejectedExecutionHandler:任务拒绝处理器
        * 两种情况会拒绝处理任务:
            - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
            - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
        * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
        * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
            - AbortPolicy 丢弃任务,抛运行时异常
            - CallerRunsPolicy 执行任务
            - DiscardPolicy 忽视,什么都不会发生
            - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
        * 实现RejectedExecutionHandler接口,可自定义处理器

不能保证先来的先执行

线程池的五种状态

image-20200516185723100

Running

能接受新任务以及处理已添加的任务

Shutdown

不接受新任务,可以处理已经添加的任务

Stop

不接受新任务,不处理已经添加的新任务,并且中断正在处理的任务

Tidying

所有任务已经终止,ctl记录任务数量为0,ctl负责记录线程池的运行状态与活动线程数量

Treminated

线程池彻底中支,则线程池转化为Treminated状态

生命状态如何在高并发的场景下保持线程安全?

把生命状态 记录在一个 Integer 中 保证原子操作

image-20200516191938260

创建多少个线程合适?

为什么使用多线程?

提高效率(提高cpu和 I/O 利用率) 异步

CPU密集型

一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分

单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程

多核CPU 处理 CPU 密集型程序,我们完全可以最大化的利用 CPU 核心数,应用并发编程来提高效率

I/O密集型

与 CPU 密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说 I/O 操作占比很大部分

I/O操作时不让cpu空闲

image-20200516231140047

从上图中可以看出,每个线程都执行了相同长度的 CPU 耗时和 I/O 耗时,如果你将上面的图多画几个周期,CPU操作耗时固定,将 I/O 操作耗时变为 CPU 耗时的 3 倍,你会发现,CPU又有空闲了,这时你就可以新建线程 4,来继续最大化的利用 CPU。

总结:

线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程。

对于 CPU 密集型来说,理论上 线程数量 = CPU 核数(逻辑) 就可以了,但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1, 为什么呢?

《Java并发编程实战》这么说:

计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

所以对于CPU密集型程序, CPU 核数(逻辑)+ 1 个线程数是比较好的经验值的原因了

I/O 密集型程序:

单核:最佳线程数 = (1/CPU利用率) = 1 + (I/O耗时/CPU耗时)

多核:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时))

按照上面公式,假如几乎全是 I/O耗时,所以纯理论你就可以说是 2N(N=CPU核数),当然也有说 2N + 1的(我猜这个 1 也是 backup)

为什么局部变量是线程安全的、

面试问我,创建多少个线程合适?我该怎么说

当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。

两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?答案是:每个线程都有自己独立的调用栈。因为如果不是这样,那两个线程就互相干扰了。

因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题

Post Directory