Java中的集合包括三大类,它们是Set(集)、List(列表)和Map(映射),它们都处于java.util包中,Set、List和Map都是接口,它们有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap。
Collection是最基本的集合接口,声明了适用于JAVA集合的通用方法,list和set都继承自collection接口。
一:Arraylist,LinkedList,Vector
对比一:Arraylist与LinkedList的比较
1、ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
2、因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
3、LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。
4、因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。
对比二:ArrayList与Vector的比较
1、Vector的方法都是同步的,是线程安全的,而ArrayList的方法不是,由于线程的同步必然要影响性能。因此,ArrayList的性能比Vector好。
2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样。ArrayList就有利于节约内存空间。
3、大多数情况不使用Vector,因为性能不好,但是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。
4、Vector可以设置增长因子,而ArrayList不可以。
适用场景分析:
1、Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
2、如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。
二:HashMap,HashTable,ConcurrentHashMap
1.HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这并不是因为HashTable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中
2.HashTable是同步的,HashMap不是,也就是说HashTable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行
3.HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对。
HashMap和ConcurrentHashMap的区别
1、HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。
2、ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。
3、ConcurrentHashMap让锁的粒度更精细一些,并发性能更好
HashMap为什么是线程不安全的?
- 首先需要强调一点,
HashMap
的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。 HashMap
的线程不安全主要是发生在扩容函数中,即根源是在transfer函数中,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点- 而jdk1.8之后进行扩容元素插入时使用的是尾插法。
- put的时候导致的多线程数据不一致。
-
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中, 首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点, 此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行 ,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后, 线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知, 以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录, 这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
- 另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%),具体分析如下:
-
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3, 需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面, 我们发现这三个entry都落到了第二个桶里面,如下图:
-
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。