【Java】同步容器与线程安全问题

Posted by 西维蜀黍 on 2019-03-26, Last Modified on 2023-02-28

问题

对于线程安全的集合类(例如Vector)的任何操作是不是都能保证线程安全?

答案

同步容器中的所有自带方法都是线程安全的,因为方法都使用synchronized关键字标注。但是,对这些集合类的复合操作无法保证其线程安全性,而需要客户端通过主动加锁来保证

分析

如果你看过JDK的源码,那么你会发现,像Vector这样的同步容器的所有public方法全都是synchronized的。

也就是说,我们可以在多线程场景中放心地单独双十一这些方法,因为这些方法本身的确是线程安全的。

那么为什么又说复合操作无法保证线程安全呢?

这里举个栗子,我们定义如下删除Vector中最后一个元素方法:

public Object deleteLast(Vector v){
    int lastIndex  = v.size() - 1;
    v.remove(lastIndex);
}

上面这个方法是一个复合方法,包括size()remove(),乍一看上去好像并没有什么问题,无论是size()方法还是remove()方法都是线程安全的,那么整个deleteLast方法应该也是线程安全的。

但事实上,如果多线程调用该方法的过程中有,remove方法有可能抛出ArrayIndexOutOfBoundsException。我们看一下remove方法具体实现,什么情况下会抛出这个异常呢。

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}

从上面代码中可以看出,当index >= elementCount时,会抛出ArrayIndexOutOfBoundsException,也就是说,当当前索引值不再有效的时候,将会抛出这个异常。

由于removeLast方法有可能被多个线程同时调用,因而可能会出现问题。

比如,这个Vector的长度为10,当线程一通过v.size()获得索引值为9,在尝试通过调用remove()方法以删除最后一个元素之前,线程二已经把最后一个元素删除掉了,这时线程一在执行时便会抛出异常。

解决方案

为了避免出现类似问题,可以尝试加锁:

public void deleteLast() {
    synchronized (v) {
        int index = v.size() - 1;
        v.remove(index);
    }
}

如上,我们在deleteLast()方法中,对v进行加锁,即可保证同一时刻,不会有其他线程删除掉v中的元素。

总结

至此,我们已经解释清楚了我们的问题。

问:对于线程安全的集合类(例如Vector)的任何操作是不是都能保证线程安全?

答:同步容器中的所有自带方法都是线程安全的,因为方法都使用synchronized关键字标注。但是,对这些集合类的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。

由于我们自己已知Vector等同步容器是线程安全的,所以我们通常在多线程场景中会直接拿来使用,并不会考虑太多,从而可能导致问题。

所以,我们在使用同步容器的时候,如果只使用其中的自带方法,那么可以放心使用,因为他们是线程安全的,但是如果我们想做复合操作,尤其是涉及到删除容器中的元素时,一定要注意是否需要客户端主动加锁。

下面,我们考虑以下代码,如果在多线程场景中使用会不会出现线程安全问题:

for (int i = 0; i < v.size(); i++) {
    System.out.println(v.get(i));
}

显然,以上代码在迭代的过程中,并不会出现线程安全问题。但是,如果在程序中还有以下代码有可能被多线程同时调用呢?

for (int i = 0; i < v.size(); i++) {
    v.remove(i);
}

由于,不同线程在同一时间操作同一个Vector,其中包括删除操作,那么就同样有可能发生线程安全问题。所以,在使用同步容器的时候,如果涉及到多个线程同时执行删除操作,就要考虑下是否需要加锁。

Reference