关于使用线程池造成tomcat进程无法正常退出的一点思考

本文探讨了在使用多线程生产者消费者模型处理消息队列时,线程退出策略的问题及优化方法。通过分析线程中断机制在与第三方库交互时的挑战,提出使用标志位来替代传统的中断方法,并介绍了如何在生产者和消费者代码中实现这一策略。同时,展示了一个定制的线程池管理器,确保在Tomcat进程中正确停止线程池。

最近在项目中使用了多线程生产者消费者模型来模拟消息队列处理问题,但是发现在要求线程退出时,由于没能处理好退出线程的操作造成了Tomcat进程无法停止的问题。经过一番折腾后想总结一下这方面的经验。

线程中断的方式常用的有两种,一种是使用标记位,在代码执行中通过对标记位的判断来决定是否应该退出线程的执行;另一种是使用Thread类的interrupt方法,来终止线程。

因此,刚开始时,我便采用第二种方式来中断执行的线程。在我的代码中,我使用了LinkedBlockingQueue来做为生产者和消费者任务传递的队列,这个队列的put方法和take方法会响应中断,因此只要当线程的中断方法被调用,这两个方法就会抛出InterruptedException,因此我只要在代码的逻辑中catch到这个异常,然后做一些处理就可以完成线程的退出了。

然而事实没有我想的那么好,按照上述的写法,我在线程池结束时调用Executor的shutdownnow方法,查看这个方法的源代码就知道,它会对所有的任务调用interrupt方法。因为我的代码中有很多地方调用了HBase的API,而HBase里有许多API,它们会创建新的线程执行一些如RPC这样的I/O调用,此时,由于线程的interrupt方法被调用,这些I/O调用会抛出未受检异常如

InterruptedIOException,由于这些异常是在新的线程中抛出的,我的代码就无法catch到,因此它会导致线程异常停止。从而导致tomcat进程无法正常退出。使用jdk下的jstack工具可以看到虚拟机一直在等待线程终止。因此必须要使用其它机制来使线程能正常停止。


发现问题后,我便采用设置标志位的方法来处理线程的退出行为。

在要结束线程时,设置线程的标志位,代码在判断标志位被修改后,便可执行退出。但这样会有个问题,即BlockingQueue中的代码使用的take或put方法可能会一直阻塞着,导致线程无法判断它的标志位。查阅LinkedBlockingQueue的源代码,发现好在有办法解决这个问题,那就是使用poll方法和offer方法,这两个方法可以设置一个timeout标志,当阻塞超时后,它会停止阻塞,从而使我们可以进行其它的一些操作。

下面,就是我使用一个很粗糙的方法来进行这些线程的管理。

在生产者和消费者的代码中都设置了一个私有标志位来判断线程是否应该停止,并设置了私有方法修改标志位。

首先是生产者Runnable的代码,它的大概逻辑是遍历HBase表中的每一行,然后把每行数据放入BlockingQueue中。若offer造成了阻塞,那么它会阻塞直到超时,然后遍历下一行或退出,没被遍历到的行,在下一次遍历中还会被遍历的,因此也不必担心。

package com.cyber_space.Queue;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.channels.ClosedByInterruptException;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.util.Bytes;

import com.cyber_space.HBaseManager.QueueHTable;
import com.cyber_space.Queue.Operation.QueueOperation;
import com.cyber_space.util.Log;

public class Producer implements Runnable {

	private QueueHTable qh;
	private ResultScanner rs;
	private final BlockingQueue<QueueOperation> bq;
	private int sleepTime;
	private volatile boolean isInterrupted = false;

	public Producer(BlockingQueue<QueueOperation> bq, int sleepTime) {
		this.bq = bq;
		this.sleepTime = sleepTime;
	}

	@Override
	public void run() {
		Log.logger.info("生产者线程开始工作");
		while (!isInterrupted) {
			try {
				while (bq.size() != 0) { // 队列不为空时,不向队列放入任务
					TimeUnit.SECONDS.sleep(sleepTime);
					Thread.yield();
				}
				qh = new QueueHTable();
				rs = qh.getAll();
				Iterator<Result> iterator = rs.iterator();
				while (iterator.hasNext() && !isInterrupted) {
					Result r = iterator.next();
					String rowkey = Bytes.toString(r.getRow());
					String tag = qh.getTag(rowkey);
					String userRowKey = qh.getUserRowKey(rowkey);
					String operationInfo = qh.getOperationInfo(rowkey);
					QueueOperation df = new QueueOperation(operationInfo, userRowKey, rowkey, tag);
					Log.logger.info("将操作" + tag + "放入队列");
					bq.offer(df, 2, TimeUnit.SECONDS);
				}
			} catch (InterruptedException | InterruptedIOException | ClosedByInterruptException e) {
				// 收到中断请求后立即退出线程
				Log.logger.info("生产者收到中断请求,退出线程");
				break;
			} catch (Exception e) {
				if (Thread.currentThread().isInterrupted())
					break;
				Log.logException(e);
			} finally {
				if (qh != null)
					try {
						qh.close();
					} catch (IOException e) {
						if (Thread.currentThread().isInterrupted())
							break;
						Log.logException(e);
					}
			}
		}
		Log.logger.info("生产者线程退出:" + Thread.currentThread().getId());
	}

	public void stopWork() {
		this.isInterrupted = true;
	}
}

下面的代码是消费者线程,消费者线程不断从BlockingQueue中读取数据,然后执行相应的操作,它使用的是poll方法,也设置了超时时间,超时后,代码需要进行一些逻辑判断,决定是否退出线程。

package com.cyber_space.Queue;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import com.cyber_space.Exception.FileException;
import com.cyber_space.Queue.Operation.QueueOperation;
import com.cyber_space.util.Log;

public class Consumer implements Runnable {

	BlockingQueue<QueueOperation> bq;
	private volatile boolean isInterrupted = false;

	public Consumer(BlockingQueue<QueueOperation> df) {
		bq = df;
	}

	@Override
	public void run() {
		try {
			Log.logger.info("消费者线程开始工作");
			while (!isInterrupted) { // 队列中有任务时,不能因为中断而直接退出
				QueueOperation df = null;
				try {
					df = bq.poll(2, TimeUnit.SECONDS);
					if (df == null) {
						continue;
					}
					df.doOperation();
				} catch (InterruptedException e) { // 队列中的数据就算没处理也不会丢失,因为队列数据是处理后才删除hbase中的表数据的
					Log.logger.info("收到中断请求,退出线程");
					break;
				} catch (FileException e) {
					Log.logException(e);
					Log.storeSystemLog("删除队列删除文件发生错误", e, Log.LogSign.DELETE_FAILED, "删除队列处理线程", "删除队列处理线程");
					bq.add(df);
				}
			}
			Log.logger.info("消费者线程退出" + Thread.currentThread().getId());
		} catch (IOException e) {
			Log.logException(e);
		}
	}

	public void stopWork() {
		this.isInterrupted = true;
	}
}


下面是我自己稍微修改的一个Executor类,用它来进行特殊的线程停止操作。代码中使用了组合的方式,对Excutors生成的线程池进行了包装。它在调用shutdown时,则先对我们自定义的runnable执行stopWork调用,以停止线程服务,然后再调用shutdown方法。注意,查看线程池的代码就可以知道,对于正在执行的任务(Worker)shutdown方法并不会调用它的interrupt方法,因为它无法获得正在执行任务的锁,即Worker的tryAcquire会返回false,所以并不会对我们的工作造成影响。
package com.cyber_space.Queue;

import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.cyber_space.util.Log;

public class QueueExcutor {

	ExecutorService exec = Executors.newFixedThreadPool(10);
	ArrayList<Runnable> runnables = new ArrayList<>();

	public void execute(Runnable r) {
		if (r instanceof Producer || r instanceof Consumer) {
			runnables.add(r);
		}
		exec.execute(r);
	}

	public void shutdown() {
		for (Runnable r : runnables) {
			if (r instanceof Consumer) {
				Consumer c = (Consumer) r;
				c.stopWork();
			} else if (r instanceof Producer) {
				Producer p = (Producer) r;
				p.stopWork();
			}
		}

		try {
			TimeUnit.SECONDS.sleep(Queue.SLEEP_TIME);
		} catch (InterruptedException e) {
			Log.logException(e);
		}
		exec.shutdown();
	}

	public void awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
		exec.awaitTermination(timeout, unit);
	}

}

然后针对线程池的启动和停止,只要在Tomcat的Listener框架下写好,就行了。在contextInitialized方法中启动线程池,在contextDestroyed线程池停止线程。

package com.cyber_space.Listener;

import java.util.concurrent.TimeUnit;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

import com.cyber_space.Queue.Consumer;
import com.cyber_space.Queue.Queue;
import com.cyber_space.Queue.QueueExcutor;
import com.cyber_space.Queue.Producer;
import com.cyber_space.util.Log;

@WebListener()
public class QueueListener implements ServletContextListener {

	Queue dq;
	QueueExcutor exec = new QueueExcutor();

	public void contextDestroyed(ServletContextEvent arg0) {
		Log.logger.info("删除队列准备停止");
		if (exec != null) {
			exec.shutdown();
			try {
				exec.awaitTermination(3, TimeUnit.SECONDS);
			} catch (InterruptedException e) {
				Log.logException(e);
			}
		}
	}

	public void contextInitialized(ServletContextEvent arg0) {
		Log.logger.info("删除队列开始工作");
		try {
			dq = new Queue();
			exec.execute(new Producer(dq.getQueue(), Queue.SLEEP_TIME));
			for (int i = 0; i < Queue.CONSUMER_NUM; ++i) {
				exec.execute(new Consumer(dq.getQueue()));
			}
		} catch (Exception e) {
			Log.logException(e);
		}
	}
}

以上的代码便能保证在Tomcat中正常的启动和终止这个线程池。这个代码其实太粗糙,其实可以创建ThreadPoolExecutor实例时定制自己的ThreadFactory,这个ThreadFactory创建我们自定义的线程,重写其中的interrupt方法,在停止操作前,首先调用stopWork方法。还是有时间再做吧,放假了。。




<think> 我们正在讨论Tomcat的本质是否为Java进程。根据用户的问题和之前的引用,我们可以明确回答:是的,Tomcat本质上是一个Java进程。下面我将详细解释这一结论。 ### 1. Tomcat的本质是Java进程 Tomcat服务器启动时,实际上是通过执行Java命令来运行一个包含`main`方法的Java类(通常是`org.apache.catalina.startup.Bootstrap`)。这个过程会创建一个Java虚拟机(JVM)实例,并在该JVM中运行Tomcat的所有组件。因此,从操作系统层面看,Tomcat表现为一个**Java进程**。具体来说: - **启动方式**:通过执行`startup.sh`(Linux)或`startup.bat`(Windows)脚本,这些脚本最终会调用`java`命令来启动JVM。 - **进程特征**:在操作系统中,可以通过`ps`或`jps`命令查看到名为`Bootstrap`的Java进程[^1][^2]。 ### 2. Tomcat作为Java进程的核心特性 #### (1) 基于JVM运行 Tomcat完全依赖JVM执行环境: - **字节码执行**:Tomcat自身及部署的Web应用(如Servlet、JSP编译后的类)均由JVM解释或编译执行。 - **内存管理**:Tomcat使用的堆内存、元空间等均由JVM分配和回收。 #### (2) 生命周期管理 Tomcat进程的生命周期与JVM绑定: - **启动**:执行`Bootstrap.main()`方法初始化容器。 - **关闭**:调用`Catalina.stop()`方法释放资源(若Web应用未正确释放资源,可能导致进程无法退出[^4])。 #### (3) 多线程模型 Tomcat内部采用**多线程**处理并发请求: - 主线程(`main`)负责初始化。 - 工作线程池(如`org.apache.tomcat.util.threads.ThreadPoolExecutor`)处理HTTP请求。 ### 3. Tomcat进程的特殊性 尽管本质是Java进程,但Tomcat具备**Servlet容器**的专属能力: - **类加载隔离**:通过自定义类加载器(如`WebappClassLoader`)实现应用间隔离。 - **Servlet生命周期管理**:动态加载/卸载Servlet类并调用其`init()`、`service()`、`destroy()`方法。 ### 4. 验证Tomcat作为Java进程 #### ▶ 实验验证 1. **启动Tomcat**: ```bash $ ./startup.sh # 启动脚本 ``` 2. **查看进程**: ```bash $ jps -l # 输出示例 1234 org.apache.catalina.startup.Bootstrap ``` #### ▶ 源码佐证 Tomcat的启动入口在`Bootstrap`类: ```java public final class Bootstrap { public static void main(String[] args) { // 初始化Catalina实例并启动 Catalina catalina = new Catalina(); catalina.start(); } } ``` ### 5. 常见问题解析 #### ❓ Tomcat进程为何无法关闭? 当部署的应用未释放资源(如线程池、数据库连接池)时,即使调用`shutdown.sh`,JVM仍可能无法退出[^4]。 #### ❓ JDK版本不兼容问题 Tomcat版本与JDK版本必须匹配,例如: - Tomcat 7 需要 JDK 1.6+ - 若使用JDK 1.5启动Tomcat 7,进程会启动失败(尽管脚本提示成功)[^2]。 ### 总结 > **Tomcat的本质是一个运行在JVM上的Java进程**,它通过`Bootstrap`类的`main`方法启动,并作为Servlet容器管理Web应用的生命周期。其特殊性在于实现了Servlet规范定义的容器行为,包括类加载隔离、请求路由和多线程并发处理。 --- ### 相关问题 1. Tomcat进程与普通Java进程有何本质区别? 2. 如何诊断Tomcat进程无法正常退出的原因? 3. 不同版本的Tomcat对JDK有何依赖关系? 4. Tomcat自定义类加载器如何实现应用隔离? 5. 为什么说Tomcat是一个"Servlet容器"? </think> ### Tomcat 的本质:Java 进程详解 Tomcat **本质是一个 Java 进程**,其运行完全依赖 Java 虚拟机(JVM)。以下是详细说明: --- #### 1. **核心运行原理** - **启动机制**: 通过执行 `startup.sh` 或 `catalina.sh` 脚本启动,脚本内部调用 `java` 命令运行 **`org.apache.catalina.startup.Bootstrap`** 类。 示例命令: ```bash java -Dcatalina.home=/tomcat -cp [类路径] org.apache.catalina.startup.Bootstrap start ``` - **进程表现**: 在操作系统中表现为标准的 Java 进程(如 Linux 的 `ps` 命令可查看到 `java` 进程,Windows 任务管理器显示为 `java.exe`)。 --- #### 2. **作为 Java 进程的特征** | **特征** | **说明** | **关联引用** | |-------------------|-------------------------------------------------------------------------|-------------| | **依赖 JVM** | 必须安装 JDK/JRE 才能运行,进程生命周期由 JVM 管理 | [^2] | | **字节码执行** | Tomcat 核心代码(如 `catalina.jar`)及部署的 Web 应用均以 `.class` 字节码形式在 JVM 中执行 | [^1][^5] | | **内存管理** | 通过 JVM 堆内存(`-Xmx` 参数)存储 Servlet 实例、会话数据等 | [^4] | | **线程模型** | 使用 JVM 线程池处理并发请求(`org.apache.tomcat.util.threads.ThreadPoolExecutor`) | [^1] | --- #### 3. **与普通 Java 进程的区别** 虽然本质是 Java 进程,但 Tomcat 具备**特殊能力**: - **Servlet 容器功能**: 动态管理 Servlet 生命周期(`init()`, `service()`, `destroy()`)并处理 HTTP/Socket 通信[^1][^5]。 - **类加载隔离**: 通过自定义 `WebappClassLoader` 为每个 Web 应用创建独立的类加载空间,实现应用隔离[^3]。 - **资源释放要求**: 关闭时需确保所有 Servlet 释放资源(如数据库连接、线程池),否则进程可能无法退出[^4]。 --- #### 4. **关键验证方式** - **进程检查**: ```bash # Linux 查看进程 ps -ef | grep java # 显示 Tomcat 的 Java 进程 # 检查 JVM 参数 jcmd <PID> VM.flags ``` - **关闭问题诊断**: 若 `shutdown.sh` 无效,需检查 Web 应用是否未释放资源(如线程泄漏),可通过 `jstack <PID>` 分析线程状态[^4]。 --- #### 5. **版本依赖关系** Tomcat 版本与 JDK 版本严格绑定: | Tomcat 版本 | 最低 JDK 要求 | 典型问题 | |------------|--------------|--------------------------| | Tomcat 7 | JDK 1.6+ | 使用 JDK 1.5 时进程假启动[^2] | | Tomcat 10 | JDK 11+ | 低版本 JDK 无法加载类 | > ✅ **结论**:Tomcat 是通过 JVM 运行的**特殊 Java 进程**,核心功能由 Java 字节码实现,并扩展了 Servlet 容器能力。 --- ### 相关问题 1. Tomcat 进程在 Linux 系统中如何监控资源占用(CPU/内存)? 2. 为什么 Tomcat 必须依赖特定版本的 JDK?不同版本兼容性如何解决? 3. Tomcat 自定义类加载机制如何防止不同 Web 应用的类冲突? 4. 如何强制终止无法正常关闭的 Tomcat Java 进程? 5. Tomcat 的 JVM 参数调优有哪些关键指标(如堆内存、GC 策略)?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值