ArrayList线程不安全分析

时间:2022-03-07 08:11:59

众所周知,ArrayList作为Collection中极重要的一员,是非线程安全的。那么它的非线程安全体现在哪些地方呢?下面我就具体分析这个问题。不对之处,欢迎大家指正。

在讨论这个问题之前,先说下线程安全的三个重要特性:操作原子性、状态一致、内存可见性。

操作原子性:该操作只能一口气做完,中间不能停顿。

状态一致性:共享对象的状态要一致。

内存可见性:每个线程都有自己的工作空间,它使用某个对象时,先将对象从主存复制到它的工作空间,然后再对对象做修改,最后将修改写入主存。那么就会出现一个问题:多个线程在同一时刻保存的同一共享对象的值可能不一样,因为可能有线程并没有及时将对对象的修改及时写入主存并通知其他线程。

它们有什么作用呢?原子性操作保证了共享对象的状态一致,而状态一致则表示着该对象处于正确的状态,内存可见性则保证了所有线程在同一时刻都取到了相同的共享变量。因此我们在多线程编程中,需要关注保证共享变量的操作原子性、状态的一致以及内存可见性。

1. 操作的非原子性

ArrayList的方法都是非原子性的,因为这些方法可以同时被多个线程访问,并且可能在任何地方终止。要保证操作原子性的方法有很多,如使用synchronized关键字修饰方法或者包围住改变对象状态的操作;使用Lock包裹代码段(相对与synchronized,Lock提供了更灵活的处理方式,也带来了更好的并发性能);使用Java并发包下的原子变量;使用CAS。

2. 状态不一致

分析它的状态不一致,通常是多个线程同时调用两个不兼容的方法导致的。下面以size和remove为例。

源码

    /**
* Returns the number of elements in this list.
*
* @return the number of elements in this list
*/
public int size() {
return size;
}

    /**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

size很简单,直接返回了。

remove操作分为三部分:边界检查;检查是否需要移动元素;清除该位置元素。

假设现在有两个线程A、B,A执行size(),B执行remove(); A进入函数,取得size的值后切换到B继续执行(非原子操作),B执行完整个方法,切换到A。注意,此时size值已经被改变了,因此A拿到的size是失效的(修改不可见),但是A并不知道,它一如既往的执行下去,就想单线程中一样。所以当我们想要通过取得的size值做一些操作(删除最后一个元素,取得最后一个元素),很明显会越界。

上面的情况并没有提到状态不一致的问题,但是它是明确存在的,比如多个线程同时执行remove方法时。