Java5线程并发库之同步集合
为什么80%的码农都做不了架构师?>>>
同步集合
接下来讲,java5中提供的同步集合。传统的集合在并发访问时是有问题的。比如HashSet,HashMap,ArrayList,像这样的类,如果是多个线程在操作,多个线程在往它里面取数据,放数据,会出问题的。它们是线程不安全的,会把它里面的数据搞得乱七八糟的。
ConcurrentHashMap
下面我们来看一个关于HashMap同步的问题:
Race Condition引起的性能问题 Race Condition(也叫做资源竞争),是多线程编程中比较头疼的问题。特别是Java多线程模型当中,经常会因为多个线程同时访问相同的共享数据,而造成数据的不一致性。为了解决这个问题,通常来说需要加上同步标志“synchronized”,来保证数据的串行访问。但是“synchronized”是个性能杀手,过多的使用会导致性能下降,特别是扩展性下降,使得你的系统不能使用多个CPU资源。 这是我们在性能测试中经常遇见的问题。 可是上个星期我却遇见了相反的情况:因为缺少同步标志也同样会使性能受影响。 那是一个ERP系统,运行在我们的T2000服务器(8核32线程)上。当500个并发用户的时候居然把所有的CPU都压得满满的(90%以上的忙碌)。这是很少有的现象,在我测试的所有项目中很少有扩展性这么好的系统能把T2000的32个线程都占满的。我狠狠的夸了他们的应用。话音没落,却发现测试结果很差,平均响应时间很长。不可能呀,所有的CPU都在干活,而且都在用户态(如果在系统态干太多的活就有问题了),结果怎么还会差呢。CPU都在干嘛呢? 通过工具发现(Dtrace for Java),我们发现很多的CPU都在做一件事情,那就是不停的执行一条Java语句(HashMap.get())。象是进入了死循环。我们进行了进一步试验,让并发用户数量为1,不停的运行10分钟,结果没有发现这种情况;接着我们让50个并发用户同时运行,但是只运行在一个CPU上(通过psrset),结果也没有出现死循环状态。只要并发用户数量超过10个,运行的CPU超过两个,不到2分钟就出现死循环。一旦死循环出现,大量CPU资源被白白浪费,性能自然很差。 通过上面的试验我们可以很肯定的判断,是由于并发控制不好,导致数据的不一致,引起的死循环。值得一提的是,HashMap不是一个线程安全的数据结构,要用到多个线程中去,需要自己加上同步标志,为什么会死循环呢,看看下面HashMap中get函数的源代码: public V get(Object key) { get函数会根据key的hashcode来锁定多个对象,并且遍历这些对象来找到key所对应的对象。当多个线程不安全的修改HanshMap数据结构的时候,有可能使得这个函数进入死循环。 我们建议客户使用ConcurrentHashMap或在使用HanshMap的时候加上同步标志,问题得到解决! |
在以前没有并发库的时候,没有ConcurrentHashMap的时候,人们是怎么解决map的同步问题的呢?人们用的是这个方法Collections.synchronizedMap(null);参数是一个Map<K,V> m,作用是返回由指定map支持的同步(线程安全的)map。他得到的是一个new SynchronizedMap<>(m);这个SynchronizedMap对象对Map做了装饰,把所有的方法中的代码都用synchronized代码块包起来了。对象锁是this,当有一个线程调用了这个对象里面的任何方法,其它的线程就不可以再调用这个对象里面的任何方法了,因为锁已经被再执行的线程拿走了,其它线程只能阻塞,等待锁,拿到锁后再操作。你原来的map是线程不安全的,我返回的map是线程安全的。当然,当有了java5的并发库之后,我们不再建议使用这个工具方法,而是建议使用ConcurrentHashMap,因为它内部做了优化,减小了锁的粒度,提高了多线程并发访问的运行效率。它将hashMap分为了若干个Segment<K, V>段,我们知道,hashMap内部其实就是一个数组Entry<K,V>[] table,那么,我们拿一个数组实现hashMap,对hashMap对象加锁的时候,就会对那个大数组加锁,每次只有一个线程可以进去操作这个数组,如果我用多个数组去维持一个hashmap,每一次进去,我们只对这个数组的一部分进行加锁,这样就减小了锁的粒度。ConcurrentHashMap会维护若干个Segment,每一个Segment都可以理解成是一个小的hashMap,它里面就会获得hashMap的Entry(表),做同步操作的时候,是先定位到这个Segment,然后锁定这一个Segment,执行put,如果有多个线程要来操作,比如说有两个线程,那么这两个线程分别定位到Segment1和Segment2,那么,这时候它们之间的操作是互不影响的。它们可以同时做这个操作,而不需要进行等待,这个竞争相对来说也就小了很多。
这里讲个题外话,讨论一下HashMap和HashSet的关系,HashSet是单列的,HashMap是双列的,有Key和value.在底层一点,HashMap和HashSet之间的关系,其实HashSet内部的实现用的就是一个HashMap。只是它只是用了HashMap的Key,就够了,我这个value部分从来不考虑,我从来不使用它。就把HashMap完全可以当作HashSet用。Key不能重复,完全符合HashSet的要求。所以说,HashSet内部使用的是HashMap.
Java5以后,提供了各种集合相关的同步类。如果你要用map,它提供了并发的HashMap,名叫ConcurrentHashMap.
还提供了ConcurrentSkipListMap
, ConcurrentSkipListSet
, CopyOnWriteArrayList
, CopyOnWriteArraySet
.等同步集合。 下面我们来介绍一下这几个类:
ConcurrentSkipListMap
ConcurrentSkipListMap
:它实现了SortedMap接口,它是一个排序的map,就是说,往这个map里面存的东西是有顺序的,排序要看比较规则。排序一定要传一个比较器进去的,告诉它排序的比较规则是什么。该map可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator
进行排序,具体取决于使用的构造方法。这个就类似于TreeMap. 但是是线程安全的。
ConcurrentSkipListSet
ConcurrentSkipListSet
:set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator
进行排序,具体取决于使用的构造方法。这个就类实于TreeSet,但是是线程安全的。
CopyonWriteArrayList
和CopyonWriteArraySet
接下来CopyOnWriteArrayList
, CopyOnWriteArraySet
这两个要好好介绍一下。
再说这两个类之前,我们先看一下另外一个问题,就是说,我们说再java5以前的某些集合是线程不安全的,除了线程不安全,他还有另外一个隐患,在集合迭代的过程中不可以执行remove操作,在读的过程中不能进行写操作。当我们执行集合的迭代器的next方法时,会检查一个变量modCount,我们把它当作一个版本号,它会去检查这个版本号是否等于expectedModCount预期的版本号的值,如果不相等会抛出ConcurrentModificationException的异常,当我们刚获得迭代器对象的时候,它们二者是相等的,但是当我们调用了集合的增加,删除,删除所有等修改集合数据的方法之后,modCount的值就会++,因此它的值就改变了。modCount的值等于集合的操作次数,集合操作了几次,这个modelCount就等于几。也就是说,在迭代集合的过程中,不能对集合进行修改。修改的话,后果很严重。当然,如果我们调用的时迭代器的remove方法是不会有问题的,程序正常执行。
public class CollectionModifyExceptionTest { public static void main(String[] args) { Collection users = new ArrayList(); users.add(new User("张三", 28)); users.add(new User("李四", 25)); users.add(new User("王五", 31)); Iterator itrUsers = users.iterator();//当得到迭代器的时候,这时modelCount等于3 while (itrUsers.hasNext()) { System.out.println("aaaa"); User user = (User) itrUsers.next();//当循环回来检查时modelCount与预期的值不符合,抛异常 if ("张三".equals(user.getName())) { users.remove(user);//执行了这次remove操作之后,此时modelCount等于4 // itrUsers.remove();//调用迭代器的remove方法时没有问题的,不会抛异常,正常执行 } else { System.out.println(user); } } } } |
public class User implements Cloneable { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof User)) { return false; } User user = (User) obj; // if(this.name==user.name && this.age==user.age) if (this.name.equals(user.name) && this.age == user.age) { return true; } else { return false; } } public int hashCode() { return name.hashCode() + age; } public String toString() { return "{name:'" + name + "',age:" + age + "}"; } public Object clone() { Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException e) { } return object; } public void setAge(int age) { this.age = age; } public String getName() { return name; } } |
接下来我们要解决集合在迭代的时候不能修改的问题,怎么解决呢?
这里我们把ArrayList对象换成CopyOnWriteArrayList,就可以了。它在我们对集合进行写操作的时候会保留一份拷贝,在remove操作时,它会创建一个新的数组对象,这个新数组对象的引用赋给了集合的数组成员变量,这个新数组中移除了要移除的元素。也就是说,其实它并不担心一边迭代,一边执行写操作。它迭代的是一个不变的对象,而写操作会保留到另一个对象。然后它又是支持并发的。它允许迭代的时候修改集合,并且是线程安全的。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。
public class CollectionModifyExceptionTest { public static void main(String[] args) { Collection users = new CopyOnWriteArrayList(); // new ArrayList(); users.add(new User("张三", 28)); users.add(new User("李四", 25)); users.add(new User("王五", 31)); Iterator itrUsers = users.iterator();//当得到迭代器的时候,这时modelCount等于3 while (itrUsers.hasNext()) { System.out.println("aaaa"); User user = (User) itrUsers.next();//当循环回来检查时modelCount与预期的值不符合,抛异常 if ("张三".equals(user.getName())) { users.remove(user);//执行了这次remove操作之后,此时modelCount等于4 // itrUsers.remove();//这里就不能调用迭代器的remove方法了,否则会抛出异常 } else { System.out.println(user); } } } } |
转载于:https://my.oschina.net/kangxi/blog/1822504
Java5线程并发库之同步集合相关推荐
- Java5线程并发库之LOCK(锁)CONDITION(条件)实现线程同步通信
为什么80%的码农都做不了架构师?>>> Lock(锁)&Condition(条件)实现线程同步通信 接下来介绍,java5线程并发库里面的锁.跟锁有关的类和接口主要是 ...
- Java5线程并发库之保障变量的原子性操作
为什么80%的码农都做不了架构师?>>> java.util.concurrent.atomic 首先我们看java.util.concurrent.atomic包,它主要是提 ...
- Java多线程与线程并发库高级应用笔记
以下内容是学习张老师Java多线程与线程并发库高级应用时所做的笔记,很有用 网络编辑器直接复制Word文档排版有点乱,提供原始文件下载 先看源文件概貌 张孝祥_Java多线程与并发库高级应用 [视频介 ...
- 线程并发库和线程池的作用_线程和并发介绍
线程并发库和线程池的作用 本文是我们名为Java Concurrency Essentials的学院课程的一部分. 在本课程中,您将深入探讨并发的魔力. 将向您介绍并发和并发代码的基础知识,并学习诸如 ...
- java基础巩固笔记(5)-多线程之线程并发库
2019独角兽企业重金招聘Python工程师标准>>> java基础巩固笔记(5)-多线程之线程并发库 标签: java [TOC] 本文主要概述java.util.concurre ...
- 线程并发库和线程池的作用_并发–顺序线程和原始线程
线程并发库和线程池的作用 不久前,我参与了一个项目,该项目的报告流程如下: 用户会要求举报 报告要求将被翻译成较小的部分 基于零件/节的类型的每个零件的报告将由报告生成器生成 组成报告的各个部分将重新 ...
- 使用Java线程并发库实现两个线程交替打印的线程题
背景:是这样的今天在地铁上浏览了以下网页,看到网上一朋友问了一个多线程的问题.晚上闲着没事就决定把它实现出来. 题目: 1.开启两个线程,一个线程打印A-Z,两一个线程打印1-52的数据. 2.实现交 ...
- Java5的 线程并发库
java.util.concurrent.atomic 一个小型工具包,支持单个变量上的无锁线程安全编程. 包含的类: 这些类的相关操作都是原子性的 java.util.concurrent 线程池 ...
- java线程并发库之--线程同步工具CountDownLatch用法
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 主要方法 public CountDownLatch(int count); pu ...
- java线程并发库之--线程同步工具Exchanger的使用
Exchanger可以在两个线程之间交换数据,只能是2个线程,他不支持更多的线程之间互换数据.今天我们就通过实例来学习一下Exchanger的用法. Exchanger的简单实例 Exchanger是 ...
最新文章
- 10.27 noip模拟试题
- [转]IaaS、PaaS、SaaS、CaaS、MaaS五者的区别
- 目标跟踪_POI算法
- 我的第一个python web开发框架(23)——代码版本控制管理与接口文档
- linux中级之ansible配置(roles)
- 云南数据中心机房资源介绍、IDC资源分布,机房数据中心建设解决方案
- 阿里巴巴矢量图标引入方法
- 基于arduino制作激光电子竖琴
- NeoKylin中标麒麟系统用户和组的管理命令
- 传统蓝牙base on pincode配对以及安全简单配对(Secure Simple Pairing)流程介绍
- TIPTOP 智能仓库扫描系统解决方案
- 如何更改虚拟光驱与物理光驱的盘符
- 搭建K8S dashboard
- RocketMQ保姆级教程
- 漏极开路 推挽输出方式和弱上拉都什么意思
- css伪类渐变,CSS3文字 渐变内发光投影效果_css
- zotero+坚果云实现同步
- 学习贴:恶意软件类型及本质
- 《炬丰科技-半导体工艺》旋转超声雾化液中新型处理GaAs表面湿法清
- Win10安装nessus8.10系列
热门文章
- 从Ibatis过渡到Mybatis-比较Mybaits较与Ibatis有哪些方面的改进
- linux---动静态库编译及使用
- linux下目录的基本命令
- 防火墙配置十大任务之五,有NAT的两个接口的配置
- 应用程序正常初始化(0xc015002)失败解决方法
- 黑客获取数据信息的目的和进攻手段及应对之策
- datetime对应的jdbc mysql_Java连接MySQL数据库
- 为什么手工drop_caches之后cache值并未减少?
- SQlite数据库的C编程接口(四) 绑定参数(Bound Parameters) ——《Using SQlite》读书笔记
- JM8.6之get_block() 1/4亮度像素内插详述