本文是一个长文,很长很长的那种【还没完结】,可能需要花费较长的时间【老司机除外】。另外个人的能力有限总结的也不全面,希望大家可以批判性的提出,不胜感激!
jdk 源码:
String :
-
- 不可变对象,内部使用final char[] value ,使用标量赋值如String str = “str”时会先查找String常量池里面有没有,有就复用,没有就创建,对于new String() 则每次都会创建,需要手动intern()放到常量池,并返回常量池的引用;
Exception Exception和Error都是Throwable的子类
-
- Exception: 分为受检异常和非受检异常(运行异常),受检异常是在编译器就要求捕获的如FileNotFoundException,IOException等等,非受检异常属于不可预判的如数组越界,空指针等在程序运行时才会知道的异常,也可以被捕获;可以被业务修复
int和Integer 区别
-
- int是基础数据类型,Integer是一个对象,也是被final修饰的不可变对象,同时为了性能,对于Integer会有一个缓存类,缓存了-128-127之间的Integer对象,这样不用每次都去new对象,而且一个对象的内存开销【对象头,实例数据,内存填充(为了满足大小是8Byte的整数倍)】远远大于基础类型;
集合类
List
-
- ArrayList:使用最多的,内部使用动态数组保存数据,不够用时会进行扩容,扩大为1.5倍,每次add时会先判断是否需要扩容,不是线程安全的。使用建议提前预估容量大小,避免扩容。
-
- Vector:可以理解为ArrayList的线程安全版本,使用synchronized实现,不同的是扩容时默认是扩大为原来的2倍。
Map
-
- HashMap : 最常用的Map,无序的,线程不安全
1).1.8之前是数组+链表,hash冲突时头插链表,线程不安全,在扩容时由于会重新hash分布并且头插会导链表出现环从而cpu到达100%;先插入再扩容时,当大小达到容量*负载因子(0.7一个经验值)时会扩容为原来的2倍,而且HashMap的容量会一直是2的指数大小,这样是为了hash时可以通过高效的位运算;
2). 1.8之后HashMap底层换成了数组+链表->红黑树,红黑树在查找时时间复杂度比链表小,但是只有链表长度达到了8才会转换,而且由头插换成了尾插,在hash函数方面也做了优化,保证在扩容时原来数组下面的数据只会出现在原处或者新增大小的位置;为啥使用红黑树不使用AVL呢,这是因为红黑树构建时快,而对于为啥不使用B+树则是因为数据量不大适合放内存,可以快速定位,而B+树则适合磁盘文件【减少树的深度,降低IO次数】。
-
- HashTable:线程安全的,通过synchronized实现,不推荐使用
-
- TreeMap :有序Map,底层使用的是红黑树来进行排序,时间复杂度都是log(n);
Set
可以简单的理解为Map的key集合,内部使用里一个HashMap,也是通过这个map来进行数据存储。
并发集合类:
1. ConcurrentHashMap:
2. CopyOnWriteArrayList:
IO/NIO
1. IO模型:
1). 同步阻塞 BIO:从read调用开始阻塞到数据传输结束
2). 同步非阻塞:read调用,没数据就返回,然后继续调用,直到有数据
3). 异步阻塞(I/O多路复用)NIO:select模式,循环访问通道,当有事件就绪时,read调用是阻塞的
4). 异步非阻塞 AIO:read调用立马返回但是会注册callback,通过callback来实现数据返回
2. InputStream/OutputStream: 字节流,一般用于图片,或者网络传输
Reader/Writer: 字符流,文件读写使用比较多
一般都会使用带缓冲的Buffer来进行读写,对于传统IO可能会涉及到用户空间和操作系统内核空间的交换所以比较慢,但是NIO使用零拷贝或者直接内存【堆外内存】来进行数据读写,性能比较好。
动态代理的原理:
动态代理目前有两种方案Jdk实现和Cglib实现,Spring AOP默认实现是JDK Proxy
1. JDK Proxy
基于反射实现,目标类需要实现接口,本质是动态生成接口的实现类,然后再该类里面进行目标类方法的调用;
2. Cglib
基于字节码框架【ASM】实现,目标类不用实现接口,本质是通过ASM动态生成目标类的子类,如果目标类是final 修饰的就不行了;性能略高。
多线程:
1. 线程生命周期
新建 -> 就绪 -> 运行 -> 死亡
运行期间调用sleep,yield,join或者遇到wait,synchronized等锁会进入暂停状态
暂停分为 等待和阻塞
等待 waiting:持有锁的情况下,执行sleep,wait,join时进入等待
阻塞 blocked:等待锁而进入,如synchronized和Lock
暂停状态恢复后进入的是就绪状态等待cpu的调度
当一个线程被new 出来后就进入新建状态,此时新建状态【有了自己的线程空间】
当线程调用start后该线程会进入就绪队列【或者就绪线程池】,等待cpu调度
cpu调度进入某个线程后就开始执行了,此时线程处于运行状态,此时的状态是比较复杂的,可以进入阻塞,等待,死亡状态;
在运行状态碰到锁时会进入阻塞,而被wait,join,sleep调用则会进入等待状态
阻塞和等待恢复后并不会立马进入运行状态,而是会进入就绪状态等待被调度。
线程运行结束后就会进入死亡状态了。
2. 线程的方法
start:线程启动入口,调用后线程进入就绪状态等待被调度,如果对同一个线程执行多次start会抛出异常
run: 线程执行的方法主体
sleep:线程睡眠,会进入等待状态,不会释放锁
join:线程等待直到线程死亡(Wait for this thread to die),本质是在方法中循环判断当前线程是否存活,然后调用wait(0),那么join会释放当前调用的对象的对象锁【join是同步实例方法】。
yield:放弃cpu,当前线程会进入就绪状态,等待cpu的调度,但是有可能会立马又被执行,比如当前线程的优先级最高
3.下面的方法都属于Object的方法
每个对象都有一个对象锁,而对象锁有两个线程的池分别是锁池和等待池
锁池:在竞争获取该对象锁失败线程会进入该池
等待池:持有该锁的线程调用了wait()方法后会进入该池
当对象执行notify()/notifyAll()后,会把等待池中的一个随机线程或者所有线程放入锁池,去进行锁的竞争。
wait:持有该对象锁的线程会进入等待,并且释放对象锁,会响应中断异常
notify:唤醒等待池中的一个线程进入锁池,也可以理解为将一个在WAITING状态的线程唤醒进入RUNNABLE状态,去竞争锁
notifyAll:唤醒等待池中所有的线程进入锁池
4. 线程池
核心参数
corePoolSize:核心线程数,线程池正常运行的线程数,该线程被创建后不会被销毁(如果设置了allowCoreThreadTimeout则会被销毁)
maxPoolSize: 最大线程数,线程池在等待队列满了后会继续创建线程池,空闲时会被销毁
queue : 队列,核心线程满了后会将新的线程放入队列中
keepAliveTime: 超过核心线程数的线程最大空闲时间
RejectedExecutionHandler : 当线程个数> coreThreadSize + maxThreadSize + queue时,对新加入的线程执行的策略
拒绝策略:默认的,会拒绝加入新任务,并抛出异常
抛弃策略:新加入的任务会被直接抛弃不执行,也不抛出异常
抛弃最早的任务策略:在没有showdown的场景下,把队列里面最早未执行的任务抛弃。然后加入新的任务
调用线程运行策略:哪个线程调用的就让调用的线程去执行新任务
数量估算
IO密集型:2N+1
CPU计算密集型:N+1
上面是普通未进优化的,具体的应该看场景,要考虑线程执行时间,CPU时间,线程切换/等待时间等多方面考虑,一般对于性能要求严格的场景会需要进行测试。
核心源码
5. 生产者-消费者实现
并发相关
1. 内存模型 JMM
2. synchronized 原理
3. volatile 原理
4. AQS 原理
5. Lock实现
6. Object的wait,notify
7. Condition 的await和single
8. CountDownLatch 实现
9. 原子类型的实现,AtomicLong的实现
10. 锁类型
11.ThreadLocal
12.Random 和ThreadLocalRandom
JVM
内存区域
GC
class文件和ClassLoader
编译
早期
运行期
JMM Java内存模型
Spring
IOC
AOP
TX
Spring MVC
MySQL
基础
数据类型
- int 4个字节
- smallint 2个字节
- tinyint 1个字节
- bigint 8个字节 【使用雪花算法时需要使用到这个属性】
- decimal 变长 高精度 【涉及到精度比较高的数据使用此类型,比如金钱】
- char
- varchar
语法注意
- group by 5.6 之后select 后面的必须是出现在group by之后的,或者使用函数计算的如,sum,avg,count等
- having 对group by后对数据进行筛选
- sql执行顺序:from ->on ->join ->where-> group by-> having-> select ->distinct-> union-> order by
存储引擎
innodb:有事务,有行锁,表锁,索引即数据
myisam:无事务,表锁,索引和数据分开放,只有hash索引
Innodb 索引
数据结构是B+树,非叶子节点存放键和指针,叶子节点存放行记录,同时会存单向指针【粗略估算数据大小】,节点之间会有双向指针;另外文件系统是以page来管理的,一般是16kb大小,在数据插入时如果比较稀疏或者不按照趋势自增的顺序会导致页分裂和页合并,这个对写性能会有比较大的损耗;
索引的工作原理:每一个节点所在的页都会有一个页目录【page dictionary 稀疏的】,从根节点开始往下查找,可以很快定位到数据所在的叶子节点,然后在数据页中的页目录中使用二分法查找确定行记录,这样只通过很少的io就可以定位到数据。
索引分为 聚簇索引 和 二级索引 两者区别是聚簇索引的叶子节点就是数据,而二级索引的叶子节点是主键,所以使用二级索引存在回表的情况【除了覆盖索引】;
联合索引也只会建立一个索引树
myisam 索引
索引只会记录主键和数据行号,索引和数据分开存储
innobd事务
隔离级别
- RU: 读未提交,脏写/脏读
- RC : 读已提交,不可重复读
- RR : 可重复读,幻读(MySQL解决了:Gap-Lock+MVCC)
- 串行化:串行化
四种隔离级别的并发能力 : RU > RC > RR【MySQL默认的】 > 串行化
MVCC
每开启一个事务mysql都会分配一个全局递增的事务id;
多版本并发控制
指的是一行记录允许存在多个版本,每个版本都有一个事务id【全局递增的,值越小越早】代表当前记录的版本信息,按照先后顺序在undo-log中存储,是一个单向版本链表,从大到低排列。
一致性读视图
ReadView: 为了判断当前事务中哪个数据版本的记录是可见而生的
为了理解ReadView需要理解几个属性:
- m_txids: 当前所有活跃【开启但未提交的事务】的事务id列表
- min_txid: 活跃的事务id列表中最小的事务id
- max_txid: 下一个需要分配的事务id【活跃id列表中最大的事务id+1】
- cur_txid: 当前事务的id
那么怎么来判断当前事务可见版本呢?分为下面几种情况
- txid【版本的事务id】 小于 min_txid ,可见,因为此时说明txid事务已经提交了
- txid 等于cur_txid,可见,因为此时处于同一事务中
- txid 大于等于 max_txid,不可见,此时txid的事务是在当前事务的下一个
- txid 大于等于min_txid 并且小于max_txid,此时如果txid在m_txids中,是不可见的,否则是可见的
根据上面四种情况可以判断某一个版的数据是否对当前事务可见,从而可以在当前事务中生成一个ReadView。
隔离级别实现原理
不同的隔离级别事务实现的的原理是不一样的
- RU: 此时每次都是读最新的版本数据
- RC: 每次查询时都会生成新的ReadView,从而解释了为啥能读到别的事务已经提交了的版本数据
- RR: 在事务开启时才会开启一个ReadView,此时别的事务提交的数据在ReadView之外,所以看不到数据
- 串行化:加了锁,表示所有的事务是串行化的不会有问题。
总结下来就是: 通过MVCC机制生成ReadView,然后不同的事务隔离级别下开启ReadView的时机不一样【RC:遇到select 就会创建,RR:事务开启时才会创建】,从而实现了不同隔离级别看到数据不一样。
锁
锁按照粒度分为行级锁和表级锁,表级锁一般只会在执行MDL时才会发生
分类
- 行锁:对一行记录进行加锁
- 间隙锁【Gap】:对记录之间的间隙加锁
- Next-Key【间隙锁+行锁】:左开右闭,形成锁定,保护记录和记录之前的间隙
RR 级别解决幻读的方法是:MVCC 和 Next-Key 锁来实现的
加锁方式
这个就比较复杂了,需要区分不同的隔离级别,需要注意的是在RC级别下面是没有GAP锁的,只会有行锁。
对于聚簇索引和非聚簇索引加锁也是不一样的,有个详细的介绍mp.weixin.qq.com/s/wSlNZcQka…
简单的说就是对有可能使用到的数据加锁,包括聚簇索引和相关的二级索引;只不过使用的顺序不一样,如果使用聚簇索引,那么先给聚簇索引加锁然后给对应的二级索引加锁,如果使用的是二级索引,则会先给二级索引加锁然后给聚簇索引加锁【存在回表的情况才会】,
ES
使用优化
“SQL” 化查询组件
分两步,第一步sql从哪来,第二步sql如何转化成ES认识的DSL查询json
- sql生成: 这里参照传统ORM框架里面动态生成sql的方式,使用freemarker插件进行动态替换,可以在应用启动时加载所有的sql,使用者只需要关心sql的逻辑【只负责查询逻辑】,这里可以抽象的理解为客户端;
- 执行查询:这里使用了es4sql+jest(可替换),本质是通过druid解析sql,然后封装成jest的查询dao去进行数据查询,这里可以抽象为服务端。
通过这两步,可以说将ES组件的使用更加方便以及降低整个团队的学习成本【只要会写sql就能使用ES】,做到开箱即用的效果。
报表类分页查询优化
实时报表一直是比较头疼的问题,而ES是准实时的,那么很多实时报表类的功能也希望能使用上ES【除了大数据分析】,但是ES有个问题就是深度为1w的分页限制【也能理解,ES的查询过程决定了实时分页会有性能问题】,另外报表一般都需要有导出功能,所以这边做了优化,单独提供scroll查询【快照版本查询】
- 分页: 在原有的基础上面加上类似>${id} 这样的条件,避免出现1w深度的问题,缺点是调用端需要去缓存每页的头尾id
- 导出: 使用scroll查询,通过游标快速导出数据,同时也不会有深度分页的问题,ES查询性能也不会降低
索引结构优化
ES的快是有多方面原因的,主要分为os cache + 索引的实现【倒排+FAT 更小的内存可以存储更多的索引,降低了随机读写磁盘的次数】,那么为了提高写性能和查性能做了以下的优化
ES性能的保证
- 合理的分片数,副本数使用1,分片数不宜过多也不宜过少
- 使用redis缓存,控制并发数【并发数不等于qps和tps】
- 同等存储的情况下使用cpu核数较多的机器
Redis
Kafka
MyCat
ZK
描述
ZK是分布式系统中的协调器【kafka,hbase等都使用ZK来管理,选主等】,更为具体的说ZK是一个内存文件系统,是一个通知/订阅系统。
部署分为单节点部署,伪集群部署,集群部署(2N+1 台机器)。整个集群中只有一个leader节点,其他的都是Follower节点,还有可能有observer节点。
需要注意的是所有的写操作都是发生在leader节点的,而读请求是被均衡到各个Follower节点和observer节点。
从而ZK最好的使用场景就是读多写少的场景。
ZK的缺点:leader单点不能扩展,tps不能扩展。但是读qps可以通过增加observer来实现
**
基本概念
四种节点:
- 持久节点
- 持久顺序节点
- 临时节点
- 临时顺序节点
Watcher机制:ZK中可以对一个节点的任何变更【数据,删除,子节点增删等】进行监听,客户端可以向zk集群注册一个wathcer,当节点发生变更时就会被回调【发布/订阅】。需要注意的是watcher是一次性的,如果客户端需要长久的监听就需要每次在事件触发后重新注册一遍。
watcher机制有个缺点是,每个节点的变更会导致所有注册在这个节点上面的watcher都会收到通知,称为“惊群效应”
常用场景
- 服务注册/发现(dubbo,rsf):通过注册节点watcher来实现注册和发现
- 负载均衡 :类似上面
- 分布式锁【公平锁】:
- 在节点下面创建临时顺序节点
- 获取该节点下面的所有子节点,最小序号的节点获取锁
- 如果获取锁失败,往比自己当前节点小的最够一个节点注册watcher【如果当前节点是20,则往19上面注册】
- 等待watcher触发然后执行b步骤
- 配置中心:读取节点信息,并注册wathcer,监听变更
- 选主:创建顺序节点
基本原理
zk本质是一个满足了CP,并尽最大可能满足A的一致性系统,其基本原理是实现了ZAB算法
ZAB算法有两种状态:
- 消息广播
使用的是类似2PC协议,首先会由leader发起写请求【请求携带了一个zxid全局唯一的“事务id”】,然后等到超过一半的Follower反馈成功后,就执行提交操作。
- 奔溃恢复
当网络发生分区,或者leader节点发生宕机时。所有的节点会进入选主状态进行选主;
选主结束后,需要同步数据
在选主和数据同步时都离不开一个zxid的数据:zxid是一个64位数据,高32位是当前时钟周期epoch,每触发一次选主,都会将上一次的epoch进行自增;低32位可以理解为提交的事务数,也是递增的,每次有写请求都会递增。
选主时会互相投票,投票会将自己的zxid和myid发送给对方,然后每个节点会收到一个列表,zxid最大的被选举为leader,如果zxid一样去myid最大的。
数据同步时,也是依据当前最大的提交了的zxid为基础,保证整个集群不会丢失已经提交了的数据。