1 设计模式
1.1 基本原则
SOLID(单一职责原则、开闭原则、接口隔离原则、里氏替换原则、依赖倒置原则)。solid:坚硬的。
1) 单一职责原则:一个类应该只有一个引起它变化的原因。
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能地遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
2) 开闭原则:软件中的对象(类,模块,函数等等)应该对扩展开放,对修改封闭。
这意味着一个实体允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循开闭原则的代码在扩展时并不发生改变,因此无需上述的过程。
3) 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上。
不应该强迫客户使用他们不用的方法。不然客户就会面临由于这些不使用的方法的改变所带来的改变。
4) 里氏替换原则:任何基类可以出现的地方,子类一定可以出现。
如果两个具体的类A、B之间的关系违反了里氏替换原则,那么根据具体的情况可以在下面的两种重构方案中选择一种:
1. 创建一个新的抽象类C,作为A、B两个类的超类,将A、B的共同行为移到C中。
2. 从继承关系改为委派(代理)关系。
5) 依赖倒置原则:程序要依赖于抽象接口,不要依赖于具体实现。
简单来说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
1.2 单例模式
采取一定的方法保证在整个软件系统中,某个类只能存在一个实例,该类提供一个取得该实例的方法。
常用的几种单例模式实现方式:
1) 饿汉式
class Singleton {
// 1. 构造器私有化
private Singleton() {
}
// 2. 创建实例
private final static Singleton instance = new Singleton();
// 3. 对外提供一个公有的静态方法,返回实例
public static Singleton getInstance() {
return instance;
}
}
2) 双重检查式
class Singleton {
// 1. 构造器私有化
private Singleton() {
}
// 2. 创建实例,这里要加一个volatile关键字
private static volatile Singleton instance;
// 3. 对外提供一个公有的静态方法,第一次调用该方法时,创建实例
public static Singleton getInstance() {
// 4. 对是否已存在单例对象进行双重检查
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3) 枚举式
enum Singleton {
INSTANCE;
}
2 HashMap
2.1 HashMap数据结构
(声明hashMap是HashMap类的实例)hashMap的数据结构是 数组+链表/红黑树。
hashMap维护了一个数组,数组中的每个元素都是Entry对象,每个Entry对象包含四个属性:key、value、next、hash。
向hashMap中插入Entry对象时,会先使用对象的key调用哈希函数计算出一个hash值,然后使用这个hash值和[数组长度-1](数组长度恒为2的n次幂,所以[数组长度-1]的每一位都是1)做位与运算得到一个数组下标。如果数组该下标的位置为空,就插入,如果下标位置上已有其他Entry对象,说明发生了hash冲突(也叫hash碰撞),就把要插入的Entry对象和该位置上的其他Entry对象通过next属性连接起来形成链表。
当某个下标位置上的结点数增加到八个时,链表转换成红黑树。但实际上在数组的一个下标位置上发生八次hash冲突的概率微乎其微,所以在绝大部分情况下,转换不会发生。
2.2 红黑树
红黑树是平衡的二叉搜索树。
红黑树是在二叉搜索树的基础上,保证了每个结点的左子树和右子树的高度差不大于2,如果超过了2就进行调平衡。
调平衡操作包括左旋、右旋和变色,红黑树的任何不平衡问题都能用三次以内的调平衡操作解决。
2.3 扩容机制
新建的hashMap的容量为16。
当hashMap中的 元素数量>=容量×加载因子 时,数组扩容,容量×2。
2.4 ConcurrentHashMap
concurrentHashMap是线程安全版的hashMap。
concurrentHashMap对链表/红黑树的头/根结点加锁,保证同一时间只能有一个线程对该链表/红黑树进行操作。
3 Java多线程
3.1 并发与并行
并发:多个线程快速地轮换执行,使得在宏观上具有多个线程同时执行的效果。
并行:多核CPU下,多个线程同时执行。
3.2 线程状态
在Java中定义了六种线程的状态:
1) NEW(新建):线程对象被创建后,进入新建状态。
2) RUNNABLE(运行):Java中将线程的就绪状态和运行状态统一为RUNNABLE状态。
3) BLOCKED(阻塞):线程暂时停止运行,等待获得锁资源。
4) WAITING(等待):等待其他线程做出特定动作(通知或中断)。
5) TIMED_WAITING(超时等待):等待其他线程做出特定动作(通知或中断),或者在指定时间后转为就绪状态。
6) TERMINATED(终止):线程执行完毕。
3.3 synchronized锁升级过程
新创建对象 => 偏向锁 => 轻量级锁 => 重量级锁
1) 偏向锁
给新创建对象上偏向锁:在对象的markword里记录当前线程的指针。
偏向锁认为,在大部分情况下,使用synchronized上锁的对象只有一个线程要使用。
只要有其他线程来争夺这个对象(轻度竞争),偏向锁就升级为轻量级锁。
2) 轻量级锁
线程轻度竞争下,每个线程在自己的线程栈里划出一块空间,然后把对象的markword复制过来,称为锁记录(Lock Record),然后以CAS的形式尝试将对象的markword更新为指向锁记录的指针,更新成功的线程就获得了该对象的锁。
3) 重量级锁
CAS本质上是程序在不停地循环运行,会占用CPU的资源,所以当线程之间竞争激烈的时候,轻量级锁升级为重量级锁。
重量级锁将竞争激烈的线程放入等待队列,由操作系统负责线程调度。放入等待队列的线程不占用CPU资源。
3.4 volitile
volatile关键字有两个作用:
1) 保证线程可见性
一个线程对主存的修改能及时地被其他线程观察到,这种特性被称为可见性。volatile可以保证线程可见性的原因是实现了缓存一致性协议。
2) 阻止指令重排序
CPU执行指令的顺序是由输入数据的可用性决定的,而不是由程序的原始数据决定的,这种技术被称为乱序执行。乱序执行在多线程下可能会出现问题。volatile可以阻止指令重排序的原因是使用了内存屏障。
3.5 自定义线程池
● 线程池的七个参数
1) int corePoolSize,核心线程池大小。
2) int maximumPoolSize,最大线程池大小。
3) long keepAliveTime,活跃时间。
4) TimeUnit unit,时间单位。
5) BlockingQueue<Runnable> workQueue,阻塞队列。
6) ThreadFactory threadFactory,线程工厂。
7) RejectedExecutionHandler handler,拒绝执行处理程序。
● 怎么理解这七个参数?
从前有一个银行营业厅。【线程池】
这个银行营业厅有五个办理业务的窗口。【最大线程池大小为5】
平时的顾客不多,只有两个窗口开放。【核心线程池大小为2】
营业厅内设有候客区,候客区有四个座位。【阻塞队列大小为4】
营业厅营业时:
先来了两位顾客,他们一来就直接到常开的两个窗口办理业务。
此时又有顾客到来,他们一看没有开放的窗口了,就坐在候客区的座位上等待。
当候客区的四个位置坐满时,还有新的顾客到来,营业厅赶紧开放其他窗口办理业务。
直到五个窗口全部开放,候客区的四个位置坐满时,营业厅不再放新的顾客进来。【拒绝执行处理程序】
业务繁忙的时段过去后,有个窗口有一个小时都没有办理过业务了,营业厅就把这个窗口关掉了。【活跃时间为1,时间单位为小时】
上文中唯一没有提到的线程工厂,是用来创建线程的。
● 这些参数一般怎么设置?
1) 核心线程池大小
根据经验,假如服务器的CPU个数为N,
对于CPU密集型的任务,将线程数设为N+1。
对于IO密集型的任务,将线程数设为2N。
对于计算和IO操作都比较多的任务,应考虑使用两个线程池,分别处理计算和IO操作。
对于计算和IO操作都比较多且不可拆分的任务,采用算式 num=N×(任务总耗时/计算耗时) 来计算。
2) 最大线程池大小
与核心线程池大小保持一致,减少任务处理过程中创建和销毁线程的开销。
3) 活跃时间、时间单位:
因为核心线程池和最大线程池大小保持一致,所以设多少都可以。
4) 阻塞队列大小
必须是有界队列。
5) 线程工厂
使用默认的Executors.defaultThreadFactory()就可以。
6) 拒绝策略
线程池的拒绝策略有四种:
AbortPolicy:中止策略,抛出异常。
CallerRunsPolicy:调用者运行策略。
DiscardPolicy:丢弃策略。
DiscardOldestPolicy:丢弃最旧任务策略。
一般使用默认的AbortPolicy就可以。
对于不容许任务失败的场景,使用CallerRunsPolicy。
对于无关紧要的任务,处理异常的收益很低,可以使用DiscardPolicy。
对于时效性比较强的任务,比如发布消息,可以使用DiscardOldestPolicy。
4 JVM
4.1 运行时数据区
运行时数据区包括:堆、方法区、虚拟机栈、本地方法栈、程序计数器。
1) 堆(Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
2) 方法区(Method Area)与堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
3) 虚拟机栈(Virtual Machine Stack):线程私有,描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法被调用直至执行完毕的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
4) 本地方法栈(Native Method Stacks):线程私有,与虚拟机栈的区别是,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机执行本地(Native)方法服务。
5) 程序计数器(Program Counter Register):线程私有,一块较小的内存空间,可以看作是当前现场所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
4.2 对象的创建过程
HotSpot虚拟机创建对象的过程:
1. 当Java虚拟机遇到一条字节码new指令时,首先会去检查待创建对象的类是否已被加载、解析和初始化过。如果没有,必须先执行相应的类加载过程。
2. 类加载检查通过后,虚拟机将为新生对象分配内存。
分配内存有“指针碰撞”和“空闲列表”两种方式,具体用哪种方式由Java堆内存是否规整决定,Java堆内存是否规整又由使用的垃圾收集器是否带有空间压缩整理的能力决定。
3. 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。
4. 接下来,Java虚拟机还要设置对象的头部信息。
5. 最后,执行构造函数,按照程序员的意愿对对象进行初始化。
4.3 对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充。
1) 对象头(Header):包括Mark Word和类型指针两部分。
Mark Word:对象自身的运行时数据,包括hashcode,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
*数组长度:如果对象是一个Java数组,在对象头中还必须有一块用于记录数组长度的数据。
2) 实例数据(Instance Data):对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容。
3) 对齐补充(Padding):仅仅起着占位符的作用,保证对象的起始地址必须是8字节的整数倍。
4.4 内存溢出和内存泄漏
内存溢出:内存不够用。
内存泄漏:内存无法释放。
4.5 垃圾回收
● 堆内存分配
虚拟机设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。占用空间大小 Young : Old = 1 : 2。
新生代又可以划分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor 和 To Survivor)。空间大小 Eden : From : To = 8 : 1 : 1。
● 可达性分析算法
通过可达性分析算法判定对象是否存活。这个算法的基本思路是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
在Java技术体系里面,固定可作为GC Roots的对象包括:
1) 在虚拟机栈中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2) 在方法区中类静态属性引用的对象,如Java类的引用类型静态变量。
3) 在方法区中常量引用的对象,如字符串常量池中的引用。
4) 在本地方法栈中Native方法引用的对象。
5) Java虚拟机内部的引用,如基本数据类型对于的Class对象、一些常驻的异常对象、还有系统类加载器。
6) 所有被同步锁持有的对象。
7) 等等。
● 垃圾回收算法
新生代:标记-复制算法。
老年代:标记-清除算法 或者 标记-整理算法。
1) 标记-复制算法
给对象分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。罕见情况下,另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便通过分配担保机制直接进入老年代。
不适用场景:对象的存活率较高时不适用。
2) 标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:空间碎片化严重。
3) 标记-整理算法
在标记完成后,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界(指针)之外的内存。
缺点:效率不高。
● 垃圾回收器
Serial - Serial Old:新生代采取标记-复制算法,老年代采取标记-整理算法。
Parallel Scavenge - Parallel Old:新生代采取标记-复制算法,老年代采取标记-整理算法。吞吐量优先。
ParNew - CMS:新生代采取标记-复制算法,老年代采取标记-清除算法。响应时间优先。
G1(单独使用):面向局部收集、基于Region,采取标记-复制算法。
4.6 类加载机制
● 类加载过程
Java虚拟机中类加载的全过程包括加载、验证、准备、解析和初始化五个阶段。
1. 加载阶段,Java虚拟机需要完成:
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2. 验证阶段,确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3. 准备阶段,正式为类中定义的变量分配内存并设置类变量初始值。
4. 解析阶段,Java虚拟机将常量池内的符号引用替换为直接引用的过程。
5. 初始化阶段,根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。
● 类加载器
Java虚拟机中的类加载器包括:
1) 启动类加载器(Bootstrap ClassLoader),负责将存放在<JAVA_HOME>\lib目录,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。
2) 扩展类加载器(Extension ClassLoader),负责加载<JAVA_HOME>\lib\ext目录中所有的类库。
3) 应用程序类加载器(Application ClassLoader),负责加载用户类路径(ClassPath)上所有的类库。
4) 自定义类加载器。
● 双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
4.7 Java内存模型
Java内存模型中定义的八个原子操作指令:
1) lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2) unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3) read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4) load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5) use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
6) assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7) store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
8) write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
5 MySQL
5.1 MySQL架构
MySQL架构主要分为四层,分别是网络连接层、服务层、存储引擎层、物理层。
1) 网络连接层:主要负责连接管理。MySQL服务器上维护了一个线程池(即数据库连接池),每个客户端对应服务器上的一个线程。
2) 服务层:MySQL的核心层,包括查询缓存、解析器、预处理器、查询优化器。
查询缓存:在进行查询之前,服务器会先检查查询缓存,如果能找到对应的查询,则直接返回缓存中的结果集。
解析器:根据查询语句构造出一个解析树。主要用于语法分析。
预处理器:进行语义分析。
查询优化器:将解析树转化为执行计划。一般情况下,一条查询可以有多种执行方式,最终返回相同的结果,优化器就是找到其中最优的执行计划。
3) 存储引擎层:负责数据的存储和提取。通过提供一系列接口来屏蔽不同引擎之间的差异。
4) 物理层:数据文件。
5.2 MySQL索引
● B+树索引:MySQL在大多数场景下使用的索引。
B+树的特点:
1) 非叶子结点不存储数据,只存储索引(索引是冗余数据,叶子结点包含了全部数据)。
2) 叶子结点之间用指针连接,形成链表。
● 为什么不用B树索引?
MySQL从磁盘中读取数据的方式是按页读取,磁盘页的默认大小是16KB。如果使用B树作为索引的数据结构,非叶子结点中既存储索引又存储数据,每个结点中能存储的索引数会减少,B树可能会很高;而且B树没有维护叶子节点之间的指针,不能进行范围查找。
● Hash索引
数组+链表,对索引的key进行一次hash计算就可以定位出数据存储的位置,适用于等值查询,且比B+树索引更高效,但不能进行范围查询。
5.3 数据库事务
● 数据库事务的ACID特性
1) A:Atomicity,原子性。
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
2) C:Consistency,一致性。
事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
3) I:Isolation,隔离性。
隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。
4) D:Durability,持久性。
在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
● 事务的隔离级别
1) 读未提交(READ UNCOMMITTED):不加锁,任何事务对数据的修改都会第一时间暴露给其它事务。可能会发生脏读、不可重复读、幻读。
2) 读已提交(READ COMMITTED):一个事务只能读到其它事务已经提交过的数据。不会发生脏读,可能会发生不可重复读、幻读。
3) 可重复读(REPEATABLE READ):事务不会读到其它事务对已有数据的修改,即使其它事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。不会发生脏读、不可重复读,可能会发生幻读。(MySQL的可重复读级别解决了幻读问题)
4) 串行化(SERIALIZABLE):将事务的执行变为顺序执行,后一个事务执行必须等待前一个事务结束。不会发生脏读、不可重复读、幻读。
● 脏读、不可重复读、幻读
1) 脏读:读未提交隔离等级下,A事务(未提交)修改一条数据后,B事务能直接读取到修改后的数据,若A事务发生回滚,B事务就读到了脏数据。
2) 不可重复读:读已提交和读未提交隔离等级下,B事务读取一条数据后,A事务(已提交)修改了这条数据,B事务再次读这条数据时,读到了修改后的数据——B事务两次读取数据的结果不一致。
3) 幻读:可重复读、读已提交、读未提交隔离等级下,A事务(已提交)插入了一条新数据,B事务在A事务提交前后读到表中的数据总数不一样。
6 Redis
6.1 Redis数据类型
Redis的基本数据格式是键值对:key-value。其中value的五大数据类型:string、hash、list、set、zset。
1) string:字符串。
Redis中所有的key都是string类型的数据。string类型是二进制安全的,意思是Redis的string可以包含任何数据,比如jpg图片或者序列化的对象。
2) hash:哈希。
hash是一个键值对集合,适合用于存储对象。
3) list:列表。
list的底层实际上是链表。
4) set:set集合。
set是string类型的无序集合,不允许重复的成员,通过HashTable实现。
5) zset:sorted set,有序集合。
zset和set一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数,Redis通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数可以重复。
6.2 Redis持久化策略
Redis提供了两种持久化策略:RDB和AOF。
1) RDB,Redis DataBase。
在指定的时间间隔下,将内存中的数据集快照(Snapshot快照)写入磁盘。恢复数据时,将快照文件直接读到内存中。
2) AOF,Append Only File。
以日志的形式,将Redis执行过的所有写指令记录下来,只允许追加新文件但不可以对已保存的文件进行修改。恢复数据时,Redis会读取日志文件重新构建数据。
7 分布式
7.1 CAP定理
一个分布式系统不可能同时满足一致性[Consistency],可用性[Availability],和分区容错性[Partition tolerance]这三个基本要求,最多只能同时满足其中的两项。
7.2 BASE理论
BASE是基本可用[Basically Available]、软状态[Soft state]、最终一致[Eventually consistent]三个术语的缩写。
BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
7.3 分布式事务的最终一致性
Zookeeper通过ZAB协议(Zookeeper Atomic Broadcast,Zookeeper原子广播)来保证分布式事务的最终一致性。
ZAB协议的内容是:
1) 所有的事务性请求必须由一台全局唯一的服务器来协调处理,这台服务器被称为leader服务器,其他服务器称为它的follower服务器。
2) leader服务器负责将客户端发送过来的事务请求转换成事务,并分发给集群中所有的follower服务器。
3) 每台follower服务器收到事务后会给leader服务器返回一个ack请求,当leader收到超过半数的follower的ack请求后,leader会再次向所有的follower服务器发送commit消息,要求提交事务。