`
coding1688
  • 浏览: 232378 次
  • 来自: 上海
社区版块
存档分类
最新评论

悲剧了,这个多线程程序为什么不能在指定时间自动退出?(详细分析)

 
阅读更多

问题描述

  在一个项目中,有一个单独的java程序,它使用了第三方类库,而且是必须使用的那种,但是这个第三方类库有个致命的问题:它如同一头永远处于饥饿状态的野兽,它会不断的吃掉内存,最终导致“ java.lang.OutOfMemoryError: Java heap space”异常。

 

 

log 写道
12:00:02.597 ERROR Thread-1 emay.sms.relay.EmaySDKClient - run
java.lang.OutOfMemoryError: Java heap space
 

 

  虽然出现了异常,但它不会退出。只有等到发现的时候,人工去这个杀掉这个进程。这种情况多次反复出现,造成了一些不良影响。

 

不管用的解决方案

  能不能让它定期重启一下呢?(这几乎总是最后的解决方法了,因为系统中未知的因素太多)程序退出之后,所占用的内存不也就释放掉了嘛!
  在 main() 方法的末尾加上下面的代码,意思是说“在每天0点的时候程序自动退出”。(因为在系统中有一个任务管理程序,它会检查进程是否还在,若已终止则重新启动。)

 

 

            while (true) {
                Thread.sleep(3600 * 1000);
                java.util.Date now = new java.util.Date();
                if (now.getHours() == 0) {
                    break;
                }
            }
 

 

  这个看上去没什么问题,于是编译重新运行。然而,在第二天 0 点的日志中没有发现重启的记录,也就是说它没有按照预想的那样,在0点并没有退出!

 

管用的改进?

  在 java 中,线程有个 daemon 属性(是否守护线程),应该将程序中主线程之外的线程设置为 daemon 属性,这样才能保证 main() 方法执行完之后程序自动退出来,否则进程就会一直等待所有的非守护线程退出。下面是关于守护线程的一些描述信息。

http://blog.csdn.net/lanniao1/article/details/1831626 写道
守护线程(Daemon)
  Java有两种Thread:“守护线程(Daemon Thread)”与“用户线程(User Thread)”。守护线程是一种“在后台提供通用性支持”的线程,它并不属于程序本体。从字面上我们很容易将守护线程理解成是由虚拟机(virtual machine)在内部创建的,而用户线程则是自己所创建的。事实并不是这样,任何线程都可以是“守护线程”或“用户线程”。他们在几乎每个方面都是相同的,唯一的区别是判断虚拟机何时离开:
  用户线程:Java虚拟机在它所有用户线程(非守护线程)已经离开后自动离开。
  守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。
  当程序只有守护线程时,该程序便可以结束运行。 
  Thread.setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为Daemon模式,false为User模式。setDaemon(boolean on)方法必须在线程启动(start)之前调用,当线程正在运行时调用会产生异常(IllegalThreadStateException)。isDaemon方法将测试该线程是否为守护线程。值得一提的是,当你在一个守护线程中产生了其他线程,那么这些新产生的线程不用设置Daemon属性,都将是守护线程,用户线程同样。
  例如,我们所熟悉的Java垃圾回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。
 

  在这个程序中,有两个线程,一个线程是不断调用第三方类库提供的方法,一个线程是一个网络客户端,将这两个线程都设置为 daemon 属性。这两个类分别为 Relay 和 Client,是 Thread 类的子类。这样之后 main() 方法的内容大体如下:

 

             // 第一个线程
             Relay relay = new Relay();
             // ...
             relay.setDaemon(true);
             relay.start();

             // 第二个线程
             Client client = new Client();
             client.setDaemon(true);
             client.start();

             // 下面的代码用于在每天0点退出
             while (true) {
                Thread.sleep(3600 * 1000);
                java.util.Date now = new java.util.Date();
                if (now.getHours() == 0) {
                    break;
                }
            }

 

这总该管用了吧?!经过检查第二天的日志,发现还是没有在0点退出,为什么?

 

这个程序到底起了多少线程?

  猜测,也许这个第三方类库中可能启动一些别的线程,否则就与 setDaemon(true) 的精神相违背。那得看一下它到底启动了哪些线程,在JDK中,jstack 可以观察到jvm中当前所有线程的运行情况和线程当前状态,使用方式是 jstack pid;其中 pid 为进程id,可用 jps 或 ps 或 top 等工具得到。

  用 jstack 看一下。下面是去掉第三方类库的程序执行线程状态信息。

 

jstack 写道
[root@web186 emay_sms_relay]# jstack 8082
2012-06-11 13:35:01
Full thread dump Java HotSpot(TM) Client VM (16.3-b01 mixed mode, sharing):

"Attach Listener" daemon prio=10 tid=0x081db400 nid=0x562e runnable [0x00000000]
java.lang.Thread.State: RUNNABLE

"Thread-1" daemon prio=10 tid=0x081d5400 nid=0x1f9d runnable [0xb41ca000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at smj.client.SmjClient.recv(SmjClient.java:96)
at smj.client.SmjClient.loop(SmjClient.java:139)
at smj.client.SmjClient.run(SmjClient.java:124)
at java.lang.Thread.run(Thread.java:619)

"Low Memory Detector" daemon prio=10 tid=0x08096400 nid=0x1f99 runnable [0x00000000]
java.lang.Thread.State: RUNNABLE

"CompilerThread0" daemon prio=10 tid=0x08093000 nid=0x1f98 waiting on condition [0x00000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=10 tid=0x08091400 nid=0x1f97 runnable [0x00000000]
java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=10 tid=0x0807e400 nid=0x1f96 in Object.wait() [0xb48b4000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x87040b00> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:118)
- locked <0x87040b00> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:134)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x0807cc00 nid=0x1f95 in Object.wait() [0xb4905000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x87040a08> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:485)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
- locked <0x87040a08> (a java.lang.ref.Reference$Lock)

"main" prio=10 tid=0x08058400 nid=0x1f93 waiting on condition [0xb6b96000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at kdx.sms.relay.Main.main(Main.java:22)

"VM Thread" prio=10 tid=0x0807b400 nid=0x1f94 runnable

"VM Periodic Task Thread" prio=10 tid=0x08098400 nid=0x1f9a waiting on condition

JNI global references: 994

[root@web186 emay_sms_relay]#

 

 

下面是增加调用第三方类库的线程之后新增的线程。其中有两个线程没有 daemon 属性,不是守护线程。

 

jstack 写道
"Thread-1" daemon prio=10 tid=0x08200c00 nid=0x1eed waiting on condition [0xb3fe3000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at emay.sms.relay.EmaySDKClient.run(EmaySDKClient.java:405)

"MonitorThread" prio=10 tid=0x081e3400 nid=0x1eec in Object.wait() [0xb4136000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x89bf61b0> (a java.util.TaskQueue)
at java.util.TimerThread.mainLoop(Timer.java:509)
- locked <0x89bf61b0> (a java.util.TaskQueue)
at java.util.TimerThread.run(Timer.java:462)

"Thread-2" prio=10 tid=0x081e2c00 nid=0x1eeb waiting on condition [0xb4187000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at cn.emay.sdk.communication.socket.ReceiveThread.run(ReceiveThread.java:53)
 

 

也就是说,由于这两个非守护线程的存在, main()方法执行完之后,程序不会自动终止,因为它一直在执着的等待这两个线程的结束,它会一直等下去,直到poweroff。

 

最终解决

当然,问题总是可以解决的,那就是显式的调用 System.exit() 方法强制退出程序啦。如下所示:

 

            while (true) {
                Thread.sleep(3600 * 1000);
                java.util.Date now = new java.util.Date();
                if (now.getHours() == 0) {
                    //break;
                    System.exit(100);
                }
            }
 

 

经过观测,这个方法达到了预期的目标,每天0点的时候就会自动退出。

 

后记

  从上述解决方案的本质上讲,算不得一个好的解决方案,这相当于让程序自己来监控自己,发现自己有问题就退出,这样的解决方法通常会有不管用的时候。因为程序在出现问题的时候,用于监控的那段程序也许也不灵了。(是不是可以联想到现实生活中来

  最好的方式还是第三方监控,就是说单独写个脚本(或程序)来监控进程是否正常,比如占用的内存、CPU、网络连接、数据库连接等是否超过指定的限度,在超过限度时进行告警或者自动终止、自动重启等。当然,这种监控也不是万能的,如果能够一劳永逸的解决问题,那还要我们搞 IT 的人干什么呢?!

 

 

 

 

 

3
1
分享到:
评论
1 楼 koujun 2012-06-11  
用linux的cron来 start/stop程序 会不会更好一点?

相关推荐

Global site tag (gtag.js) - Google Analytics