小细节令TA成为了僵尸进程

本文通过一个具体的Java程序案例,分析了程序如何因未捕获的NullPointerException异常而变成僵尸进程的问题。涉及异常处理不当、线程池管理不善等问题,并提出了相应的解决措施。

一个小小的常驻的Java程序,它只是很简单地做一些获取工作,但一些小细节可以令它极度容易成为一个僵尸进程。

症状:

在程序日志中看到抛出NullPointerException,之后没有日志了,也没有数据了,但进程还在,变成了僵尸进程。

分析:

抛出异常的地方在下面函数的return result;

    @Override
    public long getLoginMaxTimestamp(long start) {
        Long result = null;
        //按照方式一获取LoginMaxTimestamp,忽略SQLException
        try {
            result = this.getMaxTimestamp(System.currentTimeMillis(), MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
        } catch (SQLException e) {}
        if(start<System.currentTimeMillis()){
            if(result==null || result==0){
                //按照方式二获取LoginMaxTimestamp,忽略SQLException
                try {
                    result = this.getMaxTimestamp(start, MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
                } catch (SQLException e) {}
            }
            if(result==null || result<=start){
                //按照方式三获取LoginMaxTimestamp,忽略SQLException
                try {
                    result = this.getMaxTimestamp((start+System.currentTimeMillis())/2, MysqlDBUtil.getLoginStatExtConnection(), LOGIN_MAXTIMESTAMP_SQL);
                } catch (SQLException e) {}
            }
        }
        return result;
    }

这里显然是因为resultnull,导致JVM无法拆箱抛出NullPointerException。同时说明了上面三种方式都无法获取到数据。

那为什么程序会变成了僵尸进程?一直往上看调用方就知道了。
调用getLoginMaxTimestamp的是diffLogin

    protected static boolean diffLogin(boolean local){
        long startMills = dcDao.getSavedTimestamp(DcTimestampDao.LOGIN_KEY);
        // 这里调用的getLoginMaxTimestamp
        long endMills = TimeEnum.Minute.getFormat(dao.getLoginMaxTimestamp(startMills)-TMP_MINUTE_UNBACK);
        if(0==startMills){
            startMills = endMills-ONCE_MINUTE_BREAKBACK*TimeEnum.Minute.getMills();
        }
        //下面省略...

调用diffLogin的是main函数:

    public static void main(String[] args) throws ParseException{
        String boolStr = "true";
        if(null!=args && 1==args.length){
            boolStr = args[0];
        }
        final Boolean local = new Boolean(boolStr);
        // executor是ExecutorService的一个实例
        // ONCE_SLEEP是一个常量,它的值是10
        // 这里是提交一个Runnable到executor
        // Runnable里面做一次diff,然后sleep 10毫秒,再重新提交自己到executor
        executor.execute(new Runnable(){
            @Override
            public void run() {
                diffLogin(local);
                try {
                    Thread.sleep(ONCE_SLEEP);
                } catch (InterruptedException e) {
                }
                executor.execute(this);
            }
        });
        executor.execute(new Runnable(){
            @Override
            public void run() {
                diffChannel(local);
                try {
                    Thread.sleep(ONCE_SLEEP);
                } catch (InterruptedException e) {
                }
                executor.execute(this);
            }
        });
    }

看到这里就应该明白了。因为NullPointerException没有被捕获,Runnable没有再次提交自己,所以程序没有再进行下去。虽然主线程早就结束了,但是ExecutorService的线程池还在,所以进程还存活着,而且一直没有工作,成为了僵尸进程。

总结:

  1. 拆箱之前最好判断一下是不是null,否则就干脆返回类类型;
  2. 函数getLoginMaxTimestamp没有覆盖到异常情况,异常情况可以选择抛异常或者返回特殊值(其实,说不定他是知道会有这种情况的,只是懒得写,直接让它抛NullPointerException就算了);
  3. ExecutorService在异常情况下shutdown;

其实,他只要做到在异常情况下调用ExecutorService的shutdown,那么TA就不会成为一个僵尸进程了。遵循FastFail原则,出错就让它挂掉,再监控进程,出问题就发出报警,那么这个世界就会美好很多了……

要写好Java程序真不容易啊……

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值