线程池的相关概念和参数以及源码分析

参考链接:通过实例来透彻分析线程池的源码---ThreadPoolExecutor_小猪快跑22的博客-优快云博客

一,


线程池的概念:

·

1)什么是线程池:

线程池就是创建一些线程,将它们的集合称之为线程池

使用线程池可以很好地提高系统的性能,线程池在系统启动时,即创建一些空闲的线程(核心线程),程序将一个任务交给线程池,线程池就会启动一个线程来执行这个任务。执行结束以后,该(核心)线程并不会死亡,而是再次返回线程池中,成为空闲状态,等待执行下一个任务。

·

2)线程池的工作机制

1)系统是将任务传给整个线程池,线程池在拿到任务后,就将任务交给池里边的一个空闲的线程去执行任务。

·

3)使用线程池的好处

1)降低资源消耗。

通过重复利用已创建的线程,来避免了线程的创建和销毁所造成的资源消耗。

2)提高响应速度。

当任务到达时,任务可以不需要等到线程创建,就能立即执行

3)提高线程的可管理性。

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

·

4)四种线程池:

Executors:在JUC(java.util.concurrent)包下面的这个Executors执行器类中,提供了一系列的静态工厂方法,用于创建各种线程池;

其中常用的几个方法,如下:

public static ExecutorService newFixedThreadPool() //固定数量线程池
public static ExecutorService newSingleThreadExecutor() //单线程线程池
public static ExecutorService newCachedThreadPool() //可缓存线程池
public static ScheduledExecutorService newScheduledThreadPool() //可定时线程池

1、newFixedThreadPool:固定数量的线程池,

该方法返回一个 可重用的、固定线程数量的线程池;

2、newSingleThreadExecutor:单线程的线程池,

它只会用唯一的线程来执行任务,保证所有任务按照指定顺序(FIFO(先进先出), LIFO(后进先出), 优先级)执行;

3、newCachedThreadPool: 可缓存线程池,

该线程池可以根据实际情况调整池子中的线程数量,当执行当前任务时,上一个任务已经完成,会直接复用执行上一个任务的线程,而不用每次新建线程,如果上一个线程没有结束才会新建线程,可缓存型池子通常用于执行一些生存期较短的任务;

4、newScheduledThreadPool: 可定时线程池,

该线程池可以设定线程的执行时间,可以用来去执行一些定时及周期性的任务。

·

5)如何创建线程池:

 JDK提供了Executor接口(隶属于java.util.concurrent包),可以让我们有效的管理和控制我们的线程。 

在这里插入图片描述

看源码:

public static ExecutorService newSingleThreadExecutor() {
     return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
}

根据源码可以看出,在实际创建线程池的时候,实质调用的是ThreadPoolExecutor这个类,即,线程池执行器ThreadPoolExecutor

即,在Executors类的内部,创建线程池的时候,实际新建的是一个ThreadPoolExecutor线程池执行器对象。

总结:说白了,线程池是通过new 一个ThreadPoolExecutor()来创建的

·


二,线程池是如何实现线程复用的?(源码分析)

1)线程有三种实现方式,

  • 一种是继承Thread,重写run方法;
  • 一种是实现runable接口,然后重写run方法;
  • 一种是callable接口;(异步调用,有返回值)

启动方式如下:

//第一种方式,继承Thread,重写run方法后的启动
new MyThread().start();

//第二种方式,将 `实现的runable接口的task作为参数` 传入Thread构造方法
new Thread(new Runnable() {
    @Override
    public void run(){
        System.out.println("do something");
    }
}).start();

我们知道,一般一个线程在执行完任务后就结束了,怎么再让他执行下一个任务呢?

要想实现线程复用,必须从Runnable接口的run()方法上入手;

2)看源码:

// 内部类 Worker
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
	 /** 各种代码...  */        
	 Runnable firstTask;       
	 Worker(Runnable firstTask) {
		 setState(-1); // inhibit interrupts until runWorker
		 this.firstTask = firstTask;
		 this.thread = getThreadFactory().newThread(this);
	 }       
	 public void run() {
		 runWorker(this);
	 }        
}


//addWorker()方法,启动线程; 
private boolean addWorker(Runnable firstTask, boolean core) {
	w = new Worker(firstTask);
	final Thread t = w.thread;
	//...代码

	// 很明显了,从这里启动线程的; 
	t.start(); 
	...//代码
}


// getTask()方法,从 任务队列(workQueue)中,获取任务; 
private Runnable getTask() {
	 // 各种代码...
	 if ((wc > maximumPoolSize || (timed && timedOut))
		 && (wc > 1 || workQueue.isEmpty())) {
		 //...
	 } // ...    
 }


// 核心部分:runWorker()方法:
final void runWorker(Worker w) {
	 // 各种代码...
	 Runnable task = w.firstTask;
	 w.firstTask = null;
	 // 各种代码...
	 try {
	 // 重点:看这个大while循环:实现的线程复用;  
		 while (task != null || (task = getTask()) != null) {
			  // 各种代码...
		 }
	 }
}

3)源码分析:在线程池中,线程是如何创建的:

我们从第5节知道,线程池是通过 new 一个 ThreadPoolExecutor (线程池执行器)这个类 来创建的;

真正的线程池里边的线程,是在ThreadPoolExecutor类里边的 一个名叫 Worker的内部类 里边创建的;

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
	 /** 各种代码...  */        
	 Runnable firstTask;    
    
	 Worker(Runnable firstTask) {
		 setState(-1); // inhibit interrupts until runWorker
		 this.firstTask = firstTask;
         //创建线程:
		 this.thread = getThreadFactory().newThread(this);
	 }  
    
	 public void run() {
		 runWorker(this);
	 }        
}

 也就是说,只要 new Worker(),就会创建一个线程,因为Worker的构造方法里边有这么一句:this.thread = getThreadFactory().newThread(this);

并且创建线程的时候,会将这个内部类本身this 传到参数列表里边,去当task。

·

4)源码分析:在线程池中,线程是如何启动的:

在源码的addWorker()方法中,可以看到:t.start();;
很明显了,从addWorker()方法这里启动线程的;

//addWorker()方法,启动线程; 
private boolean addWorker(Runnable firstTask, boolean core) {
	w = new Worker(firstTask);
	final Thread t = w.thread;
	//...代码

	// 很明显了,从这里启动线程的; 
	t.start(); 
	...//代码
}

·

5)源码分析:在线程池中,线程是如何运行的:

我们刚才知道,在源码的addWorker()方法中进行启动线程

我们可以看到:w = new Worker(firstTask);
也就是说,在线程启动的时候,我们将内部类worker对象传入进去了,而内部类Worker是实现了runable接口、并重写了run()方法的;

这也意味着:jvm会执行Worker里的run方法,使线程进入运行状态;

·

6)源码分析:在线程池中,线程是如何实现复用的:

6.1)核心在runWorker()方法里边:

用Work内部类中的run()方法里边的runWorker()方法里边的while大循环,实现了线程的复用;

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
	 /** 各种代码...  */        
	 Runnable firstTask;    
    
	 Worker(Runnable firstTask) {
		 setState(-1); // inhibit interrupts until runWorker
		 this.firstTask = firstTask;
         //创建线程:
		 this.thread = getThreadFactory().newThread(this);
	 }  
    
	 public void run() {
		 runWorker(this);
	 }        
}

在Work内部类中的run()方法里面,有一个runWorker(this)方法;(这个this指的是内部类Work对象本身)

这个run()方法,线程启动的时候jvm会执行它,

我们再来看这个run()方法里面的runWorker()方法:

final void runWorker(Worker w) {   // 各种代码...
  // 核心:这个大while循环:
     while (task != null || (task = getTask()) != null) {
          // 各种代码...
     }
     // 各种代码...
}

线程复用的核心,就在这个runWorker()方法里边:

从源码中可以看出,这个runWorker()方法里面,有一个大大的while循环:

  • ① 当我们的task任务不为空的时候,

即,通过getTask()方法,可以一直获取到新的任务 ,那么这个while循环就永远在进行;
从而runWorker()方法不会停止,runWorker()方法外边的run()方法也就不会停止,继而线程会一直处于运行状态,去执行新的任务,从而达到了线程复用的目的

  • ② 当我们的task任务为空了,则while循环结束,也就是说,这个线程结束 。

·

7)剖析getTask()方法:

这个getTask()方法,很显然是用来 从任务队列(workQueue)中,获取任务;

private Runnable getTask() {
     // 各种代码...
     if ((wc > maximumPoolSize || (timed && timedOut))
         && (wc > 1 || workQueue.isEmpty())) {
          //...
     } // ...    
}

根据getTask()这个方法的源码可以看出,getTask()方法是从任务队列中(workQueue),获取任务;

这个getTask()方法里面有个三元表达式,

        ① 当条件为真时,从任务队列(workQueue)里面,取得要执行的任务;

        ② 当条件为假时,即任务队列没有任务了,则结束runWorker()方法里边的while大循环,从而,这个线程结束。

·

8) 线程复用流程总结:

①,创建一个线程池,new ThreadPoolExecutor()

②,新建一个线程,在线程池中,new Worker()新建一个Worker内部类时,会新建一个线程getThreadFactory().newThread(this),并且会把这个Worker内部类本身传进去当作任务去执行,

③,这个worker内部类的run()方法里的runWorker()方法,写了一个while大循环,

④,当任务队列有任务时,while大循环一直进行,从而runWorker()、run()方法也就一直进行,继而该线程一直执行新的任务,达到了线程复用的目的;

⑤,当任务队列没有任务时,则结束这个while循环,继而,这个线程也就结束

·
·


三,Java线程池的7个参数:

我们以饭店服务员为例子:

        饭店只有两个正式工的服务员(核心线程),用来维持日常的经营(处理任务)

        当饭店推出打折活动的时候,突然间会有大量的客人来吃饭(任务暴增),当客人的数量超过了两个(核心线程数)之后,剩余的客人就会在门口的凳子上(任务队列)排队等待,

        当凳子(任务队列)也坐满了之后,于是老板就不得不找了四个临时工服务员(临时线程)来帮忙。

        当打折活动过去之后,客流量高峰期过了,此时原本的两个正式工的服务员(核心线程)就已经够用了,然后等到四个临时工服务员(临时线程)的工作时间也到点了之后,老板就会撵走这四个临时工服务员(临时线程)。

        

1> 核心线程数(corePoolSize)

        核心线程的数量,也称为线程池的基本大小;

        核心线程的生命周期与该线程池一样,即永远不会被回收掉,除非该线程池都没了。

2> 最大线程数(maximumPoolSize):

  • 与上边的核心线程对应的是——非核心线程(临时线程);

  • 最大线程数(maximumPoolSize)指的是:线程池中允许的最大线程数

  • 核心线程数 + 非核心线程数 = 最大线程数;

3> 任务队列(workQueue):

  •  当任务的数量多于corePoolSize核心线程数的时候,会将多出来的这些任务放入到任务队列中。
  • 如果任务队列也满了,那么会新建一个非核心线程去执行任务

  • 如果任务队列满了,且线程池中的实际线程数量也等于最大线程数maximumPoolSize了,还是有新来的任务,那么会执行饱和策略,默认的策略是直接丢掉要加入的任务,抛出一个拒绝执行异常。

4+5> 空闲线程的存活时间 (keepAliveTime)与存活时间单位:

  • 这针对的是非核心线程,即临时线程;
  • 非核心线程空闲下来之后,且超过了这个存活时间也还没有任务执行,则结束和回收该线程
  • 注意:核心线程是不会被回收的,除非你任务设置要回收核心线程

6> 线程工厂

        创建线程的工厂,用来创建线程的。这个线程工厂可以自定义(在实际开发中方便业务区分和异常追溯)。

7> 拒绝策略(饱和策略)

        如果任务队列满了,且线程池中的实际线程数量也等于最大线程数maximumPoolSize了,还是有新来的任务,那么会执行饱和策略,默认的策略是直接丢掉要加入的任务,抛出一个拒绝执行异常。

·

补充:关于任务队列:

队列的选择,在实际开发中这是一个难点和重点。

1)队列的长度、核心线程数、最大线程数怎么设置:

        这个得根据你实际的业务去进行评估。比如说你的并发量最低的时候是多少,可以参考着去制定核心线程数;你的并发量的最高峰是多少,去参考着设定最大线程数和任务队列的长度;

2)队列有哪些种类、怎么选型:

        队列你可以自己定义,定义一个数组、一个链表,都可以;

        或者,也可以用现成的,Java里边的容器种类很多,比如说concurrentHashMap,CopyOnWriteList,SynchronizeList,等等。

        还有就是:只要是和xxxQuque这个相关的,基本上都可以去作为队列使用,比如说ArrayBlockingQueue,LinkedBlockingQueue,PriorityQueue,DelayQueue,SynchronusQueue等;

3)有界队列与无界队列:

        有界队列、无界队列的意思就是任务队列的长度,无界队列就是这个队列可以无限延长;

        注意:在实际开发中,不要使用无界队列!即使是有界队列,长度也不要过大!不然容易引起OOM。

        比如说:FixedThreadPool和SingleThreadPool默认允许的任务队列的长度是Integer.MAX_VALUE,CachedThreadPool默认允许的最大线程数为Integer.MAX_VALUE,这两种情况都可能导致OOM。

当任务数量特别多的时候:

        允许的任务队列的长度是Integer.MAX_VALUE,可能会堆积大量的任务,把内存全部给你耗光,从而导致OOM;

        允许的最大线程数为Integer.MAX_VALUE,可能会创建大量的非核心线程,把内存耗尽,从而导致OOM;

        所以在实际开发中,不要使用无界队列!即使是有界队列,也要设定合理的长度!       

·

补充:关于拒接策略:

        当任务特别多的时候,任务队列满了,最大线程数满了,此时再有新任务来了,就要去执行拒绝策略了。

        默认的拒绝策略是:直接丢掉要加入的任务,抛出一个拒绝执行异常。

        很明显在实际开发中很多时候这个默认的策略是不可取的,因为有的数据是不能随意丢掉的。那么我们该如何自定义一个策略来解决问题呢?       

1)使用持久化的方式,接住多余的任务,比如MQ;

        我们可以把新来的这部分任务给持久化起来,比如说我们在这个服务的外边搭建一个MQ的集群,当新任务来了实在处理不了,可以先扔到MQ里边,(MQ自己有持久化机制),等到线程池来得及处理了,再从MQ里边拿出来,去进行处理。

2)其他方式:

        如果任务量太大了,MQ都灌满了怎么办,动态的扩缩容MQ集群;实在实在不行,还可以从网关的位置就开始进行限流等方式;

·

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值