多线程下 ArrayList 出现null的问题

问题

在某个项目中使用了ArrayList 了,在多个启动任务中向该ArrayList中注册回调任务,有一次突然发现项目报空指针,而且该问题不必现。

背景

在项目启动时,有多个初始化任务需要执行,之前使用多个监听器listener方式执行,但listener是单线程执行的,速度很慢,后面修改为使用一个listener来监听项目启动,在listener中注册相关启动器到ListenerTaskStarter,并且启动ListenerTaskStarter,ListenerTaskStarter 定义一个容器来接收监听器listener注册的启动器,定义一个执行方法,方法中使用大小为10的线程池来多线程执行初始化任务。

web.xml

    
        csg Listener
        com.smartsecuri.webframework.listener.CsgManageListener
    

Listener.java

public class Listener implements ServletContextListener
{

    private static final Logger logger = LogManager.getLogger(CsgManageListener.class);

    @Override
    public void contextInitialized(ServletContextEvent sce)
    {
        try
        {
            ListenerTaskStarter.INSTANCE
                    .addService(new LoadHardwareInfoTask(), 0)
                    .addService(new BpRegisterTask(), 0)
                    .addService(new BpServiceLineStart())
                    .addService(new BfdListenerTask())
                    .addService(new SystemState2CsgTask())
                    .addService(new CvsgDeallssue())
                    .addService(new CvsgHeartBeatManagerTask());

            ListenerTaskStarter.INSTANCE.doService(sce);
        }
        catch (Exception e)
        {
            logger.error("CsgManageListener err...", e);
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce)
    {
        DestroyedHelper.stopServer(sce);
    }

}

ListenerTaskStarter.java

public class ListenerTaskStarter 
{
    private final Logger logger = LogManager.getLogger(ListenerTaskStarter.class);

    public final static ListenerTaskStarter INSTANCE = new ListenerTaskStarter();

    /**
     * TIMEOUT : (每一启动级别的线程任务等待时间:300s).
     */
    private static final long TIMEOUT = 300l;
    
    private static final int THREAD_MAX_LENGTH_10 = 10;

    private ListenerTaskStarter()
    {
    }

    Map> services = new TreeMap>();

    public ListenerTaskStarter addService(ListenerTask service, int level)
    {
        if (null == services.get(level))
        {
            List l = new ArrayList();
            services.put(level, l);
        }
        services.get(level).add(service);
        return this;
    }

    public ListenerTaskStarter addService(ListenerTask service)
    {
        addService(service, 1);
        return this;
    }

    public void doService(ServletContextEvent sce)
    {
        String poolName = "threadPool-ListenerTaskStarter";
        ExecutorService es = ThreadPoolManager.newFixedThreadPool(poolName, THREAD_MAX_LENGTH_10);
        logger.info("Trying to start threads dealing listener service . " + services);

        Set>> entrySet = services.entrySet();
        Iterator>> it = entrySet.iterator();
        while (it.hasNext())
        {
            List tasks = it.next().getValue();
            final CountDownLatch latch = new CountDownLatch(tasks.size());
            for (ListenerTask task : tasks)
            {
                es.submit(new Runnable()
                {

                    @Override
                    public void run()
                    {
                        try
                        {
                            task.service(sce);
                        }
                        catch (Exception e)
                        {
                            logger.error("Dealing the service failed .", e);
                        }
                        finally
                        {
                            latch.countDown();
                        }
                    }
                });
            }
            if (it.hasNext())
            {
                try
                {
                    latch.await(TIMEOUT, TimeUnit.SECONDS);
                }
                catch (InterruptedException e)
                {
                    logger.error("latch.await() err...", e);
                }
            }
        }
        ThreadPoolManager.shutdown(poolName);
        logger.info("listener service all finish...");
    }

}

排查

  • 反复查看代码,未发现明显可能会出现 null 的地方,返回的值(上文中的添加待定值)在其他方法中都不为null
  • 结合这个反馈和代码的源码进行分析,感觉可能是ArrayList 的问题,这才突然想起来ArrayList 是线程不安全的,这才百度发现确实有这个问题
  • 代码进行验证
    在大插入下,运行 3-4 次就有一次会出现 null 的情况,证实了想法
    // ArrayList 线程不安全, add 方法在多线程 环境下 会出现意外的 null 值
@Test
void testListThread() throws InterruptedException {
    ArrayList list = new ArrayList();
    CountDownLatch latch = new CountDownLatch(1000);
    for (int i = 0; i < 1000; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                list.add("abc");
                latch.countDown();
            }
        }).start();
    }

    latch.await();
    if(list.contains(null)) {
        System.out.println("list 包含 null 值");
        int index = list.indexOf(null);
        System.out.println(index);
        System.out.println(list);
    }
}

反省

对java 集合的源码阅读还是不够熟悉,有些知识明明之前看过,但还是习惯性的用哪些常用的线程不同步的类,在多线程下没用仔细思考选用的对象

解决方案

  1. List list = Collections.synchronizedList(new ArrayList(...)); 使用 Collections.synchronizedList 去包裹要使用的集合
  2. CopyOnWriteArrayList(线程安全,但是可能会对内存占用较大,垃圾回收频繁)
  3. Vector (线程同步的集合对象,操作大数据的时候会比较慢)

你可能感兴趣的:(多线程下 ArrayList 出现null的问题)