线程的基本状态
1 | public enum State { |
线程并不是始终固定于某一个状态而是随着代码的执行在不同状态之间进行切换的。
线程的创建方式
继承Thread类
继承 Thread 类,并重写 run ⽅法;
1 | public class TestThread { |
我们在程序里面调用了start()方法后,虚拟机会先为我们创建⼀个线程,然后等到这个线程第⼀次得到时间片时再调⽤run()方法。 注意不可多次调用start()方法。在第⼀次调start()方法后,再次调用start() 方法会抛出异常
实现Runnable接口
接着我们来看⼀下 Runnable 接⼝(JDK 1.8 +):
1 |
|
一个经典的函数式接口,这意味着我们可以使用Java 8 的函数式编程来简化代码。
1 | public class ImplRunnable { |
new Thread(Runnable target)
本质上是使用静态代理的方式实现的。
Thread类构造方法
1 |
|
Thread类的几个常用的方法
- currentThread():静态方法,返回对当前正在执行的线程对象的引用;
- start():开始执行线程的方法,java虚拟机会调⽤线程内的run()方法;
- yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调⽤了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
- sleep():静态方法,使当前线程睡眠⼀段时间;
- join():使当前线程等待另⼀个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
Thread类与Runnable接口的比较
实现⼀个自定义的线程类,可以有继承 Thread 类或者实现 Runnable 接⼝这两种方式,它们之间有什么优劣呢?
- 由于Java“单继承,多实现”的特性,Runnable接口使⽤起来比Thread更灵活。
- Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
- Runnable接口出现,降低了线程对象和线程任务的耦合性。
- 如果使用线程时不需要使⽤Thread类的诸多⽅法,显然使用Runnable接口更为轻量。
所以,我们通常优先使用“实现 Runnable 接口”这种方式来自定义线程类
Callable接口、Future接口
使用以上两种方式创建的线程是无返回值的。而有时候我们希望开启线程去执行某项任务时能动态的返回执行完毕后的结果。
JDK提供了 Callable 接口与 Future 类为我们解决这个问题,这也是所谓的“异步”模型。
Callable接口
1 |
|
Callable
接口一般是配合线程池工具ExecutorService
来使用的。通过submit()
方法让一个Callable
接口执行。它会返回一个Future
。通过Future
的get
方法可以得到执行结果。
1 | public class ImplCallable implements Callable<String> { |
注意:阿里巴巴开发手册:线程池不允许使用 Executors 去创建后文会提到。
Future接口
1 | public interface Future<V> { |
一般Future是和线程池搭配使用
1 |
|
运行结果:
poolSize=12
pool-1-thread-8 seed=100
pool-1-thread-4 seed=100
pool-1-thread-2 seed=100
pool-1-thread-3 seed=100
pool-1-thread-7 seed=100
pool-1-thread-5 seed=100
pool-1-thread-1 seed=100
pool-1-thread-6 seed=100
pool-1-thread-9 seed=100
pool-1-thread-10 seed=100
pool-1-thread-11 seed=100
pool-1-thread-12 seed=100
pool-1-thread-9 seed=101
pool-1-thread-1 seed=101
pool-1-thread-3 seed=101
pool-1-thread-10 seed=101
pool-1-thread-12 seed=101
pool-1-thread-11 seed=101
pool-1-thread-8 seed=101
pool-1-thread-4 seed=101
pool-1-thread-2 seed=101
pool-1-thread-7 seed=101
pool-1-thread-5 seed=101
pool-1-thread-6 seed=101
result=2412
基于线程池的方式
1 | 池化技术相信大家已经屡见不鲜了,线程池、数据库连接池、 Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 |
使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 。
ThreadPoolExecutor
类提供了4个构造方法:
1 | public ThreadPoolExecutor(int corePoolSize, |
ThreadPoolExecutor
构造函数重要参数分析
ThreadPoolExecutor
3个最重要的参数:
- corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。
核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会⼀直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
该值等于核心线程数量 + 非核心线程数量。
- workQueue :阻塞队列, 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他参数:
keepAliveTime
: 非核心线程闲置超时时长。当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁;unit
: 上面参数的时间单位。unit是⼀个枚举类型 ,包括以下属性:
- NANOSECONDS : 1微毫秒 = 1微秒 / 1000
- MICROSECONDS : 1微秒 = 1毫秒 / 1000
- MILLISECONDS : 1毫秒 = 1秒 /1000
- SECONDS : 秒
- MINUTES : 分
- HOURS : 小时
- DAYS : 天
threadFactory
: 创建线程的⼯⼚ ,⽤于批量创建线程,统⼀在创建线程时设置⼀些参数,如是
否守护线程、线程的优先级等。如果不指定,会新建⼀个默认的线程工厂
handler
:饱和策略。
饱和策略:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,
ThreadPoolTaskExecutor
定义一些策略:
RejectedExecutionHandler
具体的实现类有4种:
可以参考这4个实体类的源码,饱和后的操作都是实现
rejectedExecution(Runnable r,ThreadPoolExecutor executor)
方法 ,它们都是ThreadPoolExecutor
的静态内部类:
- ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任
务的处理 。- ThreadPoolExecutor.CallerRunsPolicy :简单点说就是后面排队的线程就在那儿等着。调用执行当前传入的线程进行运行。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果应用程序可以承受此延迟并且不能丢弃任何⼀个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
线程池主要的任务处理流程 (重要)
当线程池接收到一个任务时,主要是通过源码中的execute()
方法去执行的;
1 | public void execute(Runnable command) { |
主要分为3步:
1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
实例演示:
1 | public class ThreadPoolExecutorDemo { |
1. 当maxTask小于缓存队列的长度时(如:maxTask = 10),线程池不需要创建非核心线程来处理任务
运行结果:
pool-1-thread-2 Start.Time = 09:25:19
pool-1-thread-3 Start.Time = 09:25:19
pool-1-thread-5 Start.Time = 09:25:19
pool-1-thread-4 Start.Time = 09:25:19
pool-1-thread-1 Start.Time = 09:25:19
pool-1-thread-1 End.Time = 09:25:21
pool-1-thread-2 End.Time = 09:25:21
pool-1-thread-4 End.Time = 09:25:21
pool-1-thread-3 End.Time = 09:25:21
pool-1-thread-5 End.Time = 09:25:21
pool-1-thread-4 Start.Time = 09:25:21
pool-1-thread-3 Start.Time = 09:25:21
pool-1-thread-2 Start.Time = 09:25:21
pool-1-thread-1 Start.Time = 09:25:21
pool-1-thread-5 Start.Time = 09:25:21
pool-1-thread-3 End.Time = 09:25:23
pool-1-thread-5 End.Time = 09:25:23
pool-1-thread-1 End.Time = 09:25:23
pool-1-thread-2 End.Time = 09:25:23
pool-1-thread-4 End.Time = 09:25:23
Process finished with exit code 0
2. 当maxTask大于缓存队列的长度时(如:maxTask = 210),线程池不需要创建非核心线程来处理任务
运行结果为:
pool-1-thread-10 Start.Time = 09:26:56
pool-1-thread-7 Start.Time = 09:26:56
pool-1-thread-1 Start.Time = 09:26:56
pool-1-thread-9 Start.Time = 09:26:56
pool-1-thread-8 Start.Time = 09:26:56
pool-1-thread-2 Start.Time = 09:26:56
pool-1-thread-4 Start.Time = 09:26:56
pool-1-thread-6 Start.Time = 09:26:56
main Start.Time = 09:26:56
pool-1-thread-3 Start.Time = 09:26:56
pool-1-thread-5 Start.Time = 09:26:56
pool-1-thread-5 End.Time = 09:26:58
pool-1-thread-3 End.Time = 09:26:58
pool-1-thread-1 End.Time = 09:26:58
pool-1-thread-6 End.Time = 09:26:58
pool-1-thread-9 End.Time = 09:26:58
......