链接:http://www.importnew.com/6255.html
最近的几篇博客里我讨论了长轮询(long polling)和Spring的DeferredResult技术,并且利用这些概念将生产者消费者项目塞进了一个Web应用程序。 尽管博客中的示例代码展示了相关概念,却也包含了很多逻辑问题。除了在实际的应用程序中不会使用简单的LinkedBlockingQueue而是选择JMS或者其他强健的消息队列服务,并且只会有一个用户可以获得匹配更新。还有一个严重的问题就是在JVM关闭时,行为不良的线程不会被关闭。
你可能会问:为什么这会成为问题……好吧,对程序员来说这真的算不上一个问题,只要随便写点代码就可以解决。但是对使用软件的人而言这却会带来不必 要的麻烦。原因是这样会产生很多行为不良的线程,而执行Tomcat的shutdown.sh命令收效甚微。这时你不得不执行下面命令野蛮的杀掉web服务器:
ps
-ef |
grep
java
先得到进程pid,然后
kill
-9 <<pid>>
……接着需要有一大片web服务器需要重启,这种混乱绝对让人头痛。最后你执行shutdown.sh停止Tomcat。
在我最近的几篇博客里,我编写的那些行为不良的线程在run()方法开头都包含了下面的代码:
@Override
public
void
run() {
while
(
true
) {
try
{
DeferredResult<Message> result = resultQueue.take();
Message message = queue.take();
result.setResult(message);
}
catch
(InterruptedException e) {
throw
new
UpdateException(
"Cannot get latest update. "
+ e.getMessage(), e);
}
}
}
@Override
public
void
run() {
sleep(
5
);
// Sleep等待app重新加载
logger.info(
"The match has now started..."
);
long
now = System.currentTimeMillis();
List<Message> matchUpdates = match.getUpdates();
for
(Message message : matchUpdates) {
delayUntilNextUpdate(now, message.getTime());
logger.info(
"Add message to queue: {}"
, message.getMessageText());
queue.add(message);
}
start =
true
;
// 结束,重启
logger.warn(
"GAME OVER"
);
}
上面第二个示例中线程的行为同样很糟糕。线程会从MatchUpdates列表中取消息并在合适的时候添加到消息队列。唯一的可取之处是他们会抛出异常InterruptedException,如果正确处理线程可以正常停止。然而,没有人能确保这一点。
对上面代码的一个有效地快速修正……只要确保创建所有线程都是后台线程。后台线程的定义是:在程序结束时,即使线程还在运行但不会阻止JVM退出。一个后台线程的例子就是JVM的垃圾回收线程。将线程设置为后台线程只需要调用:
thread.setDaemon(
true
);
……接着执行shutdown.sh,然后砰的一声所有的线程都消失了。然而这种做法有一个问题:如果你的后台线程正在执行重要的任务,刚刚开始执行就被突然结束掉会导致丢失很多重要的数据该怎么办?
你需要确保所有线程都被友好地关闭,在关闭前完成所有正在执行的任务。本文接下来将为这些错误的线程给出一个修复,使用ShutdownHook让他们在关闭前互相协调。根据文档的描述:“一个shutdown hook就是一个初始化但没有启动的线程。 当虚拟机开始执行关闭程序时,它会启动所有已注册的shutdown hook(不按先后顺序)并且并发执行。”读完最后一句话,你可能已经猜到了你需要的就是创建一个负责关闭多有其他线程的线程并通过shutdown hook传递给JVM。只要在你已有线程的run() 方法里用几个小的class做一些手脚。
需要创建ShutdownService和Hook两个类。首先展示的是Hook类,它会将ShutdownService 连接到你的线程,代码如下:
public class Hook {
private static final Logger logger = LoggerFactory.getLogger(Hook.class);
private boolean keepRunning =
true
;
private final Thread thread;
Hook(Thread thread) {
this.thread = thread;
}
/**
* @
return
True 如果后台线程继续运行
*/
public boolean keepRunning() {
return
keepRunning;
}
/**
* 告诉客户端后台线程关闭并等待友好地关闭
*/
public void
shutdown
() {
keepRunning =
false
;
thread.interrupt();
try {
thread.
join
();
} catch (InterruptedException e) {
logger.error(
"Error shutting down thread with hook"
, e);
}
}
}
Hook包含两个实例变量:keepRunning和thread。thread是对Hook负责关闭实例对象的引用,而keepRunning则是告诉线程继续运行。
Hook有两个public方法:keepRunning()和shutdown()。线程调用keepRunning()来确认是否需要继续运行,而shutdown()是由ShutdownService的shutdown hook线程调用以关闭目标线程。这就是两个方法的有趣之处。首先将keepRunning变量置为false,接着调用thread.interrupt()来打断线程强制抛出一个InterruptedException,最后调用thread.join()等待线程实例关闭。
值得注意的是这种方法需要你的线程配合。如果其中某个线程出错,那么所有的工作都会失败。为了避免这种情况最好在thread.join(…)中加入一个超时。
@Service
public
class
ShutdownService {
private
static
final
Logger logger = LoggerFactory.getLogger(ShutdownService.
class
);
private
final
List<Hook> hooks;
public
ShutdownService() {
logger.debug(
"Creating shutdown service"
);
hooks =
new
ArrayList<Hook>();
createShutdownHook();
}
/**
* Protected for testing
*/
@VisibleForTesting
protected
void
createShutdownHook() {
ShutdownDaemonHook shutdownHook =
new
ShutdownDaemonHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
protected
class
ShutdownDaemonHook
extends
Thread {
/**
* 循环并使用hook关闭所有后台线程
*
* @see java.lang.Thread#run()
*/
@Override
public
void
run() {
logger.info(
"Running shutdown sync"
);
for
(Hook hook : hooks) {
hook.shutdown();
}
}
}
/**
* 创建hook class的新实例
*/
public
Hook createHook(Thread thread) {
thread.setDaemon(
true
);
Hook retVal =
new
Hook(thread);
hooks.add(retVal);
return
retVal;
}
@VisibleForTesting
List<Hook> getHooks() {
return
hooks;
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
ShutdownService 有一个public方法:createHook()。createHook()做的第一步是确保所有传递的线程都被设置为后台线程。接下来会创建一个新的Hook实例,在最终存储结果到列表返回给调用者之前作为参数传递给线程。
最后要做的就是将ShutdownService继承到DeferredResultService和MatchReporter。这两个类包含了行为不良的线程。
@Service
(
"DeferredService"
)
public
class
DeferredResultService
implements
Runnable {
private
static
final
Logger logger = LoggerFactory.getLogger(DeferredResultService.
class
);
private
final
BlockingQueue<DeferredResult<Message>> resultQueue =
new
LinkedBlockingQueue<>();
private
Thread thread;
private
volatile
boolean
start =
true
;
@Autowired
private
ShutdownService shutdownService;
private
Hook hook;
@Autowired
@Qualifier
(
"theQueue"
)
private
LinkedBlockingQueue<Message> queue;
@Autowired
@Qualifier
(
"BillSkyes"
)
private
MatchReporter matchReporter;
public
void
subscribe() {
logger.info(
"Starting server"
);
matchReporter.start();
startThread();
}
private
void
startThread() {
if
(start) {
synchronized
(
this
) {
if
(start) {
start =
false
;
thread =
new
Thread(
this
,
"Studio Teletype"
);
hook = shutdownService.createHook(thread);
thread.start();
}
}
}
}
@Override
public
void
run() {
logger.info(
"DeferredResultService - Thread running"
);
while
(hook.keepRunning()) {
try
{
DeferredResult<Message> result = resultQueue.take();
Message message = queue.take();
result.setResult(message);
}
catch
(InterruptedException e) {
System.out.println(
"Interrupted when waiting for latest update. "
+ e.getMessage());
}
}
System.out.println(
"DeferredResultService - Thread ending"
);
}
public
void
getUpdate(DeferredResult<Message> result) {
resultQueue.add(result);
}
}
thread =
new
Thread(
this
,
"Studio Teletype"
);
hook = shutdownService.createHook(thread);
thread.start();
while
(hook.keepRunning()) {
……通知线程什么时候需要结束while循环并关闭。
你可能已经注意到上面的代码里有一些System.out.println()调用。原因并不是对关闭hook线程的执行顺序不确定。需要记住,不仅仅是你编写的类试图关闭其他的子系统也是如此。这就是为这在我原来的代码中,logger.info(…)会给出下面的异常输出:
Exception
in
thread
"Studio Teletype"
java.lang.NoClassDefFoundError: org
/apache/log4j/spi/ThrowableInformation
at org.apache.log4j.spi.LoggingEvent.(LoggingEvent.java:159)
at org.apache.log4j.Category.forcedLog(Category.java:391)
at org.apache.log4j.Category.log(Category.java:856)
at org.slf4j.impl.Log4jLoggerAdapter.info(Log4jLoggerAdapter.java:382)
at com.captaindebug.longpoll.service.DeferredResultService.run(DeferredResultService.java:75)
at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.spi.ThrowableInformation
at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1714)
at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1559)
... 6
more
这里的异常是因为logger在调用时已经被卸载了,因此会给出报错。再一次,文档中是这么描述的:“Shutdown hook是在JVM生命周期中的一个微妙的时间执行,因此需要进行防御性变成。尤其是应该注意线程安全尽可能地避免死锁。Hook代码应该不对任何服务盲目依赖,因为这些服务可能会注册自己的shutdown hook并且此时也在关闭的过程中。例如,试图使用基于线程的服务比如AWT时间分发线程会导致死锁。
MatchReport类也需要进行类似的修改。主要的区别在于run() 方法中的hook.keepRunning()是一个for循环:
public
class
MatchReporter
implements
Runnable {
private
static
final
Logger logger = LoggerFactory.getLogger(MatchReporter.
class
);
private
final
Match match;
private
final
Queue<Message> queue;
private
volatile
boolean
start =
true
;
@Autowired
private
ShutdownService shutdownService;
private
Hook hook;
public
MatchReporter(Match theBigMatch, Queue<Message> queue) {
this
.match = theBigMatch;
this
.queue = queue;
}
/**
* 由Spring加载上下文之后调用。会启动匹配……
*/
public
void
start() {
if
(start) {
synchronized
(
this
) {
if
(start) {
start =
false
;
logger.info(
"Starting the Match Reporter..."
);
String name = match.getName();
Thread thread =
new
Thread(
this
, name);
hook = shutdownService.createHook(thread);
thread.start();
}
}
}
else
{
logger.warn(
"Game already in progress"
);
}
}
/**
* The main run loop
*/
@Override
public
void
run() {
sleep(
5
);
// Sleep等待应用加载
logger.info(
"The match has now started..."
);
long
now = System.currentTimeMillis();
List<Message> matchUpdates = match.getUpdates();
for
(Message message : matchUpdates) {
delayUntilNextUpdate(now, message.getTime());
if
(!hook.keepRunning()) {
break
;
}
logger.info(
"Add message to queue: {}"
, message.getMessageText());
queue.add(message);
}
start =
true
;
// Game over, can restart
logger.warn(
"GAME OVER"
);
}
private
void
sleep(
int
deplay) {
try
{
TimeUnit.SECONDS.sleep(
10
);
}
catch
(InterruptedException e) {
logger.info(
"Sleep interrupted..."
);
}
}
private
void
delayUntilNextUpdate(
long
now,
long
messageTime) {
while
(System.currentTimeMillis() < now + messageTime) {
try
{
Thread.sleep(
100
);
}
catch
(InterruptedException e) {
logger.info(
"MatchReporter Thread interrupted..."
);
}
}
}
}
最终的代码测试是在匹配更新过程到一半时执行Tomcat shutdown.sh命令。当JVM终止时会通过ShutdownDaemonHook类调用shutdown hook,其中的run()方法会对Hook实例列表循环执行通知他们关闭各自的线程。如果你执行tail -f查看服务器的日志文件(我这里是catalina.out,你的Tomcat配置可能与我不同),你会看到服务器友好地关闭记录。
本文附带的代码可以在GitHub上找到:https://github.com/roghughe/captaindebug/tree/master/long-poll。
原文链接: javacodegeeks 翻译: ImportNew.com - 唐尤华
译文链接: http://www.importnew.com/6255.html
相关推荐
tomcat shutdown后,进程还存在linux系统中的解决办法
在Linux环境下部署和管理Java Web应用程序时,Apache Tomcat作为一款广泛使用的应用服务器,其操作过程对于系统管理员和开发人员来说至关重要。本文将详细介绍如何在Linux环境下启动、关闭以及强制终止Tomcat服务。 ...
JAVA虚拟机关闭钩子(Shutdown Hook).docx
我们可以使用 ./shutdown.sh 命令来关闭 Tomcat。 但是,在关闭 Tomcat 之前,我们需要检查 Tomcat 是否已经关闭。我们可以使用 ps 命令来检查 Tomcat 的进程。具体来说,我们可以使用 ps -ef|grep java 命令来检查...
有时,由于各种原因,如应用程序挂起、内存溢出或其他系统问题,普通的`shutdown.sh`命令可能无法正常关闭Tomcat,这时就需要使用强制关闭的脚本来结束Tomcat进程。 首先,让我们了解一下Tomcat的常规关闭流程。在...
标题中的“Tomcat启动和关闭”是指在Apache Tomcat服务器中进行服务的启停操作,这是每个Java Web开发者必备的基础技能。Apache Tomcat是一个开源的Servlet容器,它实现了Java Servlet和JavaServer Pages(JSP)规范...
推荐首选使用Shutdown脚本来优雅地关闭Tomcat,这样可以确保资源得到正确释放,并减少数据丢失的风险。而在紧急情况下或者无法通过正常方式关闭时,可以考虑使用Kill命令强制关闭。无论采用哪种方式,都应确保在操作...
在Java编程中,`Runtime`类的`...在实际开发中,合理使用`shutdown hook`可以提高程序的健壮性和资源管理效率。然而,考虑到其潜在的风险和限制,应当谨慎使用,并确保清理任务能够在程序正常退出时也能顺利完成。
在使用Apache Tomcat服务器时,有时可能会遇到一个棘手的问题:当你尝试通过`shutdown.bat`脚本关闭一个Tomcat实例时,它意外地关闭了同一台机器上的其他Tomcat实例。这个问题主要是由于Tomcat的批处理脚本如何查找...
在Java编程中,后台进程(Background Process)通常指的是在主应用程序执行时,不与用户界面直接交互,而是默默地运行在后台执行特定任务的程序部分。这些任务可能包括数据处理、定时任务、服务监控等。Java提供了...
2. **停止Tomcat**:对应地,使用`bin/shutdown.sh`或`bin/shutdown.bat`可以安全关闭Tomcat。这将发送一个信号让所有正在运行的请求完成,然后优雅地关闭服务器。 3. **监控Tomcat**:`bin/catalina.sh`或`...
标题中的“自动启动Tomcat”指的是在服务器上配置Tomcat服务,使其能够在关闭后自动重新启动,以确保应用程序的连续性和稳定性。C#是.NET框架的一部分,通常用于编写Windows服务或者控制台应用来实现这样的自动化...
以下是一份详细的Tomcat使用教程,旨在帮助你快速上手。 ### 1. 安装Tomcat 首先,你需要从Apache官方网站(http://tomcat.apache.org/)下载最新版本的Tomcat。下载的是一个ZIP文件,解压到你选择的目录,例如`C:...
10. **停止Tomcat**:执行`CATALINA_HOME/bin/shutdown.sh`(Linux/Mac)或`shutdown.bat`(Windows)来关闭Tomcat服务。 以上就是解压Apache Tomcat 9.0.68后使用的基本步骤和注意事项。在实际开发和运维过程中,...
4. 使用`bin/startup.sh`启动Tomcat,`bin/shutdown.sh`关闭Tomcat。 5. 配置防火墙或者SELinux,允许必要的端口(默认为8080)通过。 6. 测试Tomcat是否正常工作,访问`http://your_server_ip:8080`,如果能看到...
- `bin`:包含Tomcat的可执行文件和脚本,如startup.sh/startup.bat用于启动Tomcat,shutdown.sh/shutdown.bat用于关闭Tomcat。 - `conf`:存放所有配置文件,如server.xml、web.xml、context.xml等。 - `lib`:存储...
在Windows上,通常通过bin目录下的startup.bat和shutdown.bat文件来启动和关闭Tomcat;在Linux中,可以使用bin目录下的startup.sh和shutdown.sh脚本。 **安全管理** 无论是Windows还是Linux,安全管理都是运行...
其中,`startup.sh` 和 `shutdown.sh` 是在Unix/Linux系统中启动和关闭Tomcat的脚本,而在Windows系统中对应的则是`startup.bat` 和 `shutdown.bat`。此外,`catalina.sh` 和 `catalina.bat` 文件提供了更多的控制...
它主要负责解析和执行JSP、Servlet等Web应用程序,是开发和部署Java Web应用的首选平台。本压缩包包含Tomcat在Windows和Linux两个操作系统上的不同版本,旨在提供跨平台的灵活性,满足开发者在各种环境下的需求。 ...