使用JDK的同步容器时,应该避免那些坑?
摘要:在使用JDK中的同步容器时,应该尽量避免哪些坑
本文分享自华为云社区《【高并发】亿级流量高并发秒杀系统商品“超卖”了,只因使用的JDK同步容器中存在这两个巨大的坑!!(踩坑实录)》,作者:冰 河。
同步容器与并发容器
在JDK中,总体上可以将容器分为同步容器和并发容器。
![](https://pic3.zhimg.com/80/v2-b6557550ffdf6deb5b9a9e60b3afc546_720w.jpg)
同步容器一般指的是JDK1.5版本之前的线程安全的容器,同步容器有个最大的问题,就是性能差,容器中的所有方法都是用synchronized保证互斥,串行度太高。在JDK1.5之后提供了性能更高的线程安全的容器,我们称之为并发容器。
无论是同步容器还是并发容器,都可以将其分为四个大类,分别为:List、Set、Map和Queue,如下所示。
![](https://pic1.zhimg.com/80/v2-f7a2df44bc8c828585def1456320eefc_720w.jpg)
接下来,我们就简单聊聊使用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等。
对于这些容器的遍历操作,一定要为容器添加互斥锁保证整体的原子性。
相关文章
- 原子设计系统
- 前端架构设计
- 学习项目管理
- Web设计思想——渐进增强
- 移动端H5通用表单验证插件
- 程序员英语高效学习法
- 让正则更上一层楼
- 移动端 H5图片裁剪插件,内置简单手势操作
- Web性能优化工具WebPageTest(三)——本地部署(Windows 7版本)
- Web性能优化工具WebPageTest(二)——性能数据
- Web性能优化工具WebPageTest(一)——总览与配置
- 几句话描述简单算法——排序与搜索
- 表格花式效果
- 谨慎能捕千秋蝉(三)——界面操作劫持与HTML5安全
- 谨慎能捕千秋蝉(二)——CSRF
- 谨慎能捕千秋蝉(一)——XSS
- Wireshark网络抓包(四)——工具
- Wireshark网络抓包(三)——网络协议
- Wireshark网络抓包(二)——过滤器
- Wireshark网络抓包(一)——数据包、着色规则和提示