2024面试知识点整理
文章目录
一、JVM
1.类加载机制
Java在运行之前,需要将类文件转换成对应字节码加载到运行时数据区中。
1.1.类加载过程
加载过程:加载、链接、初始化。
其中链接步骤又有三个子步骤,所以完整的类加载过程为:加载、验证、准备、解析、初始化。
加载:先通过类的完全限定名获取到类的二进制字节流,然后将这个二进制字节流转换成方法区能够识别的数据结果放入到方法区,同时在堆中生成一个Class对象,作为类访问的入口。
验证:验证字节流是否会对虚拟机有危害。
准备:给类对象分配内存空间。
解析:将符号引用转换为直接引用,就是将文本转换为内存中对象的指针地址值。
初始化:为静态变量赋值,并执行静态代码块。
1.2.类加载器
类加载器的作用是将类加载到运行时数据区。
类加载器的分类和作用:
引导类加载器:加载Java的核心类库,jre/lib/rt.jar
扩展类加载器:加载Java的扩展类库,jre/lib/*.jar
应用类加载:加载ClassPath下的jar包,如三方类库
自定义类加载器:实现某些特殊功能的类加载器
1.3.双亲委派模型
双亲委派模型指定了不同类加载器的加载优先级,用于避免API被篡改,保证类加载的安全。
加载的优先级为:引导类加载器、扩展类加载器、应用类加载器、自定义类加载器。
如何破坏双亲委派模型?
重写loadClass方法可以破坏。
2.运行时数据区
运行时数据区主要分为线程私有和线程共享的两大块:
线程私有:虚拟机栈、本地方法栈、程序计数器
线程共享:堆、方法区
方法区在JDK7中叫永久代,JDK8以后叫元空间。
2.1.栈帧的组成
在程序执行方法时,会将栈帧压入到栈中,方法执行完毕后,栈帧会从栈中弹出。可以说,Java程序的运行过程是由一次次的压栈和弹栈组成的。那么栈帧中有些什么东西呢?
栈帧主要是由四个部分组成,局部变量表、操作数栈、动态链接、方法返回地址。
各部分的作用:
局部变量表:用于保存方法中的局部变量值,表现为基础数据类型为变量值,引用数据类型为对象的引用地址值。
操作数栈:用于对局部变量表中的数据做计算。
动态链接:是用于在运行时,将变量与实际的对象链接起来,如多态,泛型。
方法返回地址:保存上一个栈帧运行到的位置,当前栈帧弹栈后要继续执行前一个栈帧没有执行完的指令码。
2.2.堆的组成
堆的组成有分代模型和分区模型,G1使用的是分区模型,在之前的垃圾回收器使用的都是分代模型。
分代模型:分代模型是在物理上对内存做划分,主要分为新生代和老年代,其中新生代又分为 Eden区和 Survivor区, Survivor区被划分为相等的两个部分 S0/S1。
分区模型:分区模型没有在物理上划分内存,整个内存就是一整块。但是对这一整块内存做了逻辑分区,也包括 Eden、Survivor、老年代、大对象的区分。
新创建的对象在哪里分配?
实例化一个新的对象,大多数情况都是在 Eden区分配的,在 Eden的内存达到阈值的时候,就会触发垃圾回收。这次回收会回收Eden区和其中一个Survivor(假如是s0)区的对象,并将存活的对象复制到另一个Survivor(s1)区,然后清空s0。下一次回收的时候,又会将对象复制到s0,然后清空s1。
大对象会直接分配到老年代中(具体多大的对象可以通过JVM参数配置)。
为什么大对象要直接进入老年代?
因为Survivor区使用的是复制算法,在两个s区中不断的复制对象会造成性能的浪费。
3.垃圾回收
3.1.如何确认一个对象是垃圾
Java中使用的是可达性分析,主要是使用三色标记算法来确认的垃圾对象。
3.1.1.GC Roots
所谓的GC Roots,就是本次垃圾回收中一定不会被标记成垃圾的对象,例如:局部变量表引用的对象、静态变量或常量、线程对象、类加载器等。
3.1.2.三色标记算法
三色标记算法将对象划分为三种颜色:黑色、灰色、白色。
标记的流程使用了的广度优先的遍历:
- 首先将GC Roots标记成黑色,然后去寻找GC Roots引用的对象并将其标记为灰色。
- 迭代灰色的节点,去寻找灰色节点引用的对象将其标记为灰色,等标记完成之后,就将当前迭代的灰色节点标记成黑色。
- 在标记完成之后,剩下的白色节点的对象就是垃圾对象。
3.1.3.三色标记算法的问题
不管是CMS还是G1,在并发标记阶段,GC线程和用户线程是并发执行的,会有漏标和错标的问题。
错标问题
错标问题会产生浮动垃圾。就是GC线程已经被标记为黑色或灰色节点,被断开了引用。这样的节点对象实际上就是垃圾对象,但是因为已经不是白色的节点了,本次回收就不会被回收掉。错标问题还不算严重,严重的是漏标问题。
漏标问题
漏标问题就是有一部分不应该是垃圾的对象,因为没有被标记为黑色或灰色,被当成了垃圾处理。这会使程序运行出现异常。
它产生的原因是已经标记为黑色的节点对象,在用户线程运行时,又引用了白色的对象,这个白色的对象可能会被当作垃圾回收掉。
漏标问题的解决
CMS采用增量更新的方式,对于引用白色节点的节点,重新由黑色标记为灰色。在重新标记阶段重新扫描灰色节点,对其引用的节点做标记。
G1的方式是将黑色节点对白色节点的引用放入到栈中,在重新标记阶段可以直接扫描到白色节点。
3.2.垃圾回收算法
垃圾回收算法有三种:复制算法、标记清除算法、标记整理算法。
- 复制算法:需要两块一样大小的空间,每次都将存活的对象复制到另一个空间中,并将本空间的对象清理掉。这种算法不会产生碎片,但是浪费的一半的内存。
- 标记清除算法:先对非垃圾对象进行标记,然后清理垃圾对象,这种算法会产生内存碎片。
- 标记整理算法:先对非垃圾对象进行标记,将非垃圾对象移动到内存的一段,然后将未标记的垃圾清理掉。
3.3.垃圾回收器
3.3.1.基本概念
- 并行与并发:并行指的是GC线程并行执行,并发指的是GC线程与用户线程一起执行。
- 停顿时间:垃圾回收阶段用户线程暂停的时间。
- 吞吐量:用户线程运行时间占用所有时间的比例。
- STW:Stop Thre World,垃圾回收时,暂停用户线程,就像世界静止了一样。
- Full GC:Full GC是新生代GC(YGC)和老年代GC(Old GC)的组合,一般执行老年代GC的时候都会触发新生代的GC,也就叫做触发了Full GC。
3.3.2.垃圾回收器分类
分代维度:
根据分代维度进行分类,分为新生代垃圾回收器和老年代垃圾回收器。
新生代的垃圾回收器有:Serial、Parnew、Parallel Scavenge(PS)。
老年代的垃圾回收器有:Serial Old、Parallel Old(PO)、CMS。
除此之外,还有分区模型的垃圾回收器G1,既可以做新生代垃圾回收器,又可以做老年代垃圾回收器。
多线程维度:
可以将垃圾回收器划分为:单线程收集器、并行收集器、并发收集器。
单线程收集器是指的垃圾回收阶段,只有一个GC线程在回收垃圾,如:Serial、Serial Old。
并行收集器就是在GC阶段由多个线程一起回收垃圾,如:ParNew、Parallel Old。
并发收集器是指,GC阶段有部分操作是GC线程与用户线程并发执行的,如:Parallel Scavenge、CMS、G1。
3.3.3.CMS存在的问题
问题:
CMS无法整理垃圾碎片,也无法处理标记阶段后产生的浮动垃圾。如果碎片和浮动垃圾超过阈值,会使用Serial Old进行碎片整理,此时会STW,停顿时间较长。
如何解决?
可以使用定时脚本在夜深人静的时候,触发CMS的碎片整理,避免在使用高峰期的时候进行整理。
3.3.4.CMS与G1的区别
第一个是内存模型不同,CMS还是使用的分代模型,G1使用的是分区模型。
第二个是垃圾回收的回收阶段,CMS是并发回收的,而G1会STW进行并行回收。
3.3.5.垃圾回收器的选择
- 内存小于100M,或者单核处理器,直接串行收集器。
- 关注吞吐量,用于跑后台任务,使用并行收集器。
- 关注停顿时间的互联网项目,且内存充足的情况下,可以使用并发收集器,CMS或G1。
- 其他情况可以使用PSPO。
4.JVM调优
JVM调优主要是对Java程序运行中的各种问题进行调优,例如运行OOM,运行卡顿等。
可以采用不同的垃圾回收器,并调整JVM参数,使程序运行处于一个稳定流畅的状态中。
4.1.调优的类型
4.1.1.OOM的类型
OOM的类型:
主要分为三种:栈溢出、堆溢出、元空间溢出。
-
其中栈溢出主要是 StackOverflowError,就是往栈里面压入了过多的栈帧,超过了栈的深度。一般是由于递归深度过深导致的。
-
堆溢出的情况就比较多了,常见的有以下几种:
- Jave Heap Space:堆内存溢出,就是分配对象的内存大于了堆内存。
- GC Overhead limit exceeded:GC时间的占比很大,但是每次GC回收的垃圾却很少。
- unable to create new native thread:服务器的线程资源耗尽了。
-
元空间溢出就是Java的类元信息超出了内存限制。
内存泄露:
内存泄露也是堆溢出的一种情况,有区别于正常使用时内存不足导致的OOM,内存泄露是不应该存在的。
所谓的内存泄露,就是在代码中存在让GC无法正常回收对象的逻辑,导致某些已经失效的对象无法被回收掉,随着这种对象越来越多,正常对象能够使用的内存空间就越来越小,最终导致OOM。
对于内存泄露,怎么加大对内存都是没有用的,只有排查代码,将存在内存泄露的代码修改掉才能解决。
例如ThreadLocal使用不当,就会很容易造成内存泄露。
4.1.2.OOM的排查
OOM的排查需要使用到Dump日志信息,所以我们在启动服务时就需要使用-XX:+HeapDumpOnOutOfMemoryError
开启Dump日志。
开启了之后,一旦发生OOM就会生成Dump日志,我们可以使用Dump日志分析工具进行分析,例如Eclipse的MAT。
然后根据Dump日志的信息,查看到哪行代码出现了问题,再去代码中定位问题,修改问题。
4.1.3.内存溢出的处理
-
分配大文件导致溢出
如果文件大小是符合业务要求的,那这种情况一般就是堆太小了,可以通过-Xmx和-Xms来调整堆的大小。
-
大并发下存活对象过多
这种情况下存活的对象都是合理的,除了增大堆内存之外,对于并发来讲,更好的方式是做多台机器的集群,分担压力。
-
内存泄露引起的
这种情况就是排查引起内存泄露的代码,然后修改掉。
4.1.4.CPU占用过高排查
一般这种情况是因为代码当中有死循环导致的,排查的思路是先找到是哪一个进程中的哪一个线程占用CPU过高,获取到线程号之后,使用jstack导出栈信息,用对应线程号找到栈信息中的对应线程日志,获取到代码行数,然后排查代码。
二、多线程
1.基础知识
进程与线程的区别:
进程是CPU资源分配的最小单位,线程是轻量级进程,是CPU调度的最小单位,同一个进程中的所有的线程都共享这个进程中的资源。
并行与并发的区别:
并行是指的实际意义上的同时运行,并发指的是给不同的线程分配执行时间,CPU在各个线程中切换,但是因为切换的很快,看起来就像是在同时运行一样。
并发编程的优缺点:
优点:提高CPU资源的使用率,也就是压榨CPU的性能。
缺点:线程间的切换会带来额外的资源消耗,同时也会带来线程安全问题。
上下文切换:
先得知道什么叫上下文,上下文就是指的线程运行时依赖的环境,例如寄存器,程序计数器等。
上下文切换就是指的,在CPU时间片切出时,需要保存当前线程运行的状态和环境变量,再下一次切入时又重新加载回来。
上下文切换会带来额外的消耗,我们可以通过减少创建线程的数量,在保证线程安全的情况下尽可能的不加锁等方式减少上下文切换。
内核态和用户态:
线程在操作系统中的两种状态,出于对操作系统安全的考虑,对操作系统硬件相关的操作都是由内核态的线程来处理的,其他的用户相关的指令则是有用户态的线程来处理。
线程的创建、销毁、挂起、唤醒等操作都会导致状态的切换,会消耗额外的性能,所以我们可以在实际开发中尽可能的减少这样的操作,例如在某种程度上可以使用自旋来代替重量级锁。
使用场景:
异步:可以将一个流程中不那么重要的子流程通过异步来处理,避免阻塞主流程。
并行:对于一个耗时的操作,可以拆分成多个自操作,然后并行的去执行,减少完成操作的时间。
2.Java中的线程
2.1.线程的创建方式
线程类的定义方式有有两种,继承Thread类或者实现Runnable接口。而线程对象的创建方式只有一种,就是new Thread();
除此之外,不管是Callable还是使用线程池创建的线程,本质上还是实现的Runnable接口。
2.2.run和start的区别
run方法就是java中的一个普通的方法,直接使用对象进行调用即可。
而start方法是一个native方法,表示启动一个线程,Java中本身是没有线程的,它使用的是操作系统的线程。start方法就是在通知操作系统创建一个线程,然后等待CPU调度线程,CPU给线程分配了时间片后,就会回调JVM中的这个线程对象的run方法,执行需要执行的异步逻辑。
2.3.如何中断一个线程
有三种方式可以中断线程:
- stop方法:暴力中断线程的方式,相当于直接杀死线程,这种方式可能会导致数据的不一致。
- 使用中止标识:可以使用一个加了volatile修饰变量作为中止标识,因为这种方式不管是哪个线程对中止标识做了修改,对其他线程都是立即可见的。
- 使用interrupt方法:interrupt方法对于线程的中断效果与中止标识类似,但是它还可以用于打破线程的阻塞状态。
2.4.线程的生命周期
线程的生命周期就是线程从创建到终止的过程,分为NEW, RUNNABLE, WAITING, TIMED_WAITING, BLOCKED, TERMINATED。
- 使用new Thread();创建线程对象的时候,线程处于NEW状态。
- 调用start()方法后,线程处于RUNNABLE状态。
- 使用wait(),LockSuppot.park()等方法时,线程进入到WAITING状态,使用notify或LockSupport.unLock()时又会回到Runnable状态。
- 使用带时间的等待方法,例如sleep(time), wait(time), partUtil(time)等,线程会进入到TIMED_WAITING状态。
- 线程进入synchronized的临界区,但是没有抢到锁时,就会进入BLOCKED状态。
- 线程的run方法运行完毕时,就是TERMINATED状态。
3.线程安全问题
线程安全问题指的是并发条件下,由于多个线程同时对共享资源进行了操作,共享资源的最终结果与预期值不一致的情况就是线程安全问题。
3.1.并发编程的三要素
三个要素有任意一个不满足,都有可能导致线程安全问题。
- 原子性:表示操作不可分割,在同一个临界区中的操作要么全成功,要么全失败。
- 可见性:一个线程对共享资源进行了修改,修改值对其他的线程立即可见。
- 有序性:程序按照代码编写的顺序执行。
对于可见性的理解:
CPU对变量的操作不是直接在内存中运行的,而是先将变量保存到CPU的高速缓存中,然后执行操作。在执行完毕后的某个时间内,会重新刷新到内存中。
所以一个线程修改了共享变量之后,还没有刷新到内存中,其他线程从内存中取到的还是旧的数据。或者其他线程已经将旧的数据缓存到了自己的高速缓存中了,缓存未失效不会重新去内存中取数据。这两种情况都是线程对变量的修改对另外的线程不可见。
而我们通过像加volatile修饰这样的方式,可让线程对共享变量修改后马上刷新到内存中,而其他线程要使用这个变量值的时候,会重新去内存中获取。
对于有序性的理解:
我们编写的代码需要编译后才能执行,在编译时就可能会存在指令重排序优化,这种优化可能会导致程序不是按代码编写的顺序执行的,而是选择一个更高效率的执行方式。这样的优化在单线程中是没有问题的,因为最终结果一致,但是在多线程环境下就有可能会出问题。
3.2.如何保证线程安全
保证并发编程的三要素,就可以保证线程安全问题。
原子性:
加锁,使用synchronized或者Lock的方式,让多个操作组合成一个原子操作,同一时间只有一个线程可以进入到临界区中。
可见性:
- 加锁形成的临界区中,线程对共享资源在操作前会从内存中拉取,操作后也会重新同步到内存中。
- 使用volatile修饰的变量也是同样的效果。
- 除了上面两点之外,Java中还有Happens-Before原则,遵守这个原则的操作都是具备可见性的。
有序性:
使用volatile来修改变量,通过编译器的内存屏障来禁止指令重排序优化。
3.3.绝对线程安全的代码
如果共享变量是只读的,例如使用final修饰的变量,自然就没有线程安全问题。
除此之外,一个方法中如果只对局部变量进行修改,且不存在线程逃逸的情况,也是线程安全的。
什么叫线程逃逸?
指的是线程将局部变量对于对象的指针,赋值给了可以被多个线程共享的成员变量。
4.Happens-Before规则
编译器提供给开发者使用的能够保证线程的可见性的规则,它一共包含以下几种规则。
- 程序顺序规则:指编译器会禁止对程序结果有影响的指令进行重排序。
- 监视器规则:指一个线程的解锁操作对于后续其他线程的加锁操作是可见的。
- volatile规则:volatile会触发缓存一致性协议,使一个线程对共享变量的写操作对另一个线程可见。
- start规则:一个线程对共享变量的操作,对另一个刚启动的线程可见。
- 传递性规则:就是Happens-Before具有传递性,A happens-before B ,B Happens-before C, 则A happens-before C。
5.synchronized
使用方式:
修饰代码块,修改实例方法,修饰静态方法。
锁的是什么?
锁的是Java对象,JVM为每个Java对象都分配了一个monitor对象,所以每个Java对象都可以作为锁对象。
5.1.实现原理
所有互斥锁的实现都是模拟的多个线程对一个共享资源的竞争,并且同时只能有一个线程可以获取到锁资源。对于synchronized来说,他的共享资源就是monitor对象。
在monitor对象中有几个重要的属性:
- owner:就是当前持有锁的线程。
- recursions:重入次数。
- entryList:阻塞队列,未抢到锁的现场就会在这个队列中阻塞。
- waitSet:使用wait进入等待状态的现场,会在这个队列中阻塞。
这几个属性共同实现了synchronized的重量级锁。
5.2.偏向锁与轻量级锁
偏向锁:
偏向锁严格来说并不是锁,只是在对象头里面记录了一下偏向线程的ID。偏向锁在没有线程竞争的情况下,性能很高,因为没有加锁的消耗。但是只要存在线程竞争,则会升级成轻量级锁。
偏向锁升级的过程,会等待全局安全点,会导致系统STW更加频繁,所以一般都是禁用偏向锁的。
轻量级锁:
轻量级锁是使用的CAS+自旋的方式来代替阻塞,可以有效的减少线程的上下文切换,但是在线程竞争十分激烈或单个线程持有锁的时间过长的时候,自旋的方式会导致CPU持续的空转。
针对这种情况,轻量级锁在存在3个或3个以上的现场竞争的时候,以及自选次数达到阈值的时候会升级成重量级锁。
注:锁一旦升级就是不可逆的。
5.3.死锁问题
造成死锁的原因是什么?
出现死锁是因为,锁的互斥的,同一时间只能有一个线程持有锁,并且锁不能被抢占,同时又与另一个线程形成了循环等待,互相等待对方先释放锁。
如何避免死锁?
要避免死锁,就不能让线程处于循环等待的状态,在加锁时就需要考虑按照同样的顺序进行加锁。
已经死锁了怎么处理?
需要做死锁诊断,使用JPS
查询出现死锁的Java进程号,再使用jstack 进程号
打印栈信息,可以找到出现死锁的代码位置,去修改代码逻辑并重新部署启动。
6.volatile
6.1.volatile的作用
用于保证被修饰变量的可见性和有序性。
6.2.实现原理
可见性的保证:
通过缓存一致性协议,使线程对变量在操作之前,先从内存同步最新的变量值到CPU的高速缓存中,并且在对变量操作之后,立即将修改后的变量值同步到内存中去。
有序性的保存:
通过内存屏障,禁用CPU对指令码的重排序优化。
7.ThreadLocal
Threadlocal主要的作用是实现线程间变量的隔离,并且可以实现不通过方法形参进行参数传递。
7.1.如何实现的线程隔离
ThreadLocal里面有一个静态内部类:ThreadLocalMap,这个内部类被定义成了Thread的成员变量,也就是说,每一个线程对象里面都有一个线程私有的ThreadLocalMap对象。
在使用的时候,把ThreadLocal对象作为key,需要保存的数据作为value,存入到线程私有的ThreadLocalMap中,以此来实现线程隔离。
7.2.内存泄露问题
其实ThreadLocal已经做了一部分的处理,在ThreadLocalMap中的key是弱引用的对象,弱引用的对象在没有其他强引用的情况下,下一次GC就会被回收掉。
但是value是强引用对象,是不会被回收的,所以需要我们在使用完ThreadLocal之后,主动的调用remove()方法发起清理,将ThreadLocalMap中key为null的垃圾对象全部清理掉。
7.3.父子线程数据共享
什么是父子线程?
在一个线程中调用new Thread()方法创建一个线程,这个新创建的线程就是子线程。
数据共享如何实现?
Thread类中除了ThreadLocalMap还有一个InheritableThreadLocalMap,在创建子线程的时候,会将父线程的这个Map中的数据,复制一份放到子线程的Map中。
线程池导致共享数据失效
父子线程共享数据的前提是创建新线程的过程中,将父线程InheritableThreadLocalMap中的数据复制到子线程中,而使用线程池的话,存在线程的复用,就不会重新创建线程,这种情况的话,是不能共享数据的。
8.CAS算法
CAS全称Compare And Swap,就是比较并替换的意思,通常情况下也被称为乐观锁。
8.1.CAS的实现原理
CAS使用到了三个值,分别为:需要更新的值update,预期值expect,以及内存中当前值的偏移量offset。
实现流程:
我们在更新一个变量值之前,先去内存中获取到历史数据作为预期值,然后做更新计算,最后再把更新值同步到内存中。
这个同步的过程需要比较 预期值与内存中的 当前值是否一致,如果一致,才能更新成功,如果不一致就不能更新成功。
注:CAS一般配合循环进行使用,这种使用方式叫做自旋锁。
8.2.CAS存在的问题
主要是两点:
- CAS只能保证单个变量的原子性,多个操作的原子性还是只能加锁。
- ABA问题,就是一个线程A在修改数据时,另一个线程B将数据先改了,然后又改回来了,线程A是发现不了的。但是ABA问题一般都不会有什么影响,不需要专门去解决。
8.3.乐观锁与悲观锁
- 悲观锁认为,当前的操作一定会存在线程的竞争,所以在操作之前就加锁,阻塞其他线程进入。
- 乐观锁认为,当前的操作只有很小的可能会存在竞争,所以在操作之前不加锁,而是在操作完成之后再去验证共享资源有没有被其他线程修改。
9.线程池
9.1.线程池作用
- 可以实现线程的复用,减少创建和销毁线程的开销。
- 线程池也开限制创建线程的数量,避免无限制的创建线程。
- 特殊的线程池还有特殊的功能,例如任务调度的线程池可以实现定时任务。
9.2.线程池的种类
这里的线程池的种类是指的通过Executors
工具类创建出的线程池,有以下几种:
- newFixedThreadPool:定长的线程池,可以配置固定的线程个数。
- newCachedThreadPool:不定长,每多一个任务就会创建一个线程。
- newScheduledThreadPool:任务调度的线程池,有定时任务的功能。
- newSingledThreadPool:单例线程池,一个线程池中只会有一个线程。
9.3.线程池的参数
除了使用Executos
工具类之外,更加推荐使用自定义线程池的方式,使用这种方式就得知道线程池的参数有哪些:
- corePoolSize:核心线程数量,线程池每接收一个请求就会创建一个核心线程去执行任务,直到达到核心线程数的数量上限。
- workQueue:工作队列,可以选择一种阻塞队列作为参数,作用是核心线程数都在工作的时候,如果还有更多的请求进入线程池,则先进入到工作队列等待。
- maximumPoolSize:最大线程数,在有界工作队列满了的时候,可以创建非核心线程,用于分担核心线程执行任务的压力。非核心线程数=最大线程数 - 核心线程数。
- keepAliveTime:线程的存活时间,非核心线程的空闲时间超过了这个时间,就会被回收,如果配置了
allowCoreThreadTimeOut=true
则核心线程也会被回收。 - unit:过期时间的单位
- threadFactory:创建线程的工厂,一般是用来定义创建线程的线程名,在程序出现异常时方便通过线程名定位问题。
- handler:拒绝策略的处理器,当最大线程和工作队列都满了的时候,就会执行拒绝策略,例如:直接抛弃、抛弃最先进入队列的请求、直接使用当前线程执行、或抛异常等。
9.4.线程池的实现原理
线程池就是使用工作线程worker代替直接new Thread(),当有异步请求申请线程池执行的时候,会将Runnble
或Callable
对象放入到阻塞队列中,工作线程就循环从阻塞队列中去取,然后调用他们的run()方法执行逻辑。
9.5.线程池的执行流程
9.6.线程池大小配置
分为CPU密集型和IO密集型:
- CPU密集型的线程池,配置核心线程数为CPU的核心数。
- IO密集型的线程池,可以配置为2倍CPU的核心数。
当然,这只是一个参考值,实际的线程数量可以先配置成这个参考值,然后通过一些测试的手段,例如压力测试,观察任务的运行情况,负载等。不断的调整线程数,使他区域一个合理的值。
三、AQS
AQS是Java中的一个并发编程框架,实现了一些在并发编程中使用的功能。
它主要是由一个状态字段state
和一个CLH
队列组成的。
AQS实现了哪些功能
Lock、semaphore、countdownLatch、cyclicBarrier、线程池等。
1.ReentrantLock
AQS实现的可重入互斥锁
1.1.与synchronized的区别
区别点 | synchronized | ReentrantLock |
---|---|---|
实现方式 | 通过monitor监视器实现 | 通过AQS实现 |
锁类型 | 只支持非公平锁 | 支持公平锁与非公平锁 |
是否可中断 | 不可中断 | 使用lockInterruptly方法允许中断 |
线程通信 | 使用wait/notify | 使用Condition进行条件控制 Condition可以有多个实现更加灵活。 |
释放锁 | 自动释放 | 需要手动释放 |
此外,在性能方面,JDK1.6之前,synchronized的性能比ReentrantLock更差,但是1.6之后加入了轻量级锁优化,两者的性能已经相差无几了。
所以在两者的选择上,按照业务需要的功能自由选择即可。
1.2.加/解锁流程
加锁流程:
- 先尝试使用CAS更新修改state的状态,此时如果没有其他线程持有锁,则会替换成功。
- 如果替换失败,进入tryAcquire(),再次尝试抢锁,如果失败,则判断当前持有锁的线程是不是自己,如果是自己,则持有锁,并且重入次数加1。
- 如果tryAcquire()失败,进入addWaiter(),将当前线程封装在Node节点中,加入到队列尾。
- 然后会执行一次acquireQueued(),这个方法里面有一个无限循环,每次循环都判断当前的节点是不是头节点的下一个节点,如果是则尝试抢锁。
- 如果不是,或者抢锁失败,则会尝试将自己挂起。
解锁流程:
- 每次调用unlock的时候,就会是重入次数减1。
- 重入次数减为0后,就会发起解锁操作,将唤醒阻塞队列头节点的下一个节点。
- 被唤醒的节点会在acquireQueued的循环中再次尝试抢锁。
1.3.公平锁与非公平锁
公平锁会按照线程尝试抢锁的顺序,依次让这些线程抢占锁,而非公平锁不一定。
在ReentrantLock中,两者的区别就是在于,非公平锁在调用lock()的时候,会先去尝试抢一次锁,而公平锁会直接进入到等待队列的尾部。
1.4.Condition
作用:
Condition是用来做线程间的通信的,类似于synchronized中的wait和notify。
当线程执行任务到某个节点,需要通知其他线程执行任务的时候,就可以使用condition。
使用限制:
condition必须依赖于Lock使用。
实现原理:
- 执行condition.await()的时候,会释放锁资源,也就是直接将state置为0。在释放之前会将state的值保存起来,下次被唤醒时就可以恢复state的值。
- 使用Condition等待的线程会进入到Condition的等待队列中挂起,等待condition.signal()执行或,从队列头取出一个线程节点,加入到Lock的等待队列中。
2.Callable
2.1.Callable与Runnable的区别
下面是Callable相对于Runnable多出的一些特点。
- Callbale可以有返回值,也可以抛出异常。
- Callable需要配合FutureTask进行使用。
- Callable可以使用get()方法,阻塞主线程等待子线程执行完毕。
2.2.Callable的实现原理
2.2.1.线程调度如何实现
Callable是通过FutureTask来实现线程调度的,FutureTask是一个实现了Runnable的线程类,也就是说FutureTask可以通过start来启动线程,线程获取到CPU资源后就会回调run()方法,然后在run()方法中调用Callable的call方法即可。
2.2.2.返回值的实现原理
在FutureTask中定义了一个outcome的变量,不管是返回值还是异常,都会赋值给这个变量,然后使用get()方法就可以获取到返回值。
但是,只是这么实现的话,还存在一个问题,就是父线程调用get()方法时,子线程还没有执行完毕。所以在调用get()方法时,会先阻塞父线程,等待子线程执行完毕之后再唤醒父线程。
父线程的阻塞是如何实现的?
父线程的阻塞类似于AQS的等待队列,调用get()方法时就进入到等待队列中将自己挂起,子线程在执行完run()方法后,会将队列中的父线程唤醒。
3.栅栏与回环屏障
栅栏(CountdownLatch)和回环屏障(CyclicBarrier)都可以看作是一个有阻塞功能的计数器,可以通过这种阻塞的功能,让某个或某些线程等待其他线程执行完毕之后,再继续执行。
3.1.CountdowLatch的实现原理
使用AQS的共享模式实现:
- 实例化时给state初始值。
- 需要阻塞的线程调用await()进入等锁队列中等待。
- 其他线程每调用一次countDow(),state的值-1,减到0后唤醒等锁队列中的线程。s
3.2.CyclicBarrier的实现原理
使用ReentrantLock和Condition实现:
- 使用一个成员变量count作为计数器,另一个成员变量parties记录初始化值。
- 线程需要阻塞时,调用await()方法,会使count的值-1,如果此时count不为0,则会在Condition的等待队列中挂起。
- 当count减到0后,则会唤醒等待队列中的所有线程。
CyclicBarrier是可以重复使用的,在使用完之后,调用reset方法,就是根据parties的值重置count计数器值。
3.3.两者的区别
区别点 | CountDownLatch | CyclicBarrier |
---|---|---|
使用方式 | 常用于主线程等待子线程完成任务时,主线程阻塞。 | 常用于各个线程互相等待到某一个节点时,再一同执行。 |
阻塞原理 | AQS的共享模式。 | ReentrantLock和Condition实现。 |
能否复用 | 不能 | 能 |
4.信号量
4.1.作用与使用方式
作用:
信号量(Semaphore)的作用是限流,限制同一时间线程访问共享资源的数量。
使用方式:
- 先实例化Semaphore对象,传入需要限制访问的访问许可数量。
- 使用acquire()方法可以获取访问许可,访问许可不足就会阻塞等待。
- 线程执行完成之后,需要使用release()方法释放许可,释放之后访问许可就可以让其他线程获取到了。
4.2.实现原理
Semaphore是使用AQS共享模式实现,通过初始化state的值来作为限流的访问许可。
- 实例化时,传入访问许可的数量,用于初始化state。
- 使用acquire()获取到许可的线程就可以继续执行,并且使访问许可数量(state)-1,如果访问许可数量为0了,后续的线程就会进入队列中等待。
- 使用release()方法,state + 1,并唤醒线程。
四、MySQL
1.MySQL的架构
MySQL的整体架构可以分为三层:连接层、服务层、存储引擎。
其中服务层又是由4个部分组成,分别为:查询缓存、解析器、优化器、执行器。
1.1.连接层
连接层就是用来和客户端建立连接,在建立连接时会验证客户端的访问权限,每创建一个连接服务端就会多一个监听线程,MySQL5.7默认最大连接是151个,自定义最大可以修改为10万个,每个连接的超时时间默认是8小时。
1.2.服务层
查询缓存:
如果两次查询的语句一模一样,就可以从查询缓存中直接获取数据。
但是查询缓存的触发非常严格,多个标点,多个空格都会导致无法触发缓存。除此之外,一张表发生了任何的更新操作,都会使这张表的所有查询缓存失效。
所以,查询缓存实默认关闭的,并且在MySQL8.0中,已经移除了查询缓存。
解析器:
解析器是用来判断一个sql是否合法,包括词法分析和语法分析。
第一步:词法分析,就是将一个sql语句打碎成一个一个的单词,提出字段名,查询条件等。
第二步:语法分析,判断sql语句的语法是否正确。
预处理器:
第三步:预处理,预处理器对语义进行解析,例如表名错误,别名错误等。
上面三步执行完之后,会获取一个解析树,交给优化器使用。
优化器:
优化器是对sql的执行进行优化,选择一种它认为成本最小的的执行路径去执行SQL。例如:选择走哪个索引,多表联查时选择哪个表作为基准表等。
优化完成之后,就可以获取一个执行计划,我们平时使用explain查询出的结果,就是执行计划。
执行器:
执行器获取执行计划,调用存储引擎的API来完成sql的执行操作。
需要注意的是,存储引擎只会返回数据,对于数据的排序,计算,去重等操作都是执行器做的。
1.3.存储引擎
存储引擎是MySQL存放数据的地方,根据实现的不同可以分为Innodb、MyISAM、Memory等。
- Innodb:支持事务,行级锁,崩溃恢复能力强,对于更新操作较多的表,可以选择。
- MyISAM:不支持事务,只有表级锁,但是支持全文索引,对于更新操作较少,查询较多的表,可以选择。
- Memory:查询和更新的速度快,但是不支持持久化,数据不安全,可以用来存储一些不重要的临时数据。
存储引擎会提供查询和更新的API给执行器调用。
索引也是属于存储引擎层的。
2.SQL语句执行流程
2.1.几个基本概念
以日常开发中最常用的Innodb为例,下面是Innodb的结构:
2.1.1.随机IO/顺序IO
- 随机IO:一次查询或更新的数据保存在磁盘中的不同扇区,那这次查询和更新对应的多次IO操作需要不断的在磁盘中寻址,这就是随机IO。
- 顺序IO:一次查询或更新的数据在磁盘中的位置是连续的,多次IO也不会发生多次磁盘寻址的操作,这就是顺序IO。
2.1.2.页
MySQL存储引擎在磁盘中存储数据的单位是 “页”,也就是说,一次IO操作加载的数据单位,就是一页。
操作系统的页为4KB,MySQL默认的页大小为16KB。
2.1.3.Buffer Pool
作用:
Buffer Pool是执行器和存储引擎之间的一个数据结构,目的就是用来缓存数据的,避免每次IO操作都从磁盘去查询数据,提高执行效率。
大小:
Buffer Pool在linux中默认大小为128M,这个大小是不够的,在生产环境的数据库服务器中,一般Buffer Pool会占用到内存的80%以上。
淘汰策略:
Buffer Pool的空间被填满后,会使用LRU算法对不常用的数据进行淘汰。相对于传统的Map+链表的方式,MySQL对Buffer Pool做了冷热数据分离。
2.1.4.脏页与刷盘
执行器修改Buffer Pool中的页数据,并不会同步更新到存储引擎中,这种与存储引擎数据不一致的页就叫做脏页。
Buffer Pool中的脏页数据更新到数据库存储引擎中的过程就叫做刷盘。
2.1.5.redo log
Innodb特有的一种日志,是用来保证事务的持久性的。
作用:
Buffer Pool刷盘的过程如果发生了宕机,数据同步就失败了,在崩溃恢复时,可以将redo log中还没有同步到存储引擎的数据同步过去。
大小:
redo log的记录内容是在某个数据页上做了什么修改,默认的最大值为48M,如果redo log写满了,就会触发Buffer Pool刷盘。
Log Buffer:
从Buffer Pool写入redo log的缓冲区,默认是每次提交的时候会刷新到redo log的磁盘文件中,也可以修改为1秒钟1次。
2.1.6.undo log
用来保证原子性的事务日志,一个更新的sql执行后,会记录执行前的数据在undo log中。
在多个SQL语句在同一个事务中执行的时候,可以通过undo log来回滚操作,从而保证事务的原子性。
2.1.7.bin log
redo log和undo log都是Innodb 存储引擎的日志,而bin log是server层的日志,它记录了MySQL运行过程中的DDL和DML。
作用:
用于主从同步或回复数据。
如何恢复数据?
首先需要每天都有一个全量备份的文件,例如在凌晨1点进行全量备份。我们使用这个全量备份的文件,加上1点到我们需要恢复的时间节点之间的时间段的bin log数据就可以恢复了。
2.2.查询语句
- 客户端与服务端连接,认证权限。
- 如果开启了缓存,先去查缓存,如果没有则进入解析器。
- 解析器进行词法分析和语法分析,检查有没有语法问题。
- 优化器确定sql的执行计划。
- 执行器调用存储引擎的接口,查询数据。
- 存储引擎返回执行结果,由执行引器返回给客户端。
2.3.更新语句
- 开启事务,执行器从Buffer Pool或数据库存储引擎中获取数据页。
- 执行器修改数据页中的行数据,将数据页写入到Buffer Pool中。
- 将历史数据存入到undo log中
- 并将新的数据存入到redo log中,记录为prepare状态。
- 将执行的语句写入到bin log中。
- 提交事务。
- 将redo log中的记录修改为commit状态。
崩溃恢复:
崩溃恢复时,如何得知哪些数据要提交,哪些数据需要回滚呢?
主要是看bin log中是否有数据,只要bin log中存在数据,那就是需要提交的事务,如果不存在数据,就是需要回滚的事务。
3.索引
3.1.索引概念及优缺点
概念:索引是一种有序的数据结构,用来加快查询的速度,在MySQL中是以文件的形式存在的。
优点:可以大大的加快数据检索的速度。
缺点:1.会降低插入数据的效率。2.索引会占用额外的空间。
3.2.索引的数据结构
常用的索引数据结构为:Hash、B+Tree
Hash索引的精确查询效率高,但是不支持范围查询和模糊搜索。
B+Tree索引支持排序、范围查询及模糊搜索。
3.3.B+Tree与B-Tree
B+Tree相对于B-Tree的优势:
- B+Tree的数据都存在叶子节点中,分支节点只存储索引不存储数据,这样索引的每个分支就可以保存更多的索引,每次IO操作就可以检索出更多的数据,增加查询的效率。
- 叶子节点使用链表相连,有利于范围查询和排序。
3.4.聚簇索引与非聚簇索引
聚簇索引就是索引和数据都在同一个文件中,非聚簇索引就是索引与数据不在同一个文件中。
非聚簇索引的value值保存的是聚餐索引的key。
MySQL中的聚簇索引一般是指的主键索引。
3.5.索引类型和优化
主键索引:以主键字段创建的索引,同时也是Innodb的数据文件,叶子节点中保存的是行数据。主键索引的约束为非空且唯一。
唯一索引:字段值是唯一的,叶子节点保存的是主键索引值。
普通索引:字段可以重复。
联合索引:使用多个字段创建的索引,遵循最左匹配原则。
除此之外,还有两种索引的使用方式:前缀索引,覆盖索引。
3.5.1.前缀索引
对于一些字段值特别长的数据,为了节省索引占用的空间,并让每个分支节点可以保存更多的索引值,可以通过前缀索引的方式来做。
所谓的前缀索引,就是使用字段值的一部分长度的前缀来做的。
前缀多长合适呢?
依次取不同长度的前缀计算离散度,选择与完整字段的离散度最相近的那一个长度,作为前缀的长度。
3.5.2.回表
我们通过非主键索引来检索数据时,索引命中后,获取主键值,然后通过这个主键值查询主键索引树获取行记录,这个过程叫做回表。
表现的形式就是需要查询两个不同的索引树。
3.5.3.覆盖索引
覆盖索引可以解决回表的问题。
所谓的覆盖索引,就是查询的字段完全包含在了索引中,直接从索引就可以拿到返回值了,自然就不需要回表,覆盖索引大多数情况下都是联合索引。
3.5.4.索引条件下推
在MySQL5.6之前,如果索引没有生效,则会将数据查询出来,在Server层再完成过滤,这样就会查询出较多不需要的数据。
在5.6之后,MySQL做了一个ICP优化,就是索引条件下推优化,即使索引没有生效,也会在存储引擎层先完成筛选,再把合适的结果返回到Server层,这就减少了不必要的数据传输。
用一句话来描述就是:将不生效的索引条件,由服务层下推到存储引擎层进行筛选,减少不必要的数据传输。
3.6.执行计划分析
通过explain
加上待执行的语句就可以获取到一个执行计划,执行计划一般是用来分析慢SQL的,我们可以根据执行计划来对慢sql进行优化。在执行计划中主要看以下的几个部分:
- table:表名,可以分析是哪张表的问题。
- type:访问类型,有没有全表扫描,有没有遍历索引树。
- possible_key:可能命中的索引。
- key:实际命中的索引。
- rows:扫描的行数,这是一个参考值,不是精确值。
- extra:执行情况,例如:using index就表示使用到了覆盖索引。
3.7.索引创建的原则
- 尽量不要在字段重复度高、经常修改或无序不规则的字段上创建索引。
- 尽可能的使用联合索引代替创建新的索引。
- 每张表的索引不能创建的过多。
- 如果字段过长,使用前缀索引。
3.8.索引失效
最重要的是最左匹配原则,所有违背最左匹配原则的使用方式,都会导致索引失效。
最左匹配原则是什么?
是一个索引匹配的规则,查询条件和索引key会按从左到右的顺序,依次匹配。
导致索引失效的情况有哪些?
- 违背了最左匹配原则。
- 查询条件中使用了函数,或存在隐式转换。
- 关联查询的字符集或排序规则不同。
- 字段值的重复率太高,优化器认为全表扫描效率更高。
4.锁
4.1.锁的作用
解决数据库在并发访问时,由于资源竞争导致的线程安全问题。
MySQL的锁,锁的是什么东西?
MySQL锁住的是索引,如果加锁的字段没有索引,则会锁整张表。
4.2.锁的种类
从锁的粒度上来看:分为行锁、页锁、表锁
-
表锁:顾名思义,锁的是整张表,加锁快,但是容易阻塞。
-
行锁:锁的是行数据,因为加锁时需要扫描行数据,加锁相对较慢,但是不容易发生阻塞。
-
页锁:对MySQL的页进行加锁,加锁效率和阻塞发生的概率介于表锁和行锁之间。
从锁的兼容性上看:分为共享读锁与独占写锁。
- 共享读锁:读锁与读锁之间不互斥,与写锁互斥。
- 独占写锁:与读锁和写锁都互斥。
4.3.意向表锁
以Innodb为例,Innodb除了行锁和表锁外,还有意向表锁。
意向表锁的作用是什么?
现在有这么一个场景,连接A给数据表加了一个行锁,连接B想对同一个数据表加表锁,那么就需要先一行行的检查有没有其他行锁存在。
这时候如果有意向表锁就简单了,连接A在家行锁时,同时给自己加上一个意向表锁,连接B只要判断有意向表锁存在就行了,这样就增大的判断是否存在锁的效率。
意向表锁的类型有哪些?
- 意向共享锁:加行共享锁时,同时会加上意向共享锁。
- 意向排他锁:加行排他锁时,同时会加上意向排他锁。
注意:意向锁与行锁时不会互斥的。
4.4.行锁算法
Innodb中的行锁算法有三种:记录锁、间隙锁、临键锁。
- 记录锁:对唯一性索引做等值查询,精确的命中一行记录,就会加上一个记录锁。
- 间隙锁:
- 唯一性索引没有命中,则会对查询条件到两端记录的间隙加锁,左开右开。
- 普通索引不管有没有命中,都会对条件两端加间隙锁,左闭右开。
- 临键锁:对索引做范围查询时,会对最右侧记录右边的区间及右边的第一个记录加锁,左开右闭。
注意:RR(可重复读)的隔离级别下才会有这三种锁,RC(读已提交)的隔离级别下只会加记录锁。
4.5.死锁问题
什么是死锁?
两个事务都需要对方的锁,同时等待对方先释放锁,形成了一个循环等待的过程。
如何避免死锁?
避免死锁最简单的方法就是打破循环等待,顺序加锁、减少锁的粒度都是可行的方式:
- 在程序中先对查询条件进行排序,再进行查询。
- 在RR下尽可能的使用等值查询精确匹配。
- 在允许幻读和不可重复读的情况下,可以使用RC来代替RR。
- 如果事务太长,可以考虑拆解事务。
- 适当的减少锁等待超时时间的配置。
已经发生了死锁怎么办?
可以查询出当前死锁的线程号,然后通过kill 线程号先杀死其中一个连接,打破循环等待。
SELECT * FROM information_schema.INNODB_TRX;
5.事务
事务是数据库DML的最小单元,事务中的所有操作要么都成功要么都失败。
5.1.事务特性
事务的四大特性ACID:
- 原子性:同一个事务内的操作要么都成功,要么都失败。
- 一致性:一致性和业务有关,表现为一个业务中不同存储位置的数据值要一致。
- 隔离性:事务与事务之间互不影响。
- 持久性:事务提交后,数据就会持久化到磁盘中,不会因为宕机而导致数据完全丢失。
5.2.隔离级别
MySQL事务的隔离级别有四种:
- 读未提交(RU):事务中可以读取到其他事务未提交的数据。
- 读已提交(RC):事务只能读取到其他事务已提交的数据。
- 可重复读(RR):在事务中发起了一次查询之后,后续的查询都会查到一样的结果。
- 串行化(Serializeable):每次操作都会加锁
5.3.事务并发问题
事务在存在并发的情况下,会有两方面的问题:读写冲突、写写冲突。
其中读写冲突的问题表现为三种情况:
- 脏读:一个事务会读取到其他事务未提交的数据,存在于RU隔离级别下。
- 不可重复读:一个事务中,同一个语句多次查询的结果不一样,存在与RC隔离级别下。
- 幻读:与不可重复读类似,区别在于不可重复读行数据发生了更新,幻读是新增或删除了一条数据。
Innodb中的RR隔离级别已经解决了上面的三种问题,对于写写冲突,是通过LBCC,也就是加锁的方式来解决的,读写冲突是通过MVCC来解决的。
5.4.MVCC
5.4.1.当前读与快照读
-
当前读:每次读都是获取最新的数据,通过加锁来阻塞其他事务修改数据。select xxx for update,或对书库进行增删改之前的查询,都属于当前读。
-
快照读:在读取时会生成一个快照,RR级别下,后续每次都是读取这个快照。RC级别下,每次都会生成新的快照,快照读是不加锁的。
5.4.2.实现原理
简单的说:就是快照中保存了一部分当前活跃事务的id,通过这些事务id与MySQL行数据中的隐藏字段进行比较,让满足条件的行数据可见。
快照的属性:
执行一次查询就会生成快照,快照的重要属性有四个:
- ids:当前所有活跃事务的id。
- up_limit_id:当前活跃事务id的最小值。
- low_limit_id:当前活跃事务id的最大值的下一个id值。
- creator_trx_id:创建快照的事务id。
行数据是否可以显示的条件,就隐藏在每一行的两个隐藏字段中。
两个特性:
MySQL的快照读是通过两个特性来实现的:
- 每次更新都会记录undo log。
- 每次更新的记录都会有一个唯一的事务id。
在每行数据中都有两个隐式字段:
- DB_ROLL_PTR:指向undo log中上一次修改记录的指针。
- DB_TRX_ID:记录修改这行数据的事务id。
可见性算法执行流程:
- 首先获取当前行数据的事务ID,记作DB_TRX_ID。
- 如果DB_TRX_ID < up_limit_id,表示这一行数据是在生成快照之前提交的,返回可见。
- 如果DB_TRX_ID>=low_limit_id,表示这一行数据在生成快照时还没有创建,返回不可见。
- 如果 up_limit_id < DB_TRX_ID < low_limit_id,就看DB_TRX_ID有没有在ids中,如果在表示这行数据在生成快照时,还没有提交,返回不可见,如果不在则返回可见。
- 此外,如果查询出的行数据的事务id,就是当前创建快照的事务id(creator_trx_id),无论如何都是可见的。
对于不可见的行记录,根据当前记录的DB_ROLL_PTR指针,在undo log中取出最近一次旧记录,用这个记录的事务id再做一次上面的条件判断。直到找到一个对快照读可见的旧记录或DB_ROLL_PTR指向空为止。
6.MySQL优化
可以从哪些方面考虑对MySQL的优化?
-
从索引的角度
- 查询条件有没有创建索引,没有的话创建合适的索引。
- 查询条件是否合理,索引列的数据重复度太高的话会导致索引失效。
- 有没有使用违背最左匹配原则的查询方式,连表查询的连接条件字符集和排序类型是否一致。
- 需要什么字段查什么字段,不要使用select * ,尽可能的触发覆盖索引。
-
从表结构的角度
- 单表的列是否太多了,过多的列可以按照业务的边界拆分出去。
- 对于过多的join连接,考虑使用冗余字段来减少join。
-
从数据量的角度
- 数据量过大,可以考虑对数据进行水平拆分。
- 也可以考虑做读写分离。
-
从缓存的角度
- 可以将经常查询的热点数据缓存起来,通过缓存直接返回数据。
-
从硬件的角度
- 从软件方面再怎么优化,硬件不给力效果也不好,可以按需提升MySQL服务器的性能。
五、Mybatis
1.#{}和${}的区别
${}是变量占位符,属于静态文本替换。
#{}是参数的占位符,会经过PrepareStatement预编译,文本会替换为?,然后通过反射从参数对象中获取参数值。
注:#{}可以防止SQL注入。
Mybatis缓存
Mybatis有两级缓存:
- 一级缓存作用域在session中,session commit就会失效。
- 二级缓存作用域为sessionFactory,是以Mapper.xml为单位存储的,在同一个Mapper中对数据做了增删改操作之后,二级缓存就会失效。
为什么有了二级缓存还需要redis呢?
因为我们代码往往不规范,对同一张表的操作,可能会写在不同的Mapper.xml里面,这就导致了不同Mapper对应的二级缓存可能存在不一致的情况,就有可能会读取到脏数据。
相对应的使用Redis做缓存就好用多了,不仅解决了脏数据问题,还可以灵活的组织需要缓存的数据,同时还支持持久化防止数据丢失。在分布式环境中还可以作为分布式缓存使用。
六、Spring
1.Spring的概念
Spring是一种使用Java开发的,用于管理业务开发中的对象创建、注入、使用等生命周期的轻量级框架,目的简化开发,使业务开发人员只需要专注在业务上。
Spring框架有IOC和AOP两大特性。
2.IoC与DI
IoC:
IOC是Spring的两大特性之一,就是控制反转的思想。
所谓的反转,就是将开发人员控制的对象创建、设置属性、使用、销毁的过程,转交给框架来控制,开发人员只需要专注于业务本身。
DI:
DI的全称叫依赖注入,是IOC的一种实现方式,Spring就是使用的这种方式来实现对象中字段值的注入。
2.1.IoC的作用
Spring容器是通过IoC容器来管理Bean对象的生命周期的,可以在创建Bean的时候通过前置处理器做一些对开发者无感的操作,使Bean的功能更加强大。例如为Bean创建代理对象,用于事务或AOP。
IoC的存在减少了开发者维护对象的成本,以侵入性最小的方式达到松耦合的目的。
2.2.IoC的实现原理
IoC的实现,就是将我们开发中标识为Bean的类,实例化为对象放入到IoC的容器中,在我们使用的时候,直接从容器中获取出来进行使用。使用的是工厂和反射的方式来实现的。
Spring定义了一个用于创建对象的工厂,先将使用注解或XML标识的类扫描出来,创建Bean定义对象,保存对象的全类名,然后通过全类名使用反射完成Bean对象的实例化。
2.3.依赖注入的方式
依赖注入主要有两种方式。
- 构造器注入:通过触发类的构造方法来实现注入,构造方法中的每一个参数都代码一个需要注入的对象。
- setter注入:在使用无参构造器实例化后,调用类用的set方法来注入对象。
此外还有接口注入,但是因为灵活性较差,已经弃用了。
2.4.BeanFactory
BeanFactory是IoC的顶层接口,提供Bean的创建、注入等生命周期管理功能。
ApplicationContext:
是BeanFactory的子接口,功能更多,实现了事务传输机制、国际化、多配置文件等机制。
FactoryBean:
是一个工厂Bean对象,用来实例化某一种类型的Bean,可以自定义Bean的创建过程。
2.5.Bean的生命周期
Bean的生命周期,就是Bean的实例化、属性注入、使用、销毁的全过程。
- 通过Bean定义实例化Bean对象。
- 通过依赖注入属性。
- 初始化,通过BeanPostProcess后置处理器创建代理对象。
- 在Spring上下文中使用Bean对象。
- Bean不再需要后,调用清理方法,销毁Bean。
2.6.循环依赖如何解决
什么是循环依赖?
用一个简单的例子说明,有两个类A、B,A中注入了B对象,B中注入了A对象,两个对象互相注入对方,就形成了循环依赖,只有单例Bean支持循环依赖。
如何解决?
使用构造器注入的方式无法解决循环依赖,会直接抛出异常。对于使用Setter方法注入的方式,Spring使用的是三级缓存来解决的。
首先,三级缓存分别是什么呢?
- 一级缓存(singletonObjects): 缓存创建完成的单例Bean。
- 二级缓存(earlySingletonObjects):缓存属性未填充完整的单例Bean,就是已实例化还未初始化。
- 三级缓存(singletonFactory):缓存singletonFactory。
执行流程:
以A、B互相注入对方为例,Spring解决循环依赖的执行流程如下:
- 通过getBean获取A,发现从一级缓存中无法获取,且没有在创建过程中。于是实例化A,获取创建A的工厂,放入到三级缓存中。
- 在A中注入属性B,调用getBean获取B对象,发现也无法获取到B,于是先去实例化B,并把创建B的工厂放入到三级缓存中。
- 在B中注入属性A,调用getBean,发现A处于创建中,于是从三级缓存中获取到A的工厂,创建A对象并放入到二级缓存中,同时将未初始化的A对象注入到B的属性中。
- 对B进行初始化,创建完整的B对象,放入一级缓存,同时删除二三级缓存中的B。
- B初始化完成后,A就获取到了B的完整对象,注入到自己的属性中,并进行初始化,将完整的A放入到1级缓存中,并删除二三级缓存中的A。
至此,A、B就创建完成,并且都有对方完整的引用。
不要三级缓存可以吗?
如果只是上面的步骤,只需要二级缓存就可以解决循环依赖了,但是三级缓存的存在,主要是为了在初始化那一步时,使用singletonFactory.getObject(),获取的是A、B的代理对象,他们互相注入的也是代理对象。
不要二级缓存可以吗?
不要二级缓存的话,每次调用singleFactory()都会创建新的代理对象,这会导致一级缓存中的A和B对象中引用的A不是同一个对象。
3.AOP
就是面向切换编程,使用横切的方式,在不修改原方法的情况下,可以在方法的执行前、执行后、异常返回时做一些额外的操作。
3.1.Aop的名词
- 连接点(Join Point):就是普通的方法。
- 切入点(Point Cut):匹配可以被切入的方法,一个切入点可以使用通配符匹配多个连接点。
- 通知(Advice):在切入方法的前、后或异常时要做的事,就是通知。
- 切面(Aspect):切入点+通知组成了切面,就是表达在哪些方法的执行前后需要做什么事。
3.2.通知的类型
通知的类型一共有四种,方法的执行前、后、异常以及一种更加灵活的环绕通知。
- 前置通知:方法执行前需要执行的代码。
- 后置通知:方法执行后需要执行的代码。
- 异常通知:方法抛出异常后,需要执行的代码。
- 环绕通知:一种更加灵活的方式,可以在这个通知中定义方法执行前、后或异常时分别需要做什么。
3.3.AOP的实现原理
AOP的通知是通过动态代理来实现的,在Bean实例化时,也会为每一个Bean的实例创建一个代理对象,我们在开发中使用的各个注入的对象,其实就是代理对象。
Spring的动态代理有两种实现方式,一种是JDK的动态代理,一种是CGLIB的动态代理。
JDK和CGLIB实现动态代理的区别:
JDK是通过反射的方式来实现的,要求被代理的类一定实现了某一个接口(Interface)。
CGLIB是通过ASM字节码增强技术,在运行时动态生成字节码,并通过类加载器加载到虚拟机中,然后生成对应的代理对象。CGLIB是使用继承的方式在增强实例对象,要求被代理的类不能使用Final修饰,同时如果方法是非Public的,这个方法的代理也会失效。
4.SpringMVC的执行流程
七、Linux
1.常用指令含义
1.1.kill指令
- 直接输入
kill
指令什么都不加,是显示kill的用法。 kill -l
是显示信号变量,列举有哪些信号参数可选。- kill -15 pid:关闭某个程序,但是这个程序有可能立即关闭,有可能释放资源后关闭,也有可能不做响应。
- kill pid:不加信号参数的kill,其实就是kill -15。
- kill -9 pid:表示强制关闭程序。
2.文件权限
2.1.权限类型
权限种类:
Linux的文件权限分为3种:
- 读权限、写权限、执行权限。
- 分别使用r、w、x来表示。
- 用数字表示则为4、2、1。
权限的用户类型:
权限的标识,从左到右由9个字母组成:
- 1-3位表示文件创建者的权限。
- 4-6位表示同组用户的权限。
- 7-9位表示其他用户的权限。
每3个字母又可以按照数字表示法相加,例如:只读权限则为4,读和执行权限则为5,读写权限则为6,所有权限则为7。
下面是一些常见的权限:
644:rw-r--r--
755:rwxr-xr-x
777:rwxrwxrwx
2.2.如何修改权限
使用chmod
指令可以修改权限,有两种使用方式。
- 一种是直接使用数字,例如:chmod 777 fileName,给文件赋予所有权限。
- 另一种是使用字母,u表示创建者,g表示同组用户,o表示其他用户。例如:chmod u=rwx,g=rw fileName,就是给文件的创建者赋予所有权限,并修改同组用户的权限为读写权限。
原文地址:https://blog.csdn.net/qq_38249409/article/details/120442259
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!