其实就是偶尔复习的时候随手写的东西,以后还会继续更新
1.关于jvm内存的模型的复习
1.栈
2.堆(新生代(eden和幸存代)和老年代)
3.程序计数器
4.方法区(就是永久代,jdk8移除了,)
5.字符串常量池
6.本地方法栈
7.堆外(直接内存)
2.oom发生的几种可能
1.分配一个超大对象,类似一个超大数组超过堆的最大值会oom
2.堆内存不足导致oom,xmx调节堆大小
3.程序不断递归调用,不停压栈会抛出stackoverflow,如果jvm这时候去扩展栈空间且失败就会抛出oom
4.老版本永久带因为大小有限也会经常oom比如intern字符串缓存过多。
5.直接内存不足也会oom
3.初始化一个类的步骤
- 分配对象内存空间
- 初始化对象
- 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)
由于第二步和第三步没有依赖关系所以可能会被重排优化
- 分配对象内存空间
- 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)
- 初始化对象
原子性一般是说读写等操作是不可分割的
可见性是多线程情况下对工作内存的变量修改会同步到主内存,多线程可见
有序性就是代码按照顺序执行不会被指令重排
4.happens-before原则
1.一个线程内代码循序执行
2.unlock操作现行发生于同一个锁的lock,也就是说第二次上锁,需要先解锁
3.对于volatile写操作先行于读操作
4.对与线程来说start()优先于其他动作
5.线程的其他动作又优先于线程终止规则
6.线程的中断调用interrupt()优先于中断检测interrupted()
7.一个对象的构造函数执行优先于finalize()方法
8.a操作先行于b操作,b操作先行于c操作,所以a先行于c
无锁都是基于硬件的原子操作实现的
cas会有aba问题就是要被赋值的变量在检测的时候,由a变成了b又变回了a这样就会通过检测,java是靠版本号(对变量维护一个版本号)来优化解决这个问题的
自旋避免了线程切换的开销,但是占用了处理器时间
5.关于锁
1.关于Synchronized
1.使用
修饰实例方法,对当前实例对象this加锁
修饰静态方法,对当前类的class对象加锁
修饰代码块,制定一个加锁的对象,给对象加锁
2.对象组成
对象头
mark word字段:存储对象的hashcode,存储分代年龄,存储锁标志位信息
(00轻量级锁01无锁或者偏向锁10重锁11垃圾回收标记)
klass point: 对象指向它的类元数据的指针,虚拟机通过这指针判断这对象是哪个类的实例
实例数据
存放类的数据信息,父类的信息
填充数据
虚拟机要求对象的起始地址必须是8字节的整数倍,为了字节对齐
空对象的大小就是8字节,因为会自动对齐
3.可重入性
Synchronized锁对象的时候有个计数器,会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1
直到计数器清零,就释放锁了。
4.不可中断性
就是说,一个线程获取锁之后,另一个线程处于阻塞或者等待状态,前一个不释放,后一个也已知会阻塞或者等待,不可以被中断
5.javap -c xxx.class可以查看反编译文件
首先关联到一个monitor对象。
当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
不知道大家注意到方法那的一个特殊标志位没,ACC_SYNCHRONIZED。
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
所以归根究底,还是monitor对象的争夺。
6.无锁->偏向锁->轻锁->重锁(锁升级过程)
为什么需要有锁升级这种优化?
多线程在同一时刻请求同一把锁,没拿到锁的线程需要被阻塞,所以需要用重量级锁(重锁有自旋,取消,粗化等优化)
多线程在不同时间段请求同一把锁,也就是说没有锁竞争需要轻量级锁
锁一直被同一个线程所持有,使用偏向锁即可
wait()和sleep()的区别
wait()来自Object类,sleep()来自Thread类
调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;
sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用
6.类加载的过程
1.加载 2.链接 3.初始化
1.字节码被读取到jvm中映射为class对象
2.链接分为三个过程:
1.验证:验证字节信息是否符合jvm规范
2.准备:申请静态变量需要的内存,但不执行赋值操作
3.解析:将常量池中的符号引用变成直接引用
3.真正执行类的初始化代码,包括静态变量的赋值,静态初始化块内的逻辑
什么叫做双亲委派模型
1.就是一个类被加载的时候先找父类加载器,再找当前加载器
2.类加载器分为bootstrap(.jar),ext(/jre/lib),app(classpath),自定义加载器
7.逃逸分析的判断
一是对象是否被存入堆中(静态字段或者堆中对象的实例字段),二是对象是否被传入未知代码中。
一旦对象被存入堆中,其他线程便能获得该对象的引用。即时编译器也因此无法追踪所有使用该对象的代码位置。
由于 Java 虚拟机的即时编译器是以方法为单位的,对于方法中未被内联的方法调用,即时编译器会将其当成未知代码,毕竟它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中。因此,我们可以认为方法调用的调用者以及参数是逃逸的
如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有意义。这是因为其他线程并不能获得该锁对象,因此也不可能对其进行加锁。在这种情况下,即时编译器可以消除对该不逃逸锁对象的加锁、解锁操作。就是你说的锁消除
如果逃逸分析能够证明某些新建的对象不逃逸,那么 Java 虚拟机完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象
但是java没有使用栈上分配
而是使用了标量替换
那什么是标量替换呢?
就是将原本对对象的字段的访问,替换为一个个局部变量的访问
这些字段既可以存储在栈上,也可以直接存储在寄存器中。而该对象的对象头信息则直接消失了,不再被保存至内存之中。
由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。与栈上分配相比,它对字段的内存连续性不做要求,而且,这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。
8.关于wait()和notify()用法
notify()一定要写在wait()的前面,这样才能及时唤醒
wait 必须放在同步块,或者同步方法中。而sleep可以任意位置
9.关于数据库的水平拆分和垂直拆分
举个栗子
user{
id
name
age
}
我们把user存在不同的user表里面,每个user具体去那个表可以做个hash,实际就是我们一个user表拆分成了多个user表。这就是水平拆分
特点
每个表结构的都一样
每个表数据都不一样,没有交集
每个表都不是全量数据
2.什么是垂直拆分?
一行的数据如果太大,那就分成,多张表,表与表之间用某一列数据做连接(一般是主键)
特点
每个表数据都不一样,但有交集
每个表结构都不一样
每个表都不是全量数据
什么是mysql的预读
其实这不只是mysql,而是说一次最少读一个内存页(一页4k),这样也许会多读,但是把下一次可能用到的的数据也读到了,减少了磁盘io
mysql的锁结构其实就是个内存结构
重要的两个字段
trx:代表当前锁是哪个事务生成的
is_waiting:代表当前事务是否在等待
10.关于2pc
1.第一阶段投票阶段
参与者通知协调者,协调者反馈结果
2.第二个阶段
收到参与者的反馈后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否提交还是回滚
11.关于二叉树
1.高度和深度都是从0开始的,层数从1开始
2.满二叉可以利用数组存储,为了方便计算节点一般从下标为1开始存储
3.树的前中后遍历其实是对于根节点的前中后
前:根左右
中:左根右
后:左右根
4.迭代写法无非是起一个栈把节点存进去while循环不断迭代按照根左右的顺序输出
5.dfs深度遍历分为前中后序三种
bfs宽度优先搜索
12.二叉查找树
1.先说要求
二叉查找树的要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点,右子树的每个节点都要大于这个节点。
2.查找,遍历,删除相关的操作
13.copy-on-write
本质是一种延时策略,只有在真正需要的时候才复制,而不是提前复制好。
适合场景:读多写少,弱一致性。
没有提供CopyOnWriteLinkedList是因为linkedlist的数据结构关系分散到每一个节点里面,对每一个节点的修改都存在竟态条件,需要同步才能保证一致性。arraylist就不一样,数组天然的拥有前驱后继的结构关系,对列表的增删,因为是copy on wirte,所以只需要cas操作数组对象就能够保证线程安全,效率上也能接受,更重要的是避免锁竞争带来的上下文切换消耗。有一点需要注意的是CopyOnWriteArrayList在使用上有数据不完整的时间窗口,要不要考虑需要根据具体场景定夺
设计一个rpc的路由表
思考
1.每次 RPC 调用都需要访问路由表,所以访问路由表这个操作的性能要求是很高的。
2.不过路由表对数据的一致性要求并不高,一个服务提供方从上线到反馈到客户端的路由表里,即便有 5 秒钟,很多时候也都是能接受的(这个时间参考了注册中心nacos)
3.路由表是典型的读多写少类问题
综合:对读的性能要求很高,读多写少,弱一致性。
设计
服务提供方的每一次上线、下线都会更新路由信息。
1.一种是通过更新 Router 的一个状态位来标识,如果这样做,那么所有访问该状态位的地方都需要同步访问,这样很影响性能。
2.另外一种就是采用 Immutability 模式,每次上线、下线都创建新的 Router 对象或者删除对应的 Router 对象。由于上线、下线的频率很低,所以后者是最好的选择。
结果
1.选择第二种方式来实现 使用copy-on-write
大概结构ConcurrentHashMap
路由表就是一个namenode
14.关于一次加载和懒加载
1.一次加载顾名思义就是一次把需要的数据在初始化的时候一次性全查出来加入缓存。
2.懒加载需要哪一个数据就加载那个数据,不会多加载
15.关于代码级别的锁升级与降级
1.获取读锁,在没有释放读锁的情况下获取写锁,典型的锁升级场景,java不允许这样。
此时读锁还没有释放。
2.获取写锁,获取读锁,释放读锁,释放写锁。典型的锁降级场景
16关于wait()和await()的关系
线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才能使用。如果一不小心在 Lock&Condition 实现的管程里调用了 wait()、notify()、notifyAll(),那程序可就彻底玩儿完了。
17.关于线程池
1.初始化
ThreadPoolExecutor(
int corePoolSize,//保持的最小线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//当线程空闲这么长时间后,且线程数大于corePoolSize,就要回收多余的线程资源
TimeUnit unit,
BlockingQueue
ThreadFactory threadFactory,//线程工厂,自定义如何创建线程
RejectedExecutionHandler handler//拒绝策略
如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。
)
2.四种拒绝策略
1.CallerRunsPolicy:提交任务的线程自己去执行该任务。
2.AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
3.DiscardPolicy:直接丢弃任务,没有任何异常抛出。
4.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
18.mysql方案的天花板
1.执行计划是单机的
2.如果一张表分布在不同的mysql server中那么读取下一行可能会很大
3.数据复制问题,mysql是半同步或者异步的
19.关于ioc
1.什么是控制反转
控制反转就是把创建bean的过程移交给第三方,这个第三方就是容器container
容器负责创建、配置和管理 bean,也就是它管理着 bean 的生命,控制着 bean 的依赖注入。
通俗点讲,因为项目中每次创建对象是很麻烦的,所以我们使用 Spring IoC 容器来管理这些对象,需要的时候你就直接用,不用管它是怎么来的、什么时候要销毁,只管用就好了。
2.什么是bean?
bean就是包装好的object
3,spring是如何设计容器的?
使用 ApplicationContext,它是 BeanFactory 的子类,更好的补充并实现了 BeanFactory 的。
BeanFactory 简单粗暴,可以理解为 HashMap:
k为全类名 v是对象
Key - bean name
Value - bean object
4.ApplicationContext 的里面有两个具体的实现子类,用来读取配置配件的:
ClassPathXmlApplicationContext - 从 class path 中加载配置文件,更常用一些;
FileSystemXmlApplicationContext - 从本地文件中加载配置文件,不是很常用,如果再到 Linux 环境中,还要改路径,不是很方便。
其实这就是 IoC 给属性赋值的实现方法,我们把「创建对象的过程」转移给了 set() 方法,而不是靠自己去 new,就不是自己创建的了。
这里我所说的“自己创建”,指的是直接在对象内部来 new,是程序主动创建对象的正向的过程;这里使用 set() 方法,是别人(test)给我的;而 IoC 是用它的容器来创建、管理这些对象的,其实也是用的这个 set() 方法,不信,你把这个这个方法去掉或者改个名字试试?
何为控制,控制的是什么?
答:是 bean 的创建、管理的权利,控制 bean 的整个生命周期。
何为反转,反转了什么?
答:把这个权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。
何为依赖,依赖什么?
程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。
何为注入,注入什么?
配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。
所以说,控制反转是通过依赖注入实现的。但是你品,你细品,它们是有差别的,像是「从不同角度描述的同一件事」:
IoC 是设计思想,DI 是具体的实现方式;
IoC 是理论,DI 是实践;
从而实现对象之间的解藕(这样你们不管怎么修改外部的对象,都对我们内部的对象没有影响)。
当然,IoC 也可以通过其他的方式来实现,而 DI 只是 Spring 的选择。
IoC 和 DI 也并非 Spring 框架提出来的,Spring 只是应用了这个设计思想和理念到自己的框架里去。
20.关于网络的七层模型
1.物理层面
2.链路层
3.网络层
4.传输层(tcp/udp)
5.会话层
6.表示层
7.应用层
什么是tcp?
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
什么是一个tcp连接呢?
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
Socket:由 IP 地址和端口号组成
序列号(sequence numbers):用来解决乱序问题等
窗口大小(window size):用来做流量控制
如何确定一个唯一的连接呢?
源地址 源端口 目标地址 目标端口(从哪来到哪去)从哪个主机发送到那个主机,从哪个进程发送到那个进程
拥塞控制、流量控制
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
前两次握手不能携带数据,第三次握手可以携带数据
为什么一定要三次握手
1.三次握手才可以阻止历史重复连接的初始化(主要原因)
比如旧的syn比新的先到
2.三次握手才可以同步双方的初始序列号
这样一来一回,才能确保双方的初始序列号能被可靠的同步。
3.三次握手才可以避免资源浪费
如果不回复ack,每次syn就要主动建立连接,如果客户端的syn在网络中拥堵了,且收不到ack。就会多次发送syn。这样的话就会建立多个连接,造成连接资源的浪费。
何为syn攻击?
在三次握手阶段客户端发送syn服务端发送ack+syn后,客户端不回复ack这样服务端就会一直等待客户端ack直到超时,服务端的syn队列会很快排满
1.修改内核参数
修改队列大小,以及队列满后的处理策略
四次挥手(断开连接)
主动关闭连接的,才有 TIME_WAIT 状态。
21.关于cas
1.本质
是cpu提供了compare and swap指令,该指令提供了三个参数
1.共享变量的内存地址A。
2.用于比较的值B。
3.用于共享变量的新值C。
当A=B时,更新A=C
ABA
解决方式版本号or时间戳
22.关于hashmap的一切
1.组成
是有数组和链表组成的,根据hash求出数组索引
。当索引重复的时候新的键值对就会在链表上增加一个节点
2.当hash重复的时候是如何插入的呢?
1.8之前是头插法(就是插在链表的头部)
1.8之后是尾插法
为什么会修改呢?
因为头插法会在多线程插入且rehash的时候形成循环链表
尾插法不会修改链表的顺序所以不会引发这个问题
3.为何resize何时resize
1.当容量不够的时候需要扩容,就需要resize了
2.当存储的键值对达到容量*0.75(负载因子)就需要resize
如何扩容
1.创建一个长度是原数组两倍的新数组
2.遍历原数组,把所有键值对重新hash到新数组里面
为什么rehash
1.求index的公式index = HashCode(Key) & (Length - 1)
当数组长度不一致时计算出来的值也变了。
为什么是默认初始化容量是16
因为16-1是15,15的二进制是1111
可以的hashcode&1111得出的十进制数就是4,只要length-1不是2的幂数,那么转二进制之后,
就都是1,那么hash算法算出的数字就和hashcode的后几位有关。那么只要hashcode均匀那么hash
算法就是均匀的
为什么重写equals需要重写hashcode?
因为equals是比较两个内存的地址,如果是值对象就比较两个对象的值
如果是引用对象,就比较两个引用的地址。
如果我们的两个对象算出的index都是2那么他们就存在同一个链表里面了
如果他们hashcode一致我没就没法区分到底我们想要get的是哪个。
所以我们需要重写hashcode算法.保证每一个key的hashcode都不一样
插入过程
1.链表法:先key hash func获取存在那个桶里(数组)然后用index公式获取插在数组的那个位置,如果当前数组的位置已经有内容了
就使用尾插法插入链表。
2.开放地址法:当前桶被占了,就用一定的方式去找下一个桶,直到找到空的
什么时候转红黑树
当链表大小超过8个的时候。
与hashtable的对比
1.hashtable是并发安全的,hashmap不是,所以在多线程写入的时候会有数据覆盖问题。hashtable不允许键值为null,hashmap允许
hashmap:
初始化容量是16
扩容直接乘2
hashtable:
初始化容量是11
乘2+1
fail-fast机制是怎么回事?
快速失败是java集合中的一种机制,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容做了修改,就会抛出异常。集合会维护一个modcount变量,执行一个hashNext()/Next()的时候会去判断modcount是否为expectedmodCount,如果不是就抛出异常
fail-safe
安全失败是并发包里面的机制,即查询的时候先看数据存在不存在
23.ConcurrentHashMap
使用了分段锁,锁的是数组的每一个元素。
默认容量是16
插入
1.先是尝试获取锁
2.自旋获取锁(scanAndLockForPut(key,hash,value))
3.自旋到一定次数,改为阻塞获取锁
4.put的时候会判断value==null如果等于会抛出异常
也就是说一定是获取锁再去操作
步骤和之前的hashmap一样,先算出index
然后遍历链表,如果有hashcode相等的就说明
当前节点存在,那么需要更新节点,如果当前节点不存在,
那么就new一个新节点,然后将新节点利用尾插法插入链表
修改成之后释放锁。
jdk 1.7使用的是segment分段锁
jdk 1.8使用cas+synchronize
之前的方式是获取锁然后写入,现在是先cas写入,如果容量不足则需要扩容。
如果都不满足怎利用synchronized锁写入数据
如果链表大于8个节点了,需要转成红黑树
24.回顾缓存行
1.为什么要有缓存行?
因为cpu比内存快太多了,为了更快的从内存中读取数据,于是诞生了多级缓存
2.查询过程
首先会去L1缓存查找所需数据,如果没找到,再去L2缓存查找,一次类推,知道从内存中获取数据,这也就意味着,越长的调用链,所耗费的时就越长,如果每次去主存的时候多拿一些,那么是不是就避免了频繁您访问主存了呢?一般来说每个缓存行大小为64字节,并且每个缓存行有效的引用主内存的一块儿地址,cpu每次从主内存中获取数据时,会将相邻的数据也一同拉取到缓存行中,这样当cpu执行运算时,就大大减少了于主内存的交互。
3.当多线程同时修改cache会怎么样?
core1上的线程修改了l1里面的a,同时告诉其他cpul1里面的缓存引用已经没用了,这时core2上的线程发起了修改同一个缓存中的变量b,为了可见性core会将数据回写回主存。此时core2将内存重新读取到l1缓存然后在修改。所以说一个cache line的数据被多线程访问,就会相互竞争,并且频繁回写。
4.那么如何解决未共享问题?
数据填充,填满一个缓存行。即当前数据和填充数据的大小为64字节,下一个数据放在下一个64字节里面即可。
25.随便复习一下快速排序
1.第一步选择一个基准值,一般选择头部元素作为基准值
2.将小于基准值的元素放在基准值的前面,将大于基准值的元素放在基准值后面。
26.jdk动态代理和cglib的区别
JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
27.关于ThreadLocal
28缓存更新的套路
1.Cache Aside Pattern
失效:应用程序先从cache取数据,没有得到,则从数据库读取,成功后放到缓存中。
命中:应用程序从cache中取数据,取到后返回
更新:先把数据存到数据库中,成功后,再让缓存失效。
一个读操作没有命中缓存,然后就去数据库拉数据了,同时有一个并发写操作,写完数据库之后让缓存失效,然后之前的读操作会把老数据放入缓存,所以,会造成脏数据。
2.Read/Write Through Pattern
更新数据库的操作由缓存代理,从应用来说后端就是一个单一的存储。
Read Through
如果读的时候缓存失效,缓存服务将数据从数据库读出并加入缓存。
Write Through
当有数据更新且没有命中缓存,就直接更新数据库。命中了就先更新缓存,再更新数据库
3.Write Behind Caching Pattern
更新数据的时候只更新缓存,不更新数据库,由缓存异步批量的更新数据库
操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。
29.关于LRU
最近最少使用。假设最少使用的信息,将来被使用的概率也不大,所以在内存不够的情况下,就可以吧这些不常用的信息踢出去,腾地方。
FIFO先进先出
LFU 对每个访问信息记数,踢走访问次数最的那个,如果访问次数一样,就踢走好久没用过的那个。
30.堆和堆排序
1.堆是一个完全二叉树
2.完全二叉树就是除最后一层节点,其他层的节点都是满的,最后一层的节点都靠左排序
3.堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
4.对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”。
5.因为是完全二叉树,所以适合存储在数组里。如果一个节点为下标i,那么左节点为i2,右节点为
i2+1,如果有父节点那父节点为i/2\x32
1.添加一个节点
直接将数据放在堆尾,然后开始判断有没有父节点即i/2>0。
然后判断父节点于子节点的大小
大顶堆a[i]>a[i/2]