一、Thread
1、基本介绍
Thread
类位于java.lang
包中,是Java中表示线程的一个类,实现了Runnable
接口。Thread
类中常用的方法如下:
方法 | 说明 |
---|---|
public Thread() |
创建一个新线程 |
public Thread(String name) |
创建一个指定名称的新线程 |
public Thread(Runnable target) |
创建一个带有指定目标的新线程 |
public Thread(Runnable target, String name) |
创建一个带有指定目标和名称的新线程 |
public void start() |
开启当前线程 |
public void run() |
执行当前线程任务 |
public String getName() |
获取当前线程名称 |
public static void sleep(long millis) |
使当前执行的线程在指定的毫秒数内休眠 |
public static Thread currentThread() |
返回当前正在执行的线程的引用 |
public final void setDaemon(boolean on) |
将当前线程设置为守护线程 |
public static native void sleep(long millis) |
让当前线程进入休眠状态 |
public static native void yield() |
声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 |
若需获取Thread
类更多的API,请参阅:java.lang.Thread。
2、创建线程
(1)继承Thread类
可以通过继承Thread
类并重写其中的run()
方法来创建一个自定义的线程,代码如下:
1 | class MyThread extends Thread { |
如果自定义的子类只在创建线程时出现过一次,也可以使用匿名内部类来创建一个自定义的线程:
1 | public class ThreadDemo01 { |
(2)实现Runnable接口
除了通过继承Thread
类来创建线程,我们还可以通过实现Runnable
接口来创建线程。通过这种方式创建线程的步骤如下:
- 定义一个实现类实现
Runnable
接口 - 在
Thread
类的构造方法中传入实现类
下面将演示如何创建线程:
1 | class MyRunnable implements Runnable { |
如果该实现类只在创建线程时使用一次,可以使用匿名实现类来实现Runnable
接口。在Java 8 及之后的版本支持 lambda 表达式,因此也可以用 lambda 表达式来代替匿名实现类(这里可以使用 lambda 表达式是因为该接口只有一个方法,即run()
方法)
1 | public class ThreadDemo01 { |
(3)实现Callable接口
与Runnable
不同的是,Callable
是有返回值的,返回值通过FutureTask
进行封装。
1 | class MyCallable implements Callable<Integer> { |
二、 Thread or Runnable
如果一个类继承Thread
,则不适合资源共享(因为Java不支持多继承)。但是如果实现了Runable
接口的话,则很容易的实现资源共享(因为Java支持实现多个接口)。
实现Runnable
接口比继承Thread
类所具有的优势:
- 适给多个相同的程序代码的线程去共享同一个资源。
- 可以避免Java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现
Runable
或Callable
类线程,不能直接放入继承Thread
的类。
因此推荐使用实现Runnable
接口的方式来创建线程。
三、Executor
1、简介
在Java 5之后,并发编程引入了一堆新的启动、调度和管理线程的API。Executor 框架便是 Java 5 中引入的,其内部使用了线程池机制,它在java.util.cocurrent
包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。下面给出 Executor 框架的 UML 图:
在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题( this 逃逸问题是指如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象,这会引起一些意想不到的错误,因此要避免这种情况的发生)。
Eexecutor 作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用 Runnable 来表示任务,Executor 的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。
2、Executor & ExecutorService
(1)Executor
Executor
是一个接口,在该接口中含有一个execute
方法,功能是执行一个任务,其源代码如下:
1 | public interface Executor { |
因此,我们可以使用Executor
并调用execute
方法来隐式地创建一个线程。
(2)ExecutorService
ExecutorService
继承于Executor
,是一个比Executor
更泛用的接口,该接口提供了生命周期管理的方法。下面列出ExecutorService
中的部分方法:
方法 | 说明 |
---|---|
void shutdown() |
有序关闭其中执行以前提交的任务,但不接受新任务 |
<T> Future<T> submit(Callable<T> task) |
提交一个Callable 任务以供执行并返回一个表示任务挂起结果的 Future |
Future<?> submit(Runnable task) |
提交Runnable 任务以执行并返回表示该任务的 Future |
若需获取更多ExecutorService
接口的API,请参阅:java.util.concurrent.ExecutorService。
3、Executors
Executors
是一个工具类,该类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
下面介绍Executors
中常用的工厂方法:
方法 | 返回值 |
---|---|
newFixedThreadPool(int nThreads) |
创建一个线程池,该线程池重用在共享无边界队列上运行的固定数量的线程 |
newCachedThreadPool() |
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中 |
newSingleThreadExecutor() |
创建一个单线程化的Executor |
newSingleThreadScheduledExecutor() |
创建一个支持定时及周期性的任务执行的线程池 |
若需获取更多Executors
类的API,请参阅:java.util.concurrent.Executors。
4、实例:使用ExecutorService创建线程
上面介绍了ExecutorService
接口及Executors
工具类,下面我们将演示如何使用ExecutorService
来创建线程并执行线程任务:
1 | // 1.使用Executors工具类中的工厂方法创建线程池 |
5、ThreadPoolExecutor
(1)构造方法
上面我们介绍了如何使用Executors工具类中的工厂方法创建线程池,那么这些工厂方法具体是如何实现的呢,我们以newCachedThreadPool
方法为例,来分析工厂方法是如何创建线程池的。
1 | /** |
通过阅读源码,我们可以清楚地知道Executors
工具类的工厂方法是通过具体情况创建一个ThreadPoolExecutor
对象来创建线程池的。下面我们对ThreadPoolExecutor
构造方法的参数进行简单介绍:
参数名 | 类型 | 说明 |
---|---|---|
corePoolSize |
int |
线程池中所保存的核心线程数,包括空闲线程 |
maximumPoolSize |
int |
线程池中允许的最大线程数 |
keepAliveTime |
long |
线程池中的空闲线程所能持续的最长时间 |
unit |
TimeUnit |
持续时间的单位 |
workQueue |
BlockingQueue<Runnable> |
任务执行前保存任务的队列,仅保存由execute 方法提交的Runnable 任务 |
- 注1:
TimeUnit
是一个枚举类型。 - 注2:
BlockingQueue
是一个特殊的队列,当其为空时,获取元素线程被阻塞直到队列变为非空;当其为满时,添加元素线程被阻塞直到队列不满。详细内容请参阅:java.util.concurrent.BlockingQueue。
(2)实现execute方法
介绍完ThreadPoolExecutor
类的构造方法的相关参数的含义,下面我们将说明ThreadPoolExecutor
是如何实现execute
方法的:
- 如果线程池中的线程数量少于
corePoolSize
,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务。 - 如果线程池中的线程数量大于等于
corePoolSize
,但缓冲队列workQueue
未满,则将新添加的任务放到workQueue
中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行)。 - 如果线程池中的线程数量大于等于
corePoolSize
,且缓冲队列workQueue
已满,但线程池中的线程数量小于maximumPoolSize
,则会创建新的线程来处理被添加的任务。 - 如果线程池中的线程数量等于了
maximumPoolSize
,可通过RejectedExecutionHandler
来处理线程溢出问题,详细内容请参考源码,这里不再展开。
总结起来就是:当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize
,再看缓冲队列workQueue
是否满,最后看线程池中的线程数量是否大于maximumPoolSize
。
另外,当线程池中的线程数量大于corePoolSize
时,如果里面有线程的空闲时间超过了keepAliveTime
,就将其移除线程池,这样就可以动态地调整线程池中线程的数量。
(3)缓冲队列
在前面的内容中已经多次提到了缓冲队列,下面我们将详细介绍一下缓冲队列的内容。ThreadPoolExecutor
类的构造方法中需要传入BlockingQueue<Runnable>
类型的参数,BlockingQueue<E>
是一个接口,它的实现类如下:
通过API文档,我们可以知道BlockingQueue<E>
有很多的实现类,那么我们该选择哪个实现类来实现该接口呢?
缓冲队列一般有直接提交、无界队列和有界队列这几种排队策略,下面我们来介绍一下这些策略的特点和使用场景。知道了这些,我们就可以对上面的问题做出回答了。
名称 | 说明 |
---|---|
直接提交 | 缓冲队列采用SynchronousQueue ,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中 直接提交通常要求无界 maximumPoolSizes( Integer.MAX_VALUE ) 以避免拒绝新提交的任务。newCachedThreadPool 采用的便是这种策略。 |
无界队列 | 使用无界队列(典型的便是采用预定义容量的LinkedBlockingQueue ,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过 corePoolSize,也因此,maximumPoolSize 的值也就无效了。 当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。 newFixedThreadPool 采用的便是这种策略。 |
有界队列 | 当使用有限的 maximumPoolSizes 时,有界队列(一般缓冲队列使用ArrayBlockingQueue ,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。 |
(4)缓冲型线程池和固定型线程池的区别
前面我们简单介绍了Executors的几种工厂方法,在了解Executors工厂方法的实现细节之后,我们便可总结出缓冲型线程池和固定型线程池的区别。
newCachedThreadPool()
缓存型线程池,先查看池中有没有以前建立的线程,如果有,就进行重用,如果没有,就建一个新的线程加入池中。
1 | public static ExecutorService newCachedThreadPool() { |
它将 corePoolSize 设定为 0,而将 maximumPoolSize 设定为了 Integer 的最大值,线程空闲超过60秒,将会从线程池中移除。由于核心线程数为 0,因此每次添加任务,都会先从线程池中找空闲线程,如果没有就会创建一个线程来执行新的任务,并将该线程加入到线程池中,而最大允许的线程数为 Integer 的最大值 ,因此这个线程池理论上可以不断扩大。
CachedThreadPool 通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的 daemon 型服务中用得不多。但对于生存期短的异步任务,它是Executor的首选。
newFixedThreadPool(int nThreads)
FixedThreadPool 与 CacheThreadPool 差不多,也可以进行复用,但不能随时建新的线程。任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
它将 corePoolSize 和 maximumPoolSize 都设定为了 nThreads,这样便实现了线程池的大小的固定,不会动态地扩大。另外,keepAliveTime 设定为了 0,意味着线程只要空闲下来,就会被移除线程池。
和CacheThreadPool不同,FixedThreadPool 没有IDLE机制(闲时机制),所以 FixedThreadPool 主要用于一些很稳定很固定的并发线程,多用于服务器。
四、Executor or Thread
如果项目使用的JDK版本为1.5及以上,推荐使用 Executor 框架来进行多线程应用开发。当然,如果只需开启线程去执行一些简单的任务,使用 Thread 也未曾不可。