class ThreadUnsafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
上面的代码抛出
java.lang.IndexOutOfBoundsException: Index: 0, Size: 1
我知道ArrayList不是线程安全的,但在这个例子中,我认为每个remove((调用之前都保证至少有一个add((调用,所以即使顺序混乱,代码也应该是可以的:
thread0: method2()
thread1: method2()
thread1: method3()
thread0: method3()
这里需要一些解释。
如果总是一个add()
或remove()
调用在另一个调用开始之前完全完成,则您的推理是正确的。但是ArrayList
不能保证,因为它的方法不是synchronized
。因此,可能会发生两个线程同时处于某些修改调用的中间。
让我们看看add()
方法的内部结构,以了解一种可能的故障模式。
添加元素时,ArrayList
会使用size++
来增加大小。这不是原子的。
现在假设列表为空,两个线程A和B在完全相同的时刻添加一个元素,并行执行size++
(可能在不同的CPU内核中(。让我们想象一下事情按以下顺序发生:
- A将大小读取为0
- B读取大小为0
- A的值加1,得到1
- B将其值加1,得到1
- A将其新值写回
size
字段,得到size=1
- B将其新值写回
size
字段,从而得到size=1
尽管我们有2个add()
调用,但size
只有1个。如果现在尝试删除2个元素(这次是按顺序执行的(,则第二个remove()
将失败。
为了实现线程安全性,在当前进行一次访问时,任何其他线程都不应该处理像size
(或元素数组(这样的内部结构。
多线程本质上是复杂的,因为来自多个线程的调用不仅可以以任何(预期或意外(顺序发生,而且它们也可以重叠,除非受到synchronized
等机制的保护。另一方面,过度使用同步很容易导致多线程性能不佳,还会导致死锁。
作为@RalfKleberhoff答案的补充,
我认为每个remove((调用之前都保证至少有一个add((调用,
是。
所以即使订单被搞砸了代码也应该是可以的
否,这不是关于多线程程序的有效推断。
您的程序包含数据竞赛,这是由于两个线程都访问同一个共享的非原子对象,其中一些访问是写入的,而没有适当的同步。包含数据竞赛的程序的整个行为是未定义的,所以实际上你根本无法对它的行为得出任何结论。
不要试图欺骗或节省同步时间。通过限制共享对象的使用,尽量减少你需要的数量,但在哪里需要,你就需要,以及确定何时何地需要的规则并不难学习。
java文档中的ArrayList表示,
请注意,此实现不是同步的。如果有多个线程同时访问ArrayList实例,并且线程从结构上修改了列表,必须对其进行同步外部。
为什么此代码不是线程安全的?
Machine上运行的多个线程彼此独立运行。
public void method1(int loopNumber) { for (int i = 0; i < loopNumber; i++) { method2(); method3(); } }
这里
method2()
和method3()
在线程,但不跨线程稳定的状态。
有趣的测试是在method3()
中添加空检查并设置LOOP_NUMBER = 10000
;
private void method3()
{
if (!list.isEmpty())
list.remove(0);
}
结果,您应该得到相同的运行时异常,比如java.lang.IndexOutOfBoundsException: Index: 0, Size: 1
或java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
,因为list
中变量(即size
(的相同原因不稳定状态。
要解决此问题,您可以添加如下同步或使用同步列表
public void method1(int loopNumber)
{
for (int i = 0; i < loopNumber; i++)
{
synchronized (list)
{
method2();
method3();
}
}
}