IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 集合相关学习Map、List、Set -> 正文阅读

[数据结构与算法]集合相关学习Map、List、Set

HashMap分析(线程不安全)

通过数组+链表的方式组合构成,通过hash的方式找到数组的下标

hash碰撞问题 -> 多个key通过hash运算后得到同一个数组下标

  • 线性探索(开放寻址法) ,threadLocal通过这个方法解决,用通俗易懂的语音描述就是:在插入的过程中定位到table中的位置判断是否为空,若为空直接设置值,若不为空则,且Entry对象的key刚好是设置的key值则刷新value,如果不一致则找下一个位置,知道不为空为止
  • 链式地址法,hashMap通过此方法解决,在插入的过程中,若table中有值则插入到链表中(这里分成头插法和尾插法),1.8之后换成了尾插法,因为在多线程中若用头插法可能出现环形链,会抛出异常
  • 再hash法(通过多个hash函数),布隆过滤器中使用了(主要是解决redis中的缓存穿透),通过多次hash然后去比较hash后的值是否都为1,若有一个为0,则说明不存在,若都为1,并不能说明一定存在,会有些许误差,具体可以参考redsi相关学习记录
  • 建立公共溢出区,为所有冲突的关键字记录建立一个公共的溢出区来存放。在查找时,对给定关键字通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表进行顺序查找。如果相对于基本表而言,在有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的

头插法、尾插法

  • 当发生hash碰撞时,会在数组所在的位置上通过链表的方式添加,在java8之前通过头插法的方式插入,会将原有的值记到链表下,当容量不够时需要扩容时,进行resize,新建一个2倍于之前的长度,然后进行rehash,若是在多线程的环境下有可能出现环形链,导致抛出异常
  • 在java8后优化了这种插入方式,通过尾插的方式,在rehash时会保持原有的链表顺序,不会出现环形链
  • resize -> capacity:hashMap当前长度的,LoadFactor :负载因子默认为0.75f,当容量超过0.75时进行扩容,重新创建一个entry空数组,长度为原先的两倍
  • rehash -> 因为是通过hash计算出所在数组的位置,而hash的公式是:index = hashCode (key) & (length -1) 与长度相关,所以长度发生变化之后,需要重新计算位置

在java1.8之后hashMap的链表长度超过8之后会转化成为红黑树

  • 链表的寻址时间复杂度为O(N)
  • 红黑树的寻址时间复杂度为O(lgN)
  • 根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内(即hash碰撞后)元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

初始容量为16,底层中即使传入了初始容量的值,也会找到最近的2的幂次值

  • 为了减少hash碰撞的几率,使得元素分布的更加均匀
  • 因为2的幂次方转化为二进制的低位值全是1,那么计算数组中的位置时,只要hashCode(key)的低位值够散列,那么key的位置就会更加均匀
  • hashCode(key)如何更散列 -> 通过hashCode ^ (hashCode >>> 16) 计算之后得到,
  • ^ : 为异或 , >>>:为不带符号位的右移,那么上面的公式就可以理解为hashCode异或hashCode的高16位
  • 为啥用异或,因为如果是与、或都会偏向于0或1这样没法更加散列

线程安全的map集合

  • hashTable -> 通过对每个方法加上synchronized(重量级锁)使得线程安全,并发效率低
    • synchronized 在java1.6之前是一个重量级锁,通过monitor监视器锁来实现,每个对象中都会存在一个monitor与之关联,当线程持有monitor时就变成了重量级锁,其他线程会被阻塞,通过monitorrenter和monitorexit实现
    • 线程的阻塞和唤醒,操作系统需要在用户态和内核态切换,这个切换非常的耗时
    • monitor的本质是基于操作系统的mutex Lock互斥锁实现,这种依赖于mutex互斥锁实现的称为重量级锁
    • 在java1.6之后进行了优化,有4个状态,无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,具体可以参考线程篇
  • Collections.synchronizedMap(Map)创建线程安全的map集合,也是通过对方法加锁的方式实现,会存在一个mutex互斥锁对象
  • CurrentHashMap
    • 在java1.7时,通过对table分块分到segment中,segment继承了reentranlock,采用分段锁的方式实现,但是这种方式一次锁住了多个key,在一定的程度上是比上面的两个快上不少,但是可以针对单个key,将锁的粒度细化,在效率上可以提升更多,实现线程安全 -> 通过scanAndLockForPut进行自旋尝试获取锁,如果自旋的次数到达最大阈值就会改为阻塞获取锁,保证能成功获取锁,因为数据结构还是数组+链表的方式获取,所以在查询效率上还是会很低(通过volatile实现多线程可见性、有序性,所以在get的时候不需要加锁,只需要遍历数组+链表即可)
    • 在java1.8时,优化上面的缺陷,首先数据结构发生了变化,舍去了segment分段锁的实现方式,通过CAS + synchronized实现并发安全性,也把之前的hashEntry修改成了Node,作用是一样的,然后对其的值和next用volatile修饰,来保证get的时候不需要加锁,并且引入了红黑树,在链表长度大于一定值时转化成红黑树(默认值为8)
    • put的操作比较复杂步骤大致可以分成以下步骤,具体的源码解析可以参考concurrentHashMap 1.8源码解析
      • 首先根据key计算出hashcode
      • 判断是否需要初始化,初始化的大小为16,具体原因上面分析过了
      • 为当前的key定位出具体的位置,如果为空,通过CAS原子操作进行写入,失败的话进行自旋保证成功
        • CAS -> compare and swap 比较并交换,基于底层实现,但是会出现ABA问题,但是在并发的最终结果上是不会发生变化的。
        • ABA问题:如mysql中也会存在此类问题,大致解释为:此时来了一个线程将值改为B,然后又来了一个线程将值改回为A,这时候CAS就无法区分出有没有被改动过,但是如果只追求最后的结果正确,就没有影响
          • 当然也有解决方案的,比如通过加上版本号,值与版本号一起判断,就可以避免,也可以通过时间戳去实现
      • 如果当前的位置 hashcode == move == -1 ,则需要进行扩容、
      • 如果都不满足则通过synchronized进行写入
      • 当链表数量大于treeif_threshold 阈值时转化为红黑树
  • java.util包下的集合中的一种机制快速失败- fail-fast,在使用迭代器时,此时如果值发生了改变就会抛出异常Concurrent Modification Exception
    • 具体的原理为会存在一个modCount的变量,如果值发生变化时就会改变这个值,迭代器在迭代时,会去比较这个值是否发生改变

arrayList-通过数组实现

- 查询效率高,增删效率低,且线程不安全
- 通过无参构造的方式初始化,不会初始化数组容量,只有在第一次添加进行add的时候才会进行初始化容量,默认分配为10
- 扩容的方式,重新定一个空数组,长度为原先的1.5倍,然后把原先数组的指向地址换成新的地址,完成扩容,通过ensureCapacityInternal 判断长度,是否需要进行扩容,在java8时优化了通过位移的方式去提高效率,右移一位,其实就是除以2
- 若为指定位置的插入,实现方式大概为,先复制一下数组,在指定位置中空出,然后指定的位置进行插入
- 删除与指定位置的插入相似,都是通过拷贝的操作完成,所以增删的效率比较低

线程安全的List

  • vector ,实现方式也是在所有的方法上都加上了synchronized
    • 作为vector的代替 -> CopyOnWriteArrayList ,Copy-On-Write容器即写时复制的容器,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器,其中的add与remove方法需要加锁,读不需要,实现了并发读,当然这样读到的数据不一定是最新的,因为写时复制是通过延时更新的策略完成实现最终数据一致性,并非强一致性
    • 并没有解决同步容器的复合操作的线程安全性问题
  • Collection.synchronizedList ,把普通的arrayList转化为线程安全的,也是加上一层 synchronized
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-09-01 12:10:50  更:2021-09-01 12:13:12 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 0:32:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码