ConcurrentHashMap
1、hashtable、hashmap、ConcurrentHashMap
一、线程不安全的HashMap数组
由于多线程环境下,使用Hashmap进行put操做会引发死循环,致使CPU利用率接近100%,因此在并发状况下不能使用HashMap。安全
二、效率低下的HashTable数据结构
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的状况下HashTable的效率很是低下。由于当一个线程访问HashTable的同步方法时,其余线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,而且也不能使用get方法来获取元素,因此竞争越激烈效率越低。多线程
三、锁分段技术并发
HashTable容器在竞争激烈的并发环境下表现出效率低下的缘由,是由于全部访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。高并发
2、ConcurrentHashMap的数据结构
ConcurrentHashMap为了提升自己的并发能力,在内部采用了一个叫作Segment的结构,一个Segment其实就是一个类Hash Table的结构,同HashMap同样,Segment包含一个HashEntry数组,数组中的每个HashEntry既是一个键值对,也是一个链表的头节点。能够说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。性能
ConcurrentHashMap定位一个元素的过程须要进行两次Hash操做,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,所以,这一种结构的带来的反作用是Hash的过程要比普通的HashMap要长,可是带来的好处是写操做的时候能够只对元素所在的Segment进行加锁便可,不会影响到其余的Segment,这样,在最理想的状况下,ConcurrentHashMap能够最高同时支持Segment数量大小的写操做(恰好这些写操做都很是平均地分布在全部的Segment上),因此,经过这一种结构,ConcurrentHashMap的并发能力能够大大的提升。优化
3、Segment
Segment继承了ReentrantLock,因此它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不一样Segment的数据进行操做是不用考虑锁竞争的。网站
4、get、put、remove
Get方法:spa
1.为输入的Key作Hash运算,获得hash值。
2.经过hash值,定位到对应的Segment对象
3.再次经过hash值,定位到Segment当中数组的具体位置。
Put方法:
1.为输入的Key作Hash运算,获得hash值。
2.经过hash值,定位到对应的Segment对象
3.获取可重入锁
4.再次经过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。
Remove方法:
定位元素通put方法,不过这里删除元素的方法不是简单地把待删除元素的前面的一个元素的next指向后面一个就完事了,因为HashEntry中的next是final的,一经赋值之后就不可修改,在定位到待删除元素的位置之后,程序就将待删除元素前面的那一些元素所有复制一遍,而后再一个一个从新接到链表上去
5、size()方法
1.遍历全部的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断全部Segment的总修改次数是否大于上一次的总修改次数。若是大于,说明统计过程当中有修改,从新统计,尝试次数+1;若是不是。说明没有修改,统计结束。
5.若是尝试次数超过阈值,则对每个Segment加锁,再从新统计。
6.再次判断全部Segment的总修改次数是否大于上一次的总修改次数。因为已经加锁,次数必定和上次相等。
7.释放锁,统计结束。
为了尽可能不锁住全部Segment,首先乐观地假设Size过程当中不会有修改。当尝试必定次数,才无奈转为悲观锁,锁住全部Segment保证强一致性。
6、jdk1.7和1.8中的区别
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操做,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,可是已经简化了属性,只是为了兼容旧版本
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素做为锁,从而实现了对每一行数据进行加锁,进一步减小并发冲突的几率。
改进二:将原先table数组+单向链表的数据结构,变动为table数组+单向链表+红黑树的结构。对于hash表来讲,最核心的能力在于将key hash以后能均匀的分布在数组中。若是hash以后散列的很均匀,那么table数组中的每一个队列长度主要为0或者1。但实际状况并不是老是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,可是在数据量过大或者运气不佳的状况下,仍是会存在一些队列长度过长的状况,若是仍是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);所以,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度能够下降到O(logN),能够改进性能。
CopyOnWriteArrayList
1、arraylist的问题
多线程时,若是遍历过程当中另外一个线程对list进行插入之后,遍历报错。
2、CopyOnWriteArrayList的实现原理
CopyOnWrite容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此CopyOnWrite容器也是一种读写分离的思想,读和写不一样的容器。
3、CopyOnWrite的应用场景
CopyOnWrite并发容器用于读多写少的并发场景。好比白名单,黑名单,商品类目的访问和更新场景,假如咱们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,可是某些关键字不容许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单天天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,若是在,则提示不能搜索。
4、缺点
1.内存占用问题。由于CopyOnWrite的写时复制机制,因此在进行写操做的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会建立新对象添加到新容器里,而旧容器的对象还在使用,因此有两份对象内存)。
2.数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。因此若是你但愿写入的的数据,立刻能读到,请不要使用CopyOnWrite容器。
5、总结
1.线程安全,读操做时无锁的ArrayList
2.底层数据结构是一个Object[],初始容量为0,以后每增长一个元素,容量+1,数组复制一遍
3.增删改上锁、读不上锁
4.遍历过程因为遍历的只是全局数组的一个副本,即便全局数组发生了增删改变化,副本也不会变化,因此不会发生并发异常
5.读多写少且脏数据影响不大的并发状况下,选择CopyOnWriteArrayList