面试准备
数据库:
- 聚簇索引和非聚簇索引的区别?使用场景
- MySQL 数据库的索引有哪些?
- 什么情况下建议使用索引?
- 索引的引擎?分别有什么区别
- MySQL 分页原理
- 主键索引和辅助索引有什么区别?有什么联系
数据结构:
- B+ 树和 B 树的区别?优缺点?应用场景
- 哈希冲突如何解决?
- 红黑树的特点?
- 红黑树的左旋右旋?
- 链表和数组的区别?
操作系统:
- IO 多路复用的实现方式
- BIO 和 NIO 是什么?
- 进程和线程的区别?
- 什么是协程?协程的轻量级体现在哪里?
- 同步和异步的区别?阻塞和非阻塞的区别
计算机网络:
- 介绍五层模型以及相应的协议?
- HTTP 和 HTTPS 的区别?
- HTTPS 是如何保证安全的?
- cookie 和 session 的区别?
- Http 的状态码?(这里面试官会挑其中的几个问)
线程池
- TCP 如何实现可靠传输?
- 浏览器中输入了 url 到网页显示的完整过程?整个过程涉及到了哪些协议?
C++:
- 多态是如何实现的?
- 虚函数和纯虚函数的区别?
- 虚函数实现的原理?
- 动态链接和静态链接?二者的区别?
- 函数指针和指针函数的区别?
- vector 的底层原理?如何进行容量扩增?
- 指针和引用的区别?
- struct 和 class 的区别?
- 重载和重写的区别?
- 面向对象和面向过程的区别?
https://programmercarl.com/other/jianlixiangmu.html
最有成就感的一件事
大一时候刚学了C++,自以为很强了,去接触过一个很老的图形库easyx,写过一个小游戏。大概就是控制一个不断移动的球躲避路途不断随机移动的怪物,到达终点的游戏。当时录制后投到b站,获得了10w左右的播放量。
项目在技术上遇到的难点,是怎么解决的
-
之前设计数据库表的时候,有一个难点是实现多级分类,纠结过比较久时间要怎么实现。我开始使用加入父级id字段的方式来实现,后面发现这样实现的话查询困难,因为我们用的是mysql,不像oracle有递归查询的函数,递归查询需要自己实现。而且我们做的是一个图书网站,是要对图书进行分类,后面参考了中国图书馆分类法,我们分类的图书父类id是子类id的前缀,为了区分不同级,我们再加入了一个表示层数level的字段,最后完美解决
-
之前刚开始做项目,纠结图片的存储形式。我看有直接把图片存到数据库,但我觉得这样很别扭,如果图片过多每次需要从磁盘取出大量数据,传输数据也很大,效率应该不会好,一般前端应该是得到图片url才对。这样的话就需要先解决怎么得到一个可访问的url的问题。我自己有租服务器,后面通过配置tomcat,使得可以在浏览器上访问服务器的某一路径,以此解决了图片的访问问题。至于图片的上传删除,我们后端会接收一个multipartfile格式的文件,使用ftp推到服务器相应路径,如果有必要还会对图像进行压缩。后面开始实习,我对企业怎么实现图片上传很感兴趣,专门研究过。发现他们是实现了一个公共的接口来负责图片上传,与项目分离,前端将图片通过这个接口上传,得到一个url,然后只将这个url返回给后端来存储到数据库,就不会像我们之前那样将图片上传功能嵌入到一个项目的后端,接收一个multipartfile
事务
ACID
原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
实现原理:
在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面
- 事务的并发执行
- 事务故障或系统故障
数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。
并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。
原子性
在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
持久性
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,
隔离性
锁机制
锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
行锁与表锁
按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
脏读、不可重复读和幻读
(1) 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据
(2) 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,可以读到其他事务(B)已提交的数据
(3) 幻读:读到其他事务的新增数据
事务隔离级别
读未提交、读已提交、可重复度、可串行化
MVCC
MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
2)基于undo log的版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
3)ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。
trx_sys中的主要内容,以及判断可见性的方法如下:
low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该ReadView不可见。
up_limit_id:表示生成ReadView时当前系统中活跃的读写事务中最小的事务id。如果数据的事务id小于up_limit_id,则对该ReadView可见。
rw_trx_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在low_limit_id和up_limit_id之间,则需要判断事务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中,因此数据对ReadView不可见;如果不在,说明生成ReadView时事务已经提交了,因此数据对ReadView可见。
总结
概括来说,InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。
不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离,下面是两个例子:
下面总结一下ACID特性及其实现原理:
- 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
- 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
- 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
- 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障
redis
数据类型
string(字符串),Hash(哈希),List(列表),Set(集合),zset(有序集合)
redis是什么
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
redis持久化
Redis 的持久化机制有两种,第一种是快照RDB,第二种是 AOF 日志。快照是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,基本一次五分钟左右,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长,所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
Redis 本身有持久化,为什么还要写进 MySQL?
- 权限控制
MySQL 有权限控制,用户可以精确到每个 IP 的每个账户,目标可以精确到每个表的每个操作。
Redis 则是天生设计成完全开放权限,包括完全删除数据库的操作,任何人都可以执行。要么就只能把指令重命名成空的,完全禁止任何人执行。
- 数据完整
MySQL 的数据库保存在磁盘中,万一崩溃断电,也有数据库日志可以用以完成数据库事务。
MySQL 支持主从备份,所有的写入操作都可以实时发送到异地,哪怕突然机房被核弹轰炸,也不会丢失数据(可能除了最后几条语句)。
Redis 的崩溃……嗯小心数据全丢。
Redis 的 Replication 备份……嗯小心数据全丢。
- 负载均衡
MySQL 可以单主多从,也可以胆子够大在内网做双主,也可以用 innodb 配合 galera 做集群,每台机器都有一个独立的拷贝,因此服务器之间只要传输写指令即可。
Redis 可以单主多从(然而小心数据全丢),但是不能做多主互联。最多最多只能做 sharding ,也就是每台机器只保存一部分数据,读写一律被分散到其他机器上。直接后果就是内网流量大增。
- 数据隔离
MySQL 里我可以选择删掉某个应用的所有数据而保留另一个应用的所有数据。
Redis 里要么依赖 11 个 DB 的选择,要么依赖命名空间。
- 性价比
MySQL 是内存+硬盘,上个 SSD 配合 Query Cache 那速度已经是很快了。
Redis 是纯内存。乖乖掏钱加内存换至强啦。而且你还是得配备高性能磁盘,因为定时刷到磁盘和开机加载数据的操作还是要磁盘性能的。
数据库可以使用事务
- 需求不同
开发这样想是对的,但是其他的人可能会面临新的压力,新的技术挑战,所以需求定位是最终的出发点,除非哪天 Redis 的统计也能做的很溜.
Redis常见问题解析:击穿
概念:在Redis获取某一key时, 由于key不存在, 而必须向DB发起一次请求的行为, 称为“Redis击穿”。
引发击穿的原因:
- 第一次访问
- 恶意访问不存在的key
- Key过期
合理的规避方案:
- 服务器启动时, 提前写入
- 规范key的命名, 通过中间件拦截
- 对某些高频访问的Key,设置合理的TTL或永不过期
Redis常见问题解析:雪崩
概念:Redis缓存层由于某种原因宕机后,所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。
合理的规避方案:
- 使用Redis集群
- 限流
分布式Session的实现方式
粘性Session (将用户请求固定在一台服务器)
服务器Session复制 (广播实现Session同步)
缓存存储 (Sticky模式和Non-Sticky模式)
CDN
CDN就是采用更多的缓存服务器(CDN边缘节点),布放在用户访问相对集中的地区或网络中。当用户访问网站时,利用全局负载技术,将用户的访问指向距离最近的缓存服务器上,由缓存服务器响应用户请求。
CDN=更智能的镜像+缓存+流量导流。
三大范式
- 列不可再分
- 属性完全依赖于主键
- 属性不依赖于其它非主属性 属性直接依赖于主键
- 在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。
Spring AOP底层原理
应用场景: SpringAOP主要用于处理各个模块的横切关注点,比如日志、权限控制等。
SpringAOP的思想: SpringAOP的底层实现原理主要就是代理模式,对原来目标对象创建代理对象,并且在不改变原来对象代码的情况下,通过代理对象,调用增强功能的方法,对原有的业务进行增强。
AOP的代理分为动态代理和静态代理,SpringAOP中是使用动态代理实现的AOP,AspectJ则是使用静态代理实现的AOP。
SpringAOP中的动态代理分为JDK动态代理和CGLIB动态代理。
JDK动态代理
JDK动态代理原理: 基于Java的反射机制实现,必须有接口才能使用该方法生成代理对象。
JDK动态代理主要涉及到了两个类java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler。这两个类的主要方法如下:
java.lang.reflect.Proxy:
static InvocationHandler getInvocationHandler(Object proxy),该方法用于获取指定代理对象所关联的调用处理器
static Class getProxyClass(ClassLoader loader, Class... interfaces),该方法主要用于返回指定接口的代理类
static boolean isProxyClass(Class cl),该方法主要用于返回 cl 是否为一个代理类
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)该方法主要用于构造实现指定接口的代理类的实例,所有的方法都会调用给定处理器对象的invoke()方法
java.lang.reflect.InvocationHandler:
Object invoke(Object proxy, Method method, Object[] args)该方法主要定义了代理对象调用方法时所执行的代码。
篇幅有限,就不展开介绍了,大致流程如下:
实现InvocationHandler接口创建方法调用器
通过为 Proxy 类指定 ClassLoader 对象和一组interface 创建动态代理
通过反射获取动态代理类的构造函数,参数类型就是调用处理器接口类型
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数传入
CGLib 动态代理原理:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
SpringAOP何时使用JDK动态代理,何时使用CGLiB动态代理?
当Bean实现接口时,使用JDK动态代理。
当Bean没有实现接口时,使用CGlib动态代理
HashMap的底层数据结构,如何进行扩容的?
底层
1.7 数组+链表
1.8 数组+链表,链表长度>8变成红黑树
扩容机制:
初始值为16,负载因子为0.75,阈值为负载因子*容量
resize()方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize()方法进行扩容。
每次扩容,容量都是之前的两倍
扩容时有个判断e.hash & oldCap是否为零,也就是相当于hash值对数组长度的取余操作,若等于0,则位置不变,若等于1,位置变为原位置加旧容量。
ConcurrentHashMap如何实现线程安全?size()方法是加锁的吗?如何实现的?
如何实现线程安全?
JDK1.7和JDK1.8在实现线程安全上略有不同
JDK1.7采用了分段锁的机制,当一个线程占用锁时,会锁住一个Segment对象,不会影响其他Segment对象。
JDK1.8则是采用了CAS和synchronize的方式来保证线程安全。
size()方法是加锁的吗?如何实现的?
这个问题本质是ConcurrentHashMap是并发操作的,所在在计算size时,可能还会进行并发地插入数据,ConcurrentHashMap是如何解决这个问题的?
在JDK1.7会先统计两次,如果两次结果一致表示值就是当前ConcurrentHashMap的大小,如果两次不一样,则会对所有的segment都进行加锁,统计一个准确的值。
1.8,首先会CAS地更新baseCount的值,如果存在并发,CAS失败的线程则会进行方法中,后面会执行到fullAddCount()方法,该方法就是在初始化counterCells, 这也解释了为什么在 sumCount()中通过baseCount和遍历counterCells统计sum,所以在JDK1,8中size()是不加锁的
线程池参数
线程池的常用创建方式主要有两种,通过Executors工厂方法创建和通过new ThreadPoolExecutor方法创建。
ThreadPoolExecutor构造函数的重要参数分析:
三个比较重要的参数:
corePoolSize :核心线程数,定义了最小可以同时运行的线程数量。
maximumPoolSize :线程中允许存在的最大工作线程数量
workQueue:存放任务的阻塞队列。新来的任务会先判断当前运行的线程数是否到达核心线程数,如果到达的话,任务就会先放到阻塞队列。
其他参数:
keepAliveTime:当线程池中的数量大于核心线程数时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到时间超过keepAliveTime时才会被销毁。
unit :keepAliveTime 参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂。
handler :线程池任务队列超过maxinumPoolSize 之后的拒绝策略
线程池大小如何设置
CPU 密集型应用,线程池大小设置为 N + 1(N表示CPU数量)
IO 密集型应用,线程池大小设置为 2N
IO密集=Ncpu*2是怎么计算出来
无论是CPU密集型应用的N+1还是IO密集型应用的2N都是一个经验值,在《Java并发编程实战》中,给出一种计算线程池大小的方法,在一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。 给定下列定义:
Ncpu 表示CPU的数量 ,Ucpu 表示目标CPU的使用率,其中0 <= Ucpu <= 1,W/C =表示等待时间与计算时间的比率,为保持处理器达到期望的使用率,最优的池的大小等于:Nthreads = Ncpu x Ucpu x (1 + W/C)
对于IO密集型应用,等待时间一般都会比计算时间长,如果假设等待时间等于计算时间,那么Nthreads = Ncpu x Ucpu x 2,当CPU使用率达到100%,Nthreads = Ncpu x2
CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
循环时间长开销很大。
只能保证一个共享变量的原子操作。
ABA问题。
循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
什么是ABA问题?ABA问题怎么解决?
CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
聚集索引 非聚集索引
聚簇索引和非聚簇索引最主要的区别是数据和索引是否分开存储。
- 聚簇索引:将数据和索引放到一起存储,索引结构的叶子节点保留了数据行。
- 非聚簇索引:将数据进和索引分开存储,索引叶子节点存储的是指向数据行的地址。
在InnoDB存储引擎中,默认的索引为B+树索引,利用主键创建的索引为主索引,也是聚簇索引,在主索引之上创建的索引为辅助索引,也是非聚簇索引。为什么说辅助索引是在主索引之上创建的呢,因为辅助索引中的叶子节点存储的是主键。
在MyISAM存储引擎中,默认的索引也是B+树索引,但主索引和辅助索引都是非聚簇索引,也就是说索引结构的叶子节点存储的都是一个指向数据行的地址。并且使用辅助索引检索无需访问主键的索引。
synchronized的锁优化
锁的升级
在JDK1.6中,为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,锁的状态变成了四种,如下图所示。锁的状态会随着竞争激烈逐渐升级,但通常情况下,锁的状态只能升级不能降级。这种只能升级不能降级的策略是为了提高获得锁和释放锁的效率。
偏向锁
常见面试题:偏向锁的原理(或偏向锁的获取流程)、偏向锁的好处是什么(获取偏向锁的目的是什么)
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
偏向锁的获取流程:
检查对象头中Mark Word是否为可偏向状态,如果不是则直接升级为轻量级锁。
如果是,判断Mark Work中的线程ID是否指向当前线程,如果是,则执行同步代码块。
如果不是,则进行CAS操作竞争锁,如果竞争到锁,则将Mark Work中的线程ID设为当前线程ID,执行同步代码块。
如果竞争失败,升级为轻量级锁。
偏向锁的获取流程如下图:
偏向锁的撤销:
只有等到竞争,持有偏向锁的线程才会撤销偏向锁。偏向锁撤销后会恢复到无锁或者轻量级锁的状态。
偏向锁的撤销需要到达全局安全点,全局安全点表示一种状态,该状态下所有线程都处于暂停状态。
判断锁对象是否处于无锁状态,即获得偏向锁的线程如果已经退出了临界区,表示同步代码已经执行完了。重新竞争锁的线程会进行CAS操作替代原来线程的ThreadID。
如果获得偏向锁的线程还处于临界区之内,表示同步代码还未执行完,将获得偏向锁的线程升级为轻量级锁。
一句话简单总结偏向锁原理:使用CAS操作将当前线程的ID记录到对象的Mark Word中。
轻量级锁
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
轻量级锁的获取流程:
首先判断当前对象是否处于一个无锁的状态,如果是,Java虚拟机将在当前线程的栈帧建立一个锁记录(Lock Record),用于存储对象目前的Mark Word的拷贝,如图所示。
将对象的Mark Word复制到栈帧中的Lock Record中,并将Lock Record中的owner指向当前对象,并使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,如图所示。
如果第二步执行成功,表示该线程获得了这个对象的锁,将对象Mark Word中锁的标志位设置为“00”,执行同步代码块。
如果第二步未执行成功,需要先判断当前对象的Mark Word是否指向当前线程的栈帧,如果是,表示当前线程已经持有了当前对象的锁,这是一次重入,直接执行同步代码块。如果不是表示多个线程存在竞争,该线程通过自旋尝试获得锁,即重复步骤2,自旋超过一定次数,轻量级锁升级为重量级锁。
轻量级锁的解锁:
轻量级的解锁同样是通过CAS操作进行的,线程会通过CAS操作将Lock Record中的Mark Word(官方称为Displaced Mark Word)替换回来。如果成功表示没有竞争发生,成功释放锁,恢复到无锁的状态;如果失败,表示当前锁存在竞争,升级为重量级锁。
一句话总结轻量级锁的原理:将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。
自旋锁
Java锁的几种状态并不包括自旋锁,当轻量级锁的竞争就是采用的自旋锁机制。
什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环 等待,当线程A释放锁后,线程B可以马上获得锁。
引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。
自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,一般默认为10。
自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。
常用垃圾回收器
用于回收新生代的收集器有Serial、PraNew、Parallel Scavenge
用于回收老年代的收集器包括Serial Old、Parallel Old、CMS
用于回收整个Java堆的收集器:G1
G1有哪些特点
G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,不会产生内存碎片。G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆。
慢查询优化,会考虑哪些优化
慢查询一般用于记录执行时间超过某个临界值的SQL语句的日志。
相关参数:
slow_query_log:是否开启慢日志查询,1表示开启,0表示关闭。
slow_query_log_file:MySQL数据库慢查询日志存储路径。
long_query_time:慢查询阈值,当SQL语句查询时间大于阈值,会被记录在日志上。
log_queries_not_using_indexes:未使用索引的查询会被记录到慢查询日志中。
log_output:日志存储方式。“FILE”表示将日志存入文件。“TABLE”表示将日志存入数据库。
如何对慢查询进行优化?
分析语句的执行计划,查看SQL语句的索引是否命中
优化数据库的结构,将字段很多的表分解成多个表,或者考虑建立中间表。
优化LIMIT分页。
缓存穿透 缓存击穿 缓存雪崩 以及解决办法
缓存穿透:指缓存和数据库中都没有的数据,所有请求都打在数据库上,造成数据库短时间承受大量请求而挂掉
解决方法:
增加接口校验,过滤一些不合法请求,比如大量订单号为-1的数据
从缓存和数据库都不能获取到的数据,可以先对空的结果进行缓存,比如key-null,缓存有效期要设置的短一些
采用布隆过滤器,过滤掉一定不存在的数据
缓存击穿:指缓存中没有但数据库中有的数据,一般是在高并发的情况下,某些热门key突然过期,导致所有请求直接打到数据库上
解决方法::
设置热点数据永不过期
加互斥锁
缓存雪崩:大量缓存在一段时间内集中过期,导致查询的数据都打在数据库上,和缓存击穿的区别是缓存过期的数量
解决方法:
将缓存的过期时间设置随机,避免大量缓存同时过期
服务降级或熔断
Volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
synchronized 特性
原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
可见性:保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。
悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。
非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。
可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。