读书笔记---线程池executor(任务执行框架)

 

目录

一、J.U.C包下主要类的结构图

二、线程池Executor(任务的主要抽象不是thread而是Executor)

1、创建线程池的方式

2、线程池中常用的方法

execute方法的执行逻辑

3、线程池的好处

4、线程池数目大小如何设置最好

5、Executor的生命周期

6、runable和callnble对比

7、任务的取消和中断

1)、可取消的

2)、中断

3)、通过future来实现取消

4)、处理不可中断的阻塞

5)、停止基于线程的服务

6)、处理非正常的线程终止

7)、jvm关闭

8、ExecutorcompletionService提交一组任务

9、invokeall获取一组结果

10、线程池的使用

三、核心类ThreadPoolExecutor

1、ThreadPoolExecutor参数最全的构造函数:

2、线程池中线程的变化过程

3、workQueue工作队列

4、threadFactory

5、拒绝策略RejectedExecutionHandler 

6、扩展ThreadPoolExecutor


 

一、J.U.C包下主要类的结构图

二、线程池Executor(任务的主要抽象不是thread而是Executor)

只有当大量相互独立 且同构的任务可以并发进行处理时,才能体现出经程序的工作负载分配到多个任务中带来的真正性能提升。

Executor基于生产者-消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),执行任务的线程相当于消费者(执行完这些工作单元);

通过Executor可以实现将请求处理任务的提交与任务的实际执行解耦开来;

执行策略:

1)、在线程中执行什么任务?

2)、任务按照什么样的顺序执行(FIFO、LIFO、优先级)?

3)、有多少个任务在并发的执行?

4)、在队列中有多少个任务在等待执行?

5)、如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个任务?另外如何通知应用程序有任务被拒绝?

6)、在执行一个任务之前或者之后应该进行哪些操作?

注意:通过Executor的解耦,很容易为某种类型的任务指定和修改执行策略。

1、创建线程池的方式

通过工厂类Executors的静态方法newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor可以构造线程池类ExecutorService。其底层都是调用new ThreadPoolExecutor来实现的

a、newFixedThreadPool 创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程最大数量时,线程池的规模将不再变化。(如果某个线程由于发生了未预期的异常Exception,那么线程池会补充一个新的异常),放入线程池的线程并不一定会按其放入的先后而顺序执行;
b、newCachedThreadPool 创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制,放入线程池的线程并不一定会按其放入的先后而顺序执行。
c、newSingleThreadPoolExecutor 创建一个单线程的Executor,如果这个线程异常结束,会创建另外一个来替代。newSingleThreadPoolExecutor 能够按照任务在队列中的顺序来串行执行(如FIFO、LIFO、优先级);
d、newScheduledThreadPool 创建一个固定长度的线程池,而且以延迟或者定时的方式来执行,类似Timer;


总结:

1)、在线程池中执行任务比为每个任务分配一个线程优势更多,通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊线程创建和销毁产生的巨大的开销。

2)、当请求到达时,通常工作线程已经存在,提高了响应性;

3)、通过配置线程池的大小,可以创建足够多的线程使CPU达到忙碌状态,还可以防止线程太多耗尽计算机的资源。

4)、使用Executor和thread对比,Executor可以实现调优、管理、监视、记录日志、错误报告和其他功能,如果不使用任务执行框架,实现这些功能比较困难。

2、线程池中常用的方法

提交任务
1)、execute()表示往线程池添加线程,有可能会立即运行,也有可能不会。无法预知线程何时开始,何时线束。
2)、submit()将线程放入线程池中,除了使用execute,也可以使用submit,它们两个的区别是一个使用有返回值,一个没有返回值。submit的方法很适应于生产者-消费者模式,通过和Future结合一起使用,可以起到如果线程没有返回结果,就阻塞当前线程等待线程 池结果返回。

关闭线程池
3)、shutdown()
通常放在execute后面。如果调用 了这个方法,一方面,表明当前线程池已不再接收新添加的线程,新添加的线程会被拒绝执行。另一方面,表明当所有线程执行完毕时,回收线程池的资源。注意,它不会马上关闭线程池!
4)、shutdownNow()
不管当前有没有线程在执行,马上关闭线程池!这个方法要小心使用,要不可能会引起系统数据异常!

execute方法的执行逻辑

void execute(Runnable command);
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
  
    int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

execute执行过程示意图.jpg

execute方法执行逻辑有这样几种情况:

  1. 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务;
  2. 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中;
  3. 如果当前workQueue队列已满的话,则会创建新的线程来执行任务;
  4. 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理。

需要注意的是,线程池的设计思想就是使用了核心线程池corePoolSize,阻塞队列workQueue和线程池maximumPoolSize,这样的缓存策略来处理任务,实际上这样的设计思想在需要框架中都会使用。

3、线程池的好处

1)、降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗;

2)、提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度;
3)、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。

4)、将当前任务与主线程隔离,能实现和主线程的异步执行,特别是很多可以分开重复执行的任务。

无限制创建线程的不足:

1)、线程的生命周期的开销非常高。如果请求的到达率非常高且请求的处理过程是轻量级的,如大多数服务器应用程序就是这种情况,那么为每一个请求创建一个新的线程将消耗大量的计算资源;

2)、资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可运行的处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源的时候还将产生其他的开销(如,上下文环境的切换等)。如果你已经拥有足够多的线程是CPU处于忙碌的状态,那么再创建更多的线程反而会降低性能。

3)、稳定性。可创建的线程数存在限制。如果破坏了限制就可能抛出OutOfMemoryError异常

4、线程池数目大小如何设置最好

cpuNums = Runtime.getRuntime().availableProcessors();  //获取当前系统的CPU 数目  

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

5、Executor的生命周期

public interface ExecutorService extends Executor {}

ExecutorService 的生命周期只有三种状态:运行、关闭和已终止。
shutdown平缓的关闭过程:不在接收新的任务,同时等待已经提交的任务执行完成---包括哪些还未开始执行的任务;
shutdownNow执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务;

通常在调用awaitTermination之后会立即调用shutdown,从而产生同步关闭ExecutorService的效果。

6、runable和callnble对比

1)、runnnable不能返回一个值或者抛出一个受检查的异常;而callnable将返回一个值并可能爆出一个异常

7、任务的取消和中断

Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。

在Executor框架中,已经提交但尚未开始的任务可以取消,但是对于那些已经开始的任务,只有当他们能响应中断时才能取消。取消一个已经完成的任务不会有任何影响。

future标示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果和取消任务等。

Executor的submit方法将会返回一个future;

FutureTask实现了runnable和future,因此可以将FutureTask提交给Executor来执行。

1)、可取消的

如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就是可取消的。

java没有一种安全的抢占式的方法来停止线程,只有一些协同式的机制,使得请求取消任务和代码都遵循一种协商好的协议。

一个可取消的任务必须拥有可取消的策略,在这个策略中将详细地定义取消操作的“HOW”,“WHEN”和“what”,即其他代码如何(how)请求取消该任务,任务何时(when)检查是否已经请求了取消以及响应取消请求时应该执行那些(what)操作。

java的API或语言规范中,并没有将中断与任何取消语义关联起来,但是实际上,如果在取消之外的其他操作中使用中断,其实是不合适的。(通常中断时实现取消的最合理方式)

2)、中断

public class Thread implements Runnable {

      public void interrupt() {}
      public boolean isInterrupted() {}
      public static boolean interrupted() {}

}

调用interrupt并不意味着立即停止目标线程正在执行的工作,而只是传递了请求中断的消息,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点)

a、中断策略

当发现中断请求时,应该做哪些工作(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个线程的所有者该线程已经退出。

b、响应中断

有两种实用的策略可以处理interruptexception

传递异常:从而使你的方法也变成可中断的阻塞方法;

恢复中断状态:从而使调用栈中的上层代码能够对其进行处理;

只有实现了线程中断策略的代码才可以屏蔽中断请求。

3)、通过future来实现取消

当future.get抛出异常interruptexception或timeoutexception时,如果你知道不再需要结果,那么就可以调用future.cancel来取消任务

4)、处理不可中断的阻塞

5)、停止基于线程的服务

6)、处理非正常的线程终止

7)、jvm关闭

 

8、ExecutorcompletionService提交一组任务

9、invokeall获取一组结果

方法的参数是一组任务,并返回一组future

 

10、线程池的使用

1)、在任务与执行策略之间的隐形耦合

线程饥饿死锁

三、核心类ThreadPoolExecutor

Executor<---ExecutorService<---AbstractExecutorService<---ThreadPoolExecutor(前面两个是接口,后面连个是类)

1、ThreadPoolExecutor参数最全的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
//''''
}

构造方法中的参数说明:

参数名

作用
corePoolSize核心线程池大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动
maximumPoolSize最大线程池大小。表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。
keepAliveTime线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
TimeUnitkeepAliveTime时间单位
workQueue阻塞任务队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
threadFactory新建线程工厂。创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因
RejectedExecutionHandler当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理。

饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:

  1. AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
  2. CallerRunsPolicy:只用调用者所在的线程来执行任务;
  3. DiscardPolicy:不处理直接丢弃掉任务;
  4. DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务

2、线程池中线程的变化过程

线程池执行流程图.jpg

1)、如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
2)、如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务(前提会判断当前线程的数目和最大线程数目,如果没有达到最大,则创建新的线程,否则执行3);
3)、如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
4)、如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

注意:初始化的时候不会构造线程,当有请求来的时候才会创建(不过可以通过参数配置,在初始化的时候就创建一定数量的线程)

3、workQueue工作队列

用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1)、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2)、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
3)、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
4)、priorityBlockingQuene:具有优先级的无界阻塞队列;

4、threadFactory

就是一个线程工厂。用来创建线程。作用:是为了统一在创建线程时设置一些参数,如是否守护线程。线程一些特性等,如优先级,通过这个TreadFactory创建出来的线程能保证有相同的特性。它是一个接口类,而且方法只有一个,就是创建一个线程。

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

只有一个默认的实现,它做的事就是统一给线程池中的线程设置线程group、统一的线程前缀名,以及统一的优先级等。

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);//原子类,线程池编号
        private final ThreadGroup group;//线程组
        private final AtomicInteger threadNumber = new AtomicInteger(1);//线程数目
        private final String namePrefix;//为每个创建的线程添加的前缀
 
        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();//取得线程组
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }
 
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);//真正创建线程的地方,设置了线程的线程组及线程名
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)//默认是正常优先级
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

 

 

5、拒绝策略RejectedExecutionHandler 

通常有以下四种策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6、扩展ThreadPoolExecutor

 protected void beforeExecute(Thread t, Runnable r) { }
 protected void afterExecute(Runnable r, Throwable t) { }
 protected void terminated() { }

 

 

 

 

 

 

参考:

java并发编程实战6、7、8章

https://blog.csdn.net/evankaka/article/details/51489322#

https://juejin.im/post/5aeec0106fb9a07ab379574f

https://juejin.im/post/5aeec249f265da0b886d5101

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值