写点什么

使用 JDK 的同步容器时,应该避免那些坑

  • 2022 年 8 月 01 日
  • 本文字数:1912 字

    阅读完需:约 6 分钟

本文分享自华为云社区《【高并发】亿级流量高并发秒杀系统商品“超卖”了,只因使用的JDK同步容器中存在这两个巨大的坑!!(踩坑实录)》,作者:冰 河。

同步容器与并发容器


在 JDK 中,总体上可以将容器分为同步容器和并发容器。



同步容器一般指的是 JDK1.5 版本之前的线程安全的容器,同步容器有个最大的问题,就是性能差,容器中的所有方法都是用 synchronized 保证互斥,串行度太高。在 JDK1.5 之后提供了性能更高的线程安全的容器,我们称之为并发容器


无论是同步容器还是并发容器,都可以将其分为四个大类,分别为:List、Set、Map 和 Queue,如下所示。



接下来,我们就简单聊聊使用 JDK 中的同步容器时,究竟要注意避免哪些坑。

同步容器的坑


在 Java 中,容器可以分为四大类:List、Set、Map 和 Queue,但是在这些容器中,有些容器并不是线程安全的,例如我们经常使用的 ArrayList、HashSet、HashMap 等等就不是线程安全的容器。


那么,根据我们在【精通高并发系列】专栏学习的并发编程知识,如何将一个不是线程安全的容器变成线程安全的呢? 相信有很多小伙伴都能够想到一个办法,那就是把非线程安全的容器的方法都加上 synchronized 锁,使这些方法的访问都变成同步的。


没错,这确实是一种解决方案,例如,我们自定义一个 CustomSafeHashMap 类,内部维护着一个 HashMap,外界对 HashMap 的访问都加上了 synchronized 锁,以此来保证方法的原子性,例如下面的伪代码所示。


public class CustomSafeHashMap<K, V>{    private Map<K, V> innerMap = new HashMap<K, V>();    public synchronized void put(K k, V v){        innerMap.put(k, v);    }        public synchronized V get(K k){        return innerMap.get(k);    }}
复制代码


看到这里,一些小伙伴可能会想:是不是所有的非线程安全的容器类都可以通过为方法添加 synchronized 锁来保证方法的原子性,从而使容器变得安全呢?


是的,我们可以通过为非线程安全的容器方法添加 synchronized 锁来解决容器的线程安全问题。其实,在 JDK 中也是这么做的。例如,在 JDK 中提供了线程安全的 List、Set 和 Map,它们都是通过 synchronized 锁保证线程安全的。


例如,我们可以通过如下方式创建线程安全的 List、Set 和 Map。


List list = Collections.synchronizedList(new ArrayList());Set set = Collections.synchronizedSet(new HashSet());Map map = Collections.synchronizedMap(new HashMap());
复制代码


那么,说了这么多,同步容器有哪些坑呢?

坑一:竞态条件问题


在使用同步容器时需要注意的是,在并发编程中,组合操作要时刻注意竞态条件,例如下面的代码。


public class CustomSafeHashMap<K, V>{    private Map<K, V> innerMap = new HashMap<K, V>();    public synchronized void put(K k, V v){        innerMap.put(k, v);    }        public synchronized V get(K k){        return innerMap.get(k);    }        public synchronized void putIfNotExists(K k, V v){        if(!innerMap.containsKey(k)){             innerMap.put(k, v);        }    }}
复制代码


其中,putIfNotExists()方法就包含组合操作。在高并发环境中,存在组合操作的方法可能就会存在竞态条件。


也就是说,在并发编程中,即使每个操作都能保证原子性,也不能保证组合操作的原子性。

坑二:使用迭代器遍历容器


一个容易被人忽略的坑就是使用迭代器遍历容器,对容器中的每个元素调用一个方法,这就存在了并发问题,这些组合操作不具备原子性。


例如下面的代码,通过迭代器遍历同步 List,对 List 集合中的每个元素调用 format()方法。


List list = Collections.synchronizedList(new ArrayList());Iterator iterator = list.iterator(); while (iterator.hasNext()){    format(iterator.next());}
复制代码


此时,会存在并发问题,这些组合操作并不具备原子性。


如何解决这个问题呢?一个很简单的方式就是锁住 list 集合,如下所示。

List list = Collections.synchronizedList(new ArrayList());synchronized(list){    Iterator iterator = list.iterator();     while (iterator.hasNext()){         format(iterator.next());     }}
复制代码


这里,为何锁住 list 集合就能够解决并发问题呢?


这是因为在 Collections 类中,其内部的包装类的公共方法锁住的对象是 this,其实就是上面代码中的 list,所以,我们对 list 加锁后,就能够保证线程的安全性了。


在 Java 中,同步容器一般都是基于 synchronized 锁实现的,有些是通过包装类实现的,例如 List、Set、Map 等。有些不是通过包装类实现的,例如 Vector、Stack、HashTable 等。


对于这些容器的遍历操作,一定要为容器添加互斥锁保证整体的原子性。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
使用JDK的同步容器时,应该避免那些坑_后端_华为云开发者联盟_InfoQ写作社区