JDK基础数据类型与集合类
最基础的类型分位三类:
- 原生类型
- 数组类型
- 对象引用类型
基于这几种基础类型的不同嵌套,在java.util的工具包里又构建出了很多不同种类、不同形态、不同作用的一些集合类:
- 线性数据结构
- List:ArrayList、LinkedList、Vector、Stack
- Set:LinkedSet、HashSet、TreeSet
- Queue:Deque->LinkedList
- Map:HashMap、LinkedHashMap、TreeMap
- Dictionary->HashTable->properties
ArrayList(非线程安全)
基本特点:
基于数组,便于按index访问,超过数组需要扩容,扩容成本较高。
补充:
- 下标访问,速度比较快。
- 为什么要扩容,因为他是数组,数组创建是需要指定大小的。
- 修改比较复杂:当我们往中间或者头部插入元素时,需要把后面所有的元素都往后挨个挪动一个位置。
用途:
大部分情况下操作一组数据都可以用ArrayList
原理:
使用数组模拟列表,默认大小10,扩容x1.5,newCapacity = oldCapacity + (oldCapacity >> 1)
内部实现是数组:transient Object[] elementData;
安全问题:
- 写冲突
- 两个写,相互操作冲突
- 读写冲突
- 读,特别是iterator的时候,数据个数变了,拿到了非预期数据或者报错
- 产生ConcurrentModificationException
LinkedList(非线程安全)
基本特点:
使用链表实现,无需扩容。
补充:
- 因为是链表,不是数组,没有大小限制
- 修改比较容易:因为有指针,都是每个指向下一个。中间插数据时,只需要修改对应指针,不需要移动任何严肃其他元素。
- 双向链表
用途:
不知道容量,插入变动多的情况
原理:
使用双向指针将所有节点连起来
内部实现:transient Node<E> first;
什么是Node:
安全问题:
- 写冲突
- 两个写,相互操作冲突
- 读写冲突
- 读,特别是iterator的时候,数据个数变了,拿到了非预期数据或者报错
- 产生ConcurrentModificationException
List线程安全的简单办法
既然线程安全是写冲突和读写冲突导致的,最简单的办法就是读写都加锁。
例如:
- ArrayList的方法上都加上synchronized -> Vector
- Collections.synchronizedList,强制将List的操作加上同步
- Arrays.asList,不允许添加删除,但是可以set替换元素。(不可以修改元素个数)
- Collections.unmodifiableList,不允许修改内容,包括添加删除和set。
CopyOnWriteArrayList(类似快照,读写分离)
在List上加synchronized缺点:
相当于加了一把很大的锁,作用在了所有的get、set方法上。谁先抢到读,谁先抢到写,是无法预知的,导致最终结果也是一个不可预知的结果。
核心改进原理(既保证线程安全,又能正常进行并发操作):
- 写加锁,保证不会写混乱
- 写在一个Copy副本上,而不是原始数据上(GC young区用复制,old区用本区内的移动)
- 读不加锁,并发读,因为用的是老的快照数组
- 使用迭代器的时候,也是拿到老的快照数组来做操作。此后有List的元素变动,就跟这次迭代没关系了。
适用场景:
读比较频繁,写的少。(因为读一直都是在并发执行的,而且读写分离不会出现读的数据混乱)
想想:
淘宝商品的item快照。商品价格会变,每次下单都会生成一个当时商品信息的快照。
所以快照是我们并发、程序优化、业务优化的一个非常重要的手段。
可以参照CopyOnWriteArrayList的读写实现的源码,理解一下快照的使用精髓,以及他是如何避免并发冲突的。
HashMap
基本特点:
数组+链表,空间换时间,哈希冲突不大的情况下查找数据性能很高。
用途:
存放指定key的对象,缓存的对象。
原理:
使用hash原理,存K-V数据,初始容量16,扩容x2,负载因子0.75
JDK8以后,在链表长度到8&数组长度到64时,使用红黑树
安全问题:
- 写冲突
- 读写问题,可能会死循环
- Keys()无序问题
补充:扩容会导致一部分数据之前槽对应的entry移动到其他的槽(因为取余)。
- 由于链表的变化,又有其他的写入和读取操作时,就有可能导致死循环
- 从而扩容会导致乱序
补充:
- 获取数据:
当获取其中某个key对应的value时,先通过hashcode取模,获取到对应数组元素位置,进而拿到链表。再循环这条链表(链表里放的是entry,entry里放的就是key-value),比较每个key的值跟我们要的key的值是否一样,就把对应的entry拿出来。那里面就有他的value。
- 哈希冲突:
尽量不要把hashmap装满了,否则就会导致哈希冲突,影响性能。
- 负载因子:
- 槽的数量(数组长度)与元素的数量的比例。决定是否扩容。(当超过3/4的槽已经有数据时,就需要扩容了,因为在不扩容的话就要充满了。当有一大半的槽都有数据时,这时候随机来个数据,大概率就会命中当前已有数据的这些槽,导致会让对应槽的链表边长。)
- 一般情况下,负载因子越低,哈希冲突的情况就会越小。但是反过来,负载因子越低,也就会导致大部分时间内很多空闲的槽没有被使用上,从而浪费内存空间。
- 太小:太小性能很好,但空间浪费多
- 太大,空间浪费没那么严重,但是性能就下降了。
- 初始容量:
- 太大:可能只放少量数据,导致空间浪费
- 太小:导致扩容频繁
- 扩容因子:
- 太大:空间浪费
- 太小:频繁扩容
- 复杂度(与红黑树比较):
- 数组+链表:N(想从链表中去拿key,需要循环一遍当前链表)
- 红黑树:logN
- 因为红黑树本身的维护、构建、平衡等操作的成本比较大,所以要在数组+链表达到一定规模时启用。
- 类比数组的Sort排序操作,少量数据,采用冒泡算法;超过固定数据,采用快排。
LinkedHashMap
基本特点:
继承自HashMap,对entry集合添加了一个双向链表。
用途:
保证有序,特别是Java8 stream操作的的toMap时使用
原理:
同LinkedList,包括插入顺序和访问顺序
安全问题:
同HashMap
ConcurrentHashMap-Java7 分段锁
分段锁:(每个段一个小锁)
默认16个Segment,降低锁粒度。 concurrentLevel = 16
想想:
Segment[] ~ 分库
HashEntry[] ~ 分表
ConcurrentHashMap-Java8
去掉了Segment
- 红黑树本身就可以让不同的线程去操作不同的树的分支
- CAS乐观锁 - 无锁技术
比对:
- Java 7为实现并行访问,引入了Segment这一结构,实现了分段锁。理论上最大并发度与Segment个数相等。
- Java 8进一步提高并发性,摒弃了分段锁的方案,而是直接使用了一个大的数组。