目录
2.2 CompletionService & ExecutorCompletionService 作用
2.4 AbstractExecutorService 作用
2.5 ScheduledExecutorService 作用
一. JAVA线程实现/创建方式
1.1 继承Thread类
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread: " + i);
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
1.2 实现Runnable接口
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable: " + i);
}
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable());
myThread.start();
}
}
1.3 Callable方式
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
public String call() throws Exception {
return "Callable Result";
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread myThread = new Thread(futureTask);
myThread.start();
String result = futureTask.get();
System.out.println("Result: " + result);
}
}
1.4 基于线程池的方式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Task: " + i);
}
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executorService.execute(new MyTask());
}
executorService.shutdown();
}
}
调用线程池的 shutdown
方法是为了优雅地关闭线程池,而不是强制地终止正在执行的任务。shutdown
方法会启动线程池的关闭序列,不再接受新的任务,但会等待已经提交的任务执行完成。这样可以确保已提交的任务得以执行,而不会丢失未完成的工作。
如果你不调用 shutdown
方法,线程池将一直保持运行状态,不会停止。这可能导致一些问题:
资源泄漏: 未关闭的线程池会一直占用系统资源,包括线程和内存。在长时间运行的应用中,这可能导致资源耗尽.
二. 线程池架构
ava里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
2.1 Executor 接口作用
Executor 接口是 Java 线程池框架的顶层接口,定义了一个基本的任务执行方法 execute(Runnable command),用于执行传入的任务。它是线程池的基础,允许开发者将任务提交给线程池执行,实现了任务提交和执行的解耦,提供了更灵活的线程管理方式.
-
Executor 是线程池的顶层接口,定义了一个基本的任务执行方法
execute(Runnable command)
2.2 CompletionService & ExecutorCompletionService 作用
CompletionService 接口提供了异步执行一组任务并获取结果的机制,而 ExecutorCompletionService 类是其主要实现,通过包装现有的 Executor 和 BlockingQueue,实现任务提交和结果获取的解耦。
2.3 ExecutorService 作用
ExecutorService 接口继承了 Executor 接口,扩展了任务提交和管理的功能,提供了更丰富的控制选项。AbstractExecutorService 抽象类是 ExecutorService 的基本实现,为自定义线程池提供了方便的模板。它实现了一些 ExecutorService 的方法,但核心的任务执行逻辑仍留给具体的实现类去完成。
与ExecutorService相关的任务处理下相关的有 Callable 接口、Future 接口、ScheduledFuture 接口以及 Delayed 接口. 他们都属于 Java 中用于提供任务和处理任务执行结果的接口.Callable 接口允许任务返回结果或抛出异常,而 Future 接口则用于获取异步任务的执行结果。
2.4 AbstractExecutorService 作用
AbstractExecutorService 是一个抽象类,实现了 ExecutorService 接口的一部分方法,为自定义线程池提供了一些通用的实现。它位于线程池框架的设计中,为实现自定义的线程池提供了一些基本的模板方法,简化了自定义线程池的操作。这个抽象类提供了一些基本的实现,例如任务提交、任务列表管理、Future 对象的创建等,但对于实际的任务执行逻辑,它仍然依赖于具体的实现类去完成。具体的线程池实现类(例如 ThreadPoolExecutor)需要扩展 AbstractExecutorService 并提供自己的任务执行逻辑。总体来说,AbstractExecutorService 是为了方便用户实现自定义线程池而设计的,它在实现 ExecutorService 接口的基础上提供了一些通用的实现,避免了用户在实现自定义线程池时需要从头开始编写所有方法的麻烦。
2.5 ScheduledExecutorService 作用
ScheduledExecutorService 接口继承自 ExecutorService,扩展了对定时任务的支持,包括按照固定时间间隔或延时执行任务。ScheduledThreadPoolExecutor 类是其主要实现,结合了线程池和定时任务执行的特性。
2.6 ThreadPoolExecutor 作用
ThreadPoolExecutor 是 Java 线程池的核心实现类,通过核心线程池、任务队列和最大线程池等参数实现了对多线程任务的高效管理。当任务提交给 ThreadPoolExecutor 时,首先尝试使用核心线程执行任务,若核心线程已满,将任务放入任务队列。若任务队列也已满,且总线程数未达到最大线程数,则创建新线程执行任务。若总线程数已达到最大值,则根据指定的拒绝策略处理无法执行的任务。
2.7 Executors 作用
最终,Executors 工具类提供了静态工厂方法,简化了线程池的创建过程。虽然 Executors 提供了一些便捷的方法,但在实际应用中建议直接使用 ThreadPoolExecutor 进行更精细的配置,以满足不同场景的需求。整个线程池框架通过这些接口和类协同工作,实现了对多线程任务的高效调度、执行和管理,提升了多线程编程的便捷性和性能。
三. 四大线程池
线程池可以通过Executors提供的四大线程池方式创建线程池, 本质上也是创建ThreadPoolExecutor.
3.1 newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
3.2 newFixedThreadPool
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
3.3 newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
3.4 newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去 !
四. 线程池七大参数详解
4.1 corePoolSize(核心线程数)
- 含义:线程池中一直存活的线程数量,即使它们处于空闲状态也不会被回收。
- 影响:当有新任务提交时,核心线程会优先处理,而不是放入任务队列。核心线程数一旦达到上限后,新任务将被放入任务队列等待执行。
4.2 maximumPoolSize(最大线程数)
- 含义:线程池中最大允许的线程数,包括核心线程和非核心线程。
- 影响:当任务队列已满,并且活动线程数小于最大线程数时,会创建新的非核心线程来处理任务。
4.3 keepAliveTime(线程空闲时间)
- 含义:非核心线程空闲超过该时间会被回收,直到线程数降到核心线程数的程度。
- 影响:控制非核心线程的存活时间,避免过多的空闲线程占用资源。
4.4 unit(线程空闲时间单位)
- 含义:用于定义 keepAliveTime 的时间单位,例如 TimeUnit.SECONDS。
- 影响:指定了 keepAliveTime 参数的时间单位,与 keepAliveTime 一同决定了非核心线程的回收策略。
4.5 workQueue(任务队列)
- 含义:用于保存等待执行的任务的阻塞队列,可以选择不同的队列实现。
- 影响:决定了等待执行的任务如何排队等待执行,例如 LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue 等。
4.6 threadFactory(线程工厂)
- 含义:用于创建新线程的工厂,可以定制线程的创建过程。
- 影响:通过自定义线程工厂,可以设置线程的名称、优先级、守护状态等属性。
4.7 handler(拒绝策略)
- 含义:当任务队列和线程池都满了,采取的策略来处理新提交的任务。
- 影响:可选的策略包括 AbortPolicy(默认,抛出异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(默默丢弃无法处理的任务)、DiscardOldestPolicy(丢弃最旧的任务)。
五. 线程生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换.
5.1 新建状态(new)
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
5.2 就绪状态(RUNNABLE)
- 线程进入就绪状态表示它已经准备好执行,只是等待获取 CPU 时间片
- 当线程调用 start() 方法后,线程从新建状态转变为就绪状态
5.3 运行状态(RUNNING)
-
线程获得了 CPU 时间片,执行自己的任务代码。
-
线程可以通过调用
yield()
方法主动放弃 CPU 时间片,重新回到就绪状态
5.4 阻塞状态(BLCOKED)
阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列)
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5.5 线程死亡(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
run()或call()方法执行完成,线程正常结束。
异常结束
线程抛出一个未捕获的Exception或Error。
调用stop
直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
为什么不建议用stop?
-
死锁风险: 如果线程在执行过程中持有锁,调用
stop()
可能导致线程被终止,但锁未被释放,从而可能引发死锁。 -
数据不一致: 线程在终止的瞬间可能正处于修改共享数据的状态,这样可能导致数据不一致的问题。
-
资源未释放: 线程可能在被终止之前分配了一些资源,例如打开的文件、网络连接等,直接终止线程可能导致这些资源未被正确释放。
不使用sop的优化方案
替代 stop()
方法的做法是使用协作机制,例如设置一个标志,让线程在合适的时机自行终止。例如,可以使用 volatile
关键字修饰一个标志位,线程在执行过程中定期检查这个标志位,当需要线程结束的时候, 可以调用stopThread方法. 修改标志位为 true
,线程就自行终止。
public class MyRunnable implements Runnable {
private volatile boolean shouldStop = false;
public void run() {
while (!shouldStop) {
// 执行线程的任务逻辑
// 定期检查标志位
if (shouldStop) {
break;
}
}
}
public void stopThread() {
shouldStop = true;
}
}
六. 线程的终止
接着上面的话题, 不建议使用线程stop的方式去终止线程, 可以使用标志位的方式. 那么还有哪些方式会让线程终止呢.
6.1 正常运行结束
程序运行结束,线程自动结束。
6.2 Interrupt方法结束线程
使用interrupt()方法来中断线程有两种情况:
1. 线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()) { // 非阻塞过程中通过判断中断标志来退出
try {
Thread.sleep(5 * 1000); // 阻塞过程捕获中断异常来退出
} catch (InterruptedException e) {
e.printStackTrace();
break; // 捕获到异常之后,执行 break 跳出循环
}
}
}
}
6.3 stop方法终止线程(线程不安全)
前面将线程状态终止的时候, 已经提到.
程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。
七. 线程常用的几个方法之间的区别
7.1 sleep 与 wait 之间的区别
- 1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
- 2. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
- 3. 在调用sleep()方法的过程中,线程不会释放对象锁。
- 4. 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
7.2 start 与run之间的区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
- 方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
八. java后台线程
8.1 定义
守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
8.2 优先级
优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
8.3 如何设置守护线程
- 设置:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(new DaemonTask());
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
// 主线程休眠一段时间
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread exiting.");
}
static class DaemonTask implements Runnable {
public void run() {
.......
}
}
}
- 在Daemon线程中产生的新线程也是Daemon的, 下述代码是在Daemon线程中继续创建线程.
public class DaemonThreadExample { public static void main(String[] args) { Thread daemonThread = new Thread(new DaemonTask()); daemonThread.setDaemon(true); // 设置为守护线程 daemonThread.start(); // 主线程休眠一段时间 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main thread exiting."); } static class DaemonTask implements Runnable { public void run() { // 在守护线程中创建新线程 Thread childThread = new Thread(new ChildTask()); childThread.start(); while (true) { System.out.println("Daemon thread is running."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break; } } } } static class ChildTask implements Runnable { public void run() { System.out.println("Child thread is running."); } } }
- 线程则是JVM级别的,以Tomcat 为例,如果你在Web 应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的。
8.4 守护线程举例
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
8.5 守护线程生命周期
守护线程(Daemon Thread)是一种特殊的线程,与普通线程(非守护线程)的生命周期有一些关键区别。以下是对守护线程生命周期的解释:
-
后台执行: 守护线程是运行在后台的一种特殊线程。它并不阻止程序的执行,不影响程序的主流程。当所有的非守护线程执行完毕后,守护线程会自动退出,而不管它是否执行完任务。
-
独立于控制终端: 守护线程不依赖于控制终端(用户界面),它可以在后台默默地执行任务,而不需要用户的干预。这使得守护线程适合执行一些周期性的、后台的任务。
-
周期性任务或等待事件: 守护线程通常用于执行一些周期性的任务,或者等待某些事件的发生。例如,定时清理操作、监控系统状态等。
-
依赖于系统而不是用户: 守护线程的生命周期与系统关联,而不是用户。当JVM中所有的线程都是守护线程时,JVM 就会退出。这是因为守护线程的存在不足以保证程序的正常执行,而只是用于辅助主程序的执行。
总的来说,守护线程适用于那些不需要用户干预的、在后台默默运行的任务。当所有的非守护线程执行完毕后,JVM 会认为程序的任务已经完成,守护线程也会随之退出。