同步容器存在的并发问题

同步容器

java.util 中,存在一些古老的同步容器类,如 Vector、Hashtable。这些同步容器类,主要依靠持有内部锁(synchronized 修饰方法)来保证对容器状态访问的原子性。因此,几乎所有需要访问容器状态的方法,都是 synchronized 修饰的同步方法。虽然保证了线程安全,但也极大降低了并发性,使得同步容器在并发场景下堪忧的性能令人诟病。

并发问题

如果单独使用同步容器所提供的操作,可以放心使用,不会带来任何的并发问题。因为同步逻辑已经被封装在该操作对应的方法中。但如果使用复合操作,仍然很有可能带来并发问题,复合操作的同步逻辑需要你自己去实现。常见的复合操作有:迭代(容器遍历)、跳转(根据指定顺序找到当前元素的下一个元素)和 检查执行(先检查某一条件,该条件满足了再执行指定操作。如:若没有则添加)。Java 中,对同步容器的复合操作可能会产生异常,最常见的异常有:ConcurrentModificationException 和 ArrayIndexOutOfBoundsException。

ConcurrentModificationException

该异常产生的一种原因是使用迭代器时,迭代器发现容器结构在迭代过程中被修改了。虽然异常的名字是并发修改异常,但其实对于同步容器类,只要迭代器被创建开始,任何情况下容器结构被除了该迭代器的 remove 方法外的其它手段修改,再使用该迭代器的 next 方法时均会产生这个异常(并发容器不存在这个问题)。这种行为被称为 “及时失败”(fail-fast),事实上,对于非并发容器是不允许一个线程在遍历容器时, 另一个线程对它进行修改。

那么,迭代器是如何检查迭代过程中容器是否被修改的呢?迭代器使用的是 “双计数器匹配”,即迭代器和同步容器均维护一个跟踪改写次数的计数器,迭代器在进行迭代时检查自己的计数器的值是否和容器的相同,若不同则抛出异常 。

此外,隐式迭代器也可能造成这一异常。所谓隐式迭代器,便是某个方法内部需要使用迭代器来实现,这类方法常见的有:hashCode()、equals()、toString()、containsAll()、removeAll()、retainAll()。

ArrayIndexOutOfBoundsException

Vector 底层使用数组存储元素,复合操作还有可能造成数组越界。例如,下面这段代码用来遍历 Vector,使用 size() 方法确定数组边界。

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

在多线程环境中,如果 i = vector.size() - 1 时,经过了 i < vector.size() 条件检查后,另一个线程并发删除了 vector 中的一个元素,那么执行 vecotor.get(i) 时便会造成数组越界。

如何避免同步容器的并发问题

同步容器的种种问题本质还是复合操作带来的,若使用同步机制将所有的复合操作变为原子操作,如在访问容器时持有该容器的锁,必然能解决并发问题,但也牺牲了容器的性能。还有一种策略:克隆容器。譬如,我只是想遍历当前容器中的所有元素,那么完全可以使用克隆容器的迭代器做这件事,而遍历过程中,对原始容器进行修改也毫无关系。但克隆的过程存在显著性能开销,好坏取决于多个因素,包括容器大小、每个元素上执行的操作、迭代操作的频率以及响应时间、吞吐量需求。最好的方式,还是使用并发容器来代替这些古老的同步容器,并发容器拥有更细粒度的同步机制,在并发性和同步性、线程安全和容器性能间有一个较好的权衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值