线程的生命周期
1、线程的5个生命周期
- 新建: 刚使用new方法创建出来的线程;
- 就绪: 调用线程的start()方法后,线程处于等待CPU分配资源阶段,当线程获取到CPU资源后开始执行;
- 运行: 当就绪的线程被调度并获得CPU资源时,便会进入运行状态,run()方法定义了线程的操作和功能;
- 阻塞: 在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,如sleep()、wait()之后,线程就会处于阻塞状态。这个时候需要其他机制将处于阻塞状态的线程唤醒,如notify()、notifyAll()方法。被唤醒的线程不会立即执行run方法,会回到就绪阶段,再次等待CPU分配资源进入运行状态。
- 销毁: 如果 线程正常执行完毕 或 被提前强制终止 或 出现异常导致结束,那么线程就会被销毁并释放资源。
2、线程的6种状态
- 初始状态(NEW): 线程被创建出来但没有被调用
start()
。 - 运行状态(RUNNABLE): 线程被调用了
start()
等待运行的状态。 - 阻塞状态(BLOCKED): 需要等待锁释放。
- 等待状态(WAITING): 表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待状态(TIME_WAITING): 可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- 终止状态(TERMINATED): 表示该线程已经运行完毕。
3、线程的创建
1、创建线程
创建线程三种方式:
- 继承Thread
- 实现Runnable
- 实现Callable
1、继承Thread
public class CreateThreadDemo {
public static void main(String[] args){
new ThreadDemo().start();
}
public static class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("ThreadDemo:" + Thread.currentThread());
}
}
}
缺点:Java单继承
2、实现Runnable
public class CreateThreadDemo {
public static void main(String[] args){
new Thread(new RunnableDemo()).start();
}
//不用担心单继承,没有返回值
public static class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("RunnableDemo:" + Thread.currentThread());
}
}
}
特点:没有返回值,类似stream的foreach
3、实现Callable
public class CreateThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new CallableDemo());
new Thread(futureTask).start();
//get()方法获取返回值
System.out.println(futureTask.get());
}
//有返回值也可以抛出异常
public static class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("CallableDemo:" + Thread.currentThread());
return "return CallableDemo:" + Thread.currentThread();
}
}
}
特点:有返回值,类似stream的map,在Callable< T >决定返回类型
get()方法会阻塞线程
区别:
- Thread只能单继承,Runnable和Callable可以多实现
- Thread和Runnable不能得到返回值,Callable可以获取返回值及捕获异常
2、创建线程池
1、使用线程池的好处
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2、通过构造方法实现
/*
* corePoolSize 核心线程:核心线程数定义了最小可以同时运行的线程数量
* maximumPoolSize 最大线程:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数;
* keepAliveTime 空闲线程存活时间:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁
* unit 空闲线程存活时间单位:`keepAliveTime` 参数的时间单位
* workQueue 阻塞队列:类型linked,容量10000
* threadFactory 线程工程:默认
* handler 饱和策略:抛出 RejectedExecutionException来拒绝新任务的处理
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
/*
* 核心线程:5
* 最大线程:200
* 空闲线程存活时间:10
* 空闲线程存活时间单位:秒
* 阻塞队列:类型linked,容量10000
* 线程工程:默认
* 饱和策略:抛出 RejectedExecutionException来拒绝新任务的处理
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
3.ThreadPoolExecutor7个参数
- 核心线程数
corePoolSize
: 核心线程数定义了最小可以同时运行的线程数量。 - 最大线程数
maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 - 阻塞队列
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - 存活时间
keepAliveTime
: 当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; - 存活时间单位
unit
:keepAliveTime
参数的时间单位。 - 线程工厂
threadFactory
: executor 创建新线程的时候会用到。 - 饱和策略
handler
: 如果当前同时运行的线程数量达到最大线程数量并且阻塞队列也已经被放满了任务时,会根据饱和策略来处理多余的任务。
常见饱和策略:
-
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
异常来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
4.线程池运行流程
- 线程池创建,准备好 core 数量的核心线程,准备接受任务
- 新的任务进来,用 core 准备好的空闲线程执行。
- 核心线程 core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
- 阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
- 如果线程数开到了 max 的数量,还有新任务进来,就会使用 RejectedExecutionHandler 指定的拒绝策略拒绝任务
- max 都执行完成,有很多空闲。在指定 keepAliveTime 后,会释放 Max-core 数量空闲的线程。最终保持到 core 大小。new LinkedBlockingQueue<>()默认是integer的最大值,内存不够
- 所有的线程创建都是由指定的 factory 创建的
总结:核心线程 -> 阻塞队列 -> 新线程 -> 拒绝策略 -> 自动释放空闲核心线程
3、通过 Executor 框架的工具类 Executors 来实现
四种常见线程池:
- CachedThreadPool
- FixedThreadPool
- ScheduledThreadPool
- SingleThreadExecutor
1.CachedThreadPool
/**
* 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,
* 若无可回收,则新建线程。
* 没有核心线程,所有线程都可以回收。
*/
Executors.newCachedThreadPool();
2.FixedThreadPool
/**
* 创建一个定长线程池,可控制线程最大并发数,
* 超出的线程会在队列中等待。
* 核心线程数和最大线程数相同,固定线程数大小,所有线程都不可以回收。
*/
Executors.newFixedThreadPool(10);
3.ScheduledThreadPool
/**
* 创建一个定长线程池,支持定时及周期性任务执行。
* 可以指定多长时间以后执行任务。定时任务线程池。
*/
Executors.newScheduledThreadPool(10);
4.SingleThreadExecutor
/**
* 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
* 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
* 单线程的线程池,核心和最大线程数都为 1。
* 后台从队列中取一个执行一个,相当于后台用单线程执行任务。
*/
Executors.newSingleThreadExecutor();
4、submit和execute的区别
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;-
submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
4、线程的3种阻塞
- 等待阻塞: 正在运行中的线程执行wait()方法时,JVM会把该线程放入等待队列中。
- 同步阻塞: 运行的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞: 运行的线程执行sleep()、join()方法,或者发出了IO请求时,JVM会把该线程置为阻塞状态。
5、线程的3种结束
- 正常结束: run()或call()方法执行完成,线程正常结束;
- 异常结束: 线程执行过程中抛出一个未捕获的异常导致结束;
- 强制结束: 调用线程终止方法强制结束线程:
参考:
- 使用退出标志
定义一个volatile修饰的boolean型的标志位 ,在线程的run方法中根据这个标志位是为true还是为 false 来判断是否终止,这种情况多用于while循环中。
(使用volatile目的是保证可见性,一处修改了标志,处处都要去主存读取新的值,而不是使用缓存) - Interrupt 方法
使用 interrupt 方法中断线程有两种情况
- 线程处于阻塞状态
如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的interrupt()
方法时,会抛出InterruptException
异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。
try {
threadDemo.start();
//阻塞线程
threadDemo.wait();
//中断线程
threadDemo.interrupt();
}catch (InterruptedException e){
//抛出异常,强制跳出,线程中断
e.printStackTrace();
}
- 线程未处于阻塞状态
使用isInterrupted()
判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
//中断线程
threadDemo.interrupt();
//通过while循环不断确认线程是否已经终止
while (threadDemo.isInterrupted()) {
}
System.out.println("线程中断");
- stop 方法
调用stop()方法,该方法不安全,容易导致死锁
- 调用stop方法会立刻终止run()方法中剩余的全部任务,包括catch或finally中的任务,并且抛出ThreadDeath异常,因此可能会导致任务执行失败。
- 调用stop方法会立刻释放改线程所持有的所有锁,导致数据无法完成同步,出现数据不一致的问题。
衍生应用: Java多线程异步任务