Java 容器分为 Collection 和 Map 两大类。具体的分类如下:
Collection
List(有序,可重复)
ArrayList,底层用Object数组实现,特点是查询效率高,增删效率低,线程不安全, 初始化长度是10,默认是16,通过定义更大的数组,将旧数组中的值复制到新数组实现扩容 newC = 1.5oldC +1
LinkedList,底层使用双向循环链表实现,特点是查询效率低,增删效率高,线程不安全,因为线程不同步
Vector,底层用长度可以动态增长的对象数组实现,它的相关方法用 Synchronized 进行了线程同步,所以线程安全,效率低
Stack,栈。特点是:先进后出(FILO, First In Last Out)。继承于Vector
Set 无序,不可重复
HashSet,底层用HashMap实现,本质是一个简化版的HashMap,因此查询效率和增删效率都比较高。其add方法就是在map中增加一个键值对,键对象就是这个元素,值对象是PRESENT的对象
LinkedHashSet,LinkedHashSet继承自HashSet,内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。
TreeSet,底层用TreeMap实现,底层是一种二叉查找树(红黑树),需要对元素做内部排序。内部维持了一个简化版的TreeMap,并通过key来存储Set元素。使用时,要对放入的类实现 Comparable接口,且不能放入null
Map
HashMap,采用散列算法来实现,底层用哈希表来存储数据,因此要求键不能重复。线程不安全,HashMap在查找、删除、修改方面效率都非常高。允许key或value为null
哈希表:本质是“数组+链表”,源码中Entery[] table是HashMap的核心数组结构,称为“位桶数组”。其中Entery对象时一个单向链表,存储了四部分(hash值,key,value,next)内容。
早期的hash值总是1,此时,每一个对象都会存储到索引为1的位置,每存储一个都会发生hash冲突,形成了一个非常长的链表,HashMap也就退化成了一个“链表”。
如果利用相除取余算法,能使hash值均匀地分布在[0,数组长度-1]区间内,早期的HashTable就是采用这种算法,但由于用了除法,所以效率非常低。
JDK后来改进了算法,首先约定数组的长度必须为2的整数幂,这样可以使用位运算实现取余效果,hash值 = hashcode & (数组长度-1)。而且为了获得更好的散列效果,JDK对 hashcode进行了两次散列处理,目标就是为了使分布的更散列,更均匀。
扩容问题:hashMap 的位桶数组,初始大小为16.负载因子为0.75,每次扩容2倍。需要注意的是,扩容很耗时。因为扩容的本质是定义更大的数组,并将就数组中的内容逐个复制 到新数组中。在JDK8中,HashMap在存储一个元素时,当对应链表长度大于8时,链表就转化为红黑树,这样就大大提高了查询效率
LinkedHashMap,HashMap和双向链表合二为一即是LinkedHashMap。所谓LinkedHashMap,其落脚点在HashMap,因此更准确地说,它是一个将所有Entry节点链入一个双向链表的HashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap 和 保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。
HashTable,与HashMap类似,只是其中的方法添加了synchronized关键字以确保线程同步检查,线程安全,但效率较低。不允许key或value为null
TreeMap,红黑树的典型实现。TreeMap和HashMap实现了同样的接口Map。在需要Map中Key按照自然排序时才选用TreeMap
ConcurrentHashMap, 它在JDK1.7和1.8中略有差别
JDK1.7中:
由于JDK没有对HashMap做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent
包下,专门用于解决并发问题。和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。
原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
JDK1.8中:
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。
那就是查询遍历链表效率太低
其中抛弃了原有的 Segment 分段锁,而采用了
CAS + synchronized
来保证并发安全性。其中,CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
【CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。】
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(
O(logn)
),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
更多请参考:https://blog.csdn.net/weixin_44460333/article/details/86770169