程序实例 存储管理 缓存机制 SQL解析 日志管理
权限划分 容灾机制 索引管理 锁管理
存储(文件系统)
各模块功能 存储管理:即文件转内存
缓存机制:数据缓存与淘汰机制
SQL解析:解析SQL指令
日志管理:操作日志,方便回滚
权限划分:不同用户数据权限不同
索引管理:提高查询效率
锁管理:支持并发
提高查询效率
尽量能表征数据的唯一性的信息,如主键
二叉排序树、B树、B+树、Hash结构、位图
B+树:① 磁盘读写代价低;② 效率稳定;③ 有利于数据扫描(叶子节点指针)
密集索引,每个数据均有索引
稀疏索引,部分数据有索引
聚簇索引,叶子节点存数据,InnoDB主键索引为聚簇索引
非聚簇索引,叶子节点存数据指针,InnoDB非主键索引为非聚簇索引
慢日志 -> explain -> 修改sql尽量走索引
慢日志参数:show_query_log、show_query_log_file、long_query_time
explain关键字段:type(…range>index>all)、Extra、rows、key
select count(id) from table1 不一定走主键索引
key(name,sex,age)
最左匹配原则:select * from table where name = … and sex = … and age = …
数据量小无需索引
维护索引有成本
MyISAM支持表锁
InnoDB支持行锁,走索引时锁表中某一行,不走索引时锁表
MyISAM适合场景:频繁执行全表、无事务、增删改少、查询多
InnoDB适合场景:增删改查均频繁、有事务、可靠性高
锁划分 按粒度划分:行锁、表锁、页锁
按级别划分:共享锁、排他锁
按使用方式划分:乐观锁、悲观锁
按加锁方式划分:自动锁、显式锁
事务特性:原子性、一致性、隔离性、持久性(原一隔持/ACID)
隔离级别:RU(未提交读)、RC(已提交读)、RR(可重复读)、S(串行化)
访问问题:脏读、不可重复读、幻读(简称脏 不 幻)
脏 读:事务1读取到事务2未提交的数据
不可重复读:事务1多次读取某个数据不一致
幻 读:事务1按一定条件读取数据,返回数据条数不同
隔离级别与问题:RU(脏不幻)、RC(不幻)、RR(幻)、S(无)
表象:快照读(MVCC)
内在:当前读中是范围锁(行锁+间隙锁) gap锁(间隙锁) next-key锁(范围锁)
当前读:select … lock in share mode
select … for update
update …
delete …
快照读:select … from table
快照读使用情况
① 快照读存在于RC、RR级别下
② RC级别下每次select均重新建立快照
③ RR级别下首次select建立快照,update会重新建立快照
Gap锁使用情况:Gap锁存在于RR级别下
问题:对主键索引、唯一索引会使用间隙锁吗?
① 若where条件未全部命中,则使用间隙锁,锁区间段
② 若查询走非唯一索引或不走索引,则使用间隙锁,锁全表
③ 参见"next-key个人总结"
数据有唯一行号、事务ID、回滚指针3个隐藏字段
undo日志
快照(可见性算法,根据事务ID)
next-key加锁范围是一个左开右闭/( ]的区间
主键索引、唯一索引、普通索引情况如下:
① 范围查询时,边界包含的区间会被加next-key锁
② 等值查询时,主键索引和唯一索引命中则退化为行锁,未命中则退化为间隙锁
③ 等值查询时,普通索引命中或未命中均退化为间隙锁
无索引则锁全表
1. InnoDB存储引擎,三层B+树,单表能存储多少数据
MySQl存储单元为1页,1页为16K,即16384Byte
非叶子节点存储单元为主键+指针,大小为8+6=14Byte
单页可存储16384/14 ≈ 1170个单元,即可存储1170个子节点
每个存储单元可标记1页,则前两层可标记1170x1170页
第三层叶子节点存储数据,每条数据约1K,单页则可存储16/1=16个数据
三层 B+树,单表可存数据约为1170x1170x16=21902400条数据
2. between and查询,MySQL如何优化查询速度
假设针对where条件列已建立索引
扫描索引树时,获取最大值和最小值
然后从叶子结点的链表读取数据
3. 覆盖索引与回表
假设有表t(id PK, name KEY, sex, flag),其中,id是聚集索引,name是普通索引,表中有4条数据(1, shenjian, m, A)、(3, zhangsan, m, A)、(5, lisi, m, A)、(9, wangwu, f, B),若有查询操作“select * from t where name=‘lisi’”,则执行如下图
如上图粉红色路径,先定位主键值,再定位行记录,即回表操作,性能相较扫一遍索引树更低;覆盖索引将被查询的字段,建立到联合索引里去,不需要回表,一次扫描索引即可获得数据。
4. SQL执行慢,如何优化
见MySQL知识拓扑图
5. 索引下推
6. SQL优化
不使用前导模糊
不使用负向条件
between放最后
索引列不做操作
联合索引遵循最左匹配原则
…
穿透->
客户端<-----> 缓存<-----------> 存储
<-回种
数据类型丰富
支持数据持久化存储到磁盘
支持主从
支持分片集群
速度:100000+QPS[QPS,秒查询次数]
原因:绝大部分为内存操作,无IO操作,执行效率高
数据结构简单,数据操作简单
采用单线程处理高并发请求,多核也可启动多实例(主线程为单线程,包括IO处理、IO请求、过期键等,无并发问题)
使用多路复用IO模型,非阻塞IO
纯内存操作,速度快
单线程处理主业务,避免线程切换与并发产生的开销
IO多路复用(Linux底层)实现非阻塞IO
注:多进程/线程可提高系统稳定性,不一定提高系统响应速度(比如单一业务场景和单核机器)
Redis主业务:接收客户端请求 -> 解析请求 -> 数据读写 -> 返回给客户端
FD文件描述符
阻塞IO、多路复用IO模型、select系统调用
多路复用函数:epoll、kqueue、evport、select
多路复用函数采用策略: 因地制宜
优先选择时间复杂度为O(1)的函数
以时间复杂度为O(n)的select保底
基于react设计模式监听IO事件
String,字符串
Hash,String类型的字典,适合存储对象
List,String类型的列表,保持插入顺序
Set,String类型的哈希表,去重
Sort Set(ZSet),通过分数为成员进行排序(增序),score(double)
HyperLogLog,用于计数
Geo,存储地理信息
简单动态字符串、链表、字典、跳跃表、整数集合、存储列表、对象
KEYS pattern 查找所存符合给定模式pattern的key
keys指令一次性返回所有的key
键数量大会使服务卡顿
SCAN cursor [MATCH pattern] [Count count]
基于游标的迭代器
以0作为游标开始新一轮迭代
不保证每次执行都返回给定模式的元素,支持模糊查询
每次返回数量不可控
返回key可能重复
分布式锁特点:互斥性、安全性、死锁与容错
分布式锁实现:
① SETNX key value,如果key不存在则创建显式锁
② EXPIRE key seconds,解决key长期有效问题,过期自动删除,缺乏原子性
③ SET key value [EX seconds] [Px milliseconds] [NX|XX],返回为OK/nil
④ 大量key同时过期,则会出现短时间内卡顿,设置随机数作为有效期
List做队列,RPUSH生产消息,LPOP消费消息
缺点:没有等待队列,有则直接消费
措施:在应用程序中,引入sleep,调用LPOP重试
BLPOP key[key …] timeout:阻塞到队列有消息或超时
只供一个消费者消费
pub/sub: 主题订阅模式
发送者发生消息,订阅者接收消息
订阅者可订阅多个频道(Topic)
Subscribe myTopic
Publish myTopic “hello”
RDB持久化:保存某个时间点的全量数据快照
SAVE:阻塞Redis,创建RDB文件完毕
BGSAVE:fork子进程来创建RDB,不阻塞服务
BGSAVE原理:系统调用fork()创建子进程,实现copy-on-write
RDB持久化缺点:大量数据全量同步会影响性能
挂死会丢失最近一次快照之后的数据
RDB持久化配置:save 900 1
save 300 10
save 60 1000
stop-wriles-on-bgsave-error yes
rdbcompresson yes
自动触发RDB持久化方式: 根据redis.conf中save * * 定时触发(用BGSAVE)
主从复制时,主节点自动触发
执行Debug Reload
执行shutdown 且没有开启AOF持久化
AOF持久化(保存写指令): 记录除查询外的所有变更数据库的指令
以append形式追加到AOF文件中
AOF相关配置:appendonly yes/on
appendfilename
appendfsyre everysec/aways/no
RDB优缺点:全量数据快照,文件小,恢复快
无法保存最近一次快照之后的数据
AOF优缺点:可靠性好,适合保存增量数据,不易丢失
文件体积大,恢复慢
BGSAVE保存全量持久化数据,AOF做增量持久化
Redis数据恢复,先恢复RDB,然后重放AOF文件
忽略
架构:一主多从,主(Master)负责写,从(Slave)负责读
过程:主从同步由Slave节点发起,Master生成RDB文件发送到Slave,生成RDB文件之后的操作记为AOF,发送到Slave
特点:弱一致性
全量同步:Slave发送sync命令到Master
Master使用BGSAVE命令生成RDB文件
Master使用AOF文件记录BGSAVE之后的写指令
BGSAVE完成后,发送到Slave,Slave加载RDB
Slave继续加载AOF
增量同步:Master收到增量同步
将写指令记录到AOF文件
将AOF文件发生到Slave,Slave加载
解决主从同步Master宕机后主从切换问题
Sentinel进程的作用: 监控主服务器、从服务器是否正常
通过API向管理员发送故障通知
自动故障迁移,主从切换(Slave升级为Master)
从海量数据快速找到所需
分片:将数据按某种规则划分,实现存储在多个节点
常规Hash算法无法实现节点动态增删
一致性Hash算法
一致性Hash算法:对232取模,对Hash值空间组织圆环
Hash槽
Redis集群未使用一致性哈希,而是引入哈希槽(hash slot)来实现使用数据分片
平台无关性(一次编译、到处运行)
GC(垃圾回收机制)
语言特性(泛型、反射、Lambada表达式)
面向对象(封装、继承、多态)
类库(集合、并发库、网络库、IO库等)
异常处理
编译:源码 -> 字节码
运行:字节码 -> 机器码
源码 -----> 字节码 -----> 机器码
编译 虚拟机适配不同平台
为啥JVM不直接将源码解析为机器码
每次执行需要检查
也可解析执行其他语言编译的字节码
Runtime Data Area:JVM内存模型
Class Loader:类加载器
Execution Engine:解析命令
Native Interface:融合不同开发语言的原生库,为Java所用
定义:运行状态中,对于任意类、任意对象均可获取其方法、属性,并动态调用
获取字节码对象:Class.forName(“全限定类名”)…
获取方法:getMethods()、getDeclaredMethod()…
获取属性:getFields()、 getDeclaredFields()…
调用方法:invoke(实例,参数数据)
ClassLoader:类加载器,将二进制字节码加载到JVM
ClassLoader.loadClass()方法
ClassLoader种类:
① Bootstrap Class Loader(启动类加载器)
② Extension Class Loader(扩展类加载器)
③ Application Class Loader(应用程序类加载器)
④ User Class Loader(自定义类加载器)
自定义类加载器实现关键方法:findClass()、defineClass()
↑ Bootstrap Class Loader ↓
↑ Extension Class Loader ↓
↑ Application Class Loader ↓
↑ User Class Loader ↓
自下而上检查类是否已加载 自上而下尝试加载类
为什么使用双亲委派机制加载类
运行过程中,保证字节码的唯一性
类加载方式
① 隐式加载:new
② 显示加载:loadClass、Class.forName等
类装载过程
1 加载: 通过ClassLoader加载Class字节码,生成Class对象
2 链接: 校验:检查加载Class文件正确性和安全性
准备:为类变量分配存储空间并初始化
解析:JVM将常量池内的符号引用转为直接引用
3 初始化: 执行类变量赋值和静态代码块
Class.forName得到的Class已经完成初始化
classLoader.loadClass得到的CLass是没有链接的
JDK8 内存布局,私有包括虚拟机栈、本地方法栈、程序计数器,公有包括元空间、堆
程序计数器:当前线程执行字节码的行号
改变计数器的值,选取下一条待执行的字节码
和线程一一对应,即线程私有
若为Native方法,计数器值为undifined
不会发送内存溢出
虚拟机栈:Java方法执行的内存模型
包含多个栈帧,局部变量表、操作数栈
本地方法栈:与虚拟机栈类似,主要作用于Native方法
元空间(MetaSpace)和永久代(PermGen Space)的区别
元空间使用本地内存,永久代使用JVM堆内存
元空间相比永久代的优势
字符串常量池存储在永久代中,容易出现性能问题和内存溢出
类信息和方法信息大小难以确定,不利于永久代空间大小指定
永久代会给GC带来复杂
方便HotSpot与JVM(Jrockit)的集成
Java堆:对象实例分配的区域
GC管理的主要区域
新生代分为 Eden:From:To = 8:1:1
JVM三大性能调优参数-Xms、-Xmx、-Xss的含义
-Xss:规定线程虚拟机堆栈的大小(256K足够),该数可影响并发线程数
-Xms:堆初始值(进程创建时堆的大小)
-Xmx:堆最大值,Xms和Xms可设置为相同(内存扩大时会发生内存抖动,影响程序稳定)
Java内存模型中、堆和栈的区别-内存分配策略
静态存储:编译时即可确定运行时存储空间
栈式存储:存储空间需求编译时未知,运行时进入模块前可确定
堆式存储:编译或运行时进入模块前均不可确定,动态分配
Java内存模型中、堆和栈的区别
管理方式:栈自动释放、堆需要GC
空间大小:栈比堆小
碎片相关:栈产生碎片远小于堆
分配方式:栈支持静态和动态分配,堆只支持动态分配
效率:栈效率比堆高
元空间、堆、线程独占部分之间的联系-内存角度
元空间:存储类型信息、类对象方法、属性
堆:存储对象实例
栈:存储局部变量、引用变量、行号
不同JDK版本之间的intern方法的区别-JDK6&JDK6以上
略
垃圾判定标准:没有被其他对象引用
垃圾判定算法 引用计数法:判断对象的引用数量,数量为0可被回收
可达性分析算法:判断对象的引用链是否可达,不可达则可被回收
引用计数法 优点:执行效率高,不影响程序运行
缺点:循环引用,导致内存泄漏
可达性分析法 可做GCRoot的对象:虚拟机引用对象
方法区中常量引用的对象
方法区中静态变量引用的对象
本地方法栈中JNI引用的对象
活跃线程的引用对象
标记清除
标记:扫描所有对象,标记存活的对象
清除:从头到尾遍历堆内存,回收垃圾对象
缺点:碎片化问题
半区复制
分为对象面和可用面
对象在对象面上创建
存活对象从对象面复制到可用面
清除对象面上垃圾对象
标记整理
标记:扫描所有对象,标记存活的对象
整理:移动所有存活对象,按内存地址排列,清除垃圾对象
优点:解决空间碎片化问题、避免内存的不连续性、适用于存活率较高的对象
分代收集算法
垃圾回收算法组合拳
按对象生命周期不同划分区域,不同区域采用不同算法
JDK中垃圾收集算法:JDK1.8中,新生代为半区复制,老年代采用标记清除或标记整理
GC分类: Minor GC/Young GC,半区复制,回收新生代
Major GC/Old GC,回收老年代(有异议)
Full GC,标记清除或标记整理,回收新生代+老年代
由于虚拟机发展,关于Major GC概念已混乱不清,一般指Full GC,有时特指Old GC
内存分区比例:新生代中,Eden:From:To = 8:1:1
新生代:老年代=1:2
对象进入老年代: 对象年龄超过15(通过-XX:MaxTenuringThreshold设置)
From/To区无法存放的大对象
新生大对象
常用调优参数:
① -XX:NewRatio:设置新生代和老年代的比例老年代/新生代
② -XX:SurvivorRatio:设置eden区和survivor区大小的比例
③ -XX:MaxTenuringThreshold:设置对象进入老年代的最大年龄
Full GC比Minor GC执行慢,频率低
触发Minor GC,Eden空间不足
触发Full GC : 老年代空间不足
调用Systern.gc()
Minor GC时,晋升到老年代对象的平均大小大于老年代剩余空间
JDK1.7中,永久代空间不足
stop-the-world
JVM执行GC,停止应用执行
任何GC算法都会发生
GC优化,即减少stop-the-world发生次数,提高程序性能
SafePoint安全点
分析过程中对象引用关系不会发生的点
产生SafePoint的地方:方法调用、循环跳转、异常跳转
安全点数量得适中
JVM运行模式:Server、Client
新生代垃圾收集器:Serial、ParNew、Parallel scavenge
老年代垃圾收集器:Serial Old、Parallel Old、CMS、G1
Object的finalize方法的作用和C++中析构函数是否相同
与C++中析构函数不同,析构函数调用确定,finalize调用不确定
将未被引用的对象放到F-Queue队列
方法执行随时可能会被终止
给予对象最后一次重生的机会
Java中强引用、轻引用、弱引用、虚引用有什么作用
强引用
最普通的引用 Object obj = new Object()
抛出OutOfMemoryError终止程序,但不会回收具有强引用的对象
将对象设置为null,可转为弱引用,使其被回收
轻引用
对象处在有用但非必须的状态
当内存空间不足时,GC会回收该引用对象的内存
可用来实现高速缓存
SoftRerference
弱引用
非必须的对象
GC时会被回收
WeakReference
虚引用
不会决定对象的生命周期
任何时候都可能触发GC回收
必须和引用队列ReferenceQueue联合使用
略
进程和线程的由来:串行 -> 批处理 -> 进程 -> 线程
进程和线程的区别:
① 进程是资源分配最小单位,线程是CPU调度最小单位
② 进程是独立应用,线程不是独立应用
③ 进程有独立地址空间,线程没有独立地址空间,线程是进程内一个执行路径
④ 进程切换开销大,线程切换开销小
Java进程和线程的关系:
① Java对操作系统功能进行了封装,包括进程和线程
② 运行应用会产生一个进程,进程至少包含一个线程
③ 每个进程对应一个JVM实例,多个线程共享JVM堆空间
④ Java采用单线程编程模型,应用自动创建主线程
⑤ 主线程可创建子线程,原则上要后于子线程完成执行
调用start()方法会创建一个子线程并启动
run()方法是Thread的一个普通方法调用
Thread.start() -> JVM_StartThread -> thread_entry -> Thread.run()
Thread是实现了Runnable接口的类,使run支持多线程
因类的单一继承原则,推荐使用Runnable接口
如何给run方法传参?
① 构造函数传参
② 成员变量传参
③ 回调函数传参
如何实现处理线程的返回值?
① 主线程等待法
② 使用Thread类的join方法阻塞当前线程以等待子线程处理完毕
③ 通过callable接口实现(即通过FutureTask或则线程池获取)
6个状态 New(新建):创建后尚未启动的线程状态
Runnable(运行):包含Running和Ready
Waitting(无限期等待):不会被分配CPU执行时间,需要被显示唤醒
Timed Waitting(限期等待):在一定时间后由系统唤醒
Blocked(等待):等待获取排他锁
Terminated(结束):已终止线程的状态,结束执行
基本区别:sleep是Thread的方法,wait是Object的方法
sleep可在任何地方调用,wait只能在synchronized代码块中使用
本质区别:sleep会出让CPU,不出让锁,wait出让CPU和锁
两个概念:锁池(EntryList)、等待池(WaitSet)
notifyall唤醒所有等待状态的线程,全部竞争锁
notify随机唤醒一个等待状态的线程,并竞争锁
若调用Thread.yield方法,即表示当前线程可出让CPU的信号,但线程调度器可能会忽略该信号
yield不影响锁行为
如何中断线程?
① 调用stop方法停止线程(弃用)
② 调用suspend方法和resume方法(弃用)
③ 调用interrupt方法,通知线程应该中断,需要被调用线程响应中断
通知线程中断:
① 若线程阻塞,则该线程退出阻塞,并抛出InterruptException异常
② 若线程为运行状态,则设置线程可中断标志为true,但线程继续运行
线程响应中断:
① 正常运行任务时,经常检查本线程中断标志,若为true则中断
② 正常运行任务时,则将线程中断标志置为true,但线程继续运行
线程安全的诱因:存在共享数据(也称临界资源)
存在多线程共同操作共享数据
解决方法:同一时刻只有一个线程操作共享数据,其他线程需等待
互斥锁特性:互斥性,同一时间只有一个线程持有锁和操作同步代码块
可见性,锁释放之前,修改共享变量,随后获取锁的线程是可见的
锁分类:
① 对象锁:同步代码块(synchronized(this),synchronized(对象))
同步非静态方法(synchronized method)
② 类锁:同步代码块(synchronized(类.class))
同步静态方法(synchronized static method)
对象锁锁当前对象,类锁锁当前对象的类对象
JVM中,对象实例有多个,类对象只有一个,不同对象实例的类对象是同一个,类锁和对象锁互不影响
类锁和对象锁总结:
① 某线程访问同步代码块,其他线程可访问非同步代码块
② 若锁住同一个对象,某线程访问同步代码块,其他线程访问同步代码块会阻塞
③ 若锁住同一个对象,某线程访问同步方法,其他线程访问同步方法会阻塞
④ 若锁住同一个对象,某线程访问同步代码块,其他线程访问同步方法会阻塞,反之亦然
⑤ 同一个类的不同对象,其对象锁互不干扰
⑥ 类锁是一把特殊对象锁,其表现与①、②、③、④相同,一个类只有一把类锁,同一个类的不同对象使用的类锁是同一个
⑦ 类锁和对象锁互不干扰
实现synchronized的基础:Java对象头 + Monitor
对象内存布局:对象头 + 实例数据 + 对齐填充
对象头结构:Mark Word(MW) + Class Metadata Address(CMA)
MW存对象HashCode、分代年龄、锁类型、锁标志位
CMA存类型指针执行对象的类型数据,通过该指针可知对象对应类
Mark Word存储信息与锁类型
Monitor:对象自带锁(ObjectMonitor)、Monitor竞争与释放
源码:https://hg.openjdk.java.net/jdk8u/…
monitorEnter、monitorExit
锁重入:线程再次请求自身持有的锁,即为锁重入
为什么synchronized效率差?
① 早期版本中,synchronized属于重量级,依赖于Muter Lock实现
② 线程之间切换需要从用户态转换到核心态,开销大
③ Java6后,synchronized性能较好,自旋、锁消除、锁粗化、偏向锁、轻量级锁等
自旋锁:
① 许多情况下,共享数据锁定状态持续时间短,切换线程效益差
② 让线程执行忙循环,等待锁、不出让CPU
③ 缺点:若锁被其他线程长时间占用,则产生大量性能开销
自适应自旋锁:
① 自旋次数不固定
② 由上一次获取同一个锁的自旋时间和锁的占有者状态来决定
锁消除:JVM编译时,扫描上下文,去除不存在竞争的锁
锁粗化:通过扩大加锁范围,避免反复加锁和解锁
synchronized四种状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
锁膨胀方向:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏 向 锁 :减少同一线程获取锁的代价,大多数情况下,锁不存在多线程竞争,而由同一线程获取,偏向锁运行在只有一个线程进入同步代码块的情况下
轻量级锁:由偏向锁升级而来,当第二个线程竞争锁时,则升级为轻量级锁,适应场景为线程交替执行同步代码块
重量级锁:若存在多个线程同时竞争锁,则轻量级锁升级为重量级锁,Monitor实现
偏向锁、轻量级锁、重量级锁总结
锁分类 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加解锁无CAS操作和性能开销,和执行非同步代码块有纳秒级差别 | 若存在多线程锁竞争,有锁撤销的消耗 | 只有一个线程访问同步代码块或同步方法 |
轻量级锁 | 线程竞争不会阻塞,提高了响应速度 | 若线程竞争长时间未获取到锁,自旋会消耗性能 | 线程交替执行同步代码块或同步方法 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应慢、多线程频繁加解锁时会有性能消耗 | 追求吞吐量,同步代码块或同步方法执行时间较长的场景 |
ReentrantLock(重入锁)
① 位于Java.util.concurrent.lock包
② 和CountDownLatch、FutureTask、Semphore相同,由AQS实现
③ 可实现比synchronized更细粒度的控制,如控制fairless
④ 调用lock后,需调用unlock释放锁
⑤ 性能比synchronized高,可重入
ReentrantLock可设置公平性,使用try/catch/finally
synchronized为非公平的
ReentrantLock将锁对象化 : 判断是否线程在排队等待获取锁
带超时的获取锁的尝试
感知是否成功获取锁
因此需将wait/notify/notifyall对象化(Condition)
ReentrantLock与synchronized区别:
① synchronized是关键字,ReentrantLock是类
② ReentrantLock可设置获取锁时等待时间,避免死锁
③ ReentrantLock可获取各种锁的状态
④ ReentrantLock可灵活实现多路通知
⑤ 机制:synchronized是操作Mark word,Lock是基于unsafe类
Java内存模型(JMM):一种抽象概念,是一组规范,规定程序中各变量的访问方式
线程A <—> 本地内存A (共享变量副本)<—>
JMM控制规范 / 主内存(共享变量)
线程B <—> 本地内存B(共享变量副本) <—>
JMM中主内存:存储Java实例对象
包括成员变量、类信息、常量和静态变量
属于数据共享区域,多线程并发访问时有安全问题
JMM中本地内存:存储当前方法的本地变量,对其他线程不可见
字节码行号指示器,Native方法信息
线程私有区域,无线程安全问题
JMM和Java内存区域划分是不同概念
① JMM描述的是一组规则,包括原子性、有序性、可见性
② 相似点:存在共享区域和私有区域
主内存和工作内存的数据类型及操作方式归纳
① 方法中基本数据类型的本地变量存储在工作内存的栈帧
② 引用类型的本地变量,引用存储在工作内存,实例存储在主内存
③ 成员变量、static变量、类信息均存储在主内存
④ 主内存共享变量由线程拷贝到工作内存,操作完成后刷新到主内存
JMM如何解决可见性问题 - 指令重排序
① 单线程环境下不能改变程序运行结果
② 存在数据依赖关系不允许重排序
happen-before八大原则 : 程序次序规则
锁定规则
volatile变量规则
传递规则
线程启动规则
线程中断规则
线程终结规则
对象终结规则
volatile轻量级同步机制:volatile共享变量对所有线程均可见
禁止指令重排序
volatile可见性:写volatile共享变量时,将工作内存共享变量刷新到主内存
读volatile共享变量时,强制将共享变量从主内存刷新到工作内存
volatile如何禁止重排序优化 - 内存屏障指令
① 保证特定操作的执行顺序
② 保证某些变量的内存可见性
volatile和synchronized的区别
① volatile共享变量读写需强制读写主内存,synchronized是对变量加锁
② volatile仅作用在变量上,synchronized可作用在方法、变量和类
③ volatile可保证有序性、可见性,synchronized可保证有序性、可见性、原子性
④ volatile不会造成线程阻塞,synchronized会造成线程阻塞
⑤ volatile变量不会被编译器优化,synchronized变量可被编译器优化
一种高效线程安全的方法-CAS
① 支持原子更新操作,适用于计数器、序列发生器等场景
② 属于乐观锁机制
③ CAS失败可有开发者决定是否继续尝试或执行其他操作
CAS思想 -3个操作数:内存位置V、预期原值A、新值B
CAS操作是透明的
① JUC中atomic中原子类型封装了CAS,是线程安全的首选
② Unsafe提供CAS服务,但存在内存隐患(Java9之后,可使用Variable Handle API)
CAS缺点
① 若循环时间长,性能开销大
② 只能保证一个共享变量的原子操作
③ ABA问题(版本号),AtomicStampedReference
利用Executors创建线程池满足不同场景 :
① newFixedThreadPool(),定长线程池
② newCachedThreadPool(),缓存型线程池
③ newSingleThreadExecutor),单线程线程池
④ newSingleThreadScheduledExecutor(),单线程调度线程池
⑤ newSingleScheduledThreadPool(),固定线程数量的调度线程池
⑥ newWorkStealingPool(),使用Fork/join框架,将大任务拆解为小任务,汇集小任务结果得到返回
为什么使用线程池:降低资源消耗
提高线程可管理性
JUC中三个Executor接口:
① Executor:运行新任务的简单接口
② ExecutorService:具备管理执行器和任务生命周期的接口,提交任务机制较完善
③ ScheduledExecutorService:支持Future和定期执行任务
Executors框架:
ThreadPoolExecutor工作流程:
ThreadPoolExecutor构造函数7个参数:corePoolSize/核心线程数、maximumPoolSize/最大线程数、keepAliveTime/非核心线程空闲存活时间、unit/时间单位、workQueue/任务等待队列、threadFactory/线程工厂、handler/拒绝策略
拒绝策略:①AbortPolicy抛异常、②DiscardPolicy抛弃新任务、③DiscardOldestPolicy抛弃旧任务、④CallerRunsPolicy使用调用者线程执行任务
线程池工作流程:
线程池的状态:RUNNING,可接受新任务,可处理阻塞队列中任务
SHUTDOWN,不接受新任务,可处理阻塞队列中任务
STOP,不接受新任务,不处理阻塞队列中任务
TIDYING,所有任务均终止
TERMINATED,一个状态,什么都不做
线程池状态转换:
工作线程生命周期:
线程池大小如何确定
① CPU密集型:线程数 = CPU核数 / CPU核数+1
② I/O密集型:线程数 = CPU核数 * (1 + 平均等待时间 / 平均工作时间)
String、StringBuffer、StringBuilder的区别?
异常处理机制主要回答了3个问题
① what 异常类型回答了什么被抛出
② where 异常堆栈跟踪回答了在哪里抛出
③ why 异常信息回答了为什么被抛出
Error和Exception区别
① Error:程序无法处理的系统错误,编译器不做检查
② Exception:程序处理的一次,捕获后可能恢复
③ 总结:前者为程序无法处理的错误,后者是可以处理的异常
从责任角度看
① Error是JVM需要处理的
② RuntimeException是程序需要处理的
③ CheckedException是编译器需要处理的
Java异常处理机制
① 抛出异常
② 捕获异常,寻找合适的异常处理器,否则终止
③ finally会在catch中return之前执行
Java异常处理原则
① 具体明确:通过异常类名或日志明确异常原因
② 提前抛出:尽早发现异常
③ 延迟捕获:延迟捕获异常
高效主流的异常处理框架
① 设计一个通用的继承自RuntimeException的异常统一处理
② 其他异常统一转译为上述异常的AppException
③ catch之后,抛出上述异常的子异常,提供定位信息
④ 前端接受AppException做统一处理
try-catch性能: try-catch影响JVM性能优化
异常实例需要报错堆栈快照,开销较大
Java集合框架:优秀的算法和数据结构被封装到Java集合框架
数据结构考点:数组和链表的区别
链表操作,如翻转、链表环路检测、双向链表、循环链表操作
队列、栈的应用
二叉树遍历方式及其递归或非递归实现
红黑树旋转
算法考点:内部排序,如递归排序、交换排序(冒泡、快排)、选择排序、插入排序
外部排序,利用有限内存和海量外部存储处理超大数据集(思路)
集合之Map
HashMap结构:Java8以前为数组 + 链表,Java8以后为数组 + 链表 + 红黑树
HashMap之put方法: 若未初始化,则初始化
对key求hash值,求下标
若无碰撞,则放入哈希数组
若碰撞,则以链表方式插入尾部
若链表长度超过阈值,则链表转换为红黑树
若链表长度低于6,则红黑树转为链表
若节点已存在,则更新旧值
若满载(容量 * 加载因子),则扩容(扩容之后需要重排)
HashMap减少碰撞:扰动函数,使元素位置均匀分布,减少碰撞概率
使用final对象,并采用合适的equal方法和hashCode方法
HashMap哈希算法:从hashCode到散列位置
HashMap扩容问题:多线程环境下,扩容会存在竞争,造成死锁
rehashing比较耗时
HashMap知识回顾:成员变量、数据结构、树化阈值
构造函数、延迟创建
put、get流程
hash算法、扩容、性能
HashMap、HashTable和ConcurrentHashMap
如何优化HashTable:细化锁,将整个锁拆解成多个锁
ConcurrentHashMap之put方法: 判断Node[]是否初始化,没有则初始化
计算哈希数组下标,判断是否有Node节点,没有则使用CAS添加,失败则for循环
检查是否正在扩容,若正在扩容则帮助扩容
若f != null,则使用synchronized锁住f元素(链表/红黑树头结点),执行链表/红黑树添加元素
判断链表长度,达到8则转为红黑树
ConcurrentHashMap逻辑:早期使用分段锁Segment实现
当前使用CAS + synchronized细化锁,锁数组元素
ConcurrentHashMap总结:比Segment,锁拆得更细
使用CAS插入头结点,失败则循环重试
若存在头结点,则获取头结点同步锁,再插入或更新
ConcurrentHashMap提升:size方法和mappingCount方法的异同,两者计数是否准确
多线程环境如何自动扩容
HashMap、HashTable和ConcurrentHashMap区别
① HashMap线程不安全,数组 + 链表 + 红黑树
② HashTable线程安全,锁住整个对象,数组 + 链表
③ ConcurrentHashMap线程安全,CAS + synchronized,数组 + 链表 + 红黑树
④ HashMap的key、value均可为null,其他两个不支持
java.util.concurrent提供并发编程的解决方案
① CAS是java.util.concurrent.atomic包的基础
②AQS是java.util.concurrent.locks包、Semphore、ReentrantLock等类的基础
JUC包的分类:线程执行器/executor
锁/locks
原子变量类/atomic
并发工具类/tools
并发集合/collections
JUC包明细:
并发工具类tools:CountDownLatch/闭锁,让主线程等待一组事件发生
CyclicBarrier/栅栏,阻塞并等待其他线程
Semaphore/信号量,控制资源可被同时访问数量
Exchanger/交换器,两个线程到达交换的后交换数据
BlockingQueue:阻塞队列,主要用于生产者/消费者模式,多线程场景下,隔离任务生成与消费
① ArrayBlockingQueue,数组构成的有界阻塞队列
② LinkedBlockingQueue,链表构成的有界阻塞队列
③ PriorityBlockingQueue,优先级排序的无界阻塞队列
④ DelayQueue,优先级队列实现的延迟无界阻塞队列
⑤ SynchronousQueue,不存储元素的阻塞队列(存一个元素)
⑥ LinkedTransferQueue,链表构成的无界阻塞队列
⑦ LinkedBlockingDeque,链表构成的双向阻塞队列
BIO、NIO、AIO
① Block-IO:InputStream和OutputStream、Reader和Writer,阻塞
② NoBlock-IO:构建多路复用、同步非阻塞IO
Channels,包括FileChannel、DatagramChannel、SocketChannel等
Buffers,包括ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer等
-> Channel -> Buffer
Thread -> Selector -> Channel -> Buffer
-> Channel -> Buffer
IO多路复用,调用系统级别的select/poll/epoll监测Channel可读
select、poll、epoll三者的区别(连接数、IO效率、消息传递方式)
③ Asynchronous IO:基于事件和回调机制、异步非阻塞IO
基于回调,实现CompletionHandle接口,调用时出发回调函数
返回Future,通过IsDone查看是否准备好,通过get等待返回数据
BIO、NIO、AIO对比
属性/模型 | BIO | NIO | AIO |
---|---|---|---|
blocking | 阻塞同步 | 非阻塞同步 | 非阻塞异步 |
线程数(server:client) | 1:1 | 1:N | 0:N |
复杂度 | 简单 | 较复杂 | 复杂 |
吞吐量 | 低 | 高 | 高 |
Spring Core、Spring Security、Spring Data、Spring Boot、Spring Cloud…
你了解Spring IOC吗
IOC/控制翻转:Spring Core最核心的部分
需先了解依赖注入/DI
普通依赖,定义成员变量,new对象赋予依赖的成员变量
依赖注入,定义依赖对象,使用构造函数或者Setter方法注入
依赖注入方式,Setter、Constructor、Interface、Annotation
依赖倒置原则、IOC、DI、IOC容器的关系
IOC容器的优势
① 避免使用new创建类,统一维护
② 创建实例时,无需了解其细节
Spring中Bean实例化过程:
SpringIOC支持的功能:依赖注入、依赖检查、自动装配、支持集合、支持初始化方法和销毁方法、支持回调方法
isSingleton:是否为单例
BeanDefinition:用于描述Bean的定义
BeanDefinitionRegister:提供向IOC容器注册BeanDefinition对象的方法
SpringIOC容器的核心接口:BeanFactory、ApplicationContext
BeanFactory: 提供IOC配置机制
包含各种Bean定义,便于实例化Bean
建立Bean之间的依赖关系
Bean生命周期管理
BeanFactory和ApplicationContext区别
① BeanFactory是Spring框架的基础设施
② ApplicationContext是面向Spring框架的开发者
ApplicationContext功能(继承的接口)
① BeanFactory,管理装配bean
② ResourcePatternResolver,加载资源文件
③ MessageSource,实现国际化
④ ApplicationEventPublisher,注册监听器,实现监听机制
初始化Spring容器
① 创建BeanFactory
② 创建BeanDefinitionReader注解配置读取器
③ 创建ClassPathBeanDefinitionScanner路径扫描器
注册配置类为BeanDefinition到容器
调用refresh刷新容器
① 刷新前预处理
② BeanFactory预处理 设置BeanFactory类加载器、表达式解析器、类型转化注册器
添加3个BeanPostProcessor实现类实例
记录ignoreDependencyInterface
记录ResolvableDependency
添加3个单例Bean
③ 子类处理BeanFactory
④ 执行自定义的BeanFactory后置处理器
⑤ 执行后置处理器的postProcessBeanDefinitionRegistry
扫描配置类
设置排序和名称生成器
解析配置类,包括@component、@componentScan、@Import、@Bean等标注的类/方法
⑥ 执行postProcessBeanFactory方法
⑦ 执行BeanFactory后置处理器(② ~ ⑦可简化为BeanFactory初始化及处理)
⑧ 初始化国际化资源对象messagesource
⑨ 初始化事件发布器
⑩ 注册监听器
⑪ 实例化非懒加载bean
⑫ 执行bean生命周期回调接口
⑬ 发布启动完成事件
新建SpringApplication对象,过程包括:
① 确定应用类型
② 加载spring.factories中定义的类
③ 设置带有mian方法的运行主类
执行SpringApplication.run方法,过程包括:
① 创建应用监听器SpringApplicationRunListeners
② 准备应用程序参数和环境变量
③ 创建、准备、刷新应用上下文环境context(create…Context、prepareContext、refreshContext)
④ 发布启动完成事件
getBean方法逻辑: 转换beanName
从缓存中加载实例
实例化Bean
检测parentBeanFactory
初始化依赖的Bean
创建Bean
Spring Bean的作用域
① singleton:Spring默认作用域,容器中拥有唯一Bean实例
② prototype:针对每个getBean请求,容器创建一个Bean实例
③ request:针对每个Http请求,创建一个Bean实例
④ session:针对每个session,创建一个Bean实例
⑤ globalSession:针对每个globalSession,创建一个Bean实例
Spring Bean生命周期 - 创建
① 实例化bean
② Aware(注入Bean ID、BeanFactory和AppCtx)
③ BeanPostProcess(s).postProcessBeforeInitialization
④ InitializingBean(s).afterPropertiesSet
⑤ 定制的Bean init方法
⑥ BeanPostProcess(s).postProcessAfterInitialization
⑦ Bean初始化完毕
Spring Bean生命周期 - 销毁
① 若实现了DisposableBean接口,则调用destroy方法
② 若配置了destory-method属性,则调用其配置的销毁方法
Bean配置 -> 实例化 -> 属性填充 -> 初始化 -> 使用 -> 销毁
① Bean配置 生成BeanDefinition -> 合并BeanDefinition -> 加载类
② 实例化 实例化前
实例化 -> 推断构造方法、反射创建实例
实例化后
③ 属性填充 用户属性填充
容器属性填充 -> Aware接口
④ 初始化 初始化前
初始化 -> @PostConstruct标注方法、InitializingBean.afterPropertiesSet、bean配置文件init-method标签方法
初始化后
⑤ 使用
⑥ 销毁 @PreDestroy标注方法、DisposableBean接口实现、@Bean中destroyMethod属性方法
你了解Spring AOP吗?
关注点分离:不同问题交给不同模块处理
① 切面编程(AOP)正是此种技术的实现
② 通用功能模块为切面(Aspect)
③ 业务功能模块和通用功能模块分开后,架构变得高内聚、低耦合
④ 切面需要合并到业务中,确保功能完整性
AOP三种织入方式
① 编译时织入,需要特殊编译器,如AspectJ
② 类加载时织入,需要特殊编译器,如AspectJ
③ 运行时织入,Spring采用的方式,通过动态代理实现,比较简单
AOP主要名词概念:Aspect,通用功能模块
Target,被织入Aspect的对象
Join Point,可作为切入点的地方
Point Cut,Aspect实际应用到的Join Point,支持正则
Advice,类中方法以及该方法织入到目标方法的方式
Weaving,AOP的实现过程
AOP种类:前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)、环绕通知(Around)
AOP的实现,JDKProxy和Cglib
① 由AopProxyFactory根据AdvisedSupport对象的配置决定
② 默认策略如果目标类是接口,则使用JDKProxy实现,否则用Cglib
③ JDKProxy的核心,InvocationHandle接口和Proxy类
④ Cglib,以继承的方式动态生成目标类的代理
⑤ JDKProxy,通过反射机制实现
⑥ Cglib,借助ASM实现
⑦ 反射机制在生成类的过程中比较高效
⑧ ASM在生成类之后的执行过程中比较高效
代理模式:接口 + 真实实现类 + 代理类
Spring代理模式实现
① 真实实现类的逻辑包含在getBean方法中
② getBean方法实际返回的是Proxy实例
③ Proxy实例是Spring采用JDKProxy或者Cglib动态生成的
1. Data注解是否有问题
A类继承B类,使用@Data注解的A类自动生成的toString()函数无法打印B类的成员变量
2. 切面编程如何理解
在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
3. spring容器中,实例加载顺序
******
4. Inherit组合注解使用
******
5. 注解如何起作用的
******
1. 某现场升级后Nginx问题
进程配置:主进程 + hms用户进程 + hcs用户进程,
配置文件:hms用户配置文件和hcs用户配置文件,
问题现象:hms用户进程、hcs用户进程不能同时工作,其中一个会挂死,
解决办法:将hcs用户配置文件合并到hms用户配置文件后,解决了该问题,
原因:?