ArrayList
-
存储方式:动态数组,连续内存存储,适合下标访问
-
扩容机制:初始为10, 采用位运算进行1.5倍扩容;可以定义初始大小
-
null值:允许多个null值存在
-
为什么ArrayList线程不安全?
-
多线程向一个ArrayList对象添加数据,会报ConcurrentModification
-
在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
-
解决方案
-
new Vector<>()
-
Collections.synchronizedList(new ArrayList<>())
-
new CopyOnWriteArrayList()
- 思想:写时复制
- 缺点:内存占用;不能保证实时一致性,只能保证最终一致性。
- 适用于读多写少的场景
-
-
LinkedList
-
存储方式:双向链表,存储在分散的内存中,适合数据插入与删除
-
遍历LinkedList 需要使用iterator,不能使用for(遍历链表)
-
为什么LinkedList线程不安全?
- 两个线程同时插入两个不同元素的时候,这两个元素的next指针都指向了相同的元素。
- 一个线程通过Iterator修改LinkedList结构,另一个线程通过LinkedList对象修改其结构,前一个线程会抛出ConcurrentModificationException异常
HashSet
-
实现方式:以HashMap的key作为Set集合存储元素,value是一个static final 类型的全局常量 PRESENT
-
为什么value放对象却不放null值?
- hashset的add方法调用的hashmap的put方法,put方法返回上次一存放的值;如果key不重复,则返回null,如果key重复,则返回被覆盖的value;如果将value设置成null,则put方法每一次都返回null,,add方法将返回结果和null作比较,每一次都返回true,即使插入相同值,也认为插入成功。
-
-
线程不安全
- 参考HashMap
- 解决方案:new CopyOnWriteArraySet()
HashMap
-
存储方式:数组+单向链表(红黑树)
-
扩容机制:初始数组大小为16,装载因子0.75,数组大小达到阈值时以2的指数倍大小进行扩容
-
为什么扩容时需要2的指数倍?
- 在扩容的时候,原有数组里的数据迁移到新数组里不需要重新hash
-
-
Hash冲突:随着对象的增加,hash算法计算出的对象hashcode相同,此时需要用到对象的equal方法
-
红黑树优化:如果链表的长度超过8,那么链表将转换为红黑树。(数组的大小必须大于64,小于64的时候只会扩容)
-
null值:最多只允许一条记录的键为null,允许多条记录的值为null
-
JDK 1.8加入红黑树的优点?
- 提高HashMap插入和查询整体效率
-
线程不安全
-
JDK 1.7 线程不安全问题?
- 多线程头插法为会出现环形问题:进行扩容resize()时调用transfer()时多线程进入导致的。查找时会陷入死循环
-
JDK 1.8 线程不安全问题?
- 数据覆盖:A,B线程同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null。线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全
- size变小问题:假设两个线程A、B并发进行put操作,当前HashMap的size大小为10,当线程A从主内存中获得size的值为10后,但是由于时间片耗尽只好让出CPU,还未来得及进行+1操作;
而线程B此时获得CPU资源,依然读出size的值为10,并完成+1操作将size=11写回主内存;
之后线程A再次获得时间片并继续执行size+1的操作,将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1
-
解决方案
- HashTable
- ConcurrentHashMap
- Map map = Collections.synchronizedMap(new HashMap<>());
-
-
get操作
- 先计算出key对应的hash值
- 对超出数组范围的hash值进行处理
- 根据正确的hash值(下标值)找到所在的链表的头结点
- 遍历链表,如果key值相等,返回对应的value值,否则返回null
-
put操作
- 先计算出key对应的hash值
- 对超出数组范围的hash值进行处理
- 根据正确的hash值(下标值)找到所在的链表的头结点
- 如果头结点==null,直接将新结点赋值给数组的该位置
- 否则,遍历链表,找到key相等的节点,并进行value值的替换,返回旧的value值
- 如果没有找到,采用尾插法(1.8)/头插法(1.7)创建新结点并插入到链表中
- 将存储元素数量+1
- 校验是否需要扩容
HashTable
- 线程安全:Synchronized关键字保证
- null值:记录的key、value值均不可为null
- 实现方式:数组+链表
LinkedHashMap
- 基于hashmap实现
- LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的
- LinkedHashMap同样使用table存储元素,但元素不再是Node类,而是继承自Node类的Entry类。并新增了两个属性,before, after,表示前一个元素和后一个元素。
Vector
- 存储方式:数组
- 线程安全(Synchronized),效率低
- 较为古老,基本不用
ArrayList的插入和删除操作一定慢于LinkedList吗?
- 在集合里面插入元素速度比对结果是,首部插入,LinkedList更快;中间和尾部插入,ArrayList更快;
在集合里面删除元素类似,首部删除,LinkedList更快;中间删除和尾部删除,ArrayList更快;
由此建议,数据量不大的集合,主要进行插入、删除操作,建议使用LinkedList;
数据量大的集合,使用ArrayList就可以了,不仅查询速度快,并且插入和删除效率也相对较高。