Java同步集合synchronizedX中的迭代器Iterator使用,为什么需要使用者加锁?

Posted 温柔狠角色

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java同步集合synchronizedX中的迭代器Iterator使用,为什么需要使用者加锁?相关的知识,希望对你有一定的参考价值。

(尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/129472842冷血之心的博客)

前言

大家好,我是冷血之心。上个博客中,我宣布正式回归博客,接下来我持续分享一些有用的知识点,做到共同进步、共同成长。
好了,废话不多说了,我们开始今天的主题:《Java同步集合synchronizedX中的迭代器Iterator使用,为什么需要使用者加锁?》

备注

查看本文需要一定的Java集合、线程安全知识,本文不是一篇科普文,本文的目的是和大家讨论一个容易被忽略的问题,引导大家思考。

正文

我们先来看一些基本的概念。

常见集合

集合

常见的集合包括List、Set、Map等,List和Set集群的父接口是Collection接口。从实现类上看,我们常见的实现类包括:

  • ArrayList、LinkedList
  • HashSet、TreeSet
  • HashMap、LinkedHashMap

以上的常见集合都是线程不安全的。

同步集合

那么什么是同步集合?

同步集合是指通过java.util.Collections.synchronizedX(其中,X可以是Set、List、Map等)方法,通过装饰器模式将指定的非线程安全的集合对象对外暴露为线程安全的对象(外包装对象)。这个方法将返回指定集合的外包装对象,这些集合称之为同步集合(Synchronized Collection)。

并发集合

在JDK1.5中引入了java.util.concurrent包,在该包中定义了一组线程安全的集合,称为并发集合,这些集合可以作为同步集合的替代品。

并发集合是怎么做到线程安全遍历的?

首先,上述的并发集合本身就支持对其线程安全的遍历,其实现方式一般有两种:

  • 快照方式:对待遍历对象的快照进行遍历
  • 准实时方式:准实时是指遍历操作不是针对待遍历对象的副本进行的,但又不借助锁来保障线程安全,从而使得遍历操作可以与更新操作并发进行。

那么准实时是怎么实现的?

  • 常见的准实时遍历方式包括CAS、使用粒度极小的锁

我们简单看下CopyOnWriteArrayList的源码实现吧。

基本元素:

读取操作:

更新操作:(增、删、改)

遍历操作:

好了,类似使用场景等更多的分析我们就不说了。都是一些比较基础的理解,大家在书本以及别的博客上已经看了无数遍了。

同步集合是怎么实现线程安全的?

我们首先从大的方面说:同步集合是通过装饰器模式来实现线程安全的。

简单点理解就是通过在非线程安全的集合对象外边包装一层,通过加互斥锁的方式来实现。客户端代码直接访问线程安全的外包装对象,外包装对象通过互斥锁提供了线程安全的访问方式。并且,外包装对象内部直接引用了内部集合对象的接口,所以两者具有相同的接口。

我们来简单看下源码实现:

基本元素:

接口实现:

OK,看到这里,你也许会想,这不是很简单么,我已经得到了真传,无非就是加锁实现呗,但是这样肯定会降低并发度的。
是的,你说的很有道理,但是你查阅别的资料的时候,经常会看到这么一段分析:

同步集合的iterator方法返回的Iterator实例并不是线程安全的。为了保障对同步集合的遍历操作的线程安全性,我们需要对遍历操作进行加锁

如下所示:

public class SyncCollectionSafeTraversal 
    final List<String> syncList = Collections.synchronizedList(new ArrayList<>());
    public void test()
        Iterator<String> iterator = syncList.iterator();
        //需要对该对象进行加锁,因为返回的Iterator是非线程安全的,降低了并发性能
        synchronized (syncList)
            while (iterator.hasNext())
                System.out.println(iterator.next());
            
        
    

前面不都说了,同步集合是在非线程安全的集合外层通过互斥锁包装了一层么,为啥遍历器又成了非线程安全的了?

我们再来看下iterator实现源码:
嗯?作者竟然真的没有加互斥锁,还特别标注了请使用者自行保证同步。
奇怪了,为啥作者就不加锁了呢?作者是不是有啥难言之隐?

我们来简单分析下:
如果iterator方法上增加互斥锁是否可以实现线程安全

类似这样:

public Iterator<E> iterator() 
        synchronized (mutex) 
            return c.iterator();
        
    

答案显然是不行的。为啥不行呢?

在iterator方法上使用了互斥锁,只能保证我们获取迭代器对象是线程安全的,也就是说只有一个线程会获取迭代器对象。然后呢?我们获取到迭代器对象是需要遍历操作的,在这个过程中,依然会有其余线程不断的对同步集合进行增删改操作。所以在我们迭代遍历的过程中就会出错,所以是无法保证线程安全的。

OK,说到这里,我们解释了本文标题的问题,也就是为什么Java同步集合synchronizedX中的迭代器Iterator使用,需要使用者加锁?

那么我们再来看一下,如果加一把普通的锁可以吗?如下所示:

public class SyncCollectionSafeTraversal 
    final List<String> syncList = Collections.synchronizedList(new ArrayList<>());
    public void test()
        Iterator<String> iterator = syncList.iterator();
        // 加了一把普通的内部锁(这是锁不住滴!!!)
        synchronized (new Object())
            while (iterator.hasNext())
                System.out.println(iterator.next());
            
        
    

答案必然也是不可以的?为啥呢?

因为我们需要保证在迭代器遍历的过程中,集合中的元素不会被增删改,这里加一把普通的内部锁,无法锁住当前同步集合上的增删改操作,也就无法保证集合遍历操作的线程安全性。

正确的使用方法如上上边代码所示,我们应该使用被遍历的同步集合对象本身作为内部锁,利用内部锁的排他性,从而阻止了遍历过程中其他线程改变了同步集合的内部结构。

总结

这篇文章,我们简单分析了Java同步集合在遍历的时候应该如何做好线程安全。知识点很小,主要的目的是引导大家思考。

我们平时看了太多的八股文了,都是简简单单的说同步集合的遍历需要自己加锁,但是鲜有文章会具体阐述为什么需要加锁?应该怎么加锁?为什么不从源码上来保证迭代的线程安全性?所以,在日常的学习中,我们要多加思考,不能死记硬背,要结合实践来理解。

以上是关于Java同步集合synchronizedX中的迭代器Iterator使用,为什么需要使用者加锁?的主要内容,如果未能解决你的问题,请参考以下文章

java 集合框架-ArrayList

java-集合框架3-迭代器的使用

java笔记java中的迭代器Iterator

java 什么是迭代器iterator

JAVA集合集合迭代器快速失败行为及CopyOnWriteArrayList

Java 迭代器Iterator的详解