前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次ArrayList的使用不当导致线上jstorm任务启动失败的案例

一次ArrayList的使用不当导致线上jstorm任务启动失败的案例

作者头像
我是攻城师
发布2018-12-25 11:47:29
1.3K0
发布2018-12-25 11:47:29
举报
文章被收录于专栏:我是攻城师我是攻城师

起因:

最近一次的的项目版本迭代中,我们的jstorm项目里面增加了一些新的功能,开发完毕后,按照正常的上线流程,代码是需要在开发,测试和预发布环境,测试完毕后才能上线。 这次上新版本也不例外,在所有的环境都测试之后并无任何问题,然后由OP上线,结果发布失败。

然后查看异常log,大致如下:

代码语言:javascript
复制
Exception in thread "Thread-16" Exception in thread "Thread-18" Exception in thread "Thread-38" java.lang.ArrayIndexOutOfBoundsException
    at java.lang.System.arraycopy(Native Method)
    at java.util.ArrayList.addAll(ArrayList.java:562)
    at ArrayListTest$PutThread.run(ArrayListTest.java:24)

基本确定了是拷贝执行了ArrayList.addAll方法,导致的拷贝索引越界问题,追查发生异常地方的源码大致如下:

代码语言:javascript
复制
public class CountBolt extends BaseBasicBolt {


    static List<String> list=new ArrayList<>();
    String data[]=new String[]{"1","2","3","4","5","6","7","8","9","10","11"};

    public void prepare(Map stormConf, TopologyContext context) {
        //......
        list.addAll(Arrays.asList(data))//异常代码处
        //.......
    }

}

从上面能够看出,异常代码的触发代码是:

代码语言:javascript
复制
list.addAll(Arrays.asList(data))//异常代码处

但奇怪的是在开发,测试,预发环境均没有出现类似问题,最初怀疑是环境问题,但在仔细的检查各种配置文件之后,并一步步检查发布步骤之后,依然没有线索,仅仅只有线上环境有问题,其他环境均不能复现。

基本排除了是操作步骤的和环境的问题之后,又仔细的检查了代码,发现了这个ArrayList是静态变量:

代码语言:javascript
复制
static List<String> list=new ArrayList<>();

而jstorm的Bolt是以多线程的方式运行的,所以静态变量是类共享的,这意味着有多个线程同时在向list里面添加数据,所以这个addAll方法并不是线程安全的,但抛出的异常是索引越界异常,为了弄清原因,继续追查源码,在ArrayList里面的addAll源码如下(jdk8):

代码语言:javascript
复制
public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

ensureCapacityInternal是扩容的方法,每次追加数据前,都会检查当前的数组容量是否能够装的下,如果不能则会扩容50%,而log中的异常是发生在:

代码语言:javascript
复制
System.arraycopy(a, 0, elementData, size, numNew);

然后,我们继续深追arraycopy方法,发现其是native方法,其五个参数的意思是:

(原数组,原数组的开始位置,目标数组,目标数组的的开始位置,拷贝的个数)

具体的方法,如下:

代码语言:javascript
复制
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

也就是说是底层使用C/C++写的,直接查看不了,也就是说线索断了,不过看这个方法的声明,知道其最多会抛出三种异常,如下:

代码语言:javascript
复制
IndexOutOfBoundsException 
ArrayStoreException
NullPointerException

很明显IndexOutOfBoundsException异常就是我们需要关注的,但是源码里面没有细写什么情况下会出现这种异常,到这里似乎线索又断了,不过不着急,我们还可以移步到Oracle的官网文档界面再查找一下,这下找到了有关的详细的方法说明,如下:

代码语言:javascript
复制
Otherwise, if any of the following is true, an IndexOutOfBoundsException is thrown and the destination is not modified:

The srcPos argument is negative.
The destPos argument is negative.
The length argument is negative.
srcPos+length is greater than src.length, the length of the source array.
destPos+length is greater than dest.length, the length of the destination array.

这里面解释了出现IndexOutOfBoundsException的5种情况,其中前四种经过分析,应该是不可能出现的,而最后一种值得怀疑:

代码语言:javascript
复制
destPos+length is greater than dest.length, the length of the destination array.

我们来分析下,什么时候会出现问题:

代码语言:javascript
复制
public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew); 
        System.arraycopy(a, 0, elementData, size, numNew);//1
        size += numNew;
        return numNew != 0;//2
    }

多个线程同时执行,当A线程扩容完执行到2处,B线程刚好执行到1,这个时候如果B线程恰巧看到了A线程已经更新过的最新的size的值,就会出现size+numNew大于elementData.length的情况,这个时候相当于扩容后的容量,仍然不能装下最新添加集合的数据,所以就自然会抛出越界异常:

代码语言:javascript
复制
IndexOutOfBoundsException

知道原因后,我们来思考下,如何让其复现:

条件(1):必须有多线程同时添加数据的情况,或者多个线程不停的添加数据

条件(2):必须触发了ArrayList内部的Object数组的扩容动作

下面,我们看下复现问题的程序,注意这里我为了符合和我们生产环境一致的写法,用的多线程同时并发的插一批数据,而并不是无限循环的向里面的追加数据,虽然这种方法,更能复现问题,但为了严谨性,有必要保持和尽量生产环境一样的写法,这样才更能接近真相。

代码如下:

代码语言:javascript
复制
package concurrent.thread_unsafe_example;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/***
 *测试ArrayList在多线程环境下扩容异常问题
 * cpu核数越多,几率越大
 */
public class ListAddAllTest {



    static class PutThread extends Thread{

        static List<String> list=new ArrayList<>();

        CountDownLatch latch;

        public PutThread(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            try {

            String data[]=new String[]{"1","2","3","4","5","6","7","8","9","10","11"};
            latch.await();//必须等到所有的线程到达之后,才能向下执行
            list.addAll(Arrays.asList(data));
            Thread.sleep(2000);//等待一会再结束,避免结束的块,共享数据会刷新到主内存里面
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


    public static void main(String[] args) throws InterruptedException {
        //使用 CountDownLatch 作共享锁
        CountDownLatch latch=new CountDownLatch(1);
        for (int i = 0; i < 40; i++) {
            PutThread pt=new PutThread(latch);
            pt.start();
        }
        //释放栅栏
        latch.countDown();






    }

}

在运行第五次时,出现了异常:

代码语言:javascript
复制
java.lang.ArrayIndexOutOfBoundsException
    at java.lang.System.arraycopy(Native Method)
    at java.util.ArrayList.addAll(ArrayList.java:580)
    at concurrent.thread_unsafe_example.ListAddAllTest$PutThread.run(ListAddAllTest.java:32)

虽然出现了异常,但奇怪的是:为什么生产环境每次都是必现,而在我自己的开发机上却是有几率的出现?

其实,这个很好解释,虽然我开的线程是40个,并且故意让其同时触发,但我本地的开发机cpu个数只有4个,而生产环境应该有32个cpu,多线程的情况下,如果cpu的个数越多,那么同时并行运行的几率就越大,注意我这里用词是并行,而并不是并发,在复现上面的问题中,一定是并行的几率越大,复现的几率就越大,因为并发会涉及线程的短暂调度,在这短暂的周期之间,是有一定的先后顺序,所以这会降低异常发生的几率。

为了验证我的想法,我把程序部署在另外一台拥有20个cpu的机器上,这下几乎每次都能抛出异常。

现在一切真相大白了,但还有最后一个疑问,为什么当初同样的代码,没有在开发,测试和所谓的预发环境测出来呢?

原因是因为这几个环境storm的bolt的并发task的个数只有2个,也就说最多只有2个线程,所以能导致出现问题的几率非常之小,这才发生了文章开头的一幕。

一些思考与总结:

(1)遇到问题时候,尽量先通过参数一致排除环境问题

(2)排除环境问题之后,查看是否有线程安全问题

(3)如果没有线程安全问题,最后可从最近更新或者发布的代码中及一些框架的源码中追加问题,是否有死循环,内存泄漏等bug

(4)发现问题后,尽量先在本地复现,并且要注意尽量接近线上的真实情况。

相关文章:

Java里面关于数组拷贝的几种方式

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档