文章目录
- 索引相关问题
- 优化你的索引
- 密集索引和稀疏索引
- 如何定位并优化慢查询sql
- MyISAM与InnoDB 关于锁方面的区别是什么?
- MyISAM
- InnoDB
- 事务隔离级别
- 多线程并发的相关问题
- Thread中的start和run方法的区别
- Thread和Runnable是什么关系?
- 如何处理线程的返回值?
- 线程的状态
- sleep和wait方法的区别
- notify和notifyAll的区别
- 如何中断一个线程?
- synchronized的底层实现原理
- synchronized和ReentrantLock的区别
- valitile 和synchronized 的区别
- 线程池
- 线程的大小如何选定?
- Java异常体系
- Error和Exceptionde的区别
- Java异常的处理原则
- 高效主流的异常处理框架
- Java异常处理消耗异常的地方
- 容器
- Collection体系:
- Map体系
- HashMap,HashTable,ConccurentHashMap
- J.U.C知识点梳理
- BIO,NIO,AIO
索引相关问题
优化你的索引
平衡多路查找树 B-Tree
- 根节点至少包括两个孩子
- 树中每个节点最多含有m个孩子(m>=2)
- 除根节点和叶节点外,其他节点至少有ceil(m/2)个孩子 向上取整
- 所有的终端叶子节点都位于同一层
B + Tree
B+ 树是B树的变体,其定义基本与B树相同,除了:
- 非叶子节点的子树指针与关键字个数相同
- 非叶子节点的子树指针p[i],指向关键字 [ k[i],k[i+1] )的子树
- 非叶子节点仅用来索引,数据都保存在叶子节点中
- 所有叶子节点均有一个链指针指向下一个叶子节点
B+Tree更适合用来做存储索引
- B+树的磁盘读写代价更低
- B+树的查询效率更稳定
- B+树更有利于对数据库的扫描
Hash索引:
hash索引的查询效率是很高的,hash索引是通过has函数运算后,只需要经过一次定位,就可以找到查询数据的头,不像B树要从根节点到非叶子节点再到叶子节点,最后才能访问到我们要查询的数据,这样就会进行多次I/O访问
缺点:
- 仅能满足”=“ , ”IN“ ,不能使用范围查询
- 无法用来避免数据的排序操作 (由于Hash索引中存放的是经过Hash算法运算后的值,而且Hash值的大小关系,并不一定和hash运算以前的键值完全一样)
- 不能做组合索引查询
- 不能避免表扫描
- 运到大量Hash值相等的情况后,性能并不一定就会比B性能高
位图索引 ---------- 仅有Oracle支持
密集索引和稀疏索引
密集索引文件中的每个搜索码值都对应一个索引值------- 即叶子节点不仅保存了键值,还保存了位于同一条记录的其他列信息,由于密集索引决定了表的物理排列顺序,一个表只能创建一个密集索引
稀疏索引文件只为索引码的某些键建立索引项------- 即叶子节点只保存了键位信息以及索引主键
MySQL主要有两种存储引擎:
InnoDB ------------ 索引和数据是存在一块的
- 若一个主键被定义,该主键列则作为密集索引
- 若没有主键被定义,该表的唯一非空索引则作为密集索引
- 若不满足以上条件,innodb内部会生成一个隐藏主键(密集索引)
- 非主键索引存储相关键位和其对应的主键值,包含两次查找:
MYISAM ----------- 索引和数据是分开的
为什么要使用索引?
因为索引能让我们避免全表扫描去查找数据,提升检索效率
什么样的信息能成为索引?
主键,唯一键等,只要是能让数据具备一定区分性的字段,都能成为索引
索引的数据结构?
主流是B+树,还有Hash结构
如何定位并优化慢查询sql
根据慢日志定位慢查询sql
使用explain等工具分析sql
修改sql或者尽量让sql走索引
mysql有很多系统变量,查询和慢日志相关的配置信息,慢日志就是用来记录执行比较慢的sql
show variables like '%quer%'
-------- 关注慢日志,打开慢日志,查看慢日志时间
show status like '%slow_queries%'
---------- 慢查询sql的数量
set global slow_query_log = on
----------- 打开慢日志,这个是临时改变的,重启数据库又会到默认值
set global long_query_time = 1
------- 设置慢查询的时间为1s
关键字type字段:index,all就代表实在全表扫描了,需要进行sql优化
extra
最左匹配原则:非常重要的原则,mysql会一直向右匹配直到范围查询(<,>,between,like)就停止匹配,比如a=3,b=4, and c>5 and d=6 ,建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则可以用到
索引建立的越多越好吗?
- 数据量小的表不需要建立索引,建立会增加额外的索引开销
- 数据变更需要维护索引,因此更多的索引意味着的更多的维护成本
- 更多的索引意味着也需要更多的空间
MyISAM与InnoDB 关于锁方面的区别是什么?
MyISAM默认用的是表级锁,不支持行级锁
InnoDB默认用的是行级锁,也支持表级锁
MyISAM
读锁 ------ 共享锁 lock tables 表名 read
对表加上读锁时,在进行范围查询时,其他人仍然可以对数据进行查询
写锁-------- 排它锁
需要等待写锁的释放,才能执行其他语句
InnoDB
当不走索引时,就会使用表级锁,若SQL用到了索引,就会使用行级锁
MYISAM使用的场景:
- 频繁执行全表count语句
- 对数据进行增删改的频率不高,查询非常频繁
- 没有事务
InnoDB使用的场景
- 数据库的增删改查都相当频繁
- 可靠性要求比较高,支持事务
数据库锁的分类
- 按锁的粒度划分:可分为表级锁,行级锁,页级锁
- 按锁的级别划分:共享锁,排它锁
- 按加锁的方式划分:可分为自动锁,显示锁
- 按操作划分:可分为DML锁,DDL锁
- 按使用方式划分:可分为乐观锁,悲观锁
- 悲观锁:对数据,对外界的修改持保守态度,基于数据库的锁机制,先锁再访问,是对安全的一种保证,但该方式效率低,而且容易造成死锁
- 乐观锁:认为数据一般情况不会造成冲突,所以在数据提交更新的时候,才会检查数据是否冲突,如果发现冲突,则返回用户的错误信息,让用户决定如何去做,相对于悲观锁,在对数据库进行操作时,乐观锁并不会使用数据库提供锁机制。一般实现乐观锁的方式就是记录数据版本-------通过版本号或者时间戳 ------- 增加一个数字类型version列
事务隔离级别
select @@tx_isolation
查看事务的隔离级别
set session transaction isolation level read uncommited
设置当前会话窗口的事务隔离级别为 读未提交,该方式会发生脏读,即允许读到其他事务未提交的数据
start transction;
开启手动事务
**事务并发访问引起的问题以及如何避免?**考察事物的隔离级别
InnoDB可重复读隔离级别下,如何避免幻读?
表象:快照度(非堵塞读)— 伪MVCC(多版本并发控制)
当前读:select .... lock in share mode select .... for update, insert update delete
快照读:不加锁的非阻塞读,在可重复读级别下可能读取到之前版本的数据,取决于快照的时间
RC,RR级别下的InnoDB的非阻塞读(快照度)如何实现?
1.数据行里的DB_TRX_ID(事务id),DB_ ROLL_PTR(回滚指针),DB_ROW_ID(行号)字段
2.undo日志
日志的工作方式:这里以更新为例
在修改前,先将数据拷贝一份到Undo log中
数据的各个版本就是这样实现的,按照修改的时间,从近代远,按照DB_ROLL_PTR
连接起来
3.read view 做可见性判断,根据可见性算法,显示可以看见的数据版本
内在:next-key锁(行锁+gap锁)
- 行锁:对某条记录加锁
- Gap锁:避免两次当前读,出现幻读
对主键索引或者唯一索引会用Gap锁吗?
- 如果where条件全部命中,则不会用Gap锁,只会使用行级锁
- 如果where条件部分命中或者全部不命中,则会使用Gap锁
- Gap索引会用在非唯一索引或者不走索引的当前读中
多线程并发的相关问题
Thread中的start和run方法的区别
调用start()方法会创建一个新的子线程并启动
run()方法只是Thread的一个普通方法的调用
Thread和Runnable是什么关系?
Thread是实现了Runnable接口的类,使得run支持多线程
因类的单一继承原则,推荐多使用Runnable接口
如何处理线程的返回值?
主线程等待法------------- 让主线程循环等待,需要自己实现循环等待的逻辑,无法控制循环的时间
使用Thread类的join()阻塞当前线程以等待子线程处理完毕 -------- 无法可控制粒度更细的依赖关系
通过Callable接口,通过FutureTask 或者 线程池获取
实现Callable接口,重写含有返回值的Call方法,FutureTask的构造函数中需要一个Callable接口实现类的对象,futureTask是间接继承Runable接口的,所以可以将FutureTask对象传入到Thread构造函数中
线程的状态
在Thread源码中,有一个State的枚举类型,该枚举类型中有6个值
从源码和官方说明中,线程有6个状态
- 新建(New):创建后尚未启动的线程的状态
- 运行(Runnable):包含Running和Ready
- 无限等待(Waitting):不会被分配CPU执行时间,需要显式被唤醒
造成无限等待的情况:没有设置Timeout参数的Object.wait()方法;没有设置Timeout参数的Thread.join()方法,调用LockSupport.park()方法 - 限期等待(Timed Waitting):在一定时间后会由系统自动唤醒
出现情况:Thread.sleep()方法;设置Timeout参数的Object.wait()方法;设置了Timeout参数的Thread.join()方法,LockSupport.parkName()方法;LockSupport.parkUntil()方法,都需要传入时间参数 - 阻塞(Blocked):等待获取排它锁
- 结束(Terminated):已终止线程的状态,线程已结束执行
sleep和wait方法的区别
基本差别:
- sleep是Thread类中的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用,而wait()方法只能在synchronized方法或在synchcronized代码块中使用(因为需要获取锁后才能释放锁)
本质区别:
- Thread.sleep只会让出CPU,不会释放锁,会一直持有锁
- Object.wait不仅会让出CPU,还会释放已占有的同步锁
notify和notifyAll的区别
锁池:假设线程A已经拥有了某个对象锁,而其他线程B,C想要调用这个对象的某个synchronized方法或者代码块之前必须获得该对象的锁,而恰巧该对象的锁正被A线程占用,此时B,C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池
等待池:假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入了该对象的等待池中,进入的等待池的线程不会去竞争该对象的锁
notifyAll会让所有处于等待池中的线程,全部进入锁池去竞争获取锁的机会
notify只会随机选取一个处于等待池中得线程,进入锁池中去竞争获取锁的机会
yield:当调用Thread,yield()方法时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽视这个暗示
如何中断一个线程?
已被抛弃的方法:通过调用stop()方法停止线程 -------- 可以由一个线程调用stop方法,终止另一个线程,该方法太过暴力,而且是不安全的
线程A调用线程B的stop方法,去停止线程B,但线程A其实并不知道线程B的具体执行情况,这种突然的停止动作会导致线程B的一些清理工作无法完成,还有就是执行stop方法后,线程B会马上释放自己的锁,这样有可能会引发数据不同步的问题
目前使用的方法:调用interrupt()
方法,通知线程应该中断了
- 如果线程处于被阻塞状态,那么线程应该立即退出被阻塞状态,并抛出一个InterruptExceptioon异常
- 如果线程处于正常活动状态,那么将该线程的中断标志设置为true,被设置中断标志的线程将继续正常运行,不受影响。
因此Interrupt并不能真正是线程中断,需要被调用的线程配合中断
线程状态以各个状态之间的转换:
synchronized的底层实现原理
实现synchronized的基础
- Java对象头
- Monitor
- 对象在内存中的布局
- 对象头
- 实例数据
- 对齐填充
+对象头结构:- Mark Word 默认存储对象的hashcode,分代年龄,锁类型,锁标志位等信息,是实现轻量级锁和偏向锁的关键
- Class Metadata Address 类型指针指向对象的元素类型,JVM通过这个指针确定该对象是哪个类的数据类型
- Monitor:每个Java对象天生自带了一把看不见的锁
自旋锁与自适应自旋锁
许多情况下,共享锁的锁定状态持续时间较短,切换线程不值得,通过让线程执行忙循环等待锁的释放,不让出CPU;若锁被其他线程很长时间占用,会带来许多性能上的开销
自适应自旋锁:自旋的次数不在固定,由前一次在同一个锁上的自旋时间及锁的拥有者状态来定
锁消除:JVM对锁另一种更彻底的优化,JVM在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化:另一种极端:通过扩大锁的范围,避免反复加锁和解锁
synchronized的四种状态
无锁 —> 偏向锁—> 轻量级锁 —>重量级锁
偏向锁:减少获取同一锁的代价 CAS(Compare And Swap)
大多数情况下,锁不存在多线程竞争,总是由一个线程多次获得
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向所的结构,当该线程再次请求锁时,无需再做任何同步操作,及获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadId即可,这样就省去了大量有关锁申请的操作
不适用于锁竞争比较激烈的多线程场合
轻量级锁:轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步快的情况下,而第二个线程加入锁争用的时候,偏向锁就会升级位
轻量级锁:适应的场景:线程交替执行同步快
若存在同一时间访问同一锁的情况,就会导致轻量级锁升级到重量锁
synchronized和ReentrantLock的区别
- ReentranLock(再入锁)
- 位于java,util,concurrent,locks包
- 和CountDownLatch,FutureTask,Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制,如控制faireness
- 调用lock()后,必须调用unlock()释放锁
- 性能未必比synchronized高,并且也是可重入的
ReentranLock公平性的设置
ReentanLock faireLock = new ReentanLock(true)
参数为true时,倾向于将锁赋予等待时间最长的线程
公平锁:获取锁的先后顺序按先后调用Lock()方法的顺序
非公平锁:抢占顺序不一定,看运气
synchronized是非公平锁
总结
- synchronized是关键字,ReentraLock是类
- ReentranLock可以对获取锁的等待时间进行设置,避免死锁
- ReentranLock可以获取各种锁的信息
- ReentranLock可以灵活地实现多路通知
机制:sync操作Mark Work,lock调用Unsafe类的park()方法
指令重排需要满足的条件:
- 在单线程环境下不能改变程序执行的结果
- 存在数据依赖关系的不允许重排序
- 无法通过happends-before原则推导出来的,才能进行指令的重排
valitile 和synchronized 的区别
- volitile 本质是告诉JVM当前变量在工作内存中的值是不确定的,需要从主内存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞直到该线程完成变量操作为止
- volitile仅能使用在变量级别,synchronized则可以用在变量,方法和类级别
- volitile 仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
- valitile不会造成线程的阻塞,synchronized则可能会造成线程的阻塞
- valitile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
CAS(Compare And Swap):一种高效实现线程安全性的方法
支持原子更新操作,适用于计数器,序列发生器等操作
属于乐观锁机制,号称lock-free
CAS操作失败时由开发者决定是继续尝试,还是执行别的操作
CAS多数情况下对开发者来说是透明的
J.U.C的atomic包提供了常用的原子性数据类型以及引用,数组等相关原子类型的更新操作工具,是很多线程安全的首选
Unsafe类虽然提供CAS服务,但因为能够操作任意内存地址读写而又隐患
缺点:若循环时间长,则开销很大,只能保证一个共享变量的原子操作,ABA问题
线程池
在web开发中,服务器需要接收并处理请求,所以会为一个请求来分配一个线程来进行处理,如果并发的请求数量比较多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,这样一来就会大大降低系统的效率,可能出现服务器在为每个线程创建和销毁的时间比实际处理请求消耗的时间更多
利用Excutors创建不同的线程池满足不同场景的需求
newFixedThreadPool(int nThreads)
------ 指定工作线程数量的线程池newCachedThreadPool()
-------- 处理大量短时间工作任务的线程池- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
- 如果线程闲置时间超过阈值,则会被终止并移出缓存
- 系统长时间闲置的时候,不会消耗什么资源
newSingleThreadScheduledExcutor()
与newScheduledThreadPool(int corePoolSize)
定时或者周期的工作调度,两者的区别在于单一工作线程还是多个线程newSingleThreadEcecutor()
创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代
为什么要使用线程池
降低资源消耗
提高线程的可管理性
J.U.C的三个Excutor接口
Excutor
:运行新任务的简单接口,将任务提交和任务执行细节解耦
ExcutorService
:具备管理执行器和任务生命周期的方法,提交任务机制更完善
ScheduledExcutorService
:支持Future的定期执行任务
TreadPoolExcutor的工作模式如图:
ThreadPoolExcutor的构造函数:
corePoolSize
:核心线程数量maximumPoolSize
:线程不够用时能够创建 的最大线程数workQueue
:任务等待队列keepAliveTime
:抢占的顺序不一定,看运气threadFactory
:创建新线程,Excutors.defaultThreadFactory()
,优先级不变,也设置了线程名称handler
:线程池的饱和策略AbortPolicy
:直接抛出异常,默认策CallerRunsPolicy
:用调用者所在的线程来执行任务DiscardOldsPolicy
:丢弃队列中最靠前的任务,并执行当前任务DiscardPolicy
:直接丢弃任务- 实现
RejectExcutorHandler
接口的自定义handler
新任务提交excutor
执行后的判断:
- 如果运行的线程少于
corePoolSize
,则创建新线程来处理任务,即使线程池中的其他线程是空闲的; - 如果线程池中的线程数量大于
corePoolSize
且小于maximumPoolSize
,则只有当workQueue
满时,才创建新的线程去处理任务 - 如果设置的
corePoolSize
和maximumPoolSize
相同,则创建的线程池大小是固定得,这时如果有新任务提交,若workQueue
未满,则将请求放入workQueue
中,等待有空闲的线程去从workQueue
中去取任务并处理; - 若运行的线程数量大于等于
maximumPoolSize
,这时如果workQueue
已经满了,则通过handler
所指定的策略来处理任务
线程池的状态
Running
:能接收新提交的任务,并且也能处理阻塞队列中的任务shutdown
:不再接收新提交的任务,但可以处理存量任务stop
:不再接收新提交的任务,也不处理存量任务Tidying
:所有 的任务都已终止TERMINATED
:terminated()方法执行后进入该状态
线程的大小如何选定?
CPU密集型:线程数 = 按照核数或者核数+1设定
I/O密集型:线程数 = CPU数 * (1+平均等待时间/平均工作时间 )
Java异常体系
Error和Exceptionde的区别
Error:程序无法处理的系统错误,编译器不做任何检查
Exception:程序可以处理的异常,捕获后可能恢复
总结:前者是程序无法处理的错误,后者是程序可以处理的异常
Java异常的处理原则
具体明确:抛出的的异常应该能通过异常类名和message准确说明异常的类型和产生异常的原因;
提早抛出:应尽可能早的发现并抛出异常,便于精确定位问题;
延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常
高效主流的异常处理框架
在用户看来,应用系统发生的所有异常都是应用系统内部的异常
设计一个通用的继承自RuntimeException的异常来统一处理,设为AppException
其余异常都统一转义为上述异常AppException
在catch之后,抛出上述异常的子类,并提供足以定位的信息
由前端接收AppException做统一处理
Java异常处理消耗异常的地方
try-catch代码块影响JVM的优化
异常对象实列需要保存堆栈快照等信息,开销较大
容器
Collection体系:
Map体系
HashMap,HashTable,ConccurentHashMap
HashMap(Java8以前):数组+链表
Java8及以后:数组+链表+红黑树:当链表的长度大于8时,就会转为红黑树
HashMap:put方法的逻辑
- 如果HashMap未被初始化过,则初始化
- 对key求Hash值,然后在计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式连接到后面
- 如果链表长度超过域值,就把链表转为红黑树
- 如果链表长度小于6,就把红黑树转回链表
- 如果节点已经存在,就会替换旧值
- 如果桶满了(容量16*加载因子0.75),就需要resize(扩容2倍后重排)
HashMap:如何有效减少碰撞
- 扰动函数:促使元素位置分布均匀,减少碰撞几率
- 使用final对象,并采用合适的equals()和hashCode()方法
HashMap:扩容的问题 - 多线程环境下,调整大小会存在条件竞争,容易造成死锁
reHashing是一个比较耗时的过程
如何优化Hashtable?
通过锁细粒度化,将整锁拆解成多个锁进行优化
ConcurrentHashMap:put方法的逻辑
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环
- 检查到内部正在扩容,就帮助他一块扩容
- 如果 f != null ,则使用synchronized锁住 f 元素(链表/红黑二叉树的头元素)
- 如果Node(链表结构)则执行链表的添加操作
- 如果是TreeNode(树形结构),则执行树的添加操作
- 当链表达到临界值8时,这个8是默认值,可以自己去调整,当节点树超过这个值就需要把链表转为树结构
三者的区别?
- HashMap线程不安全,数组+链表+红黑树
- Hashtable线程安全,锁住整个对象,数组+链表
- ConcurrentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
- HashMap的key,value均可为null,而其他的两个类不支持
J.U.C知识点梳理
java,util.concrrent
:提供了并发编程的解决方案
- CAS :是
java,util,concrrent.atomic
包的基础 - AQS:是
java,util,concrrent.locks
包及一些常用类,比如Semophore,ReentranLock等类的基础
J.U.C包的分类
- 线程执行器executor
- 锁 locks
- 原子变量类 atomic
- 并发工具类 tools
- 并发集合 collections
BIO,NIO,AIO
Block-IO:inputStream和OutputStream,Reader和Writer
NonBlock-IO:构建多路复用,同步非阻塞的IO操作
NIO的核心
- Channels
- FileChannel
- transferTo:把fileChannel中的数据拷贝到另外一个Channel
- transferFrom:把另外一个Channel中的数据拷贝到FileChannel。避免了两次用户态和内核态之间的切换,即“零拷贝”,效率较高
- DatagramChannel
- SocketChannel
- ServerSocketChannel
- FileChannel
- Buffers
- Selectors
Asynchronous IO:基于事件和回调机制
AIO如何进一步加工处理结果 - 基于回调:实现
CompletionHandler
接口,调用时触发回调函数 - 返回Future:通过
isDone()
查看是否准备好,通过get()
等待返回数据
更多面试题、电子书、学习路线、视频教程等学习资源,可在我的个人网站:程序员波特 免费获取。