学习记录 3/25

79 阅读5分钟
  1. ConcurrentHashMap
    1. jdk1.7
      1. 数据结构是Segment+数组+链表
      2. 使用Segment分段锁机制控制并发,默认并发级别是16(一定是2的幂),默认容量是16(即默认每个Segment中的Hash数组的长度是16/16=1==》2,一定是2的幂),默认负载因子是0.75
      3. 扩容是Segment独立扩容,每个Segment中都是独立的HashMap,扩容的触发条件是单个Segment中的元素个数达到其扩容阈值。
      4. ConcurrentHashMap的总容量只用在初始化的时候计算每个Segment中的容量。
      5. 插入会首先hash计算选择Segment再计算桶的位置,获取锁之后先遍历是否有相同的,有则覆盖,没有则头插法
      6. key和value都不允许为NULL
      7. size()方法统计元素数量,需要遍历所有Segment进行累加,且当统计过程中modCount发生变化时需要对所有Segment加锁重新统计【效率低】
    2. jdk1.8
      1. 数据结构是Node+链表/红黑树
      2. 使用CAS+synchronized控制并发,锁的粒度是单个Node
      3. 默认数组容量是16(一定是2的幂),默认的负载因子是0.75
      4. 当链表长度达到8且数组的长度大于等于64时,会将链表转化成红黑树,但如果数组长度没有达到64,会进行扩容。而在删除节点时如果树节点的个数小于等于6则会将树结构退化为链表。
      5. 扩容时可以多线程并发扩容,每个线程分别负责一组桶进行扩容,因为扩容后旧的桶中的元素要么在原来位置的桶中,要么在原来位置+旧桶容量的新位置(这依托于数桶数组的大小一定是2的幂,在rehash时只需要判断多出的最高位是0还是1,即可知道该元素的位置,使用位运算判断rehash的位置相较于重新计算hash也提高了rehash的计算效率)。所以多线程并发控制不同的桶进行扩容不会产生冲突,可以提高扩容效率。
      6. key和value都不允许为NULL
      7. size()方法统计元素数量,引入了CounterCellLongAdder统计元素数量,更高效且无需加锁。当更新元素数量时,可以让每个线程更新自己持有的CounterCell,最终统计所有CounterCell得到总的元素数量,当线程竞争比较激烈时,即CounterCell不够用的时候,LongAdder会继续扩容出CounterCell给线程使用。
      8. put()插入流程
        • (1)判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; (2)根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向(6),如果table[i]不为空,转向(3); (3)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向(4),这里的相同指的是hashCode以及equals; (4)判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向(5); (5)遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; (6)插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
      9. get()获取key对应的value流程
        1. 首先根据hash找到桶
        2. 如果桶不为空则根据hash、地址、equals判断是否与头结点相同,相同则直接返回
        3. 否则,判断头结点hash值是否小于0,如果小于0则表明正在扩容或者桶中是红黑树结构,使用find方法查找。如果正在扩容会在旧桶和新桶中都进行查找(不用加锁,且避免遗漏),如果是树则在树中查找。
        4. 否则,顺着链表查找。
  2. HashTable
    1. 线程安全,使用synchronized关键字给方法加锁控制并发。
    2. key和value都不允许为NULL
    3. 默认容量11,默认负载因子0.75,达到阈值扩容为原容量的2n+1。
    4. 哈希冲突采用拉链法解决,尾插法
  3. 使用JMH对ConcurrentHashMapHashTable进行性能测试,8个线程并发,性能差距还是比较大的
    1. 测试环境
      • CPU:12th Gen Intel(R) Core(TM) i5-12400
      • RAM:62G
    2. 每个线程:存储的同时读取一百万个简单元素
      1. ConcurrentHashMap:401.407 ms/op
      2. HashTable: 3159.129 ms/op
  4. HashSet
    1. 底层数据结构是HashMap(数组+链表),以Key存储元素值,Value设置定固定的对象。
    2. 不可重复,无序,线程不安全
    3. 支持存储NULL
  5. LinkedHashSet
    1. 底层数据结构是LinkedHashMap(数组+链表+双向链表),以Key存储元素值,Value设置定固定的对象。
    2. 不可重复,按照插入顺序进行遍历,线程不安全
    3. 支持存储NULL
  6. TreeSet
    1. 底层数据结构是TreeMap(红黑树),以Key存储元素值,Value设置定固定的对象。
    2. 不可重复,按照存储Comparator或者存储对象的Comparable定义的比较规则进行排序
    3. 因为需要可比较,所以不支持存储NULL
  7. 线程安全的Set
    1. 通过 Collections.synchronizedSet() 方法包装一个线程不安全的 Set
    2. CopyOnWriteArraySet
      1. 基于 CopyOnWriteArrayList 实现,允许存储NULL
    3. ConcurrentSkipListSet
      1. 底层数据结构是ConcurrentSkipListMap,基于跳表实现,不允许存储NULL
  8. LeetCode 33.搜索旋转排序数组
    1. 考察二分查找