Java类集框架总结

类集框架

List
java.util.Vector
  1. Vector对几乎所有方法都加了锁,且大部分都是synchronized方法,包括get方法,锁的粒度较粗,性能较差,在jdk1.0版本就已经发布,很老旧的类,目前Java已经不推荐使用。

  2. Vector初始默认容量为10,默认每次扩容为原来的一倍,可手动指定扩容大小

  3. 数据存放在一个Object类型的数组中

  4. 线程安全

java.util.ArrayList
  1. 线程不安全

  2. 默认初始容量为10,扩容每次增加原来的一半

    • oldCapacity + (oldCapacity >> 1)
      
  3. 数据存储在Object类型的名为elementData的数组中,除此之外还有名为EMPTY_ELEMENTDATA的Object空数组用于空实例共享,名为DEFAULTCAPACITY_EMPTY_ELEMENTDATA的Object类型空数组用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,以知道在添加第一个元素时要膨胀多少。

  4. 默认数组最大长度为Integer.MAX_VALUE - 8,这是因为有些虚拟机需要在数组中保留一些头字,如果尝试分配较大的数组可能会导致OutOfMemoryError

  5. 每次扩容时会传递一个名为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
  1. 线程不安全

  2. 双向链表,存储结构为一个私有的静态内部类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;
          }
      }
      
  3. jdk1.6后加入了descendingIterator方法返回迭代器用于逆向遍历

  4. 添加元素时int类型的size会++,但是源码中并没有考虑溢出的情况,没有判断也没有抛出任何异常

java.util.concurrent.CopyOnWriteArrayList
  1. 线程安全(废话,JUC包下的)
  2. 使用ReentrantLock加锁
  3. 使用volatile修饰的Object类型的数组
  4. 默认创建空数组
  5. 读不加锁写加锁,且都是在finally中释放锁
  6. 执行写操作时会调用Arrays.copyOf方法创建新数组,并在新数组中写,写后将原引用替换为新数组,所有操作都在加锁情况下进行,这样确定不会频繁触发gc?
Map
java.util.HashMap
  1. 线程不安全

  2. 成员变量

    1. DEFAULT_INITIAL_CAPACITY = 1 << 4;
      /**
      *初始容量为16,必须为2的整数幂,原因后面会说
      */
      
    2. MAXIMUM_CAPACITY = 1 << 30;
      /**
      *最大容量2的30次方
      */
      
    3. DEFAULT_LOAD_FACTOR = 0.75f;
      /**
      *默认负载因子
      */
      
    4. TREEIFY_THRESHOLD = 8;
      /**
      *树化阈值,该值必须大于2,且至少应为8,才可以符合转化回链表的一个假设
      */
      
    5. UNTREEIFY_THRESHOLD = 6;
      /**
      *转回链表的阈值,应小于TREEIFY_THRESHOLD且不超过6
      */
      
    6. MIN_TREEIFY_CAPACITY = 64;
      /**
      *最小树化阈值,只有当哈希表容量大于该值,才允许进行树化,否则会进行一次扩容
      */
      
    7. static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
      }
      /**
      *hash表基本结构
      */
      
    8. Node<K,V>[] table;
      /**
      *存放每一根链表的数组,在第一次使用时初始化
      */
      
    9. Set<Map.Entry<K,V>> entrySet;
      /**
      *键值对集合,没什么好说的
      */
      
    10. table数组会在第一次使用时初始化,第一次使用时会调用resize方法,在resize方法中会进行创建数组等操作

    11. 之所以使用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这个数字作为树化阈值。

    12. 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的孙子而非儿子

    13. hash操作为取键的hashcode方法的返回值记为h,然后h ^ (h >>> 16)取得最终哈希码,即保留高16位并将低16位与高16异或的结果作为低16位

    14. 取得桶的位置的方式为(n - 1) & hash,其中n为table的大小,很好理解,假设table大小为16,那n - 1就是15,15&任意一个数的结果都不会超过15,所以就不会造成数组越界

    15. 初始容量可以自己设置,比如设置15,但是他会计算出一个大于15且是2的整数幂的数存在threshold变量中,该变量代表下一次resize时的数组大小,由于HashMap会在第一次使用时才创建table,而创建table时会调用resize方法,所以即便一开始传入的初始容量不是2的整数幂,也依然会创建一个符合HashMap建议的table

    16. 之所以HashMap要求table容量一直是2的整数幂,是因为在计算桶的位置时,采用的方式是(n - 1) & hash,2的整数幂减去1后的二进制应该是全1,这样一来再进行与运算,可以保证充分的散列,减少hash碰撞,使元素均匀的落到table中

java.util.LinkedHashMap
  1. 线程不安全
  2. 继承于HashMap
  3. TreeNode继承于HashMap.Node,增加了before和after字段
  4. accessOrder字段标记了是访问顺序还是插入顺序
  5. 总体跟HashMap差不多
java.util.HashTable
  1. 线程安全
  2. 使用类型为Entry的数组table来存储数据
  3. 默认初始容量为11,默认负载因子为0.75
  4. hash算法为计h为键的hashcode方法的返回值,index = (hash & 0x7FFFFFFF) % tab.length,其中0x7FFFFFFF为首位为0其他位都为1的数,即整数最大值,通过取余防止数组越界
  5. 通过synchronized方法保证同步线程安全
  6. 当元素个数超过容量*负载因子时,会触发rehash方法
  7. 扩容为原来的二倍+1
  8. 扩容后重新计算hash并存入新的数组中
  9. 没有继承AbstractMap而是继承与Dictionary,但实现了Map接口
  10. 同样采用链地址法解决冲突
  11. iterator遍历过程中其他线程对Hashtable的put、 remove、clear操作都会被成功执行,所以现在并不推荐使用hashtable
java.util.concurrent.ConcurrentHashMap
  1. 线程安全

  2. 大部分成员变量与HashMap一样,新增的为:

    1. MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
      /**
      *数组最大可能长度,toArray和一些方法会用到
      */
      
    2. DEFAULT_CONCURRENCY_LEVEL = 16;
      /**
      *并发级别,为了兼容性从之前版本遗留下来的,因为JDK1.8之前ConccurentHashMap保存的是一个个Segment,这个值设定的就是Segment的数量,也就是说设置为16之后,就有16个Segment,而Segment数组一旦初始化后是不能进行扩容的,所以他就会一直保持16的并行度,也就是说最多同时允许16个线程执行安全的并发写操作,当然这是在每一个线程操作的Segment都不同的基础上。
      */
      
    3. MIN_TRANSFER_STRIDE = 16;
      /**
      *重新绑定每一个转换步骤的最小值
      */
      
    4. RESIZE_STAMP_BITS = 16;
      /**
      *sizeCtl中用于生成戳的位数,32位的数组至少为6
      */
      
    5. MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
      /**
      *帮助调整大小的线程的数量
      */
      
    6. RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
      /**
      *sizeCtl中记录大小标志的位变换
      */
      
    7. int MOVED = -1; //表明当前正在扩容中,当前的节点元素已经被转移到新table中,头元素hash = -1。
      int TREEBIN = -2; //表示当前的桶是一个红黑二叉树桶,头元素hash = -2。
      int RESERVED = -3; //一般用于当key对应的值缺失需要计算的场景,在计算出新值之前临时占坑位用的,计算出来之后就用普通Node节点替换掉,头元素hash = -3。
      int HASH_BITS = 0x7fffffff; //正常节点哈希的可用位
      
  3. 桶列表table默认初始大小为16,最大为2^31,负载因子为0.75,当桶中普通链表的元素数量超过8个就会转成红黑树,当桶中红黑树的元素减少到6个就会转成普通的单链表形式。在扩容的过程中,每个线程转移数据的索引数量步伐为Max(NCPU > 1 ? (n >>> 3) / NCPU : n, 16),最小值为16,其中NCPU就是CPU的核心数。

  4. 对于table大小为n的表格,其散列计算方法为((hash^(hash >>> 16))&0X7FFFFFFF) & n,其中n为2的幂值(n = 2^x)

  5. ConcurrentHashMap使用的锁分段技术,首先将数据分成一段一段的存储**,**然后给每一段数据配一把锁,当一个线程占用锁修改其中一个段数据的时候,其他段的数据也能被其他线程访问

  6. ConcurrentHashMap中频繁使用到了UnSafe类中的native方法,主要用到的有Unsafe.putObjectVolatile(obj,long,obj2)Unsafe.getObjectVolatileUnsafe.putOrderedObject等,在这些本地方法中,有write_barrierread_barrier这两个内存屏障,对应的就是硬件的写屏障和读屏障,JMM中的LoadLoad、LoadStore、StoreStore、StoreLoad四个内存屏障就是基于这两个内存屏障做的

  7. ConcurrentHashMap中保存了一个Segment类型的数组,Segment类继承自ReentrantLock来实现加锁,所以每次锁住的就是数组中的一个Segment。

  8. 每一个Segement中保存了一个HashEntry类型的数组,这就基本对应到了HashMap中的table了。其中还有一个字段为MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;用来保存在scanAndLockForPut方法中自旋获取锁的最大自选次数

  9. scanAndLockForPut方法中,会用一个while循环尝试获取锁,每次循环会把初始值为-1的retries变量+1,如果retries>MAX_SCAN_RETRIES,就直接进入阻塞状态,如果尝试获取锁失败,就会遍历一遍对应的链表,找到需要put的所在位置,这样可以把遍历过的entry都放入高速缓存中,当获取到锁时再次定位就会非常高效。

  10. 在put方法中,会首先尝试获取锁,如果没有获取到就会调用scanAndLockForPut获取锁,由于table本身被volatile关键字修饰,而且put方法已经加了锁,所以在put方法中的变量都没有加volatile关键字,这是如果加了volatile的话编译器就无法对这些变量涉及到的代码进行优化,所以在put方法中将table赋值给了一个局部变量,也是为了这样的优化提升性能

  11. 在jdk1.8中,也引入了红黑树,而且取消掉了Segement数组,变成了直接对Node进行加锁,这里加锁就直接采用了synchronized块进行加锁,虽说这玩意被优化过(锁膨胀技术),但是总感觉效率是不如juc包中的锁的。

java.util.TreeMap
  1. 线程不安全

  2. 成员变量

    1. final Comparator<? super K> comparator;//保存了键的比较器
      
    2. Entry<K,V> root;//红黑树根节点
      
  3. 数据结构就是一颗红黑树,没什么好说的,需要注意的一点是Entry中还保存了一个parent属性,可以通过孩子节点找到父节点

Set
java.util.HashSet
  1. 线程不安全

  2. 成员变量

    1. HashMap<E,Object> map;//保存了一个HashMap
      
    2. Object PRESENT = new Object();//存放到HashMap中的Value值
      
  3. 基本所有方法都是调用了HashMap的方法,add方法中调用map.put方法,键就是键,传进去的值是PRESENT。

  4. 其他什么容量扩容之类的都与HashMap保持一致

java.util.LinkedHashSet
  1. 线程不安全

  2. 记录了前一个和后一个节点

  3. 继承自HashSet,而且除了迭代器以外全部用的是HashSet的方法,而HashSet中保存的是一个HashMap,HashMap如何记录前后节点呢?在HashSet的构造方法中,有这样一个重载

    1. HashSet(int initialCapacity, float loadFactor, boolean dummy) {
          map = new LinkedHashMap<>(initialCapacity, loadFactor);
      }
      

    实际上LinkedHashSet的构造方法中就是调用了父类HashSet的这个重载后的构造,这个构造将自己保存的HashMap创建成了LinkedHashMap(LinkedHashMap继承自HashMap),所以自然就可以保存前后关系啦。

java.util.TreeSet
  1. 线程不安全

  2. 成员变量

    1. NavigableMap<E,Object> m;//保存了一个可导航的Map
      
    2. Object PRESENT = new Object();//跟其他Set一样保存了一个空的值对象
      
  3. 在构造方法中给m创建了TreeMap对象,并且基本所有方法都是直接调用的TreeMap中的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值