1. 数据结构
廊腰缦回, 檐牙高啄. 纵使相同一砖一瓦, 不同雕琢设计, 一声错落有致的廊榭美景. 数据结构的魅力也源于此中道理
1.1 数据结构的定义
逻辑意义上的数据组织方式及其相应的处理方式
- 什么是逻辑意义
- 什么是数据组织方式
比如树, 队列, 图, 哈希
- 什么是数据处理方式
在既定的数据组织方式上, 以某种特定的算法实现数据的增加,删除, 修改, 查找和遍历
1.2 数据结构分类
一. 从直接前继和直接后继个数的维度来将数据结构分类
- 线性结构
0至1个直接前继和直接后继
- 树结构
0至1个直接前继和0至n个直接后继(n大于或等于2)
- 图结构
0至n个直接前继和直接后继(n大于或等于2)
- 哈希结构
没有直接前继和直接后继
二. 如何衡量数据处理的性能
5. 算法时间复杂度
衡量计算性能的指标, 反映了程序执行时间随输入规模增长而增长的量级.
用一个大写的O和一个函数描述
- 从最好到最坏的常用算法复杂度排序
- 常数级
- 对数级
- 线性级
- 线性对数级
- 平方级
- 立方级
- 指数级
- 优秀的程序实现不会因为数据规模的急剧上升导致程序性能的急剧下降
2. 集合框架图
2.1 List集合
- ArrayList
- 容量可以改变的非线程安全集合
- 内部实现: 数组
- LinkedList
- 内部实现: 双向链表
- 内存利用率较高: 可以讲零散的内存单元通过附加引用的方式关联起来
2.2. Queue集合
- 阻塞操作
- 阻塞与非阻塞
- BlockingQueue具有阻塞操作的特点
- 你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
2.3 Map集合
- 哈希结构, key是唯一的, value是可以重复的
- 提供三种Collection视图
- Collection视图
- 多线程并发场景中, 优先推荐使用ConcurrentHashMap
- TreeMap是Key有序的Map类集合
2.4 Set集合
- 不允许出现重复元素的集合类型
- HashSet: 使用HashMap来实现的, 只是Value固定为一个静态对象, 使用Key保证集合元素的唯一性
- TreeSet: 使用TreeMap来实现, 底层为树结构
- LinkedHashSet: 继承自HashSet, 内部使用链表维护了元素的插入顺序
3. 集合初始化
变量修饰符. 标记的成员变量不参与序列化过程。
- 使用位运算
计算效率更高
- ArrayList无参构造
默认大小为10, 后续每次扩容都会调用Array.copyOf方法, 创建新数组再复制.
可以想象, 加入需要将1000个元素放置在ArrayList中, 采用默认构造方法, 则需要被动扩容13次才可以完成存储
- HashMap
如果它需要放置1000个元素, 同样没有设置初始容量大小, 随着元素的不断增加, 则需要被动扩容7次才可以完成存储. 扩容时需要重建hash表, 非常影响性能
- 计算落槽位置
槽位应该是value的落点, 具体不清楚
- 牢记每种数据结构的默认值和初始化逻辑, 是开发工程师基本素养的提现
4. 数组与集合
- BCPL语言
- 加减法对CPU来说是一种双数运算
百度也查不到啥叫双数运算
- Java体系中的数组
- 存储同一种类型的对象
- 一旦分配内存后无法扩容
- 提倡类型与中括号紧挨相连来定义数组
- 数组的遍历邮件推荐foreach方式
- length是数组对象的一个属性, 而不是方法
- Arrays
针对数组对象进行操作的工具类, 包括数组的排序, 查找, 对比, 拷贝等操作. 另外可以通过这个工具类把数组转换成集合
- 作用域就近原则
同样的类名, 在类A中有内部类B, 同包中有类B, A类方法中如果初始化B, 会初始化内部类B.
冒充李逵的二百五
- final引用
用于引用始终被强制指向原有对象
- 数组的气节
要么直接用我, 要么小心异常
- 可以用李逵的构造方法转李鬼为李逵
- 集合转数组
当数组容量等于集合大小时, 运行总是最快的, 空间消耗也是最少的.
5. 集合与泛型
- 协变
- 数组可以把Integer加入Object[]中, 因为数组是协变的, 而集合不是
- List<?>
通配符集合
- 可以接收任何类型的集合引用赋值,
- 不能添加任何元素,
- 但可以remove和clear, 并非immutable集合
- List(T) 这里的小括号是尖括号, 因为打不出来, 下面用它来代指
它最大的问题是只能放置一种类型
- <? extends T>
Get First
- 适用于, 消费集合元素为主的场景. 取出类型带有泛型限制
- 可以复制给任何T及T子类的集合, 上界为T
- null可以表示任何类型. 除null外, 任何元素都不得添加进<? extends T>集合内
- put功能受限
- <? super T>
Put First
- 适用于, 生产集合元素为主的场景
- 可以复制给任何T及T的父类集合, 下界为T
- gei功能受限, 会类型丢失
- 可以往里面放元素, 但是只能往里面放T以及T的子类信息
6. 元素的比较
6.1 元素的比较
- Comparable
自己和自己比, 可以看做是自营性质的比较器
- 实现Comparable时, 可以加上泛型限定
- Comparator
第三方比较器, 可以看做是平台性质的比较器
- 归并排序
对有序子集进行两两归并, 并保证结果子集合有序.
- 插入排序
当有了K个已排序的元素, 将第k+1个元素插入已有的K个元素中合适的位置, 就会得到一个长度为K+1的已排序的数组
- TimSort排序
2002年, Tim Peter结合归并排序和插入排序的优点实现TimSort排序算法
- 相对传统归并排序, 减少了归并次数; 每次先查找当前最大的排序号的数组片段run, 然后对run进行扩展并利用二分排序. 最后将run和其他排好的归并
- 相对插入排序, 引入了二分排序概念, 提升了排序效率 . 将一次查找新元素合适位置的时间复杂度由O(n), 降低到O(logn)
6.2 hashCode 和 equals
- Object类定义对 hashCode 和 equals 的要求
- 如果两个对象的equals的结果是相等的, 则两个对象的hashCode的返回结果也必须是相同的
- 任何时候覆写equals, 都必须同时覆写hashCode
- 如果自定义对象作为Map的键, name必须覆写hashCode和equals. Set存储的对象也是
一个优秀的哈希算法应尽可能地让元素均匀分布, 降低冲突概率, 即在equals不相等时尽量使hashCode也不相等
- Object.hashCode()的默认实现
为每一个对象生成不同的int数值. 根据对象的地址进行相关计算得到int类型数值
- 局部变量类型推断
Local Variable Type Inference. JDK10引入的变量命名机制, 当然这仅仅是一个语法糖, java仍然是一种静态语言.
在初始化阶段, 在处理var变量的时候, 编译器会检测右侧代码的返回类型, 并将其类型用于左侧
- 尽量避免使用通过实例对象来调用equals方法, 否则容易抛出空指针异常.
推荐使用JDK7引入的Object的equals方法, 这里已经做了非null判断
7. fail-fast机制
- fail-fast机制
集合中比较常见的错误检测机制, 通常出现在遍历集合元素的过程中
举例: 点名时有人进来或者有人出去, 要重新点名
机制负责范围: java.util下的所有集合类
- fail-safe
举例: 拍照后根据照片点名, 不再关心同学的进出
机制负责范围: concurrent包中所有集合类
- 多线程
运行逻辑并非自然思维;
自然思维: 在某个时间段或某个深度具有方向性
- 在某种情况下, 需要从一个主列表master中获取字列表branch,master集合元素个数的增加或删除, 均会导致字列表的遍历、增加、删除,进而产生fail-fast异常
- subList子列表无法序列化, 它的修改会导致主列表的修改
- 遍历删除主列表时, 使用Iterator机制, 否则会触发fail-fast异常。
- 并发思路
- 读写分离;
1.1 写操作: 复制一个新集合, 在新集合内添加或删除元素
- 使用COW的注意点
COW使用的是读写分离的方法
- 尽量设置合理的容量初始值, 它扩容的代价比较大
- 使用批量添加或删除方法. 可以攒一下要添加或者删除的元素, 避免增加一个元素复制整个集合.
8. Map类集合
- 在任何Map类集合中, 都要尽量避免KV设置为null值
8.1 红黑树
8.1.1 树
- 树
树是一个由有限节点组成的一个具有层次关系的集合, 数据就存在树的这些节点中.
- 节点高度
从某节点触发, 到叶子节点位置, 最长简单路径上边的条数
- 节点深度
从根节点出发, 到某节点边的条数
- 树结构的特点
- 一个节点, 即只有根节点, 也可以是一棵树
- 其中任何一个节点与下面所有节点构成的树称为子树
- 根节点没有父节点, 而叶子节点没有子节点
- 除根节点外, 任何节点有且仅有一个父节点
- 任何节点可以有0~n个子节点
- 二叉树
至多有两个子节点的树称为二叉树
8.1.2. 平衡二叉树
- 如果以树的复杂结构来实现简单的链表功能, 则完全埋没了树的特点
- 高度差是一棵树是否为平衡二叉树的先决条件
- 平衡二叉树的性质
- 树的左右高度差不能超过1
- 任何往下递归的左子树与右子树, 必须符合第一条性质
- 没有任何节点的空树或只有根节点的树也是平衡二叉树
8.1.3 二叉查找树
- 二叉查找树
又称二叉搜索树, Binary Search Tree, 其中Search也可以替换为Sort, 所以也称为二叉排序树
- 二叉查找树要求
- 对于任意节点来说, 他的左子树上所有节点的值都小于它, 而它右子树上所有节点的值都大于它.
- 前序遍历、中序遍历、后序遍历
- 在任何递归子树中, 左节点一定在右节点之前先遍历
- 前序、中序、后续,仅指根节点在遍历时的位置顺序
- 前序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
8.1.4 AVL树
- 平衡二叉树算法
以苏联数学家名字命名的平衡二叉树算法, 可以使二叉树的使用效率最大化
- AVL树
是一种平衡二叉查找树,增加和删除节点后通过树型旋转重新达到平衡
- 右旋
以某个节点为中心, 将它沉入当前右子节点的位置, 而让当前的左子节点作为新树,也称为顺时针旋转
- 左旋
以某个节点为中心, 将它沉入当前左子节点的位置, 而让当前右子节点作为新树的跟节点,也称为逆时针旋转
- 其实旋转是两个节点的事情,剩下的那个节点看着插就完事了
8.1.5 红黑树
- 红黑树
1972年发明的, 1978年优化
- 主要特征: 每个节点上增加一个属性来表示节点的颜色, 可以是红色, 也可以是黑色
- 也是在进行插入和删除元素时, 通过特定的旋转来保持自身平衡的, 从而获得较高的查找性能.
- 不追求所有递归子树的高度差不超过1, 而是保证从根节点到叶子节点的最长路径不超过最短路径的2倍
- 红黑树在本质上还是二叉查找树
- 红黑树的约束条件
- 节点只能是红色或黑色
- 根节点必须是黑色
- 所有NIL节点都是黑色的. NIL: 叶子节点下挂的两个虚节点
- 一条路径上不能出现相邻的两个红色节点
- 在任何递归子树内, 根节点到叶子节点的所有路径上包含相同数目的黑色节点
- 使用红黑树的目的
通过重新着色和左右旋转, 能够更加高效地完成插入和删除操作后的自平衡调整
- 总结: 有红必有黑, 红红不相连
8.1.6 红黑树与AVL树的比较
- 复杂度分析
- 相同节点数的情况下, 红黑树高度可能更高. 查找次数会高于相同情况下的AVL
树- 插入: 红黑树和AVL都能在至多两次旋转内恢复平衡
- 删除: 由于红黑树只追求大致上的平衡, 因此红黑树能在至多三次旋转内回复平衡; 而追求绝对平衡的AVL树,则至多需要O(logn)次旋转
- 任意节点的黑深度
当前节点到NIL途径的黑色节点个数
8.2 TreeMap
- TreeMap
按照Key的排序结果来组织内部结构的Map类集合
- 插入和删除的效率远远没有后两者高
- 并非一定要覆写hashCode和equals方法来达到Key去重的目的. 因为依靠Comparable或者Comparator去重
- 基于红黑树实现
- SortedMap
它的Key是有序不可重复的, 支持获取头尾Key-Value元素, 或者根据Key指定范围获取子集合. 因为需要比较所以key不能为null
- TreeMap新增的前提条件
- 需要调整的新节点总是红色的
- 如果插入的新节点的父节点是黑色的, 无需调整. 因为依然能符合红黑树的五个条件
- 如果插入新节点的父节点是红色的, 因为红黑树规定不能狐仙相邻的两个红色节点, 所以进入循环判断, 或重新着色,或重新旋转,最终达到红黑树的五个约束条件
- TreeMap行文思路
相关同胞继承树
比较
类名,属性
新节点属性, 需要变幻的状态
- 插入: 举例
- fix: 举例; 颜色举例
- 左旋
3.1 根节点右子节点: 双向
3.2 右子节点父节点: 双向
3.3 根节点的根节点: 双向
8.3 HashMap
- 使用场景
局部方法或绝对线程安全的情形
- 存在问题
- 死链问题
- 扩容数据丢失问题
8.3.1 数据丢失问题
- 负载容量
- jstack
得到运行java程序的java stack和native stack的信息
- 默认分配空间: 16, 扩容因子: 0.75
- 对象丢失
插入元素时,取出原来的链表;操作插入
- 泊松分布
- HashMap在高并发场景中,新增对象丢失原因
- 并发赋值时被覆盖
- 已遍历区间新增元素会丢失
- 新表被覆盖
- 迁移丢失: 这我真没看出来, 看不出来算了. 后面也用不到
- 数据丢失问题复现
- 启动十万个线程采用均匀的Key往同一个HashMap放置不同的自定义对象,观察内存中的对象数量
- 观察: snapshot-Perform GC
- 两种丢失
3.1 对象赋值阶段产生的覆盖
3.2 迁移过程中丢失引用的对象
- System.nanoTime()
纳秒
8.3.2 死链问题
- 运行环境
JDK7
- 运行条件
以System.nanoTime()返回的Long值为Key, 自定义对象EasyCoding为value. 放入HashMap
- 运行结果
对象计数达到1万个左右时, load呈阶梯式上升, 4核机器的CPU使用率上升到300%以上
正在运行和准备好运行的进程的总数
- 死链不能出现的情况
使用JDK8以及以上, 因为采用对与原先链表头结点和尾结点的引用, 保证"有序性"
- 监测线程工具
可以查看哪些线程一直处于Running状态
- jstack命令
可以查看线程详细信息
- 监测工具能够监测到死链对象的循环指向
- 这是什么监测工具, 我也想要. 书上也不说, 干馋身子
- 分析结论: hashbucket中存在循环指向
- 详细计算两个Key是否落在同一个slot哈希槽
- 通过监测工具中的key值来计算hashCode()值; 异或
- hash()方法, 用到hashSeed
- indexFor()方法, 计算slot槽的数组下标
- hashSeed
用这玩意儿对hashCode进行重新计算, 用来尽可能离散
- Long的hashCode()
- Long的hashCode()不同于Integer 不是自身值.
- 目的: 泊松分布. 高位与低位进行异或
看完发现我还不如买馒头的
- 验证
- 计算出slot槽数组下标
- 根据slot槽下标直接访问table数组, 查看链表元素
- 死循环
虽然是读操作, 但是既成事实的死链会导致该查询陷入死循环
- 死链是如何形成的
transfer()方法生产死链
遍历旧表Entry, 放到新表
- 死链的生成, 以下是原因
- 原先没有死链的同一个slot上节点遍历一定能够按顺序走完: 因为局部变量不会互相干扰
- table数组是各线程都可以共享修改的对象
- put()、get()和transfer()三种操作在运行到此拥有死链的slot上, CPU使用率都会飙升
- 死链的生成, 以下是结果
Entry中的链表是共享的. 产生问题的根源是Entry的next被并发修改, 这可能导致
- 对象丢失
- 两个对象互链
- 对象自己互链
码出高效没看懂, 简书老哥帮忙解惑
8.4 ConcurrentHashMap
- 对于线程不加锁,让系统执行所有的步骤
- JDK8对ConcurrentHashMap使用了大量的lock-free技术来减轻锁的竞争而对性能造成的影响
保证不同线程对这个变量进行操作时的可见性
adj. 易变的
- Hashtable
JDK1.0引入
全互斥方式处理并发情况;
全互斥方式: 不明白, 查不到
- HashMap
JDK1.2中引入
非线程安全
- ConcurrentHashMap
JDK5引入
线程安全的哈希式集合
JDK8以前使用分段锁设计理念
将数据分段上锁,把锁进一步细粒度化
- segment
n. 部分
v.划分
- reentrant
adj. 再进去的
独占锁的一种
可重入锁,实现了 Lock 和 Serializable 接口
- Entry
n. 入口
- HashEntry
不知道, 百度失败
即比较再交换
有说comapre and swap的 - 百度
也有说是compare and set的 - java编程思想
8.4.1JDK11版本分析
- 对JDK7版本的改造
- 取消分段锁机制, 进一步降低冲突概率
- 引入红黑树结构. 同一哥slot上的元素个数超过一定阈值后, 单向链表改为红黑树结构
- 使用了更加优化的方式统计集合内元素数量.
3.1 最大数量从31次方增加到63次方
3.2 元素总数更新时, 使用了CAS和多种优化以提高并发能力
- bin
n. 箱
v. 丢掉
- Reservation
n. 保留意见
- compute
v. 计算
- absent
adj. 缺席
n. 缺席
adj. 临时的
n. 临时工
不参与序列化过程