vlambda博客
学习文章列表

java并发体系 -- 线程池与死锁

java并发体系

线程池与死锁

executor[ɪɡˈzekjətər]:执行者;执行

scheduled[ˈskedʒuːld]:安排

commands[kəˈmændz]:命令

delay[dɪˈleɪ]:延迟

periodically[ˌpiriˈɑdɪkəli]:定期

stealing[ˈstiːlɪŋ]:偷

parallelism[ˈpærəlelɪzəm]:并行 parallelism level:并行度级别

multiple[ˈmʌltɪpl]:多

contention[kənˈtenʃn]:竞争

correspond[ˌkɔːrəˈspɑːnd]:对应于

engage[ɪnˈɡeɪdʒ]:吸引住(注意力、兴趣);雇用 engage in:参加

shrink[ʃrɪŋk]:缩小

dynamically[daɪˈnæmɪk(ə)li]:动态地

unbounded[ʌnˈbaʊndɪd]:无限的

terminates[ˈtɜːrmɪneɪt]:终止

execution[ˌeksɪˈkjuːʃn]:执行

prior[ˈpraɪər]:先前的;优先的

subsequent[ˈsʌbsɪkwənt]:后来的

sequentially[səˈkwɛntʃəli]:按顺序

equivalent[ɪˈkwɪvələnt]:相等的

Unlike the otherwise:不同于其他

at any point:任何时候

explicitly[ɪkˈsplɪsətli]:明确地

additional[əˈdɪʃənl]:其他的

idle[ˈaɪdl]:空闲的

excess[ɪkˈses , ˈekses]:多余的;过量的

block[blɑːk]:堵塞

abort[əˈbɔːrt]:中止

discard[dɪˈskɑːrd , ˈdɪskɑːrd]:丢弃

stack[stæk]:堆



一、线程池

1.是什么

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用,控制最大并发数,管理线程。

优点

降低资源消耗通过重复利用己创建的线程降低线程创建和销毁造成的消耗。

提高响应速度当任务到达时,任务可以不需要的等到线程创建就能立即执行。

提高线程的可管理性线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.线程池3个常用方式

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(工具类),ExecutorService,ThreadPoolExecutor这几个类。


2.1了解

2.1.1Executors.newScheduledThreadPool()

java并发体系 -- 线程池与死锁

2.2.2 Executors.newWorkStealingPool(int)

Java8新增,使用目前机器上可用的处理器作为它的并行级别

java并发体系 -- 线程池与死锁

2.2 重点
2.2.1 Executors.newSingleThreadExecutor()

java并发体系 -- 线程池与死锁

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

newSingleThreadExecutor将corePoolSize和maximumPoolSize

都设置为1,它使用的LinkedBlockingQueue。

2.2.2 Executors.newFixedThreadPool(int)

java并发体系 -- 线程池与死锁

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

newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。

2.2.3 Executors.newCachedThreadPool()

java并发体系 -- 线程池与死锁

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。


2.3实例

前置举例抽象:

java并发体系 -- 线程池与死锁

2.3.1 Executors.newFixedThreadPool(int)

java并发体系 -- 线程池与死锁java并发体系 -- 线程池与死锁

用户0,1,2,3,4分别进入并占用核心线程,用户5进入时,线程1已执行完毕,线程1继续执行用户5业务,依次类推。

ps:如果放开注释TimeUnit.MILLISECONDS.sleep(10);

java并发体系 -- 线程池与死锁

2.3.2 Executors. newSingleThreadExecutor ()

java并发体系 -- 线程池与死锁

java并发体系 -- 线程池与死锁

2.3.3 Executors.newCachedThreadPool()

java并发体系 -- 线程池与死锁


java并发体系 -- 线程池与死锁

来了线程便创建线程。

如果放开注释TimeUnit.MILLISECONDS.sleep(10);

因为内部使用的是同步队列SynchronousQueue,1个线程处理完当前业务,另一个业务进入,该线程仍可继续处理,即1个线程够用。

java并发体系 -- 线程池与死锁


2.4七大参数

java并发体系 -- 线程池与死锁

2.4.1 基础知识

this,1.放在构造方法的第一句,引用其他构造方法,用于重载。

2.普通的直接引用,this相当于是指向当前对象本身。

3.形参与成员名字重名,用this来区分

super,可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

重载和覆盖(重写):

java并发体系 -- 线程池与死锁

2.4.2七大参数

java并发体系 -- 线程池与死锁

1.corePoolSize:线程池中的常驻核心线程数

核心线程会一直存活,即使没有任务需要执行

当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理,直到已创建的线程数大于或等于corePoolSize达到corePoolSize后,就会把到达的任务放到缓存队列当中

2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

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

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

3.keepAliveTime:多余的空闲线程的存活时间。

当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止

4.unit:keepAliveTime的单位。

5.workQueue:任务队列,被提交但尚未被执行的任务。

6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)。


2.4.3线程池原理与流程

再次看抽象举例图:

java并发体系 -- 线程池与死锁

线程池底层工作原理

java并发体系 -- 线程池与死锁

java并发体系 -- 线程池与死锁

1.在创建了线程池后,等待提交过来的任务请求。

当调用execute()方法添加一个请求任务时,线程池会做如下判断:

如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

2.当一个线程完成任务时,它会从队列中取下一个任务来执行。

3.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:

如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。


2.4.4线程池的4种拒绝策略理论简介

等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。

这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK拒绝策略:

AbortPolicy(默认):丢弃任务,直接抛出 RejectedExecutionException异常。

CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了RejectedExecutionHandler接口。


DiscardOldestPolicy和DiscardPolicy的处理非常类似,不同的是DiscardOldestPolicy丢弃的是当前队列中最老的任务,从而腾出空间用于当前任务执行





3.线程池的实际应用

3.1使用选择

你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多?

答案是一个都不用,我们生产上只能使用自定义的。

Executors 中JDK已经给你提供了,为什么不用?

java并发体系 -- 线程池与死锁

3.2具体实现

java并发体系 -- 线程池与死锁

3.2.1 AbortPolicy

丢弃任务,直接抛出 RejectedExecutionException异常。


多次运行结果:

运行结果1:


java并发体系 -- 线程池与死锁

运行结果2:

java并发体系 -- 线程池与死锁

3.2.2 CallerRunsPolicy

CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。


多次运行结果:

运行结果1:

java并发体系 -- 线程池与死锁

回退调用者main函数,并继续执行。


运行结果2:

java并发体系 -- 线程池与死锁

3.2.3 DiscardOldestPolicy

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

java并发体系 -- 线程池与死锁

3.2.3 DiscardPolicy

DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

java并发体系 -- 线程池与死锁

4.线程池配置合理线程数

合理配置线程池你是如何考虑的?

4.1 CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),

而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。


CPU密集型任务配置尽可能少的线程数量:

一般公式:(CPU核数+1)个线程的线程池


4.2 lO密集型 (例如频繁与数据库交互)

IO密集型,即该任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。

所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

4.2.1 方案1

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,

如CPU核数 * 2。

4.2.1 方案2

IO密集型时,大部分线程都阻塞,故需要多配置线程数:


参考公式:CPU核数/ (1-阻塞系数)


阻塞系数在0.8~0.9之间

比如8核CPU:8/(1-0.9)=80个线程数



二、死锁

1.是什么

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

java并发体系 -- 线程池与死锁


2. 产生死锁主要原因

1.系统资源不足

2.进程运行推进的顺序不合适

3资源分配不当


3. 发生死锁的四个条件

1.互斥条件,线程使用的资源至少有一个不能共享的。

2.至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。

3.资源不能被抢占。

4.循环等待。

4. 如何解决死锁问题

破坏发生死锁的四个条件其中之一即可。

产生死锁的代码(根据发生死锁的四个条件)

4.1 产生死锁的代码

java并发体系 -- 线程池与死锁

java并发体系 -- 线程池与死锁

4.2查看是否死锁工具

jps命令定位进程号

java并发体系 -- 线程池与死锁

注意:idea jdk版本必须与系统一致。jdk版本修改后需重启idea


jstack找到死锁查看