问题
在某个项目中使用了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 集合的源码阅读还是不够熟悉,有些知识明明之前看过,但还是习惯性的用哪些常用的线程不同步的类,在多线程下没用仔细思考选用的对象
解决方案
- List list = Collections.synchronizedList(new ArrayList(...)); 使用 Collections.synchronizedList 去包裹要使用的集合
- CopyOnWriteArrayList(线程安全,但是可能会对内存占用较大,垃圾回收频繁)
- Vector (线程同步的集合对象,操作大数据的时候会比较慢)