同步容器(如Vector)的所有操作一定是线程安全的吗

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

为了方便编写出线程安全的程序,java提供了一些线程安全类和并发工具,比如:同步容器、并发容器、阻塞队列等。
最常见的同步容器就是Vector和HashTable了,那么同步容器的所有操作都是线程安全的吗?
本文就来深入分析一下这个问题,一个很容易被忽略的问题。


提示:以下是本篇文章正文内容,下面案例可供参考

一、解析

在java中,同步容器主要包括2类:
1.Vector、Stack、HashTable
2.Collections类中提供的静态工厂方法创建的类

二、Vector举例

1.同步容器

代码如下(示例):

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

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;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

可以看到,Vector这样的同步容器的所有公有方法全都是synchronize的,也就是说,我们可以在多线程场景中放心的单独使用这些方法,因为这些方法本身就是线程安全的。
注意:
虽然同步容器的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性,需要客户端通过主动加锁来保证。
举例,我们通过定义删除Vector中最后一个元素的方法:

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

上面这个方法是一个复合方法,包括size()和remove(),乍一看上去好像并没有什么问题,无论是size还是remove方法都是线程安全的,那么整个deleteLast方法应该也是线程安全的。
但是,如果多线程调用该方法的过程中,remove方法有可能抛出ArrayIndexOutOfBoundsException。

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 879
    at java.util.Vector.remove(Vector.java:834)
    at com.hollis.Test.deleteLast(EncodeTest.java:40)
    at com.hollis.Test$2.run(EncodeTest.java:28)
    at java.lang.Thread.run(Thread.java:748)

从remove的源码中,我们可以分析得出:当index >= elementCount时,会抛出ArrayIndexOutOfBoundsException ,也就是说,当当前索引值不再有效时,会抛出这个异常。
因为,removeLast方法,有可能被多个线程同时执行,当线程2通过index()获得的索引值为10,在尝试通过remove()删除该索引位置的元素之前,线程1把该索引位置的值都删除掉了,这时线程2再执行时便会抛出异常
在这里插入图片描述
为避免出现类似问题,可以尝试加锁:

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

如上,我们在deleteLast中对v进行加锁,即可以保证同一时刻,不会有其他线程删掉v中的元素。
另外,如果以下代码会被多线程执行时,也要特别注意:

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

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

2.同步容器的问题

前面说过,同步容器直接保证单个操作的线程安全性,但是无法保证复合操作的线程安全,遇到这种情况必须通过主动加锁的方式来实现。
而且,除此之外,同步容器由于对其所有方法都加了锁,这就导致了多个线程访问同一个容器的时候,只能进行顺序访问,即使不同的操作,也要排队,如get和add要排队执行,这就大大降低了容器的并发能力。

3.并发容器

针对前文提到的同步容器存在并发度低的问题,从java5开始,在java.util.concurrent包下,提供了大量支持高效并发的访问的集合类,我们称之为并发容器。
在这里插入图片描述
针对前文提到的同步容器的复合操作的问题,一般在Map中发生的比较多,所以在ConcurrentHashMap中增加了对常用复合操作的支持,比如putIfAbsent()、replace(),这两个都是原子操作,可以保证线程安全性。

另外,并发包中的CopyOnWriteArrayList和CopyOnWriteArraySet是Copy-On-Write的两种实现。
Copy-On-Write容器即写时复制的容器。通俗的理解是我们往一个容器中添加元素的时候,不直接往当前元素添加,而是先将当前容器进行copy,复制出一个新容器,然后往新的容器里添加元素,添加完元素后,再将原容器的引用指向新容器。

CopyOnWriteArrayList中add/remove等写方法是需要加锁的,而读方法是没有加锁的。

这样做的好处是我们可以对CopyOnWrite容器进行并发读,当然,这里读的数据可能不是最新的,因为写时复制的思想是通过延迟更新策略来实现数据最终一致的,并非强一致性。

但是,作为替代Vector的CopyOnWriteArrayList并没有解决同步容器的复合操作的线程安全性问题


总结

本文介绍了同步容器和并发容器

同步容器是通过加锁来实现线程安全的,并且只能保证单独的操作是线程安全的,无法保证复合操作的线程安全,并且同步容器的读和写操作之间会相互阻塞。

并发容器是java5提供的,主要用来替代同步容器。有更好的并发能力,而且其中的CopyOnWriteArrayList定义了线程安全的复合操作。

在多线程场景中,如果使用并发容器,一定要注意复合操作的线程安全问题,必要时需要主动加锁。

在并发场景中,建议直接使用java.util.concurrent包中提供的类,如果需要复合操作时,建议使用有些容器自身提供的复合方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值