更加强健的线程模型,解决线程卡死,退出异常情况
Posted 独立游戏开发者号
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了更加强健的线程模型,解决线程卡死,退出异常情况相关的知识,希望对你有一定的参考价值。
package net.sz;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.log4j.Logger;
/**
*
* <br>
* author 失足程序员<br>
* mail 492794628@qq.com<br>
* phone 13882122019<br>
*/
public class ThreadTest implements Runnable {
private static final Logger log = Logger.getLogger(ThreadTest.class);
public ConcurrentLinkedQueue<Runnable> runs = new ConcurrentLinkedQueue<>();
Runnable run;
long lastTimer;
@Override
public void run() {
while (true) {
run = null;
lastTimer = 0;
try {
/*如果队列为空强制线程等待*/
while (runs.isEmpty()) {
synchronized (runs) {
/*直到收到通知消息*/
runs.wait();
}
}
/*取出任务*/
run = runs.poll();
lastTimer = System.currentTimeMillis();
if (run != null) {
/*执行任务*/
run.run();
}
} catch (Exception e) {
/*捕获异常*/
log.error("", e);
}
}
}
}
我相信这段代码,70%左右的java开发人员,在线程队列执行的线程,线程模型都是这样设计的
我也是,而且这样的代码持续了很多年;
线程执行 Runnable 接口实现,内部封装了任务列表,
线程执行的时候取出队列里面第一个,执行,
之所以加上开始执行时间就是为了检查当前线程对象执行任务的时候卡死了,比如一直等待;
并且在死循环里面添加了 try catch 捕获异常现象;
看上去连线程退出的机会都没有了是不是呢?
感觉没啥问题啊, 接下来我们看看
线程模型使用
public class TestMain {
private static final Logger log = Logger.getLogger(TestMain.class);
static ThreadTest threadTest = new ThreadTest();
public static void main(String[] args) throws InterruptedException {
/*创建线程任务队列*/
Thread thread = new Thread(threadTest);
/*启动线程*/
thread.start();
long i = 0;
Random random = new Random();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
/*相当于没 2秒 有一个任务需要处理*/
Thread.sleep(2000);
} catch (Exception e) {
}
/*创建任务*/
threadTest.runs.add(new Runnable() {
@Override
public void run() {
int nextInt = random.nextInt(10000);
if (nextInt < 2000) {
try {
/*相当于有20%的概率暂停 5秒 模拟任务的执行耗时*/
Thread.sleep(5000);
} catch (Exception e) {
}
} else if (nextInt < 5000) {
try {
/*相当于有50%的概率暂停 2秒 模拟任务的执行耗时*/
Thread.sleep(2000);
} catch (Exception e) {
}
}
log.error(System.currentTimeMillis());
}
});
/*通知线程有新任务了*/
synchronized (threadTest.runs) {
threadTest.runs.notify();
}
}
}
});
thread1.start();
while (true) {
try {
/*相当于没 1秒 检查*/
Thread.sleep(1000);
} catch (Exception e) {
}
long timer = System.currentTimeMillis() - threadTest.lastTimer;
if (threadTest.lastRun != null) {
if (timer > 500) {
log.error("线程可能已卡死:" + timer);
} else {
log.error("线程执行耗时:" + timer);
}
}
}
}
}
以上代码,我们线程在处理队列任务的时候使用的一般情况
[01-20 15:43:10:0544:ERROR: sz.TestMain.run():93 ] -> 1484898190544
[01-20 15:43:10:0544:ERROR: sz.TestMain.run():93 ] -> 1484898190544
[01-20 15:43:10:0557:ERROR: sz.TestMain.main():115] -> 线程执行耗时:13
[01-20 15:43:11:0557:ERROR: sz.TestMain.main():113] -> 线程可能已卡死:1013
[01-20 15:43:12:0544:ERROR: sz.TestMain.run():93 ] -> 1484898192544
[01-20 15:43:12:0558:ERROR: sz.TestMain.main():115] -> 线程执行耗时:14
[01-20 15:43:13:0558:ERROR: sz.TestMain.main():113] -> 线程可能已卡死:1014
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:16:0559:ERROR: sz.TestMain.main():115] -> 线程执行耗时:13
[01-20 15:43:17:0559:ERROR: sz.TestMain.main():113] -> 线程可能已卡死:1013
这些都是我在模拟线程执行流程操作完全没有问题,
可能很多看过我之前代码的童鞋也知道,我的线程模型一直都是这么定义,这么使用的;
并且使用了很多年,我有理由相信很多小伙伴都是这样写的!
如果是请请举个爪。恩,扥,对,我是这样的
好吧接下来我们改造一下抛错异常,处理
线程执行任务的时候总有可能抛错异常对不对;
抛出异常的线程任务
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
/*相当于没 2秒 有一个任务需要处理*/
Thread.sleep(2000);
} catch (Exception e) {
}
/*创建任务*/
threadTest.runs.add(new Runnable() {
@Override
public void run() {
int nextInt = random.nextInt(10000);
if (nextInt < 1000) {
try {
/*相当于有10%的概率暂停 5秒 模拟任务的执行耗时*/
Thread.sleep(5000);
} catch (Exception e) {
}
} else if (nextInt < 3000) {
try {
/*相当于有30%的概率暂停 2秒 模拟任务的执行耗时*/
Thread.sleep(2000);
} catch (Exception e) {
}
} else if (nextInt < 5000) {
throw new UnsupportedOperationException("模拟抛错异常");
}
log.error(System.currentTimeMillis());
}
});
/*通知线程有新任务了*/
synchronized (threadTest.runs) {
threadTest.runs.notify();
}
}
}
});
thread1.start();
修改一下线程添加任务的代码模拟跑出异常
[01-20 15:49:08:0874:ERROR: sz.TestMain.run():95 ] -> 1484898548874
[01-20 15:49:08:0875:ERROR: sz.TestMain.run():46 ] ->
java.lang.UnsupportedOperationException: 模拟抛错异常
at net.sz.TestMain$1$1.run(TestMain.java:93)
at net.sz.ThreadTest.run(TestMain.java:42)
at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:09:0875:ERROR: sz.TestMain.run():46 ] ->
java.lang.UnsupportedOperationException: 模拟抛错异常
at net.sz.TestMain$1$1.run(TestMain.java:93)
at net.sz.ThreadTest.run(TestMain.java:42)
at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:11:0875:ERROR: sz.TestMain.run():46 ] ->
java.lang.UnsupportedOperationException: 模拟抛错异常
at net.sz.TestMain$1$1.run(TestMain.java:93)
at net.sz.ThreadTest.run(TestMain.java:42)
at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:13:0889:ERROR: sz.TestMain.main():117] -> 线程执行耗时:13
[01-20 15:49:14:0890:ERROR: sz.TestMain.main():115] -> 线程可能已卡死:1014
[01-20 15:49:15:0876:ERROR: sz.TestMain.run():95 ] -> 1484898555876
[01-20 15:49:15:0876:ERROR: sz.TestMain.run():95 ] -> 1484898555876
我们可以看出,当线程执行的时候抛出异常
其实这种异常,也不存在我那天,对不对,我们能try catch ,
并且可能童鞋都知道,java里面catch 的代码块如果没有异常出现,是不会有性能消耗的,如果
抛出了异常,会创建 exception 对象,并且收集异常信息,比如调用堆栈,行号,文件名,路劲等等;
所以写代码的时候可以加入try catch 语句,放心一般不会影响你的性能;
来来接着
看到这里我们的线程也没有出问题啊,而且,很安全啊
接下来我先不说情况,给各位模拟一下线程出问题的情况
也是一直困扰我很久的事情,曾经游戏服务器上线后一度出现线程卡死,很长时间,一直怀疑数据库卡着不动!
public class TestMain {
private static final Logger log = Logger.getLogger(TestMain.class);
static ThreadTest threadTest = new ThreadTest();
static Thread thread;
static Thread thread1;
public static void main(String[] args) throws InterruptedException {
/*创建线程任务队列*/
thread = new Thread(threadTest);
/*启动线程*/
thread.start();
long i = 0;
Random random = new Random();
thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
/*相当于没 2秒 有一个任务需要处理*/
Thread.sleep(2000);
} catch (Exception e) {
}
/*创建任务*/
threadTest.runs.add(new Runnable() {
@Override
public void run() {
new TestRun();
log.error(System.currentTimeMillis());
}
});
/*通知线程有新任务了*/
synchronized (threadTest.runs) {
threadTest.runs.notify();
}
}
}
});
thread1.start();
while (true) {
try {
/*相当于没 1秒 检查*/
Thread.sleep(1000);
} catch (Exception e) {
}
long timer = System.currentTimeMillis() - threadTest.lastTimer;
if (threadTest.lastRun != null) {
if (timer > 500) {
showStackTrace();
} else {
log.error("线程执行耗时:" + timer + " " + threadTest.lastRun.getClass().getName());
}
}
}
}
/**
*
* 查看线程堆栈
*/
public static void showStackTrace() {
StringBuilder buf = new StringBuilder();
/*如果现场意外终止*/
long procc = System.currentTimeMillis() - threadTest.lastTimer;
if (procc > 5 * 1000 && procc < 864000000L) {//小于10天//因为多线程操作时间可能不准确
buf.append("线程[")
.append(thread.getName())
.append("]")
.append("可能已卡死 -> ")
.append(procc / 1000f)
.append("s
")
.append("执行任务:")
.append(threadTest.lastRun.getClass().getName());
try {
StackTraceElement[] elements = thread.getStackTrace();
for (int i = 0; i < elements.length; i++) {
buf.append("
")
.append(elements[i].getClassName())
.append(".")
.append(elements[i].getMethodName())
.append("(").append(elements[i].getFileName())
.append(";")
.append(elements[i].getLineNumber()).append(")");
}
} catch (Exception e) {
buf.append(e);
}
buf.append("
++++++++++++++++++++++++++++++++++");
String toString = buf.toString();
log.error(toString);
}
}
}
同样先改造一下任务模拟线程;
这次我们直接new 一个对象;
并且我们增加如果判断线程卡住的话打印线程堆栈情况
[01-20 16:09:39:0787:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 8.008s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:40:0787:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 9.008s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:41:0787:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 10.008s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:42:0790:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 11.011s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:43:0791:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 12.012s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:44:0791:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 13.012s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:09:45:0792:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 14.013s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
一般当玩家反映我们游戏出问题,以后ssh登录服务器查看日志情况的时候满篇基本都是这样的情况;
看上去是线程执行某个任务的时候出现了卡住,当时线上游戏出现的是执行数据库获取玩家数据加载,然后卡住了,
当时一度怀疑,我们mysql数据库不稳定,是不是数据库问题,但是DBA很明确的告诉我们数据库没问题,连接量也很正常;服务很正常;
但是没办法,只能现在关闭服务器;
我们通常使用https的gm命令停止服务器,可是此时发现,使用http是的gm命令停止服务器依然没有任何效果;;
当时已经瓜了,只能痛 kill 命令停止java进程,导致玩家回档,有时间回档几分钟,有时候回档几个小时;
玩家一度下滑;
那个时候非常的煎熬,因为不知道问题到底在哪里,看代码无法反应出来;
于是,去排除最近新添加的所有代码;看上去貌似有一个些不合理的代码(有新手童鞋),改掉;
提交,编译,上传到服务器,启动游戏;能够正常运行;
一直都是这样怀疑服务器逻辑问题;
错误日志
然后公司运维,在查询服务器日志异常打印情况是给我反馈了一部分异常日志;
我这里展示我测试代码的异常日志
Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
at net.sz.TestMain$1$1.run(TestMain.java:87)
at net.sz.ThreadTest.run(TestMain.java:47)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsupportedOperationException
at net.sz.TestRun.<clinit>(TestRun.java:18)
... 3 more
[01-20 16:09:36:0782:ERROR: sz.TestMain.showStackTrace():149] -> 线程[Thread-0]可能已卡死 -> 5.001s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
看上去是有错误打印的,然后紧接着就出现了线程卡死情况;很奇怪明明有try catch 啊
现在我们再来看看,我们的TestRun 这个类的写法
package net.sz;
import org.apache.log4j.Logger;
/**
*
* <br>
* author 失足程序员<br>
* mail 492794628@qq.com<br>
* phone 13882122019<br>
*/
public class TestRun {
private static final Logger log = Logger.getLogger(TestRun.class);
static {
if (true) {
throw new UnsupportedOperationException();
}
}
}
由于我是在模拟测试代码,
所以直接在loader这个class的时候就init错误,抛出exception,然后一直以为这样的exception是可以try catch 的,原谅我这么多年的小白鼠
PS:(当然,当时我游戏服务器出现的情况是hibernate和c3p0获取socket 数据连接导致的;)
直到这两天我在开发过程中发现这样的异常情况,导致线程卡死,我就开始怀疑了,
当时一个偶发情况想到先去查看线程状态;
于是在查看线程堆栈里面加入线程当前状态;
/**
*
* 查看线程堆栈
*/
public static void showStackTrace() {
StringBuilder buf = new StringBuilder();
/*如果现场意外终止*/
long procc = System.currentTimeMillis() - threadTest.lastTimer;
if (procc > 5 * 1000 && procc < 864000000L) {//小于10天//因为多线程操作时间可能不准确
buf.append("线程[")
.append(thread.getName())
.append("]")
.append("]当前状态->")
.append(thread.getState())
.append("可能已卡死 -> ")
.append(procc / 1000f)
.append("s
")
.append("执行任务:")
.append(threadTest.lastRun.getClass().getName());
try {
StackTraceElement[] elements = thread.getStackTrace();
for (int i = 0; i < elements.length; i++) {
buf.append("
")
.append(elements[i].getClassName())
.append(".")
.append(elements[i].getMethodName())
.append("(").append(elements[i].getFileName())
.append(";")
.append(elements[i].getLineNumber()).append(")");
}
} catch (Exception e) {
buf.append(e);
}
buf.append("
++++++++++++++++++++++++++++++++++");
String toString = buf.toString();
log.error(toString);
}
}
Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
at net.sz.TestMain$1$1.run(TestMain.java:87)
at net.sz.ThreadTest.run(TestMain.java:47)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsupportedOperationException
at net.sz.TestRun.<clinit>(TestRun.java:18)
... 3 more
[01-20 16:26:50:0593:ERROR: sz.TestMain.main():110] -> 线程执行耗时:1 net.sz.TestMain$1$1
[01-20 16:26:55:0599:ERROR: sz.TestMain.showStackTrace():151] -> 线程[Thread-0]]当前状态->TERMINATED可能已卡死 -> 5.006s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:26:56:0600:ERROR: sz.TestMain.showStackTrace():151] -> 线程[Thread-0]]当前状态->TERMINATED可能已卡死 -> 6.008s
执行任务:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
好家伙,不看不知道猛一看吓了我一条;
难怪线程卡死啊,线程当前状态是线程已经结束退出执行了;
于是百度了一下,线程,java线程;
看到一篇文章,就是当java代码抛出 jvm异常
Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
at net.sz.TestMain$1$1.run(TestMain.java:87)
at net.sz.ThreadTest.run(TestMain.java:47)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsupportedOperationException
at net.sz.TestRun.<clinit>(TestRun.java:18)
... 3 more
类似这样的异常的时候,是不会抛出来而是直接走
线程的
1 public interface UncaughtExceptionHandler
而是线程的这个接口的代码;
如果你对线程是属于线程组的,会调用线程组的
1 public UncaughtExceptionHandler getUncaughtExceptionHandler() {2 return uncaughtExceptionHandler != null ?3 uncaughtExceptionHandler : group;4 }
1 void uncaughtException(Thread t, Throwable e)
如果线程实现了这个接口,或者是归于线程组,走线程组的这个方法,抛错异常,并且结束了当前线程;
哎,这就是基础知识不扎实的我;导致了这个问题;
其实我们在做底层代码或者框架设计的时候,能避免一些代码和异常规范,但是不能保证没人都能写的很规范,或者基础知识很扎实;
起码我在这个事情之前也不算过关对吧;
线程状态的监听
百度的时候看到一个文章;
可以监听线程状态如果线程改变了就通知你,
大家可以看这个文章,我就不在赘述《Java线程监听,意外退出线程后自动重启》
线程修改结果,同时监听线程
class MyThread extends Thread {
Runnable run;
public MyThread(Runnable run) {
super(run);
this.run = run;
}
}
我们新增一个mythread类;描述当前线程
修改线程启动方式
/*创建线程任务队列*/
thread = new MyThread(threadTest);
uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("收到未能正常捕获的异常", e);
if (t instanceof MyThread) {
/*判断是我们自定义线程模型,创建新的线程*/
thread = new MyThread(((MyThread) t).run);
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
/*启动线程*/
thread.start();
}
}
};
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
/*启动线程*/
thread.start();
并且修改一下模拟添加任务线程
thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
/*相当于没 2秒 有一个任务需要处理*/
Thread.sleep(2000);
} catch (Exception e) {
}
/*任务队列一直不能执行*/
if (threadTest.runs.isEmpty()) {
/*创建任务*/
threadTest.runs.add(new Runnable() {
@Override
public void run() {
/*20%概率增加错误异常*/
if (random.nextInt(10000) < 2000) {
new TestRun();
}
log.error(System.currentTimeMillis());
}
});
/*通知线程有新任务了*/
synchronized (threadTest.runs) {
threadTest.runs.notify();
}
}
}
}
});
--- exec-maven-plugin:1.2.1:exec (default-cli) @ net.sz.game.engine.thread ---
[01-20 17:17:55:0419:ERROR: sz.TestMain.run():118] -> 1484903875419
[01-20 17:17:57:0419:ERROR: sz.TestMain.run():118] -> 1484903877419
[01-20 17:17:59:0419:ERROR: sz.TestMain.run():118] -> 1484903879419
[01-20 17:18:01:0419:ERROR: sz.TestMain.run():118] -> 1484903881419
[01-20 17:18:01:0420:ERROR: sz.TestMain.main():142] -> 线程执行耗时:1 net.sz.TestMain$2$1
[01-20 17:18:03:0420:ERROR: sz.TestMain.run():118] -> 1484903883420
[01-20 17:18:05:0420:ERROR: sz.TestMain.run():118] -> 1484903885420
[01-20 17:18:07:0421:ERROR: sz.TestMain.run():118] -> 1484903887421
[01-20 17:18:09:0422:ERROR: sz.TestMain.uncaughtException():82 ] -> 收到未能正常捕获的异常
java.lang.ExceptionInInitializerError
at net.sz.TestMain$2$1.run(TestMain.java:116)
at net.sz.ThreadTest.run(TestMain.java:47)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsupportedOperationException
at net.sz.TestRun.<clinit>(TestRun.java:18)
... 3 more
[01-20 17:18:11:0421:ERROR: sz.TestMain.run():118] -> 1484903891421
[01-20 17:18:13:0421:ERROR: sz.TestMain.uncaughtException():82 ] -> 收到未能正常捕获的异常
java.lang.NoClassDefFoundError: Could not initialize class net.sz.TestRun
at net.sz.TestMain$2$1.run(TestMain.java:116)
at net.sz.ThreadTest.run(TestMain.java:47)
at java.lang.Thread.run(Thread.java:745)
[01-20 17:18:15:0421:ERROR: sz.TestMain.run():118] -> 1484903895421
[01-20 17:18:17:0422:ERROR: sz.TestMain.run():118] -> 1484903897422
[01-20 17:18:19:0422:ERROR: sz.TestMain.run():118] -> 1484903899422
[01-20 17:18:21:0423:ERROR: sz.TestMain.run():118] -> 1484903901423
好吧,到此为止,我们线程模型,起码保证了,一定的稳健性,
在异常退出以后,线程能重新启动运行,保证程序正常运转;当然我们在收到消息的异常处理,可以加入通知消息,起码知道哪里代码会导致这样的情况
以上是关于更加强健的线程模型,解决线程卡死,退出异常情况的主要内容,如果未能解决你的问题,请参考以下文章