线程池

线程池

先看几个概念: 线程:进程中负责程序执行的执行单元。一个进程中至少有一个线程。

多线程:解决多任务同时执行的需求,合理使用CPU资源。多线程的运行是根据CPU切换完成,如何切换由CPU决定,因此多线程运行具有不确定性。

线程池:基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

线程池的优点 1)避免线程的创建和销毁带来的性能开销。 2)避免大量的线程间因互相抢占系统资源导致的阻塞现象。 3}能够对线程进行简单的管理并提供定时执行、间隔执行等功能。


线程

创建线程的两种方式

继承Thread类,扩展线程


备注

线程和进程的区别 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

Thread 类中的 start() 和 run() 方法有什么区别? 调用 start() 方法才会启动新线程;如果直接调用 Thread 的 run() 方法,它的行为就会和普通的方法一样;为了在新的线程中执行我们的代码,必须使用 Thread.start()


实现Runnabke接口

学习笔记

1)用 Runnable 还是 Thread ? 我们都知道可以通过继承 Thread 类或者调用 Runnable 接口来实现线程,问题是,创建线程哪种方式更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口更好了。

2)Runnable 和 Callable 有什么不同? Runnable 和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。


多线程

多线程的概念很好理解就是多条线程同时存在,但要用好多线程确不容易,涉及到多线程间通信,多线程共用一个资源等诸多问题。 使用多线程的优缺点: 优点: 1)适当的提高程序的执行效率(多个线程同时执行)。 2)适当的提高了资源利用率(CPU、内存等)。 缺点: 1)占用一定的内存空间。 2)线程越多CPU的调度开销越大。 3)程序的复杂度会上升。

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。

线程池的优点 1)避免线程的创建和销毁带来的性能开销。 2)避免大量的线程间因互相抢占系统资源导致的阻塞现象。 3}能够对线程进行简单的管理并提供定时执行、间隔执行等功能。

实现Runnable接口的Cache线程池

Executor执行Runnable任务

运行结果如下

***** a0 ***** pool-1-thread-1线程被调用了。 ***** a1 ***** ***** a2 ***** ***** a3 ***** pool-1-thread-1线程被调用了。 pool-1-thread-3线程被调用了。 ***** a4 ***** pool-1-thread-2线程被调用了。 pool-1-thread-4线程被调用了。

我们从结果可以看出,CachedThreadPool在创建新线程前会检查线程池中有没有闲置的线程,如果有就调用,没有就新建线程


实现Callable的线程池

Executor执行Callable任务 '''Java import java.util.ArrayList; import java.util.List; import java.util.concurrent.*;

运行结果如下:

call()方法被自动调用!!! pool-1-thread-1 call()方法被自动调用,任务返回的结果是:0 pool-1-thread-1 call()方法被自动调用!!! pool-1-thread-2 call()方法被自动调用!!! pool-1-thread-3 call()方法被自动调用!!! pool-1-thread-4 call()方法被自动调用,任务返回的结果是:1 pool-1-thread-2 call()方法被自动调用!!! pool-1-thread-5 call()方法被自动调用,任务返回的结果是:2 pool-1-thread-3 call()方法被自动调用,任务返回的结果是:3 pool-1-thread-4 call()方法被自动调用,任务返回的结果是:4 pool-1-thread-5 call()方法被自动调用!!! pool-1-thread-6 call()方法被自动调用,任务返回的结果是:5 pool-1-thread-6 call()方法被自动调用!!! pool-1-thread-7 call()方法被自动调用,任务返回的结果是:6 pool-1-thread-7 call()方法被自动调用!!! pool-1-thread-8 call()方法被自动调用!!! pool-1-thread-9 call()方法被自动调用,任务返回的结果是:7 pool-1-thread-8 call()方法被自动调用,任务返回的结果是:8 pool-1-thread-9 call()方法被自动调用!!! pool-1-thread-10 call()方法被自动调用,任务返回的结果是:9 pool-1-thread-10

从结果中可以同样可以看出,submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。


自定义线程池

自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池,这里先贴上示例程序:

运行结果如下:

pool-1-thread-2正在执行。。。 pool-1-thread-3正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-3正在执行。。。 pool-1-thread-2正在执行。。。 pool-1-thread-2正在执行。。。

*从结果中可以看出,七个任务是在线程池的三个线程上执行的。这里简要说明下用到的ThreadPoolExecuror类的构造方法中各个参数的含义。

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue workQueue) corePoolSize:线程池中所保存的核心线程数,包括空闲线程。

maximumPoolSize:池中允许的最大线程数。

keepAliveTime:线程池中的空闲线程所能持续的最长时间。

unit:持续时间的单位。

workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。

根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理:

1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;

2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);

3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;

4、如果线程池中的线程数量等于了maximumPoolSize,有4种才处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。

总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。

另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。*

学习笔记 我们大致来看下Executors的源码,newCachedThreadPool的不带RejectedExecutionHandler参数(即第五个参数,线程数量超过maximumPoolSize时,指定处理方式)的构造方法如下:

它将corePoolSize设定为0,而将maximumPoolSize设定为了Integer的最大值,线程空闲超过60秒,将会从线程池中移除。由于核心线程数为0,因此每次添加任务,都会先从线程池中找空闲线程,如果没有就会创建一个线程(SynchronousQueue决定的,后面会说)来执行新的任务,并将该线程加入到线程池中,而最大允许的线程数为Integer的最大值,因此这个线程池理论上可以不断扩大。

再来看newFixedThreadPool的不带RejectedExecutionHandler参数的构造方法,如下:

它将corePoolSize和maximumPoolSize都设定为了nThreads,这样便实现了线程池的大小的固定,不会动态地扩大,另外,keepAliveTime设定为了0,也就是说线程只要空闲下来,就会被移除线程池,敢于LinkedBlockingQueue下面会说。

下面说说几种排队的策略:

1、直接提交。缓冲队列采用 SynchronousQueue,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。直接提交通常要求无界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒绝新提交的任务。newCachedThreadPool采用的便是这种策略。

2、无界队列。使用无界队列(典型的便是采用预定义容量的 LinkedBlockingQueue,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过 corePoolSize,也因此,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。newFixedThreadPool采用的便是这种策略。

3、有界队列。当使用有限的 maximumPoolSizes 时,有界队列(一般缓冲队列使用ArrayBlockingQueue,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。

评论

此博客中的热门博文

Qbittorent下载完成后自动上传谷歌网盘脚本

记一次被骗的经历

循序渐进Socket网络编程(多客户端、信息共享、文件传输)