Office直通车

数据库篇

数据库架构
image

为什么要使用索引?

对于数据量较大的表,建立索引避免全表扫描,能够提高查询速度,提高效率

什么样的信息能成为索引?

主键、唯一键、普通键

索引数据结构?

主流:B+Tree
Hash索引
不能使用范围查询、排序操作,仅能满足“=”,“IN”查询
不能利用部分索引查询
不能避免表扫描

密集索引和稀疏索引的区别?

Innodb存储引擎中,主键所在的索引树称之为密集索引,或者说数据所在的索引树称之为密集索引,有且仅有一个,非主键所在的索引树称之为稀疏索引
MyISAM存储引擎中,都是稀疏索引,索引和数据是分开存储的

如何定位并优化慢查询sql

  • 根据慢日志定位慢查询sql
    开启慢日志记录
-- 查询数据库相关变量
SHOW VARIABLES LIKE '%query%'
-- 本次会话慢查询数量,关闭客户端后慢查询数量会清零
SHOW STATUS LIKE '%slow_queries%'
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON
-- 修改最长查询时间为1秒,超过1秒就被慢查询记录
SET GLOBAL long_query_time = 1

以上设置在数据库服务重启后丢失,可在配置文件中配置永久保存
image

  • 使用explain等工具分析sql
  • 修改sql,建立索引,尽量走索引

联合索引的最左匹配原则

image
最左匹配原则里有比较多的坑,在查询的时候,如果查询的列在索引树能全部查询到,即使没有遵循最左匹配原则,仍然可能使用索引

CREATE TABLE `test_index` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` varchar(10) DEFAULT NULL,
  `b` varchar(10) DEFAULT NULL,
  `c` varchar(10) DEFAULT NULL,
  `d` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `abc` (`a`,`b`,`c`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

INSERT INTO `test_index` (`id`, `a`, `b`, `c`, `d`) VALUES (1, 'a', 'b', 'c', 'd');
INSERT INTO `test_index` (`id`, `a`, `b`, `c`, `d`) VALUES (2, 'aa', 'bb', 'cc', 'dd');
INSERT INTO `test_index` (`id`, `a`, `b`, `c`, `d`) VALUES (3, 'aaa', 'bbb', 'ccc', 'ddd');
INSERT INTO `test_index` (`id`, `a`, `b`, `c`, `d`) VALUES (4, 'aaaa', 'bbbb', 'cccc', 'dddd');

建立一个表,并且给a,b,c字段建立联合索引

SELECT a,b,c FROM test_index WHERE b='b' and c='c';

根据最左匹配原则,上面的sql是不满足最左匹配原则的,按道理不会使用到索引,但实际情况是使用到了索引
image
这是因为我们要查询的列在abc联合索引上能全部查询到,如果sql语句写成下面这样

SELECT a,b,c,d FROM test_index WHERE b='b' and c='c';

字段d在abc索引树上找不到,就不会使用到索引
image

索引是建立越多越好吗?

答案是否定的

  • 数据量小的表不需要建立索引,建立索引会增加额外的索引开销
  • 数据变更需要维护索引,因此越多的索引意味着更多的维护成本
  • 更多的索引也意味着需要更多的空间

MyISAM与InnoDB关于锁方面的区别?

MyISAM默认用的是表级锁,不支持行级锁
InnoDB默认用的是行级锁,也支持表级锁

对于MyISAM,对表进行查询时,会锁住整张表,阻塞其他语句对数据的更新

-- 给表加读锁
lock tables xxx read;
-- 给表加写锁
lock tables xxx write;
-- 解锁
unlock tables;

读锁也叫做共享锁
写锁也叫做独占锁

对于InnoDB

-- 查看事务自动提交
show variables like '%autocommit%';
-- 关闭自动提交
set autocommit=0;
-- sql语句加共享锁
select name from person_info where id = 1 lock in share mode;
-- sql语句加排他锁
select name from person_info where id = 1 for update;

当sql没有用到索引的时候,用到的表级锁

MyISAM适用场景

  • 频繁执行全表count语句
  • 对数据进行增删改的频率不高,查询非常频繁
  • 没有事务

InnoDB适用场景

  • 增删改查都相当频繁
  • 可靠性要求比较高,要求支持事务

数据库锁的分类

  • 按照锁的粒度划分,可分为表级锁、行级锁、页级锁
  • 按锁级别划分,可分为共享锁、排他锁
  • 按加锁方式划分,可分为自动锁、显示锁
  • 按操作划分,可分为DML锁,DDL锁
  • 按试用方式划分,可分为乐观锁、悲观锁

数据库事务的四大特性

ACID

  • 原子性:要么全部成功、要么失败
  • 一致性:改变前后数据呈现一致状态,转账例子,个人理解有点像能量守恒一样
  • 隔离性:多个事务并发执行时,一个事务的执行不会影响另一个事务
  • 持久性:对数据的修改是持久的

事务隔离级别以及各级别下的并发访问问题

事务并发访问引起的问题

  • 脏读:一个事务读取到另一个事务没有提交的数据
  • 不可重复读:事务内两次读取结果不一致
  • 幻读:两次读取到的结果集不一致

事务隔离级别

  • 读未提交(read uncommited):级别最低
  • 读已提交(read commited):避免脏读
  • 可重复读(repeatable read):避免脏读、不可重复读
  • 串行化(serializable):级别最高,避免脏读、不可重复读、幻读

隔离级别越高,性能越低

InnoDB可重复读隔离级别下如何避免幻读?

Redis篇

Memcache与Redis的区别

Memcache:代码层次类似Hash

  • 支持简单数据类型
  • 不支持数据持久化存储
  • 不支持主从
  • 不支持分片

Redis

  • 数据类型丰富
  • 支持持久化存储
  • 支持主从
  • 支持分片

Redis为什么这么快?

完全基于内存,绝大部分的请求是纯粹的内存操作,执行效率高
数据结构简单,对数据操作简单
采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
采用多路I/O复用模型,非阻塞IO
单线程结构指主线程是单线程结构,所有的客户端的操作都由主线程串行的处理,不会产生并发的操作,锁竞争的问题

你的项目中Redis都用来干什么?

结合博主个人使用redis的情况,redis用来做缓存、分布式锁、共享session、登录校验

Redis数据类型

  • String:最基本的数据类型,二进制安全【set,get】
  • Hash:String元素的字典,适合用于存储对象【hmset,hget】
  • List:列表,按照String插入顺序排序【lpush,lrange】
  • Set:String元素组成的集合,通过哈希表实现,不允许重复【sadd,smembers】
  • Sorted Set:通过分数来为集合中的成员进行从小到大的排序【zadd,zrangebyscore】
  • 用于技术的HyperLogLog,用于支持存储地理位置信息的Geo

image

从海量Key里查询出某一固定前缀的Key

留意细节:摸清数据量

使用KEYS指令

KEYS pattern:查找所有符合给定模式pattern的key
keys指令特点:

  • KEYS指令会一次性返回所有匹配的key
  • 键的数量过大会使服务卡顿

使用SCAN指令

image

  • 基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
  • 以0作为游标开始一次新的迭代,直到命令返回游标0完成一次迭代
  • 不保证每次执行都返回某个给定数量的元素,支持模糊查询
  • 一次返回的数量不可控,只能是大概率符合count参数

如何使用Redis实现分布式锁?

分布式锁要解决的问题

  • 互斥性:任意时刻,只能有一个客户端获取锁
  • 安全性:锁只能被持有该锁的客户端删除,不能被其他客户端删除
  • 死锁:客户端因为某些原因宕机不能释放锁,导致其他客户端无法获取锁
  • 容错:当部分节点宕机后,客户端仍然能够获取锁

如何保证互斥性?

SETNX key value:如果key不存在,则创建并赋值
因SETNX具有该上述特点且是原子性的,所有使用SETNX指令可以实现互斥性
但这样set成功后的value是长期存在的,当因某些原因没有释放掉锁,该锁就会成为死锁

如何解决死锁问题?

EXPIRE key seconds:设置key的过期时间
通过设置key的过期时间可以预防死锁问题,但是这样操作就不是原子性的了,设置key过期过期时间的过程中仍然存在失败的可能,仍然存在死锁问题

如果解决原子性问题?

将上面两步操作合二为一
在Redis2.6.2之后支持在set的时候同时指定过期时间
image
除此之外还可以通过lua脚本实现,lua脚本具有原子操作的特点:Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。

如何保证安全性呢?

客户端加锁的时候指定一个唯一的value,当解锁之前校验一下value。校验成功后才可解锁

key的过期时间设置多久合适?

在给key设置过期时间时,过长过短都不太好。在Redisson中,针对key的过期时间设计一个看门狗机制,他会定时去更新key的过期时间,俗称续命

如何使用Redis实现异步队列?

使用List作为队列,RPUSH生产消息,LPOP消费消息
缺点:没有等待队列里有值就直接消费
弥补:可以通过在应用层引入Sleep机制去调用LPOP重试
BLPOP key [key...] timeout:阻塞队列直到有消息或超时
缺点:只能有一个消费者消费
pub/sub主题订阅者模式
缺点:消息的发布是无状态的,无法保证可达

Redis持久化策略之RDB

RDB(快照)持久化:保存某个时间点的全量数据快照
默认RDB持久化策略
image
禁用RDB配置,在配置文件redis.conf中加入save ""
RDB文件的创建

  • SAVE:阻塞Redis的服务器进程,直到RDB文件被创建完毕
  • BGSAVE:FORK出一个子进程来创建RDB文件,不会阻塞服务器进程

自动触发RDB持久化的方式

  • 根据redis.conf配置里的SAVE m n 定时触发(用的是BGSAVE)
  • 主从复制时,主节点自动触发
  • 指定Debug Reload
  • 执行Shutdown且没有开启AOF持久化

缺点

  • 内存数据的全量同步,数据量大会由于I/O而影响性能
  • 可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据

Redis持久化策略之AOF以及混合模式

AOF(Append-Only-File)持久化:保存写状态

  • 记录下除了查询以外的所有变更数据库状态的指令
  • 以append的形式追加保存到AOF文件中(增量)

开启AOF:在redis.conf文件中修改appendonly yes

因为AOF是以增量的方式保存指令,随时间推移,文件会越来越大
Redis通过日志重写角色AOF文件大小不断增加的问题,原理如下:

  • 调用fork(),创建一个子进程
  • 子进程把新的AOF写到一个临时文件中,不依赖原来的AOF文件
  • 主进程持续将新的变动同时写到内存和原来的AOF里
  • 主进程获取子进程重写AOF的完成信号,往新的AOF同步增加变动
  • 使用新的AOF文件替换掉旧的AOF文件

RDB和AOF文件共存情况下恢复数据流程
image
存在AOF加载AOF文件,忽略RDB,否则加载RDB文件

Redis4.0之后推出了RDB-AOF混合持久化方式
BGSAVE做镜像全量持久化,AOF做增量持久化

RDB和AOF的优缺点

RDB优点:全量数据快照,文件小,恢复快
RDB缺点:无法保存最近一次快照之后的数据
AOF优点:可读性高,适合保存增量数据,数据不易丢失
AOF缺点:文件体积大,恢复时间长

Redis同步机制

全同步过程

  • Salve发送sync命令到Master
  • Master启动一个后台进程,将Redis中的数据保存到文件中
  • Master将保存数据快照期间接收到的写命令缓存起来
  • Master完成写文件操作后,将该文件发送给Salve
  • 使用新的AOF文件替换掉旧的AOF文件
  • Master将期间收集的增量写命令发送给Salve端

增量同步过程

  • Master接收到用户的操作指令,判断是否需要传播到Slave
  • 将操作记录追加到AOF文件
  • 将操作传播到其他Slave:1、对齐主从库;2、往响应缓存写入指令
  • 将缓存中的数据发送给Slave

Redis之过期策略

  • 惰性删除:key过期后不会删除,等待下一次获取时再检查key是否过期,若过期,则删除
  • 定期删除:每隔一段时间执行一次删除过期key操作。
  • 定时删除:设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。(没人用)

redis采用的过期策略
惰性删除+定期删除。

缓存穿透,缓存击穿,缓存雪崩

  • 缓存穿透:访问一个不存在的key,请求打到数据库,数据库不堪重负噶了
    解决方案:
    • 使用布隆过滤器,对所有可能存在的key存入一个map,对不存在的key进行拦截
    • 对这个不存在的key也进行缓存并设置过期时间
  • 缓存击穿:一个热点key过期了,大量请求打到数据库,数据库不堪重负噶了
    解决方案:
    • 对热点key设置不过期
  • 缓存雪崩:大量的key在同一时间过期,大量请求打到数据库,数据库不堪重负噶了
    解决方案:
    • 在设置key过期时间时,在过期时间的基础上再附加一个随机值,使key的过期时间分散开

JVM篇

JVM如何加载clss文件?

image
加载过程分为几个阶段

  • 加载
    通过ClassLoader加载class文件,生成Class对象
  • 链接
    • 验证
      检查加载的class是否符合jvm要求
    • 准备
      给静态变量分配存储空间和赋零值
    • 解析
      将常量池符号引用转为直接引用
  • 初始化
    调用编译期间生成的<cinit>(由静态变量和静态代码块生成)方法给静态变量赋初始值

ClssLoader的种类?

  • BootStrapClassLoader:C++编写,加载核心库java.*(jre/lib)
  • ExtClassLoader:java编写,加载扩展库javax.*(jre/lib/ext)
  • AppClassLoader:java编写,加载程序所在目录
  • 自定义ClassLoader:java编写,定制化加载

ClassLoader的双亲委派机制

image
收到类加载请求,首先会自底向上查找该类是否已经被加载,如果已经被加载则直接返回,如果没有加载,再自顶向下尝试加载类,每个ClassLoader加载的范围都不一样,不在自己加载范围里的类会委托给下面的ClassLoader加载,直到加载成功,如果到最后没有ClassLoader能加载则抛出ClassNotFoundException。
目的:安全,防止修改核心类库、避免重复加载

loadClass和forName的区别

Class.forName得到的class是已经初始化完成的(就是经历了完整的加载过程,加载、链接、初始化)
Classloader.loadClass得到的class是还没有链接的(只进行了加载过程,没有链接、初始化的过程)

java内存模型(JVM运行时区域)

线程私有

  • 程序计数器
    • 当前线程所执行的字节码行号指示器(逻辑)
    • 改变计数器的值来选取下一条需要执行的字节码指令
    • 只对java方法计数,不对native方法计数
    • 不会发生内存泄露
  • java虚拟机栈
    • java方法执行的内存模型
    • 包含多个栈帧,每执行一个方法,创建一个栈帧,栈帧又包括操作数栈、局部变量表、方法出口等
  • 本地方法栈
    • 与虚拟机栈类似,作用于标注了native的方法

线程共享

  • 方法区(元空间)
    • 元空间和永久代:元空间和永久代都是方法区的实现,所以方法区只是JVM的一种规范
    • java8之后元空间替代了永久代
    • 对象实例的分配区域
    • GC管理的主要区域
    • 分为新生代和老年代

递归为什么会引起java.lang.StackOverflowError异常?

递归过深,每调用一次方法就创建一个栈帧,栈帧数超过了虚拟机栈深度,java虚拟机栈被消耗尽了,虚拟机栈过多还会引起java.lang.OutOfMemoryError异常

限制递归的深度

JVM三大性能调优参数-Xms -Xmx -Xss的含义

  • -Xms:规定了每个线程虚拟机栈的大小
  • -Xmx:堆的初始值
  • -Xss:堆能达到的最大值

一般将-Xmx和-Xss设置为一样,因为堆不够扩容时会发生内存抖动,影响程序运行稳定性

Java内存模型中堆和栈的区别?

管理方式:栈自动释放,堆需要GC
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆
分配方式:栈支持静态和动态分配,而堆仅支持动态分配
效率:栈的效率比堆高

不同版本intern()方法的区别?

image

public static void main(String[] args) {
    String s = new String("a");
    s.intern();
    String s2 = "a";
    System.out.println(s == s2);

    String s3 = new String("a") + new String("a");
    s3.intern();
    String s4 = "aa";
    System.out.println(s3 == s4);
}

输出结果
jdk6运行结果:false、false
String s = new String("a")这一句代码首先"a"会放入常量池,new String("a")会在堆中创建一个实例"a",s.intern()会尝试复制一个堆中"a"的实例副本到常量池,但此时常量池中已经有"a",放不进去,所有s指向的是堆中的实例,s2指向常量池中的"a",故为false
String s3 = new String("a") + new String("a")会在堆中创建一个实例"aa",s3.intern()会尝试复制一个副本实例到常量池,此时常量池并没有"aa",所以会放入成功,s4指向的就是常量池中的副本实例,s3指向的是堆中的实例,所以引用不一样,故为false


jdk6+运行结果:false、true
String s = new String("a")这一句代码首先"a"会放入常量池,new String("a")会在堆中创建一个实例"a",s.intern()会将堆中"a"实例的引用放入常量池,但此时常量池中已经有"a",放不进去,所以s指向的是堆中的实例"a",s2指向常量池中的"a",故为false
String s3 = new String("a") + new String("a")会在堆中创建一个实例"aa",s3.intern()会将堆中"aa"的实例引用放入常量池,此时常量池并没有"aa",所以会放入成功,s4指向的是常量池中"aa"在堆中的引用,所以s3和s4最终指向都是堆中"aa"的引用,故为true

判断对象是否存活(如何定义一个对象为垃圾)

运用计数法
引用计数法,当创建对象时给绑定一个计数器,当有引入指向该对象时计数器加1,当引入删除时计数器减1,当计数器为0时代表死亡。
优点:简单、高效
缺点:无法检测到环(循环引用)
应用场景:暂无
可达性分析法
从GC Roots开始向下搜索,如果一个对象无法到达GC Roots说明这个对象是可以回收的
image
Object1、Object2、Object3、Object4、Object5可到达GC Roots,所以是不可回收的,
Object6、Object7、Object8无法到达GC Roots,是可以回收的
可以作为GC Roots的对象有:

  • 局部变量表中引用的对象
  • 静态变量引用的对象
  • 常量引用的对象
  • Native方法中引用的对象

优点:有效解决循环引用问题
缺点:和引用计数法没得缺点
应用场景:主流虚拟机都采用的算法

垃圾回收算法

  • 标记-清除法:为对象存储一个标记位,标记对象存活、死亡状态。
    • 标记阶段:更新对象的标记
    • 清除阶段:对标记死亡的对象进行清除,执行GC操作
    • 优点:可以解决循环引用问题、在必要时才回收
    • 缺点:回收时,应用需挂起;效率较低,尤其扫描对象较多时;会造成内存碎片;
  • 标记-整理法:在标记-清除法的基础上解决了内存碎片化问题
    • 标记阶段:更新对象的标记
    • 整理阶段:不直接对死亡对象清除,先把存活对象整理好放到一处空间,剩下的对象全部清除,达到整理的目的
    • 优点:解决标记清除算法出现的内存碎片化问题
    • 缺点:由于移动了可用对象,需更新引用
  • 复制算法:将内存分为两部分,每次只使用一部分,当这一部分满的时候,将所有存活对象复制到另一个部分,清空这一部分,循环往复
    • 优点:对象不多时性能高,能解决碎片化和更新引用问题
    • 缺点:造成内存空间浪费,对象多时性能差
    • 场景:应用在新生代当中。
  • 分代算法:根据对象的生命周期的不同将内容划分为新生代和老年代区域,不同区域采用不同的GC算法,新生代对象生命周期短,使用复制算法,老年代生命周期长,使用标记清除或标记整理算法

Java堆分为两部分区域:新生代和老年代,默认大小比例为1:2
新生代又可以划分为三个区域,分别是1个Eden、2个Survivor,默认大小比例为8:1:1,新生代对象存活率低,采用复制算法
老年代只有一个区域,老年代对象存活率高,采用“标记-清理”或“标记-整理”算法

对象首先会分配到Eden区域,如果是大对象直接放入到老年代中,如果Eden区域空间不足,就会触发一次Minor GC,如果对象经过一次Minor GC没被回收且被Survivor接受,那么对象将会被移动到Survivor当中,且年龄等于1,之后每熬过一个Minor GC,年龄就会加1,如果年龄到达15(默认值,也是最大值),那么对象就会晋升到老年代中。
Minor GC:新生代GC,执行频繁
Major GC:老年代GC,执行速度慢于Minor GC,执行Major GC会连着Minor GC执行
Full GC:清理整个堆空间,包括新生代和老年代
触发Full GC的条件

  • 老年代空间不足
  • 永久代空间不足(对于jdk8之前来说,jdk8之后取消了永久代,目的之一是为了减少Full GC的执行次数)
  • CMS GC时出现promotion failed,concurrent mode failure
  • Minor GC晋升到老年代的平均大小小于老年代的剩余空间
  • 调用System.gc()
  • 使用RMI来进行RPC或管理的JDK应用,每小时执行1次Full GC

常用的调优参数

  • -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
  • -XX:NewRatio:老年代和年轻代内存大小的比例
  • -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数最大的阈值

Stop-the-World

  • JVM由于要执行GC而停止了应用程序的执行
  • 任何一种GC算法中都会发生
  • 多数GC优化通过减少Stop-the-World发生的时间来提高程序性能

常见的垃圾收集器

JVM的运行模式分为Server模式和Cient模式
垃圾收集器之间的联系
image
年轻代垃圾收集器
Serial收集器(--XX:+UseSerialGC,复制算法)

  • 单线程收集,进行垃圾收集时,必须停止所有工作线程
  • 简单搞笑,Client模式下默认的年轻代收集器
    image

ParNew收集器(--XX:+UseParNewGC,复制算法)

  • 多线程收集,其余的行为、特点和Serial收集器一样
  • 单核执行效率不如Serial,在多核下执行才有优势
    image

Parallel Scavenge收集器(--XX:+UseParallelGC,复制算法)

  • 比起关注用户线程的停顿时间,更关注系统的吞吐量(吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间))
  • 在多核下执行才有优势,Server模式下默认的年轻代收集器
    image

老年代垃圾收集器
Serial Old收集器(--XX:+UseSerialOldGC,标记-整理算法)

  • 单线程收集,进行垃圾收集时,必须停止所有工作线程
  • 简单高效,Client模式下默认的老年代收集器
    image

Parallel Old收集器(--XX:+UseParallelOldGC,标记-整理算法)

  • 多线程,吞吐量优先
    image

CMS收集器(--XX:+UseConcMarkSweepGC,标记-清除算法)
垃圾回收过程

  1. 初始标记:仅只是标记一下GC Roots能直接关联到的对象
  2. 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,
  3. 并发预处理:查找执行并发标记阶段从年轻代晋升到老年代的对象
  4. 可中断的并发预处理:处理新生代指向老年代的新引用
  5. 重新标记:重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  6. 并发清理:清理垃圾对象,程序不会停顿
  7. 并发重置:重置CMS收集器的数据结构
    image

既用于年轻代又用于老年代
G1收集器(--XX:+UseG1GC,复制+标记-整理算法)
特点

  • 并行和并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

内存布局

  • 将整个Java堆内存划分为多个大小相等的Region
  • 年轻代和老年代不再物理隔离

Java中的强引用,软引用,弱引用,虚引用有什么作用?

强引用

  • 最普遍的引用:Objject obj = new Object()
  • 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
  • 通过将对象设置为null来弱化引用,使其被回收

软引用

  • 对象处在有用但非必须的状态
  • 只有当内存空间不足时,GC会回收该引用的对象的内存
  • 可以用来实现高速缓存
String str = new String("abc"); // 强引用
SoftReference<String> softRef = new SoftReference<>(str); // 软引用

弱引用

  • 非必须的对象,比软引用更弱一些
  • GC时被回收
  • 被回收的概率也不大,因为GC线程优先级比较低
  • 适用于引用偶尔被使用且不影响垃圾手机的对象
String str = new String("abc");
WeakReference<String> weakRef = new WeakReference<>(str);

虚引用

  • 不会决定对象的生命周期
  • 任何时候都可能被垃圾收集器回收
  • 跟踪对象被垃圾收集器回收的活动,起哨兵作用
  • 必须和引用队列ReferenceQueue联合使用
String str = new String("abc");
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomRe = new PhantomReference(str,referenceQueue);

image
引用队列

  • 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
  • 存储关联的且被GC的软引用、弱引用以及虚引用

多线程与并发篇

Thread中start和run方法的区别

image
调用start()方法会创建一个新的子线程并启动
run()方法只是Thread的一个普通方法的调用

Thread和Runnable的关系

Thread是实现了Runnable接口的类,使得run支持多线程
因类的单一继承原则,推荐多使用Runnable接口

线程的状态

六个状态

  • 新建(New):创建后尚未启动的线程的状态
  • 运行(Runnable):包含Running和Ready
  • 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
  • 限期等待(Timing Waiting):在一定时间后由系统自动唤醒
  • 阻塞(Blocked):等待获取排他锁
  • 结束(Terminated):已终止线程的状态,线程已经结束执行

wait和sleep的区别

  • sleep 是 Thread 的静态方法,wait 是Object 的方法,任何对象实例都能调用。
  • sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
  • 它们都可以被 interrupted 方法中断。

notify和notifyAll的区别

锁池
image

等待池
image

  • notifyAll会让所有处于等待池中的线程去锁池竞争锁
  • notify会随机在等待池中挑选一个线程去锁池竞争锁

synchronized实现原理

java对象在内存中的结构
对象头
对象头的结构
image
Mark Word的结构是不确定的
image
Monitor:存在每个对象的对象头中,每个对象都有一个,可以理解为一种同步工具或同步机制
在HotSpot虚拟机中,Monitor由ObjectMonitor实现,由C++编写
image
实例数据
对齐填充

jdk6之前
synchronized属于重量级锁,依赖于mutex lock实现,线程之间的切换需要从用户态转换为和心态,开销大
jdk6之后
对synchronized进行了优化,减少了重量级锁的使用
自旋锁:不断循环尝试获取锁,不阻塞线程,不放弃CPU时间片
锁消除:
锁粗化:通过扩大加锁的范围,避免反复加锁解锁
synchronized的四种状态
无锁:没有加任何锁
偏向锁:大多数情况下,不存在多线程竞争,总是由一个线程获取
轻量级锁:由偏向锁升级来,当有其他线程来进行锁的竞争时,偏向锁升级为轻量级锁
重量级锁:

synchronized和ReentrantLock的区别

  • synchronized是关键字、ReentrantLock是类
  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
  • ReentrantLock可以获取各种锁的信息
  • ReentrantLock可以灵活的实现多路通知

机制:sync操作Mark Word,lock调用Unsafe类的park()方法

Java内存模型JMM

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一种规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
image
JMM主内存
存储Java实例对象
包括成员变量、类信息、常量、静态变量等
属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存
存储当前方法的所有本地变量信息,本地变量对其他线程不可见
字节码行号指示器,Native方法信息

volatile和synchronized的区别

  • volatile本质是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中获取;synchronized则是锁定当前变量,只有当前线程能够访问,其他线程访问会阻塞直到该线程完成变量操作为止
  • volatile仅能作用在变量级别,synchronized可以作用在变量、方法、类级别
  • volatile仅能实现可见性和禁止指令重排序,不能保证原子性;synchronized可以保证可见性和原子性
  • volatile不会造成线程阻塞,synchronized会造成线程的阻塞
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化(这里的优化应该就是指的指令重排序)

CAS

CAS(Compare And Swap)
是一种高效的实现线程安全的方法

  • 支持原子更新操作,适用于计数器,序列发生器等场景
  • 属于乐观锁机制

CAS思想
包含三个操作数——内存位置(V)、预期原值(A)、新值(B)
当要更新为B之前,拿出V和A进行比较,如果一致就更新为B,不一致则不更新
缺点是“ABA”问题,即值从A变为B再变为A,对于,“A->B->A”
可以通过增加版本号来解决这个问题

ThreadPoolExecutor的构造参数

image

  • corePoolSize:核心线程数量
  • maximumPoolSize:核心线程不够时,能够创建的最大线程数量
  • keepAliveTime:空闲线程在终止前等待新任务的最长时间
  • unit:时间单位
  • workQueue:任务等待队列
  • threadFactory:创建新线程
  • handler:拒绝策略
    • AbortPolicy:直接抛出异常,默认策略
    • DiscardPolicy:直接丢弃任务
    • DiscardOldestPolicy:丢弃队列中等待最长时间的任务,并执行当前任务
    • CallerRunsPolicy:用调用者所在的线程来执行任务

线程池的状态

  • RUNNING:能接收新提交的任务,也能处理阻塞队列中的任务
  • SHUTDOWN:不在接收新提交的任务,可以处理存量任务
  • STOP:不在接收新提交的任务,也不处理存量任务
  • TIDYING:所有任务都终止
  • TERMINATED:terminated()方法执行后进入该状态
    image

线程池大小如何选定

根据业务情况决定

  • CPU密集型:线程数=CPU核数+1
  • I/O密集型:线程数=CPU核数 * (1 + 平均等待时间/平均工作时间)

HashMap、HashTable、ConcurrentHashMap

HashMap
JDK8之前:数组+链表
hash(key.hashCode())%len
image
最差情况下,时间复杂度为O(n)
JDK8之后:数组+链表+红黑树
image
image

通过属性TREEIFY_THRESHOLD来控制把链表转换为红黑树,值为8
通过属性UNTREEIFY_THRESHOLD来控制把红黑树转为链表,值为6
HashMap:put方法的逻辑

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        // HashMap未被初始化,进行初始化
        n = (tab = resize()).length;
    // 对key进行hash计算,计算出key在数组中的位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        // hash运算后得到的位置还没有元素,创建一个元素放到这个位置
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 该位置存在元素且与该位置key值相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 替换该位置的值
            e = p;
        // 判断当前位置存储的是否是树化了的结点
        else if (p instanceof TreeNode)
            // 以树的方式存入键值对
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 添加到链表最后位置
                    p.next = newNode(hash, key, value, null);
                    // 判断链表元素数是否超过阈值TREEIFY_THRESHOLD
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 链表转成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // key值在map中存在
        if (e != null) { // existing mapping for key
            // 替换旧值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果桶满了(容量16 * 加载因子0.75),就需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap散列过程

// 散列方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

计算出hash值,再右移16位,然后与hash值进行异或运算。
image

如何将非线程安全的集合变成线程安全

通过Collections.synchronizedXXX()方法实现
其中包括
Collections.synchronizedList()
Collections.synchronizedSet()
Collections.synchronizedMap()
......
HashTable
线程安全,锁住整个对象,数组+链表,不能插入空值的键值对
ConcurrentHashMap
早期使用分段锁机制保证线程安全,提高效率
JDK8之后采用:CAS+synchronized使锁的粒度更细化
数据结构同HashMap

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ConcurrentHashMap不允许插入空的键值对
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            // 初始化数组
            tab = initTable();
        // 通过hash值找到链表或红黑树的头结点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 找不到头结点,尝试用CAS进行添加
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                // 失败循环重试
                break;                   // no lock when adding to empty bin
        }
        // 找到元素,但此时正在移动
        else if ((fh = f.hash) == MOVED)
            // 协助扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 锁住头结点
            synchronized (f) {
                // 判断头结点是否是链表的头结点
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        // 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果结点存在
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    // 更新value
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 不存在,在链表尾部添加新结点
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 头结点是红黑树的头结点
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 尝试往树里添加结点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            // 结点不为空,替换旧值
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                // 判断链表长度是否到达树化阈值
                if (binCount >= TREEIFY_THRESHOLD)
                    // 链表转为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

Spring篇

IOC的应用

image
支持的功能

  • 依赖注入
  • 依赖检查
  • 自动装配
  • 支持集合
  • 指定初始化方法和销毁方法
  • 支持回调方法

BeanDefinition
主要用来描述Bean的定义
BeanDefinitionRegistry
提供了向IOC容器注册BeanDefinition对象的方法
BeanFactory

  • 提供了IOC的配置机制
  • 包含了Bean的各种定义,便于实例化Bean
  • 建立Bean之间的依赖关系
  • Bean生命周期的控制

image

ApplicationContext的功能(继承多个接口)

  • BeanFactory:能够管理、装配Bean
  • ResorucePatternResolver:能够加载资源文件
  • MessageSource:能够实现国际化等功能
  • ApplicationEventPublisher:能够注册监听器,实现监听机制

Bean的生命周期
image

AOP的实现:JDKProxy和Cglib
image
image

image

posted @ 2022-08-18 14:10  Liming_Code  阅读(33)  评论(0编辑  收藏  举报