类集框架
List
java.util.Vector
-
Vector对几乎所有方法都加了锁,且大部分都是synchronized方法,包括get方法,锁的粒度较粗,性能较差,在jdk1.0版本就已经发布,很老旧的类,目前Java已经不推荐使用。
-
Vector初始默认容量为10,默认每次扩容为原来的一倍,可手动指定扩容大小
-
数据存放在一个Object类型的数组中
-
线程安全
java.util.ArrayList
-
线程不安全
-
默认初始容量为10,扩容每次增加原来的一半
-
oldCapacity + (oldCapacity >> 1)
-
-
数据存储在Object类型的名为elementData的数组中,除此之外还有名为EMPTY_ELEMENTDATA的Object空数组用于空实例共享,名为DEFAULTCAPACITY_EMPTY_ELEMENTDATA的Object类型空数组用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,以知道在添加第一个元素时要膨胀多少。
-
默认数组最大长度为Integer.MAX_VALUE - 8,这是因为有些虚拟机需要在数组中保留一些头字,如果尝试分配较大的数组可能会导致OutOfMemoryError
-
每次扩容时会传递一个名为minCapacity的参数,该参数为当前长度+1,如果这个值是负数(超出int最大值会变为负数,补码+1)会抛出OOM,如果新空间大于最大长度,会调用hugeCapacity方法,该方法会判断当前容量+1是否已经超过最大长度,如果超过会返回Integer.MAX_VALUE,如果未超过会返回最大长度,也就是说并不一定在任何情况下扩容都是原来的1.5倍,有以下两种情况:
1. 计算新容量后发现新容量比当前长度+1还要小(溢出等因素),新容量会改成当前长度+1 2. 计算新容量后发现比MAX_ARRAY_SIZE还要大,新容量会根据情况改为Integer.MAX_VALUE或者MAX_ARRAY_SIZE或者抛出OOM。
java.util.LinkedList
-
线程不安全
-
双向链表,存储结构为一个私有的静态内部类Node,结构如下
-
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
-
jdk1.6后加入了descendingIterator方法返回迭代器用于逆向遍历
-
添加元素时int类型的size会++,但是源码中并没有考虑溢出的情况,没有判断也没有抛出任何异常
java.util.concurrent.CopyOnWriteArrayList
- 线程安全(废话,JUC包下的)
- 使用ReentrantLock加锁
- 使用volatile修饰的Object类型的数组
- 默认创建空数组
- 读不加锁写加锁,且都是在finally中释放锁
- 执行写操作时会调用Arrays.copyOf方法创建新数组,并在新数组中写,写后将原引用替换为新数组,所有操作都在加锁情况下进行,这样确定不会频繁触发gc?
Map
java.util.HashMap
-
线程不安全
-
成员变量
-
DEFAULT_INITIAL_CAPACITY = 1 << 4; /** *初始容量为16,必须为2的整数幂,原因后面会说 */
-
MAXIMUM_CAPACITY = 1 << 30; /** *最大容量2的30次方 */
-
DEFAULT_LOAD_FACTOR = 0.75f; /** *默认负载因子 */
-
TREEIFY_THRESHOLD = 8; /** *树化阈值,该值必须大于2,且至少应为8,才可以符合转化回链表的一个假设 */
-
UNTREEIFY_THRESHOLD = 6; /** *转回链表的阈值,应小于TREEIFY_THRESHOLD且不超过6 */
-
MIN_TREEIFY_CAPACITY = 64; /** *最小树化阈值,只有当哈希表容量大于该值,才允许进行树化,否则会进行一次扩容 */
-
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; } /** *hash表基本结构 */
-
Node<K,V>[] table; /** *存放每一根链表的数组,在第一次使用时初始化 */
-
Set<Map.Entry<K,V>> entrySet; /** *键值对集合,没什么好说的 */
-
table数组会在第一次使用时初始化,第一次使用时会调用resize方法,在resize方法中会进行创建数组等操作
-
之所以使用8这个数字作为树化阈值,是因为当使用分布良好的哈希算法时,很少会树化,也就是说每一个桶中链表的长度很少可以达到8个。在理想情况下,使用随机hash算法,桶中节点的的频率(数量)服从泊松分布,当负载因子为0.75时,平均参数约为0.5,忽略方差,每一个桶中的节点数量的预期出现次数为 (exp(-0.5) * pow(0.5, k) / factorial(k)),即:
-
出现次数 概率 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006
也就是说桶中节点数量达到8的概率为0.00000006,已经几乎是不可能事件了,所以采用8这个数字作为树化阈值。
-
-
table数组的类型为Node,但是他也有可能存放TreeNode的根节点,TreeNode并非直接继承Node,而是
-
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> static class Entry<K,V> extends HashMap.Node<K,V>
也就是说其实TreeNode是Node的孙子而非儿子
-
-
hash操作为取键的hashcode方法的返回值记为h,然后h ^ (h >>> 16)取得最终哈希码,即保留高16位并将低16位与高16异或的结果作为低16位
-
取得桶的位置的方式为(n - 1) & hash,其中n为table的大小,很好理解,假设table大小为16,那n - 1就是15,15&任意一个数的结果都不会超过15,所以就不会造成数组越界
-
初始容量可以自己设置,比如设置15,但是他会计算出一个大于15且是2的整数幂的数存在threshold变量中,该变量代表下一次resize时的数组大小,由于HashMap会在第一次使用时才创建table,而创建table时会调用resize方法,所以即便一开始传入的初始容量不是2的整数幂,也依然会创建一个符合HashMap建议的table
-
之所以HashMap要求table容量一直是2的整数幂,是因为在计算桶的位置时,采用的方式是(n - 1) & hash,2的整数幂减去1后的二进制应该是全1,这样一来再进行与运算,可以保证充分的散列,减少hash碰撞,使元素均匀的落到table中
-
java.util.LinkedHashMap
- 线程不安全
- 继承于HashMap
- TreeNode继承于HashMap.Node,增加了before和after字段
- accessOrder字段标记了是访问顺序还是插入顺序
- 总体跟HashMap差不多
java.util.HashTable
- 线程安全
- 使用类型为Entry的数组table来存储数据
- 默认初始容量为11,默认负载因子为0.75
- hash算法为计h为键的hashcode方法的返回值,index = (hash & 0x7FFFFFFF) % tab.length,其中0x7FFFFFFF为首位为0其他位都为1的数,即整数最大值,通过取余防止数组越界
- 通过synchronized方法保证同步线程安全
- 当元素个数超过容量*负载因子时,会触发rehash方法
- 扩容为原来的二倍+1
- 扩容后重新计算hash并存入新的数组中
- 没有继承AbstractMap而是继承与Dictionary,但实现了Map接口
- 同样采用链地址法解决冲突
- iterator遍历过程中其他线程对Hashtable的put、 remove、clear操作都会被成功执行,所以现在并不推荐使用hashtable
java.util.concurrent.ConcurrentHashMap
-
线程安全
-
大部分成员变量与HashMap一样,新增的为:
-
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** *数组最大可能长度,toArray和一些方法会用到 */
-
DEFAULT_CONCURRENCY_LEVEL = 16; /** *并发级别,为了兼容性从之前版本遗留下来的,因为JDK1.8之前ConccurentHashMap保存的是一个个Segment,这个值设定的就是Segment的数量,也就是说设置为16之后,就有16个Segment,而Segment数组一旦初始化后是不能进行扩容的,所以他就会一直保持16的并行度,也就是说最多同时允许16个线程执行安全的并发写操作,当然这是在每一个线程操作的Segment都不同的基础上。 */
-
MIN_TRANSFER_STRIDE = 16; /** *重新绑定每一个转换步骤的最小值 */
-
RESIZE_STAMP_BITS = 16; /** *sizeCtl中用于生成戳的位数,32位的数组至少为6 */
-
MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; /** *帮助调整大小的线程的数量 */
-
RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; /** *sizeCtl中记录大小标志的位变换 */
-
int MOVED = -1; //表明当前正在扩容中,当前的节点元素已经被转移到新table中,头元素hash = -1。 int TREEBIN = -2; //表示当前的桶是一个红黑二叉树桶,头元素hash = -2。 int RESERVED = -3; //一般用于当key对应的值缺失需要计算的场景,在计算出新值之前临时占坑位用的,计算出来之后就用普通Node节点替换掉,头元素hash = -3。 int HASH_BITS = 0x7fffffff; //正常节点哈希的可用位
-
-
桶列表table默认初始大小为16,最大为2^31,负载因子为0.75,当桶中普通链表的元素数量超过8个就会转成红黑树,当桶中红黑树的元素减少到6个就会转成普通的单链表形式。在扩容的过程中,每个线程转移数据的索引数量步伐为
Max(NCPU > 1 ? (n >>> 3) / NCPU : n, 16)
,最小值为16,其中NCPU就是CPU的核心数。 -
对于table大小为n的表格,其散列计算方法为
((hash^(hash >>> 16))&0X7FFFFFFF) & n
,其中n为2的幂值(n = 2^x
) -
ConcurrentHashMap使用的锁分段技术,首先将数据分成一段一段的存储**,**然后给每一段数据配一把锁,当一个线程占用锁修改其中一个段数据的时候,其他段的数据也能被其他线程访问
-
ConcurrentHashMap中频繁使用到了UnSafe类中的native方法,主要用到的有
Unsafe.putObjectVolatile(obj,long,obj2)
、Unsafe.getObjectVolatile
、Unsafe.putOrderedObject
等,在这些本地方法中,有write_barrier
和read_barrier
这两个内存屏障,对应的就是硬件的写屏障和读屏障,JMM中的LoadLoad、LoadStore、StoreStore、StoreLoad四个内存屏障就是基于这两个内存屏障做的 -
ConcurrentHashMap中保存了一个Segment类型的数组,Segment类继承自ReentrantLock来实现加锁,所以每次锁住的就是数组中的一个Segment。
-
每一个Segement中保存了一个HashEntry类型的数组,这就基本对应到了HashMap中的table了。其中还有一个字段为MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;用来保存在scanAndLockForPut方法中自旋获取锁的最大自选次数
-
scanAndLockForPut方法中,会用一个while循环尝试获取锁,每次循环会把初始值为-1的retries变量+1,如果retries>MAX_SCAN_RETRIES,就直接进入阻塞状态,如果尝试获取锁失败,就会遍历一遍对应的链表,找到需要put的所在位置,这样可以把遍历过的entry都放入高速缓存中,当获取到锁时再次定位就会非常高效。
-
在put方法中,会首先尝试获取锁,如果没有获取到就会调用scanAndLockForPut获取锁,由于table本身被volatile关键字修饰,而且put方法已经加了锁,所以在put方法中的变量都没有加volatile关键字,这是如果加了volatile的话编译器就无法对这些变量涉及到的代码进行优化,所以在put方法中将table赋值给了一个局部变量,也是为了这样的优化提升性能
-
在jdk1.8中,也引入了红黑树,而且取消掉了Segement数组,变成了直接对Node进行加锁,这里加锁就直接采用了synchronized块进行加锁,虽说这玩意被优化过(锁膨胀技术),但是总感觉效率是不如juc包中的锁的。
java.util.TreeMap
-
线程不安全
-
成员变量
-
final Comparator<? super K> comparator;//保存了键的比较器
-
Entry<K,V> root;//红黑树根节点
-
-
数据结构就是一颗红黑树,没什么好说的,需要注意的一点是Entry中还保存了一个parent属性,可以通过孩子节点找到父节点
Set
java.util.HashSet
-
线程不安全
-
成员变量
-
HashMap<E,Object> map;//保存了一个HashMap
-
Object PRESENT = new Object();//存放到HashMap中的Value值
-
-
基本所有方法都是调用了HashMap的方法,add方法中调用map.put方法,键就是键,传进去的值是PRESENT。
-
其他什么容量扩容之类的都与HashMap保持一致
java.util.LinkedHashSet
-
线程不安全
-
记录了前一个和后一个节点
-
继承自HashSet,而且除了迭代器以外全部用的是HashSet的方法,而HashSet中保存的是一个HashMap,HashMap如何记录前后节点呢?在HashSet的构造方法中,有这样一个重载
-
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
实际上LinkedHashSet的构造方法中就是调用了父类HashSet的这个重载后的构造,这个构造将自己保存的HashMap创建成了LinkedHashMap(LinkedHashMap继承自HashMap),所以自然就可以保存前后关系啦。
-
java.util.TreeSet
-
线程不安全
-
成员变量
-
NavigableMap<E,Object> m;//保存了一个可导航的Map
-
Object PRESENT = new Object();//跟其他Set一样保存了一个空的值对象
-
-
在构造方法中给m创建了TreeMap对象,并且基本所有方法都是直接调用的TreeMap中的方法。