我在接受采访时被问到以下问题。给定以下代码,如果多个线程调用方法add
和doAction
,那么在打印NullPointerException
时如何获得toString
?**
public class Test{
private List<Object> obj = new ArrayList<Object>();
public void add(Object o){
obj.add(o);
}
public void doAction(){
for(Object o: obj){
System.out.println(o.toString()); // maybe NPE, why?
}
}
}
删除所有其他多线程问题。
答案 0 :(得分:7)
首先,让我们改变变量的名称; List<Object> list=new ArrayList<>();
因为&#34; obj&#34;对于引用List
的变量来说,这是一个非常糟糕的名称。
好的,当程序调用{{1}}时,它可能需要增长数组。这意味着它必须:
list.add(o);
,如果线程A正在执行该操作,同时线程B正在调用null
,则线程B可能会在之后从中读取新数组中的iterator.next()
值线程A已经将对象引用复制到该数组的成员中。
记住:当线程访问没有同步的内存时,读者线程有可能看到变量/字段/数组成员的更新发生在不同顺序的程序顺序中写线程实际上执行了它们。
答案 1 :(得分:4)
Test t = new Test();
t.add(null);
t.doAction(); //NPE triggered
obj
列表中没有可空性保证,因此它可能包含空值。
关于多线程的问题是指ConcurrentModificationException
,因为“for-all”外观在内部使用Iterator
。如果在迭代时添加元素,则会引发异常。
答案 2 :(得分:4)
我不喜欢这个问题,因为它意味着知道ArrayList
的实施。
如果我们假设传递的Object
不为空,那么您必须使用代码进行推理。这是添加类似
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
那怎么可能发生?好吧,size
字段已成功递增,但作业elementData
尚未可用于阅读主题。
在这种情况下,迭代器将size
存在多个元素并且可以返回一个空元素(因为它还没有完成写入)。
基本上,它是两个步骤
(1)可以成功,而(2)仍在飞行中。
答案 3 :(得分:2)
一种可能的方式:
obj
中的对象是可变的。.toString
方法查看某些可变字段,如果该字段为null
,则可能会失败,或者当字段为null
且某些其他条件不成立时,则会失败。很可能,对象的方法使其变异,以便在方法完成后,对象的状态是一致的,.toString
不会失败。.toString
被另一个线程调用,并且调用以NPE结束。没有堆栈跟踪很难分辨。