线程的生命周期?怎么终止线程?线程和线程池有什么区别?如何创建线程池?说一下 ThreadPoolExecutor 的参数含义?
说一下线程的生命周期?
线程的生命周期指的是线程从创建到销毁的整个过程,通常情况下线程的生命周期有以下 5 种:
- 初始状态
- 可运行状态
- 运行状态
- 休眠状态
- 终止状态
怎么终止线程?
在 Java 中终止线程的实现方法有以下 2 种:
- 使用 interrupt 中断线程方法,此方法是发送一个中断信号给线程,它可以及时响应中断,也是最推荐使用的方法;
- 最后是 stop 方法,虽然它也可以停止线程,但此方法已经是过时的不建议使用的方法,在 Java 最新版本中已经被直接移除了,所以不建议使用。
线程和线程池有什么区别?
线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。
相比于线程来说,线程池具备以下优点:
- 可重复使用已有线程,避免对象创建、消亡和过度切换的性能开销。
- 避免创建大量同类线程所导致的资源过度竞争和内存溢出的问题。
- 支持更多功能,比如延迟任务线程池(newScheduledThreadPool)和缓存线程池(newCachedThreadPool)等。
如何创建线程池?
线程池的创建可以分为以下两类:
- 通过 ThreadPoolExecutor 手动创建线程池。
- 通过 Executors 执行器自动创建线程池。
而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)。
- ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
推荐使用哪种方式来创建线程池?
推荐使用 ThreadPoolExecutor 的方式来创建线程池,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 创建线程池的弊端如下:
- FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
- CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
说一下 ThreadPoolExecutor 的参数含义?
ThreadPoolExecutor 最多支持 7 个参数的设置,如下代码所示:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||// maximumPoolSize 必须大于 0,且必须大于 corePoolSizemaximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}
这 7 个参数的含义如下:
- 第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程(创建和销毁的原因会在本课时的下半部分讲到);如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值;
- 第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到;
- 第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程;
- 第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的;
- 第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行;
- 第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程;
- 第 7 个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
说一下线程池的执行流程?
线程池的执行流程是:先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行,否则则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务,否则将执行线程池的拒绝策略。
线程池的拒绝策略都有哪些
当任务过多且线程池的任务队列已满时,此时就会执行线程池的拒绝策略,线程池的拒绝策略默认有以下 4 种:
- AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
- CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
- DiscardPolicy:忽略此任务,忽略最新的一个任务;
- DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
默认的拒绝策略为 AbortPolicy 中止策略。
如何实现自定义拒绝策略?
除了 JDK 提供的四种拒绝策略之外,我们还可以实现通过 new RejectedExecutionHandler,并重写 rejectedExecution 方法来实现自定义拒绝策略,实现代码如下:
new RejectedExecutionHandler(){@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {System.out.println("自定义拒绝策略");}
}
线程池中 shutdownNow() 和 shutdown() 有什么区别?
shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。
多线程存在什么问题?
多线程的优点可以同时执行多个任务,而缺点是多线程存在线程安全问题,也就是线程的执行不符合预期结果的问题。
为什么会有线程安全问题?
导致线程安全问题的因素有以下 5 个:
- 多线程抢占式执行;
- 多线程同时修改同一个变量;
- 非原子性操作:原子性操作是指操作不能再被分割就叫原子性操作,而导致线程安全性问题的一大因素就是非原子性操作;
- 内存可见性:多个线程同时操作统一变量,但因为某些原因导致变量已经被一个线程修改,但另一个线程不可见,从而导致了线程安全性问题;
- 指令重排序:指令重排序是指 Java 程序为了提高程序的执行速度,所以会对一下操作进行合并和优化的操作,比如某些操作本来的顺序是 A -> B -> C,但被指令重排序之后就变成了 A -> C -> B,但这样重排之后就会导致程序的执行结果和预期的结果不相符的问题。
如何解决线程安全问题?
在 Java 中,解决线程安全问题有以下 3 种手段:
-
使用线程安全类,比如 AtomicInteger;
-
加锁排队执行
使用 synchronized 加锁;
使用 ReentrantLock 加锁;
-
使用线程本地变量 ThreadLocal。
线程安全类通常是使用锁机制(乐观锁或悲观锁)来保证程序的正常执行的。