前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程踩坑实录一:盘点JDK中同步容器的两大坑!!

并发编程踩坑实录一:盘点JDK中同步容器的两大坑!!

作者头像
冰河
发布2021-08-13 14:36:23
3480
发布2021-08-13 14:36:23
举报
文章被收录于专栏:冰河技术

大家好,我是冰河~~

关注【冰河技术】微信公众号,学习最牛逼的【精通高并发系列】专栏。

说实话,在实际的工作过程中,我在使用JDK中的并发容器时,确实踩过不少坑。为了让小伙伴们更好的消化这些知识,

今天,首先和小伙伴们分享下使用同步容器时需要注意哪些问题,后续再为大家分享使用并发容器时需要注意哪些问题,以便大家在实际工作过程中尽量少走弯路。

相信很多小伙伴都知道,并发编程一直都是一个难点,不仅仅是Java语言,其他编程语言也是如此。

说它难,不只是并发编程需要我们掌握的知识点比较繁杂,包括:操作系统知识、系统调度知识、CPU、时间片、内存、同步、异步、锁、重排序、多线程、线程池、悲观锁、乐观锁等等一系列知识。

而且并发编程还可能会导致线上生产环境出现一系列的诡异问题,并且这些问题重现几率小,排查困难,需要深入理解并发编程的相关知识才能很好的解决遇到的问题。

如何更好的学习高并发编程,不用着急,关注【冰河技术】微信公众号,订阅【精通高并发系列】就好啦,我们一起进阶,一起吃透高并发编程。

啰嗦了这么多,接下来,我们开始今天的主题,为大家分享下在使用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锁,以此来保证方法的原子性,例如下面的伪代码所示。

代码语言:javascript
复制
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。

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

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

坑一:竞态条件问题

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

代码语言:javascript
复制
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()方法。

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

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

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

代码语言:javascript
复制
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等。

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

好了,今天就到这儿吧,下期我们一起聊聊并发容器中的那些坑,我是冰河,我们下期见~~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 冰河技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 同步容器与并发容器
  • 同步容器的坑
    • 坑一:竞态条件问题
      • 坑二:使用迭代器遍历容器
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档