Java并发编程-线程池底层工作原理

关于线程池的具体介绍,可以参看ThreadPool线程池,这里主要介绍线程池底层工作原理。

1.线程池的底层工作流程

1.1.线程池的底层工作原理图

线程池的主要处理原理图和流程图如下
在这里插入图片描述
在这里插入图片描述

1.2.银行办理业务案例

为了便于理解,我们用银行的例子来说明银行网=线程池
corePoolsize=今日当值窗口。今日当值窗口由两个,当银行客人来了之后,先去候客厅等着,现在来了3个即3,4,5都来了,在候客厅等着,此时候客区也满了。突然,6,7,8三个人又来了,由于候客区也满了,此时可以用到maximumPoolSize,
它即银行的扩容窗口。此时银行经理可以商量增加窗口,即扩容。然后就增加了3个扩容窗口。从核心数扩容到最大数。此时候客区的客人3,4,5就可以去新增窗口办理服务。而6,7,8就去候客区等待。
maximumPool包含corePool
如果此时又来了两个客户9,10。此时当值窗口、新增窗口和候客区都已经满了,只能采用拒绝策略。经理在门口立个牌子,今日客满。9,10看到了只能走了。
随着时间的流逝,当某些客户被处理完时,业务量也随之下降,3,4,5办理完了,6,7,8上来之后也办理完了。keepAlive指的是在多长时间以内,没有收到新的需求了,线程池就会缩过来,恢复到今日单值窗口。

银行办理业务的流程可总结如下:

N个窗口 : corePoolSize

总共有K个窗口:maximumPoolSize

候客厅 :BlockedQueue

1 银行开门,等待顾客上门办理业务
2 今天开N个窗口,有N个柜员提供服务。
	2.1 如果顾客数小于等于N,可以立即为每一个顾客办理业务,不用等待。
    2.2 如果又来了M个顾客,M个顾客在候客厅坐得下,那么顾客就在候客厅等候
    2.3 如果候客厅坐满了,银行大堂经理发现窗口不够用,叫人过来加班,开启其他没有工作的窗口。
    2.4 如果候客厅满了,开启的工作窗口也是最大数K,就会启动拒绝策略。(今天人太多了,明天来吧;或者您等会再来看看,等人少的时候来)
3 当一个顾客的业务办理完成后,叫一下个顾客来办理业务
4 当人数很少的时候,额外加班的窗口会慢慢关闭,留下N个窗口。

在这里插入图片描述
即上面的四大步骤:当值窗口(核心线程)->候客区(阻塞队列)->扩容窗口(最大线程数)->拒绝策略。

1.3.线程池的底层工作流程总结

以下重要:

1 在创建了线程池后,等待提交过来的任务请求。
2 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
  2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建马上创建线程运行这个任务。
  2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
  2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。(队列出列,刚来的去队列中)
  2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAlilveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小

2.线程池用哪个?生产中如何设置合理参数

2.1.在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑

答案时一个都不用,我们工作中只能使用自定义的
Executors中JDK已经给你提供了?为什么不用?
以下是阿里开发手册中的内容:

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用Executors去创建,而是通过ThreadPooLExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(out of memory)。

2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

public static ExecutorService newCachedThreadPool() {
    
    
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

2.2.在工作中如何使用线程池,是否自定义过线程池

public static void main(String[] args) {
    
    
		ExecutorService threadPool=new ThreadPoolExecutor(2, 5, 2000L,TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
		try {
    
    
			//模拟有6个顾客过来银行办理业务
			for(int i=1;i<=9;i++){
    
    
				//execute方法里面有一个参数,参数类型是Runnable,Runnable是函数式接口,可以用lambda表达式,并且runnable就是这10个顾客
				threadPool.execute(()->{
    
    
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
    
    
			// TODO: handle exception
		}finally{
    
    
			threadPool.shutdown();
		}		
	}

在这里插入图片描述

public static void main(String[] args) {
    
    
		ExecutorService threadPool=new ThreadPoolExecutor(2, 5, 500L,TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
		try {
    
    
			//模拟有6个顾客过来银行办理业务
			for(int i=1;i<=9;i++){
    
    
				//execute方法里面有一个参数,参数类型是Runnable,Runnable是函数式接口,可以用lambda表达式,并且runnable就是这10个顾客
				threadPool.execute(()->{
    
    
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}finally{
    
    
			threadPool.shutdown();
		}		
	}

在这里插入图片描述

而执行了9次之后,则可能会报错,说明这个池子最多容纳5+3=8.
最大容纳数=队列数+最大线程数

报的异常是java.util.concurrent.RejectedExecutionException: 拒绝执行异常。也是默认策略报的异常。

3.线程池的拒绝策略

3.1.是什么

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

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

3.2.线程池的四大拒绝策略

3.2.1.AbortPolicy(默认)拒绝策略

直接抛出RejectedExecutionException异常阻止系统正常运行。
在上面已经演示过

3.2.2.CallerRunsPolicy拒绝策略

调用者运行一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
谁调用我你就找谁。也就是打回去,谁让你找我你就找谁。

public static void main(String[] args) {
    
    
		ExecutorService threadPool=new ThreadPoolExecutor(2, 5, 500L,TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.CallerRunsPolicy());
		try {
    
    
			//模拟有6个顾客过来银行办理业务
			for(int i=1;i<=10;i++){
    
    
				//execute方法里面有一个参数,参数类型是Runnable,Runnable是函数式接口,可以用lambda表达式,并且runnable就是这10个顾客
				threadPool.execute(()->{
    
    
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}finally{
    
    
			threadPool.shutdown();
		}		
	}

在这里插入图片描述

是main让其来找线程池的,现在线程池办理不了了,就把它回退给main函数。

3.2.3.DiscardPolicy拒绝策略

该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略。

public static void main(String[] args) {
    
    
		ExecutorService threadPool=new ThreadPoolExecutor(2, 5, 500L,TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.DiscardPolicy());
		try {
    
    
			//模拟有6个顾客过来银行办理业务
			for(int i=1;i<=10;i++){
    
    
				//execute方法里面有一个参数,参数类型是Runnable,Runnable是函数式接口,可以用lambda表达式,并且runnable就是这10个顾客
				threadPool.execute(()->{
    
    
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}finally{
    
    
			threadPool.shutdown();
		}		
	}

在这里插入图片描述
这里只打印出了8个,还有两个线程被丢弃掉。这就要求我们的事务满足弱一致性,允许少部分的丢失。

3.2.4.DiscardOldestPolicy拒绝策略

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

public static void main(String[] args) {
    
    
		ExecutorService threadPool=new ThreadPoolExecutor(2, 5, 500L,TimeUnit.MILLISECONDS, 
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.DiscardOldestPolicy());
		try {
    
    
			//模拟有6个顾客过来银行办理业务
			for(int i=1;i<=10;i++){
    
    
				//execute方法里面有一个参数,参数类型是Runnable,Runnable是函数式接口,可以用lambda表达式,并且runnable就是这10个顾客
				threadPool.execute(()->{
    
    
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				});
			}
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}finally{
    
    
			threadPool.shutdown();
		}		
	}

在这里插入图片描述

4.配置线程池

Runtime.getRuntime().availableProcessors() 查看核心数

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

在这里插入图片描述

corePoolSize :1或者0

如何设置 maximumPoolSize,分情况考虑,要看业务是CPU密集型还是IO密集型

4.1.CPU密集型

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

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

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

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

4.2.IO密集型

比如:数据库读写

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

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

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

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

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

参考公式:CPU核数/1-阻塞系数阻塞系数在0.8~0.9之间比如8核CPU:8/(1-0.9)=80个线程数

猜你喜欢

转载自blog.csdn.net/qq_39736597/article/details/113384327
今日推荐