《码出高效:java开发手册》六-数据结构与集合(二)

前言

接上篇,第六章第二部分,上篇讲到了红黑树的FixAfterInsertion方法,这个方法原理与fixAfterDelete类似,只讲这个添加时的调整方法

红黑树

在这里插入图片描述在这里插入图片描述在这里插入图片描述
代码可以看到,调整后的根节点一定是黑色的,叶子节点可红可黑,叶子下挂的虚节点一定是黑色,体现了红黑树的性质
左旋和右旋的代码如下,这里只有左旋,右旋类似
在这里插入图片描述
下图为color of 方法代码
在这里插入图片描述
下图为以一个treemap为例演示平衡策略
在这里插入图片描述
在这里插入图片描述
这里去除57,插入59会触发右旋和左旋
在这里插入图片描述

如上图,插入55为根,为黑,之后插入56为红,之后插入57为红,发现两红相邻,重染色并且旋转
在这里插入图片描述

这里插入58,发现父节点是爷爷节点的右节点,左叔为红,所以把父节点和左叔都变黑,爷节点变红,因为56是根,退出循环同时执行根节点变黑
在这里插入图片描述
这里插入83,发现左叔不存在,父节点是爷节点右子树,认为是黑色NIL,将爷节点作为输入参数执行左旋
之后代码里要删除57,57是叶节点,为红,不影响红黑树,直接删除
之后59插入,比56大,比58大,比83小,放在83左子树
在这里插入图片描述
对于59,满足这些条件,开始右旋
· 父亲是爷爷的右子节点,
· 当前节点是父亲的左子节点;
· 左叔是黑色的( 删除57 的原因所在)
右旋之后, 把59 涂黑, 把58 置为红色, 然后以58 为输入参数, 进入左旋操作
树的演化中,有三种情况
1.父红叔红,重新做色
2.父红叔黑, 新节点是父节点左节点,右旋
3.父红叔黑,新节点是父节点右节点,左旋。
可以看到,任何旋转都在三次以内,每次回溯步长2,对于频繁插入删除场景,性能较好。
总结:TreeMap时间复杂度较高,但有序,稳定,支持范围查找,在排序场景中高效
线程不安全,不能在多线程中共享数据进行写操作,需要添加互斥机制,或者把对象放在Collections .
synchroinzedMap(treeMap) 中实现同步。

  • JDK7 之后的HashMap 、Tree Set 、ConcurrentHashMap , 也使用红黑树,单个元素排序使用treeSet即可,TreeSet底层是TreeMap,value共享一个静态object对象
  • 在这里插入图片描述

HashMap

在有线程安全需求场景下,推荐使用ConcurrentHashMap
HashMap有两个缺陷:死链和扩容数据丢失
场景:init时初始化一个static的HashMap集合,从数据库读入集合,但inti错误执行两次,启动失败,cpu占用过高,解决办法三种
第l 种解决方案是用ConcurrentHashMap 替代Hasmap
第2 种解决方案是使用Collections.synchronizedMap(hashMap ) 包装成同步集合
第3 种解决方案是对init()进行同步操作。此案例最终选择第3 种解决方案, 毕竟只有启动时调用而已。
场景2:
在这里插入图片描述
这里猜测是与并发读写有关,首先需要了解一下hashmap基本概念
在这里插入图片描述
在这里插入图片描述
如上图,左侧是table数组,红色是位置标示,虚线里是哈希桶(包含链表和树)
为了分析死链问题,需要看hashmap源码,这里是put方法
在这里插入图片描述
在这里插入图片描述
这里的createentry新加入元素会放在slot槽,为了可以更快访问,如果多个线程都执行到1处,就会产生覆盖现象
同时,还有扩容条件下的数据迁移
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这些都是十分眼熟却易混淆的HashMap 相关概念值。理想的哈希集合对象的存
放应该符合,
· 只要对象不一样, hashcode就不一样;
· 只要hashCode 不一样,得到的hash Code 与hashSeed 位运算的hash 就不一样;
· 只要hash 不一样,存放在数组上的slot 就不一样。
但是现实中往往达不到,比如12个人分到8个位置,第1个和第9个用8取模就会有重合,如果位置扩大,比如12个人分100个位置,虽然可以减少冲突,但是资源浪费了,所以要用负载引入,比如0.75负载因子,就是12个人分开(12/0.75),这个12也是临界值,就是人数超过12时,就会引发扩容,在HashMap中,扩容会变为2倍
HashMap多个元素落在一个位置上就会向链表里放,遍历时有两个方向:
第一个方向,从下标table [0] 至table[length -1], 遍历所有哈希槽
第二个方向,如果哈希槽上存在元素,遍历哈希桶里的所有元素。

  • 插入第13个元素时,会进行resize如下图
  • 在这里插入图片描述
    这里可以看到,旧表数据逆序放入新表,主要靠resize和transfer方法
    在这里插入图片描述
    在这里插入图片描述

transfer方法消耗资源较大,而且在transfer元素时,新元素可能落到旧表已经遍历过的哈希槽,旧表会被回收,这些元素也就丢失了
同时resize完成后,后续元素会插入新表,如果是多线程同时resize,每个线程单独执行newEntry[ newCapacity],由于这是线程单独可见,resize的线程赋给table共享变量,就会覆盖其他线程操作,因此新表中插入的元素会丢失

总结,hashmap对象丢失情况有:
1、并发赋值时被覆盖。
2、己遍历区间新增元素会丢失。
3、“新表”被覆盖。
4、迁移丢失。在迁移过程中,有并发时, next 被提前置成null 。

书中用10万个线程放对象,上来丢失1092个对象,因为对象覆盖在这里插入图片描述
之后gc又少了8055个,属于引用丢失,剩下的被Map强引用持有
在这里插入图片描述
接下来的代码启动10 万个线程,以System.
nano Time()返回的Long 值为Key , 自定义对象EasyCoding 为Value
在这里插入图片描述
在这里插入图片描述
这里到一万个对象时,cpu使用率就超过300%
在这里插入图片描述

排查到两个线程,一直在running,说明有死链
在这里插入图片描述

使用jstack命令查看
在这里插入图片描述

这两处都存在对哈希桶内链表的遍历访问,其中, 601 行是循环提取同一个哈希
桶里的所有元素, 然后逐一倒序插入新表中。而494 行也是遍历访问, 在新增元素之前进行hashcode和equals比较,决定是覆盖value还是延伸到链表里
在这里插入图片描述
如下图,可以看到5415指向5416,之后又回指5415
在这里插入图片描述
下图计算了两个key是否落在一个哈希槽内
在这里插入图片描述
这里扩展了一点,long的hashcode不同于integer的hashcode,先获取Long( 1534820390 l 965 l 4000L )的二进制值,然后右移32 位后, 再与原来的值进行异或,最后进行int 的强制类型转换, 相当于高位被截掉,这样有利于泊松分布,上表得到哈希槽值5883,验证如下,访问下标5883的链表元素
在这里插入图片描述
之前两个无限get的线程,始终在running状态,为了分析死链,要查看slot=5883
在这里插入图片描述
一直在第二步和第三步之间互相数据覆盖,死链生成有三点
原先没有死链的同一个slot节点遍历一定能按顺序走完
table数组是各线程共享的
put,get,transfer三种操作运行到有死链的slot上,cpu使用率会飙升
在这里插入图片描述

AB两个线程,执行transfer方法,产生问题的根源是Entry的next被并发修改,导致
( I )到象丢失。
( 2 )两个对象互链。
( 3)对象自己互链
环路问题原因是两个线程遍历完第一个节点后,第二个节点处产生互链
在这里插入图片描述

在这里插入图片描述
JDK7当中,是( size >= threshold) && (n ull != table[bucketlndex ]),先扩容后新添加
JDK8则是先添加后扩容
现实中扩容后,get请求赶到死链的slot上,或者put已经形成死链时,get命中,使得服务器运算超出负荷
JDK8的HashMap从头结点就开始操作数据迁移,就是为了规避这一点
如果在JDK8之前,最好记住这些坑,或者用ConcurrentHashMap

ConcurrentHashMap

ConcurrentHashMap是一种并发线程安全的哈希集合,它使用了大量的lockfree 技术
经常会与hashtable进行对比,Hashtable是JDK1.0中的,用全互斥处理并发,性能极差
HashMap是JDK1.2引入的,非线程安全,并发写时容易死链,导致服务不可用
ConcurrentHashMap是JDK1.5引入,JDk8前用分段锁技术,相当于Hashtable和HashMap的折中
分段锁就是一种分开操作,各个segment单独操作,内部不会冲突
JDK11对7的版本有三处变化
1.取消分段锁,降低冲突概率
2.引入红黑树
3.优化统计集合内元素数量的方式,原先size只能到2的32次方减一,现在mappingCount能到2^63-1,元素个数更新时也有CAS和优化办法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从源码中可以总结它的存储结构如下
在这里插入图片描述
这里table长度64.数据存储结构分为链表或红黑树
槽内元素超过8且table容量大于等于64,链表转为红黑树,table小于64时,只会扩容而不会变链表为红黑树
,转换时先锁住当前槽的首元素,就不会受到其他进程的修改了,之后用CAS替换原链表;如果是红黑树转链表,就只需要遍历节点并且把TreeNode转为Node即可
下图为链表转红黑树过程
在这里插入图片描述
触发转换的操作是put方法,基本思想类似HashMap,多了一个锁的处理
在这里插入图片描述
ConcurrentHashMap 元素插入流程如图
Node还有两个子类ForwardingNode 和ReservationNode
前者是table扩容时,原table的槽内放置一个ForwardingNode 节点,它会把find请求转发到扩容后的table上,put方法碰到这个节点也会协助迁移
后者是用来在computeIfAbsent中作为预留节点使用,它会占位加锁,来防止槽被其他线程操作
最后,ConcurrentHashMap优化了计算集合size的方法,之前这个方法是不准确的,因为在计算时也会有增删操作,导致最终结果有差异
,停止操作又会很浪费性能,于是JDK8它主要用CAS代替了锁操作
JDK7之前put和remove方法,segment内部元素和计数器更新都是上锁的
在这里插入图片描述
JDK7的ConcurrentHashMap获取集合大小如下
在这里插入图片描述
如上图,可以看到,已经尽力避免上锁,但如果3次计算后发现总有结构变化,仍然会上锁
JDk8对获取size的优化用到这些属性
在这里插入图片描述
通过baseCount 和counterCells 两个属性,配合多个CAS方法,避免了加锁
思路主要是:
当并发量较小时,优先使用CAS 的方式直接更新baseCount 。
·当更新baseCount 冲突, 会认为进入到比较激烈的竞争状态, 通过启用counterCells 减少竞争, 通过CAS 的方式把总数更新情况记录在counterCells对应的位置上。
·如果更新counterCells 上的某个位置时出现了多次失败, 会通过扩容counterCells 的方式减少冲突。
·当counterCells 处在扩容期间时,会尝试更新baseCount 值。
统计时只需让baseCount加上各个counterCells内数据,就可以得到哈希内元素总数,不需借助锁
这也就是为什么推荐并发使用这个集合进行KV键值对的存储和使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《码高效:java开发手册》这本书是由阿里巴巴Java开发团队所编写的,它主要讲解了Java开发中的一些最佳实践和优化技巧,旨在帮助开发者写高质量、高效率的代码。 整本书包括了Java开发的方方面面,涵盖了从Java基础、集合框架、并发编程、IO操作、性能优化等方面的内容。最为重要的是这些知识点都是讲解得非常详细,大量体现了作者们得实践经验和理论能力。在这个过程中,书籍所提供的大量实战示例和最佳实践都可以在实际工作中获得收益。 此外,书籍还讲解了很多阿里巴巴内部的开发流程和代码规范,诸如代码注释、变量规范、枚举类设计等,这些规范奉行着“规模化、高效化、标准化”的开发理念,从而可以节省开发时间和代码质量的提高。 总的来说,《码高效:java开发手册》这本书是一本很不错的Java开发指南,对于任何一位Java开发者都是一本必备参考书,尤其是在阿里体系中工作的开发者更不能错过这本宝典。 ### 回答2: 《码高效:java开发手册 pdf》是一本非常实用的Java开发书籍,旨在通过系统分析Java开发及其常见问题,提供一些高效的实用代码和技巧。全书共分三部分,分别是Java基础知识、Java编码规范和高效Java编程,共计十二章。本书的主要内容包括编程规范、异常处理、集合使用、IO处理、线程和并发编程、数据库操作及JDK新特性等。其中每一章都传授了一些实用的技巧,可以让Java开发者编写更加高效的代码。 《码高效:java开发手册 pdf》的优点主要有三点。第一,本书内容涵盖Java开发的方方面面,从Java基础到高效编程技巧都有所涉及。这让读者可以系统全面地掌握Java开发技能,是一本非常适合初学者和中级开发者的Java开发书籍。第二,本书提供了大量实用的代码示例,这些代码可以快速解决开发中常见的问题,节省了开发者解决问题的时间。第三,本书的编写风格通俗易懂,语言简洁明了,结构清晰,非常适合读者阅读和学习,即使是非专业人士也能轻松理解其中的内容。 总之,《码高效:java开发手册 pdf》是一本值得Java开发者阅读的优秀书籍,通过学习这本书,读者可以获得许多实际的Java开发技巧和经验,提高编码水平,编写更加高效的代码。同时,读者也可以将书中所学的知识运用到实际开发中,实现自己的技术突破和知识更新。 ### 回答3: 《码高效: Java开发手册PDF》是一本关于Java软件开发的实践指南。本书作者通过自己多年的实践经验以及技术积累,总结归纳了一套行之有效的Java开发规范和最佳实践。本书涵盖了Java开发中的方方面面,涉及Java语言的基础知识、常见开发框架、设计模式、性能优化、调试技巧、代码安全以及项目管理等内容,适用于Java程序员、软件架构师、开发经理等读者。 本书主要包括三个部分:编码规范,工程实践和架构实践。其中,编码规范部分详细介绍了Java编码规范、命名规范、注释规范、代码格式规范等,这些规范都是编写高质量Java代码的基础。在工程实践部分,作者主要介绍了Java开发中的常见问题以及解决方案,包括环境配置、开发工具、单元测试、集成测试等。架构实践部分则从架构设计、服务发现、数据缓存、性能优化等方面给了一些实用的建议。 通过阅读本书,读者可以学习到一些高效的Java开发技巧和实践经验,并且将这些技巧应用到实际项目中,提高开发效率和代码质量,从而在竞争激烈的软件开发市场中占据更好的位置。此外,本书还提供了很多案例分析和代码示例,为读者解决实际问题提供了可参考的经验。总之,《码高效: Java开发手册PDF》是一本值得Java开发人员阅读的经典参考书籍。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值