Java八股文总结
推荐网站:
Java基础
反射
反射(Reflection)是Java被视为(准)动态语言的关键,反射机制允许程序在执行期借助Reflection API获得任何类的内部信息,并能直接操作。
1、通过反射创建对象
1 | Class aClass=Class.forName("com.ClassLoader.Demo.User"); |
2、通过反射,去操作实例的方法和属性
1 | //通过反射调用普通方法 |
反射的优缺点
优点:
1、 增加程序的灵活性,避免将程序写死到代码里
2、代码简洁,提高代码的复用率,外部调用方便
3、对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法
缺点:
1、 性能问题: 使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此Java反射机制主要应用在对灵活性和扩展性要求很高的系统框架上,普通程序不建议使用。
2、使用反射会模糊程序内部逻辑:程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
3、安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。
4、内部暴露:反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
注解
注解的作用:
不是程序本身,可以对程序做出解释
可以被其他程序(例如编译器)读取
可以在package、class、method、field等,相当于起辅助功能,我们可以通过反射机制编程实现对这些元数据的访问
元注解的作用就是负责注解其他注解
@ Target:用于描述注解的使用范围(即被描述的注解可以用在什么地方)
@ Retention:表示需要在什么级别保存该注释信息,用于描述注解的生命周期
@ Document:说明该注解将被包含在 Javadoc中
@ Inherited:说明子类可以继承父类中的该注解
自定义注解
使用@interface自定义注解时,自动继承了 java. lang annotation. Annotation接口
其中的每一个方法实际上是声明了一个配置参数
方法的名称就是参数的名称
返回值类型就是参数的类型(返回值只能是基本类型, Class, String,enum)
可以通过 default来声明参数的默认值
如果只有一个参数成员,一般参数名为vaue
注解元素必须要有值,我们定义注解元素时,经常使用空字符串,0作为默认值
1 | public class TestCustomAnnotation { |
容器
Java容器主要由Collection和Map两大接口派生而来。参考:
List,Set,Queue,Map四者区别
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),”x” 代表 key,”y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
ArrayList和LinkedList的区别
ArrayList: 基于动态数组,连续内存存储,适合下标访问(随机访问)
扩容机制:因为数组的长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组。
如果不是尾部插入数据,会涉及到元素的移动,使用尾插法并指定初始容量(不用扩容,浪费时间)可以极大提升性能,甚至超过LinkedList(需要创建大量node对象)。
LinkedList:基于链表,可以存储在分散的内存,适合做数据的插入和删除,不适合查询。
遍历LinkedList必须使用iterator,不适合使用for循环,因为每次for循环体内通过get(i)取得某一元素时都要对list重新进行遍历,性能消耗极大。
不要试图使用indexOf返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,而且结果为空时遍历了整个列表。
HashMap
参考:
Java基础常见面试题
Java如何实现多态
1、重写
2、接口
3、抽象类和抽象方法
接口和抽象类的区别
相同点:
- 都不能被实例化
- 其子类(实现类)只有实现了其方法才能实例化
不同点:
接口只有定义,不能有方法的实现。(JDK1.7之后可以有默认的实现)抽象类方法可以有实现。
类可以实现多个接口,只能继承一个抽象类。
接口强调功能的实现,抽象类强调所属关系。
接口成员变量默认为
pulic static final
,必须赋初值,不能被修改,其所有方法都是public、abstract的。抽象类中的成员变量可以用public private protected修饰,也可以重新赋值。抽象方法被abstract修饰,不能用private(是用来被继承的,也不适合用default,用default不一定保证对子类可见)、static(static修饰的方法可以通过类名去访问该方法,在抽象类中无意义),synchronized(锁的是对象)等修饰符修饰。
访问修饰符
1、private 不能被外部类或对象直接使用。
2、default 可以被同一包下的其他类使用。
3、protected 可以被不同包下的子类使用,但不能被不同包下的其他类使用。
4、public 可以被任何类调用。
int a = 3在内存中是怎么存的
首先在栈中创建一个变量名为a的引用,然后查找有没有字面值为3的地址,没有则开辟一个存放3这个字面值的地址,然后将a指向3的地址。
String为什么设置成不可变的
1、不可变性支持字符串常量池。这样在大量使用字符串的情况下,可以节约内存,提高效率。
2、不可改变 – 执行效率高
3、不可变支持线程安全。
HashMap的长度为什么设置成2的整数次方
计算机中直接求余的效率不如位运算,源码中做了优化,hash&(length-1)
hash%length == hash&(length-1) 的前提是length是2的n次方。
JVM
Java内存区域
JVM常见面试题
字节码及其好处
- Java程序通过编译器将程序编译为字节码文件,也就是.class文件。
- 采用字节码最大的好处:可以实现一次编译,到处运行,与平台无关。
- JVM通过解释器将字节码文件转为机器可执行的二进制机器码。
- Java源代码 -> 编译器 -> JVM可执行的Java字节码 -> JVM -> JVM中的解释器 -> 机器可执行的二进制文件 -> 程序运行
类加载的过程
加载->连接(验证->准备->解析)->初始化
加载(加载和连接的部分内容是交叉进行的,加载未结束,连接可能已经开始了)
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接 :连接阶段是正式为类变量分配内存并设置类变量初始值的阶段, 这些内存都将在方法区中分配。
验证: 确保被加载的类的正确性
文件格式验证:字节流是否符合Class文件格式规范,如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法等。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:方法中的类型转换是否有效。
符号引用验证:确保解析动作能正确执行,如:通过符号引用能找到对应的类和方法。
准备:为类的静态变量分配内存,并将其赋默认值 ,对final修饰的静态变量直接赋值。
解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
初始化:为类的静态变量赋初值
clinit是类构造器, 主要作用是在类加载过程中的初始化阶段进行执行, 执行内容包括静态变量初始化和静态块的执行。
init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
对象创建的过程
1、类加载检查
首先检查这个指令的参数能否在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。有则直接引用,没有则必须先执行相应的类加载过程。
2、分配内存
对象所需的内存在类加载检查过程中可以确定,分配内存等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种。
- 指针碰撞:堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的都放在另一边,中间放着一个指针作为分界点的指示器,那么分配内存就仅仅是把那个指针向空闲空间挪动一段与对象大小相等的距离。
- 空闲列表:堆中的内存并不是规整的,已使用的和未使用的空间相互交错,没办法使用指针碰撞。虚拟机就必须维护一个列表,记录哪些内存块是可用的,分配的时候从列表中找到一块足够大的空间划分给实例,并更新列表上的记录。
3、初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,将一些信息设置到对象头。
对象头存放的信息:
- 对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等)
- 类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
5、执行init方法
执行完new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化。
对象的内存布局:
对象头(如上)
实例数据部分:存储对象真正的有效信息
对齐填充部分:无实际意义,仅占位作用。Hotspot 虚拟机的自动内存管理系统要求,对象的大小必须是 8 字节的整数倍
对象的访问定位
1、句柄
Java堆中划分出一块内存作为句柄,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
2、直接指针
reference中存储的对象的地址,对象实例数据中存放到对象类型数据的指针。
优缺点:
句柄的好处:reference中存储的是稳定的句柄地址,对象被移动是只会修改句柄中实例数据指针,而reference本身不需要修改。
直接指针的好处:速度快,节省了一次指针定位的时间开销。
GC如何判断对象可以被回收
引用计数法:每个对象有一个引用计数器属性,新增一个引用计数器+1,引用释放引用计数器-1,计数器为0时,可回收。
缺点:可能出现A引用B,B引用A,这时候就算他们都不再使用了,但因为相互引用,计数器一直为1,无法回收。
有点:效率高。
可达性分析法:从GC Roots开始向下搜索,搜索走过的路径成为引用链。当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,那么虚拟机就判断可回收对象。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法去中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少经历两次标记过程:第一次是经过可达性分析发现没有与GCRoots相连接的引用链,第二次是由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
判断对象是否废弃
1、常量:如果当前没有任何对象引用改常量的话,说明常量就是废弃常量。此时发生内存回收且有必要的话,常量就被清理出常量池了。
2、类:满足以下三个条件。
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 。
- 加载该类的
ClassLoader
已经被回收 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
逃逸分析
对象不一定分配在堆中,JVM通过逃逸分析,那些逃不出方法的对象会在栈上分配。
一个对象的指针被多个方法或线程引用时,那么就称这个对象的指针发生了逃逸。
JUC
JUC常见面试题
线程间是如何通信的
共享内存和消息传递
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的内存通信方式,就是通过共享内存通信。
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送信息来显示进行通信。Java中典型的消息传递方式:wait()和notify(),或者BlockingQueue。
父子进程
Linux系统中创建进程需要消耗较大资源,所以使用fork函数生成一个子进程,子进程的PCB(进程控制块)会复制父进程的数据!
在进程结束后,Linux系统会自动回收进程消耗的 内存和IO,但是进程本身占用的资源(task_struct和栈内存)不会被回收,需要被父进程来进行回收 。
(1)僵尸进程: 子进程比父进程先结束。如果父进程没有显示调用wait或waitpid函数的话,会直到父进程结束时才会回收子进程的资源!这样的子进程,就是僵尸进程!
(2)孤儿进程: 父进程先于子进程结束,子进程于是成为进程1(init进程)的子进程,直到关机才会回收!
并发的三大特性
原子性
可见性
总线Lock:总线锁定协议
MESI:缓存一致性协议
有序性(禁止指令重排)
synchronized:同时保证三大特性
volatile:可见性+有序性,final可见性
原子性+可见性来保证线程安全,如:AtomicInteger:volatile(可见)+cas(原子)
synchronized和ReentrantLock
synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。(monitor是依赖于底层的操作系统的Mutex Lock来实现的,效率低)在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1、等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当Synchronized来说可以避免出现死锁的情况。
2、可实现公平锁
3、锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。
volatile关键字
如何保证可见性
- 如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。 但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
- 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
如何保证有序性
有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
指令重排是通过加内存屏障来实现的。(重排序时不能将后面的指令排序到内存屏障之前) JMM为volatile加内存屏障有以下4种情况:
在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
在每个volatile读操作的前面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
为什么不能保证原子性
简单的说,修改volatile变量分为四步:
1、读取volatile变量到local
2、修改变量值
3、local值写回
4、插入内存屏障,即lock指令,让其他线程可见
这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。
对线程池的理解
1、使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程池可以对线程进行统一的分配,调优和监控。
2、线程池的使用
ThreadPoolExcutor xx = new ThreadPoolExecutor()
核心参数:
corePoolSize:核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
maxinumPoolSize:最大线程数,表示最大允许被创建的线程数。当任务很多,将核心线程数用完了,还是无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。
keepAliveTime:超出核心线程数之外的线程的空闲存活时间。
TimeUnit:一个时间类型的枚举类。有从纳秒到天的时间量度,配合上面的keepAliveTime确定非核心线程的存活时间。
workQueue:用来存放待执行的任务,假设核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会创建新的线程
ThreadFactory:线程工厂,用来生产线程执行任务。可以使用默认的创建工厂,产生的线程都在一个组内,拥有相同的优先级,且都不是守护线程。也可以选择自定义线程工厂,一般根据业务置定不同工厂。
一个创建线程的接口,里面只有一个创建线程的方法,设置名字,设置线程参数。
Handler:任务拒绝策略。两种情况,第一种是掉用shutdown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,由于线程池已经关闭,我们再继续向线程池提交任务就会被拒绝。另一种是达到最大线程数,线程池已经没有能力处理新提交的任务时,拒绝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 四种拒绝策略:
*
* new ThreadPoolExecutor.AbortPolicy()
* 银行满了,还有人进来,不处理这个人的,抛出异常
*
* new ThreadPoolExecutor.CallerRunsPolicy()
* 哪来的去哪里!比如你爸爸 让你去通知妈妈洗衣服,妈妈拒绝,让你回去通知爸爸洗
*
* new ThreadPoolExecutor.DiscardPolicy()
* 队列满了,丢掉任务,不会抛出异常!
*
* new ThreadPoolExecutor.DiscardOldestPolicy()
* 队列满了,尝试去和最早的竞争,也不会抛出异常!
*/
IO密集型和CPU密集型线程数的选择
1、CPU密集型: cpu使用率较高(也就是一些复杂运算,逻辑处理),所以线程数一般只需要cpu核数的线程就可以了。
2、CPU使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余时间出来,所以通常就需要开cpu核数的两倍的线程, 当线程进行I/O操作cpu空暇时启用其他线程继续使用cpu,提高cpu使用率 通过上述可以总结出:线程的最佳数量: 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
线程池的生命周期
1 | private static final int RUNNING = -1 << COUNT_BITS; |
构造前是初始状态,一旦完成构造线程池就进入了RUNNING状态。
这几个状态的转化关系为:
1、调用shundown()方法线程池的状态由RUNNING——>SHUTDOWN
2、调用shutdowNow()方法线程池的状态由RUNNING——>STOP
3、当任务队列和线程池均为空的时候 线程池的状态由STOP/SHUTDOWN——–>TIDYING
4、当terminated()方法被调用完成之后,线程池的状态由TIDYING———->TERMINATED状态
Executors与ThreadPoolExecutor的区别
Executors创建的线程有四种:
Cached | Fixed | Scheduled | Single | |
---|---|---|---|---|
核心线程 | 没有核心线程,都是非核心 | 全是核心线程,没有非核心 | 核心线程数固定,非核心线程没有限制 | 只有一个核心线程 |
线程数 | 可以无限创建 | 核心线程数固定 | 核心线程数固定,非核心线程数无限 | 只有一个核心线程 |
空闲存活时间 | 60s | 无 | 0s,空闲立即回收 | 就一个线程 |
阻塞队列 | 无 | 无界阻塞队列 | 无 | 无界阻塞队列 |
适用场景 | 任务量大耗时少 | 任务量固定,耗时长 | 定时任务和具体固定周期的重复任务 | 多个任务顺序执行 |
线程复用的原理
线程池中,有一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理是对Thread进行了封装,并不是每次都是调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,不停检查是否有任务需要被执行,如果有则直接执行,也就是直接调用run方法,将run方法当成一个普通方法执行,通过这种方式只是用固定的线程就将所有的run方法串联起来。
阻塞队列
作用:
1、阻塞队列可以保证队列中没有任务时,阻塞获取任务的线程,使其进入wait状态,释放cpu资源。
2、在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响整体效率。
设置时要注意:
1、不能设置的过大或过小,过大时可能会造成OOM,过小时会拒绝新提交的任务,可能造成数据丢失。
2、如果是CPU密集型,可以设置容量较大的队列和较小的最大线程数,就可以减少上下文切换带来的开销。如果是IO密集型,可以设置较小的队列和较大的最大线程数,这样整体的效率更高,不过也会带来更多的上下文切换。
CAS
原理
CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:变量的内存地址、旧的预期值、准备设置的新值。只有当内存地址的值与旧的预期值相同时,才会将内存地址的值设置为新值。
缺点
- ABA问题:可以通过设置版本号、添加时间戳来解决。
- 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销
- 只能保证一个共享变量的原子操作:多个可以通过AtomicReference来处理或者使用synchronized实现。
AQS
AQS : AbstractQueuedSynchronizer 抽象队列同步器
作用:用来构建锁和同步器
AQS 核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列(虚拟的双向队列)锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
AQS定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如ReentrantLock。
- Share(共享):多个线程可同时执行,如
CountDownLatch
、Semaphore
、CyclicBarrier
、ReadWriteLock
。
常用组件:
Semaphore
(信号量)-允许多个线程同时访问:synchronized
和ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源。CountDownLatch
(倒计时器):CountDownLatch
是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。CyclicBarrier
(循环栅栏):CyclicBarrier
和CountDownLatch
非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch
更加复杂和强大。主要应用场景和CountDownLatch
类似。CyclicBarrier
的字面意思是可循环使用(Cyclic
)的屏障(Barrier
)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier
默认的构造方法是CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await()
方法告诉CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞。
锁粗化和锁消除
锁粗化: 将临近的同步 代码块用同一个锁合并起来。
锁消除: 虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
sleep,wait,join,yield的区别
一、锁池
所有需要竞争同步锁的线程都会被放到锁池中,若当前对象的锁已被其中一个线程得到,其他线程需要在锁池中等待,当前面的线程释放同步锁之后锁池中的线程去竞争同步锁,得到锁之后会进入就绪队列等待cpu的分配。
二、等待池
当我们掉用wait方法后,线程会放到等待池中,等待池的线程不会去竞争同步锁。只有调用了notify后等待池的线程才会开始去竞争锁,notify是随机从等待池选出一个线程放到锁池,而notifyAll是将等待池的所有线程放到锁池中。
sleep和wait的区别:
- sleep是Thread类的静态本地方法,wait则是Object类的本地方法。
- sleep方法不会释放lock,但是wait方法会释放,而且会加入到等待队列中。
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后退出阻塞),而wait需要(不指定时间,需要被别人中断)。
- sleep一般用于当前线程休眠,或者轮询暂停操作,wait多用于多线程之间的通信。
- sleep会让出CPU执行时间且强制上下文切换,而wait不一定,notify后可能还是有机会重新竞争到锁继续执行的。
yield执行后,线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留cpu的执行资格,所以有可能cpu下次进行线程调度还是会让这个线程获取到执行权继续执行。
join执行后,线程进入阻塞状态。例如:线程B中调用线程A的join,那么B线程会进入到阻塞队列,直到线程A结束或中断线程。
1 | public static void main(String[] args) throws InterruptedException { |
对线程安全的理解
堆:堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完要还给操作系统,不然就是内存泄漏。堆是线程安全问题发生的位置。
Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动的时候创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈:栈是每个线程独有的,保存其运行状态和局部变量的。栈在线程开始的时候初始化,每个线程的栈相互独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要再高级语言里面显示的分配和释放。
对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;
守护线程依赖于整个进程而运行;哪天其他线程都结束了,程序就结束了,会直接结束守护线程。因此不要把IO、File等重要操作逻辑分配给守护线程。
守护线程的作用:
举例:GC垃圾回收线程:就是一个经典的守护线程,当我们的程序不再有任何运行的Thread,程序就不会在产生垃圾,垃圾回收器就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中进行,用于实时监控和管理系统中的可回收资源。
应用场景:
来为其他线程提供服务支持的情况。
在任何情况下,程序结束时,这个线程必须正常且立即关闭;
反之,若一个正在执行某个操作的线程必须要正确地关闭掉否则会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。
设置:thread.setDeamon(true), 必须要在start之前设置。
在守护线程中产生的线程也是守护线程。
守护线程不能用于访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至一个操作的中间发生中断。
Java自带的多线程框架,如ExecutorService,会将守护线程转换为用户线程,所有如果要使用后台线程就不能用java的线程池。
框架相关问题
Spring
Spring是什么
轻量级的开源J2EE框架。它是一个容器框架,用来装JavaBean(java对象),中间层框架可以起一个连接作用,可以让我们的企业开发更快更简洁。
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
- 从大小和开销两方面而言,Spring都是轻量级的。(侵入性低)
- 通过IoC达到松耦合的目的
- 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发(如专门开发一个打日志的切面)
- 包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器
- 将简单的组合配置、组合成复杂的应用,这个意义上是一个框架
对AOP的理解
系统是由许多不同的组件所组成的,每个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志,事务管理和安全这样的核心业务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为他们会跨越系统的多个组件。
当我们需要为分散的对象引入公共行为的时候,OOP(面向对象)显得无能为力,OOP允许定义从上到下的关系,但并不合适定义从左到右的关系。如日志功能。
在OOP设计中,它导致大量代码的复用,而不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(如安全、日志、事务),封装成一个切面,然后注入到目标对象(具体的业务逻辑)中。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前、之后额外做一些事情。
对IoC的理解
三个方面:容器概念、控制反转、依赖注入
IoC容器:实际上就是一个map(key, value),里面存放的是各种对象(在xml里配置的bean节点、@repository、@service、@controller、@component),在项目启动的时候会读取配置文件中的bean节点,根据全限定类名使用反射创建对象放到map里,扫描到带以上注解的类还是通过反射将对象放到map里。
这时候map里面就有各种对象了,接下来在代码里需要用到里面的对象时,再通过依赖注入(autowired, resource等注解,xml里bean节点的ref属性,项目启动的时候就会读取xml节点的ref属性根据id注入,也会扫描这些注解,根据类型或id注入,id就是对象名)。
控制反转:没有引入IoC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己需要主要去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IoC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IoC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,不难看出:对象A获得依赖对象B的过程,有主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”。
全部对象的控制权全部上缴给“第三方”IoC容器,所以IoC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系。
依赖注入:控制被反转之后,获得依赖对象的过程由自身管理变为了由IoC容器自动注入。依赖注入是实现IoC的方法,就是由IoC容器在运行期间,动态地将某种依赖注入到对象之中。
Autowired和Resource注解的区别
1、共同点
两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
2、不同点
@Autowire是Spring提供的注解,只按照byType注入,如果想按照名称来装配,可以结合@Qualifier注解一起使用。
@Resource默认按照ByName自动注入,如果想使用byType,设置type属性即可。
@Resource装配顺序:
1、同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
2、指定了name,则从上下文中查找id匹配的bean进行装配。
3、指定了type,则从上下文中找到唯一匹配的type进行装配,找不到或者找到多个,都会抛出异常。
4、都没指定,自动按照byName,没有匹配,则回退一个原始类型进行匹配。
依赖注入的方式
1、构造器注入:通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。
优点:对象初始化完成之后便可获得可使用的对象。
缺点:注入的对象很多时,构造器参数列表会很长。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数。
2、 setter方法注入: IoC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给依赖类。
优点: 灵活。可以选择性地注入需要的对象。
缺点: 依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。
3、 接口注入:依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖
注入。该函数的参数就是要注入的对象。
优点:接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即
可。
缺点:侵入行太强,不建议使用。
BeanFactory与ApplicationContext的区别
ApplicationContext是BeanFactory的子接口。
ApplicationContext提供了更完整的功能:
继承MessageSource(其中 getMessage(String code, Object[] obj, Locale locale) 方法用于对国际化的支持),因此支持国际化。
统一的资源文件访问方式。(Resources接口,通过不同的实现访问不同的资源 URLResource 、 ClassPathResource 、 FileSystemResource )
提供监听器中注册bean的事件。
同时加载多个配置文件。
载入多个(有继承关系)上下文,使每一个上下文都专注于一个特定的层次,如应用的web层。
BeanFactory采用的是延迟加载的形式来注入Bean,即只有在使用到某个Bean的时候才对该Bean进行加载实例化。这样我们就不能发现一些存在的Spring配置问题,如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext是在容器启动的时候一次性创建了所有的Bean。这样,容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单例Bean,通过预载入单例Bean,确保当你需要的时候不用等待。
ApplicationContext唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者的区别是:BeanFactory需要手动注册,而ApplicaitonContext则是自动注册。
SpringBean的生命周期
- 解析类得到BeanDefinition(所有Bean的抽象,存放 class名,bean工厂的名称,依赖的bean等等…)。
- 通过反射创建一个Bean实例
- 对对象中的加了@Autowired注解的属性进行属性填充
- 回调Aware方法,获取容器中的一些组件,如BeanNameAware(获取bean在IoC的注册名称),BeanFactoryAware(获取当前BeanFactory,可以调用容器服务)
- 调用BeanPostProcessor的初始化前的方法
- 调用初始化方法
- 调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
- 如果当前创建的bean是单例的则会把bean放入单例池
- 使用bean
- Spring容器关闭时调用DisposableBean的destory()方法
解释Spring支持的Bean的作用域
- singeton:默认,每个容器中只有一个bean的实例,单例模式由BeanFactory自身来维护。该对象的生命周期是与SpringIOC容器一致的(单子第一次被注入的时候才会创建)。
- prorotype:为每一个bean请求提供一个实例。在每次注入时都会创建一个新的对象
- request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说单个请求中都会复用这个对象。
- session:与request范围类似,确保每个session中有一个bean实例,在session过期后,bean会随之失效。
- application:bean被定义为ServletContext(跨IoC容器)的生命周期中复用一个单例对象。
- websocket:websocket被定义为websocket的生命周期中复用一个单例对象。
Spring框架中的单例Bean是线程安全的吗
Spring中的Bean默认是单例模式的,框架并没有对Bean进行多线程的封装处理。实际上,大部分Bean 是无状态的,在某种程度上说Bean也是安全的,但如果Bean有状态的话,就要开发者自己去保证线程安全了,最简单的就是改变bean的作用域,把“singleton”改为“prototype”,这样请求bean相当于new Bean()了,保证了线程安全。
- 有状态就是有数据存储的功能。
- 无状态就是不会保存数据。
问:Spring如何处理线程并发问题?
答:一般只有无状态的Bean蔡可以在多线程下共享,大部分是无状态的Bean。当存在有状态的Bean时,spring一般是用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,所以没有相同变量的访问冲突问题,所以可以把不安全的变量封装进ThreadLocal。
Spring框架中用到了哪些设计模式
简单工厂:有工厂类根据传入的参数,动态决定应该创建哪一类产品类。
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,会调用该Bean的getObject方法,所以返回的不是factory这个bean,而是bean.getObject()方法的返回值。
工厂方法:
实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean调用该bean时,会自动调用该bean的getObject方法,所以返回的不是factory这个bean,而实bean。getObject返回值。
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式:
Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替Controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
装饰器模式:动态地给一个对象添加一些额外的职责。就添加功能来说,Decorator模式比生成子类更灵活。
Spring中用到装饰器模式在类名上有两种表现:一种含Wrapper,另一种含Decorator。
动态代理:
切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
织入:把切面应用到目标对象并创建新的代理对象的过程。
观察者模式:
Spring事务驱动模型使用的是观察者模式,Soring中Observer模式常用的地方是listener的实现。
策略模式:
Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring框架本身大量使用了Resource接口访问底层资源。
Spring事务的实现方法、隔离级别
事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。
实现方式:
- 编程式:自己编写相关sql。
- 声明式:@Transactional
可以在某个方法上加上@Transactional开启事务,这个方法的所有sql都会在一个事务中执行,统一成功或失败。
在一个方法上加了@Transactional注解之后,Spring会基于这个类生成一个代理对象,将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务提交,若出现异常,则回滚。默认会对RuntimeException和Error进行回滚,可以利用rollbackFor属性进行配置。
隔离级别:
spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
- read uncommitted(未提交读)
- read committed(提交读、不可重复读)
- repeatable read(可重复读,Spring,Mysql默认)
- serializable(可串行化)
最终配置的隔离级别以Spring的配置为准,若数据库不支持Spring配置的隔离级别,效果取决于数据库。
Spring事务传播机制
多个事务相互调用时,事务如何在这些方法间传播。
如:方法A是一个有事务的方法,方法A执行的过程中调用了方法B,那么方法B有无事务对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行影响,
REQUIRED:Spring默认的事务传播类型, 当前没有事务,则自己新建一个事务,若当前存在事务,则加入这个事务。
SUPPORTS:当前存在事务,则加入当前事务,若没有事务,就以非事务方法执行。
MANDATORY:当前存在事务,则加入当前事务,当前事务不存在,则抛出异常。
REQUIRES_NEW:创建一个新事物,如果存在当前事务,则挂起该事务。
NOT_SUPPORTED: 以非事务方法执行,如果当前存在事务,则挂起当前事务。
NEVER:不使用事务,如果当前事务存在,则抛出异常。
NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样。
NESTED与REQUIRES_NEW的区别
REQUIRES_NEW是创建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时,会开启一个嵌套事务。在NESTED情况下,父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,不会影响新开启的事务。
NESTED与REQUIRED的区别
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚。而在NESTED情况下,被调用方发生异常时,调用方可以用catch捕获其异常。
Spring事务什么时候会失效
Spring事务的原理是AOP,进行了切面增强,那么失效的原因是这个AOP不起作用了!常用情况如下几种:
发生自调用,类里面使用this调用本类的方法,此时这个this不是代理类,而实UserService对象本身。
解决方法:不要用this去调本类方法,将this改为UserService。
方法不是public的
@Transaction只能用于public的方法上,否则事务不会失效,如果要用在非public方法上,可以开启AspectJ代理模式。
数据库不支持事务
没有被spring管理
异常被吃掉,事务不会滚(或抛出的异常没有被定义,默认为RuntimeException)
Spring循环依赖
A对象有属性对象B,B对象有属性对象A。
解决方案:提前曝光,放到缓存
创建A的Bean,把A的Bean放到缓存池中,由于A的Bean依赖B的Bean,B的Bean不存在,去创建B的Bean,B的Bean依赖A的Bean,可以直接去缓存拿,B创建完成,A创建完成。
如果A的原始对象诸如给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时对A而言,他的Bean对象其实应该是AOP之后的代理对象,B的A属性对应的并不是AOP之后的代理对象,这就产生了冲突。B依赖的A对象和最终的A对象不是一个对象。
在Bean的生命周期最后,Spring提供了BeanPostProcessor可以去对Bean进行加工,这个加工不仅仅只是能修改Bean的属性值,也可以替换掉当前Bean。也会产生B依赖的A对象和最终的A对象不是一个对象。
Spring三级缓存
SpringAOP的一般过程:A类->生成一个普通对象->属性注入->基于切面生成一个代理对象->把代理对象放到单例池中
三级缓存:
- singletonObjects:缓存某个beanName对应的经过了完整的生命周期的bean
- earlySingletonObjects:缓存提前拿原始对象进行了AOP之后得到的代理对象,原始对象还没有进行属性注入和后续的BeanPostProcessor等生命周期
- singletonFactories:缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖的Bean,那么这个工厂无用,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到的一个AOP之后的代理对象。
还有一个缓存是earlyProxyReference,它用来记录某个原始对象是否进行了AOP。
Spring中的事务是如何实现的
1、Spring事务底层是基于数据库事务和AOP机制的
2、首先对于使用了@Transaction注解的Bean,Spring会创建一个代理对象作为Bean
3、当调用代理对象的方法时,会先判断方法上是否加了@Transaction注解
4、加了则利用事务管理器创建一个数据库连接
5、修改数据库连接的autocommit属性为false,禁止自动提交
6、执行当前方法,方法中执行sql
7、执行完没有异常,直接提交事务
8、有异常并且需要回滚时回滚,否则仍提交事务
9、Spring事务的隔离级别对应是就是数据库的隔离级别
Spring容器启动流程
1、创建Spring容器
2、扫描得到所有的BeanDefinition对象,放到一个map中
3、筛选出非懒加载的单例BeanDefinition进行创建Bean,多例Bean会在每次获取Bean时利用BeanDefinition去创建
4、利用BeanDefinition创建Bean就是创建bean的生命周期
5、单例bean创建完成之后,Spring会发布一个容器启动时间
6、Spring启动结束
Spring处理全局异常的方式
使用Spring MVC提供的SimpleMappingExceptionResolver
1
2
3
4
5
6
7
8
9
10
11
12
13<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定义默认的异常处理页面,当该异常类型的注册时使用 -->
<property name="defaultErrorView" value="error"></property>
<!-- 定义异常处理页面用来获取异常信息的变量名,默认名为exception -->
<property name="exceptionAttribute" value="ex"></property>
<!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常也页名作为值 -->
<property name="exceptionMappings">
<props>
<prop key="IOException">error/ioexp</prop>
<prop key="java.sql.SQLException">error/sqlexp</prop>
</props>
</property>
</bean>实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器
- 定义一个类实现HandlerExceptionResolver接口
- 加入spring的配置中
- 只要实现了 HandlerExceptionResovler这个接口,Spring都会拦截下异常进行处理。
使用@ExceptionHandler注解实现异常处理
Bean的自动装配
开启自动装配: 在
autowired属性的五种装配方式:
no - 缺省情况下,自动装配是通过“ref”属性手动设定
byName - 根据bean的id通过setter进行自动装配
byType - 根据bean的类型进行自动装配
找到多个bean,报错,可以使用@Qualified指定
constructor - 类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型相同,则自动装配,否则导致异常。
构造函数中存在需要装配的类则进行自动装配
autodetect - 如果有构造器,则通过constructor进行装配,否则使用byType进行装配。
@Autowired自动装配bean,可以在字段、setter方法,构造函数中的使用。
Spring、SpringMVC、SpringBoot之间的区别
Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等。
SpringMVC是Spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。
SpringBoot是Spring提供的一个快速开发工具包,让程序员能更方便、更快速开发Spring+SpringMVC应用,简化了配置(约定大于配置),整合了一系列的解决方案(starter机制),redis,mongodb,es可以开箱即用。
SpringMVC
1. SpringMVC的工作流程
1、用法发送请求至前端控制器DispatcherServlet
2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。
HandlerMapping维护url到Handler的映射。
3、处理器映射器找到具体的处理器,生成处理器及处理器拦截器(如果有则生成)并返回给DispatcherServlet。
4、DispatcherServlet调用HandlerAdapter处理器适配器。
处理器可能是Controller、@RequestMapping、Servlet,需要通过适配器适配。
5、HandlerAdapter经过设配器调用具体的处理器。
6、Controller执行完成,返回ModelAndView。
7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、DispatcherServlet将ModelAndView传给ViewReslover试图解析器。
9、ViewResolver解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图。(即将模型数据填充至视图中)
11、DispatcherServlet响应用户。
SpringMVC的九大组件
Handler:处理器,它直接应对MVC中的Controller层,它的具体表现形式有很多,可以是类,也可以是方法。如Controller层中@RequestMapping标注的所有方法。只要可以实际处理请求就可以是Handler。
1、HandlerMapping
处理器映射器,类似于一个Map,存储url->handler的映射。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。
2、HandlerAdapter
处理器适配器,因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来尽心处理就是HandlerAdapter要做的事情。
类比:HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具
3、HandlerExceptionResolver
其他组件都是用来干活的,其过程中难免会出现异常。HandlerExceptionResolver根据异常设置ModelAndView,之后再交给render方法进行渲染。
4、ViewResolver
ViewResolver用来将String类型的视图名解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(或其他类型)文件。两个关键问题:使用哪个模板、使用什么技术?这是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术进行渲染,具体的渲染过程则交由不同的试图自己完成。
5、RequestToViewNameTranslator
ViewResoler根据ViewName查找View。如果Handler返回void,就需要从request获取ViewName。
6、LocaleResolver
解析试图需要两个参数,一个是视图名,一个是Locale。LocaleResolver从request解析出Locale,Locale就是zh-cn之类的,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。
两个作用:1.ViewResolver视图解析的时候。2. 用到国际化资源或主题的时候。
7、ThemeResolver
用于解析主题,SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的资源(图片,css样式)。
8、MultipartResolver
处理上传请求。将request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。
9、FlashMapManager
用于管理FlashMap,用于再redirect中传递参数
SpringBoot
SpinrgBoot相比于Spring的优势
1、依赖管理
通过设置spring-boot-starter的版本,便几乎声明了所有开发中常用的依赖的版本号。(自动版本仲裁机制)
引入非版本仲裁的jar,要写版本号。也可以在当前项目里面重写配置修改默认版本号。
2、自动配置
自动配置好tomcat、SpringMVC、Web常见功能(如:字符编码问题)。
SpringBoot自动配置原理
@SpringBootApplication三个主要的注解:
- @SpringBootConfiguration:标记当前类为配置类
- @EnableAutoConfiguration:开启自动配置
- @ComponentScan:扫描主类所在的同级包以及下级包里的Bean
其中最主要的是@EnableAutoConfiguration:
1)SpringBoot启动的时候加载了主配置类,开了自动配置@EnableAutoConfiguration
2)@EnableAutoConfiguration作用:
利用EnableAutoConfigurationImportSelector给容器导入一些组件
可以查看selectImports方法的内容:
List
configurations = getCandidateConfigurations(annotationMetadata,attributes);获取候选的配置 SpringFactoriesLoader.loadFactoryNames(); 扫描所有jar包类路径 META-INF/spring.factories 把扫描到的这些文件的内容包装成Properties对象 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
将类路径下META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值加入到了容器中。
3)每一个自动配置类进行自动配置功能
4)以 HttpEncodingAutoConfiguration配置为例:
```java
@Configuration
@EnableConfigurationProperties(HttpEncodingProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
@Configuration:标明为配置类
@EnableConfigurationProperties(HttpEncodingProperties.class)声明开启属性注入
@ConditionalOnClass(CharacterEncodingFilter.class)当CharacterEncodingFilter在类路径的条件下
@ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true)当spring.http.encoding=enabled的情况下,如果没有设置则默认为true,即条件符合
@ConditionalOnMissingBean当容器中没有这个Bean时新建Bean
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个全类名的数组,返回的全类名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。
总结:
- SpringBoot先加载所有的自动配置类xxxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认会绑定配置文件指定的值。xxxProperties
- 生效的配置类会给容器装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 定制化配置
- 直接用自己的Bean替换底层的组件
- 去看这个组件是获取的配置文件什么值,去修改
xxxAutoConfiguration —> 组件 —> xxxProperties —>application.properties
如何理解SpringBoot中的Starter
使用Spring+SpringMVC时,如果需要引入Mybatis等框架,需要到xml中定义mybatis需要的bean。
starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后再starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载配置类
开发人员只需要将对应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了。
嵌入式服务器
节省了下载安装tomcat,应用也不需要再打war包,然后放到webapp目录下运行
只需要安装了java虚拟机,就可以直接在上面部署应用程序了
springboot已经内置了tomcat.jar,运行main方法时会去启动tomcat,并利用tomcat的spi机制加载SpringMVC。
Mybatis
MyBatis的优缺点
优点:
1、基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
2、与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
3、很好地与各种数据库兼容(因为Mybatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4、能与Spring很好的集成;
5、提供映射标签,支持对象与数据库的ORM字段关系映射,提供对象关系映射标签,支持对象关系组件维护。
缺点:
1、SQL语句的编写工作量比较大,尤其当字段多、关联表多时,对开发人员编写SQL语句功底有一定的要求。
2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}和${}的区别是什么
#{}是预编译处理、是占位符
${}是字符串替换、是拼接符
Mybatis在处理#{}时,会将sql中的#{}替换为?,调用PreparedStatement来赋值
Mybatis在处理${}时,就是把${}替换为变量的值,调用Statement来赋值
#{}的变量替换是在DBMS中,变量替换后自动加上单引号
${}的变量替换是发生在DBMS外,不加单引号
使用#{}可以有效地防止SQL注入。
最佳实践相关
问:最佳实践中,通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
答:Mapper
接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个MappedStatement
。举例:com.mybatis3.mappers.StudentDao.findStudentById
,可以唯一找到 namespace 为com.mybatis3.mappers.StudentDao
下面id = findStudentById
的MappedStatement
。在 MyBatis 中,每一个<select>
、<insert>
、<update>
、<delete>
标签,都会被解析为一个MappedStatement
对象。
Dao 接口里的方法可以重载,但是Mybatis的XML里面的ID不允许重复。
Mybatis插件运行原理
Mybatis的插件,指的就是拦截器。Mabatis只支持对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是invocationHandler的invoke()方法,拦截那些你指定需要拦截的方法。
插件编写:实现MyBatis的Interceptor接口并复写intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中编写配置编写的插件。
MySQL
MySQL常见面试题
数据库三大范式
1、属性不可再分
2、非主属性对于码无部份依赖
3、非主属性对于码无传递函数依赖
MySQL索引的数据结构
索引的数据结构和具体存储引擎的实现相关。在MySQL中使用较多的索引有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现有:B+树索引。对于哈希索引来说,底层的数据结构是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择B+Tree索引。
B+树:
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互连接。在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景。
哈希索引:
哈希索引就是采用哈希算法,把键值换算成新的哈希值,检索时只需要一次哈希算法即可立即定位到相应的位置,速度非常快。
如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值:前提是键值都是唯一的。如果键值不是唯一的,就需要找到该键所在的位置,然后再根据链表往后扫描,直到找到相应的数据。
如果是范围查询检索,这是哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,可能变成不在连续的了,就没办法再利用索引完成范围查询检索。同理,哈希索引也没办法利用索引完成排序,也不支持多列联合索引的最左匹配规则。
MySQL聚簇和非聚簇索引
聚簇和非聚簇都是用的B+数。
- 聚簇索引:将数据存储和索引放到一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻的存放在磁盘上的。适合范围查询。
- 非聚簇索引:叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找数据行的位置再取磁盘查找数据,则就有点类似一本书的目录,根据目录的对应页码再去对应的页码看文章。
优势:
查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率更高
覆盖索引:只查索引,不查数据
非覆盖索引:需要查找索引之外的其他数据
聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
聚簇索引适合用在排序的场合,非聚簇索引不合适
劣势:
- 维护索引很昂贵,特别是插入新行或者主键被更新导致要(操作系统)分页的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片。
- 表因为使用UUID(随机ID)作为主键,是数据存储稀疏(无序),这就会出现聚簇索引有可能有比全表扫描更慢,索引建议使用int的auto_increment作为主键。
- 如果主键比较大的话,那么辅助索引(id)将会变得更大,因为辅助索引的叶子存储的是主键值:过长的主键值,会导致非叶子节点占用更多的物理空间。
InnoDB一定有主键,主键一定是聚簇索引,不手动设置、则会使用unique索引,没有unique索引,则会使用数据库内部的一个行的隐藏id来作为主键索引。在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要两次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不在是行的物理位置,而是主键值。
MyISAM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两颗B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
为什么MySQL选择B+数做索引
1、 B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。
2、B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3、B+树更便于遍历:由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。
4、B+树更适合基于范围的查询:B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
索引类型对数据库性能的影响
普通索引:允许被索引的数据列包含重复的值。
唯一索引:可以保证数据记录的唯一性。
主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字PRIMARY KEY来创建。
联合索引:索引可以覆盖多个数据列,如INDEX(columnA, columnB)索引。
全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术。可以通过ALTER TABLE table_name ADD FULLTEXT(column);来创建全文索引。
索引可以极大的提高数据的查询速度。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。
索引需要占物理空间,除了数据表空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变。
索引的分类
1、按照存储结构来划分:BTree索引,Hash索引、全文索引
2、从应用层次来分:普通索引、唯一索引、复合索引
3、数据的物理顺序与键值的逻辑顺序:聚集索引、非聚集索引
索引设计的原则
查询更快、占用空间更小
1、适合索引的列是出现在where子句中的列,或者连接子句中指定的列
2、基数较小的表(数据量少),索引效果较差,没必要再此列建立索引
3、使用短索引,如果对长字符串进行索引,对应指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
4、不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间越长,所以只保持需要的索引有利于查询即可。
5、定义有外键的数据列一定要建立索引。
6、更新频繁字段不适合创建索引。
7、若是不能有效区分数据的列不适合做列索引(如:性别,就两种:男、女,区分度低)
8、尽量的扩展索引,不要新建索引。如:标中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
9、对于那些查询中很少涉及的列,重复值比较多的列不要建立索引表。
10、对于定义为text、image和bit的数据类型的列不要建立索引。
索引什么时候会失效
1、 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
2、 对于多列索引,不是使用的第一部分,则不会使用索引
3、 like查询是以%开头
4、 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5、 如果mysql估计使用全表扫描要比使用索引快,则不使用索引
Mysql中一条查询语句如何执行
1、取得链接,使用使用到 MySQL 中的连接器。
2、查询缓存,key 为 SQL 语句,value 为查询结果,如果查到就直接返回。
3、分析器,分为词法分析和语法分析。此阶段只是做一些 SQL 解析,语法校验。
4、优化器,是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时候(join),决定各个表的连接顺序。
5、执行器,通过分析器让 SQL 知道你要干啥,通过优化器知道该怎么做,于是开始执行语句。
MySQL锁的类型有哪些
基于锁的属性分类:共享锁、排它锁
基于锁的粒度分类:行级锁(InnoDB)、表级锁(InnoDB、MYISAM)、页级锁(BDB引擎)、记录锁、间隙锁、临键锁。
基于所得状态分类:意向共享锁、意向排他锁
共享锁:又称读锁:当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。
共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
排他锁:又称写锁:当一个事务为数据加上写锁时,其他请求将不能为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排它锁的目的是在数据库修改时,不允许其他人同时修改,也不允许其他人读取,避免出现脏数据和脏读的问题。
表锁:表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务放了锁才能进行访问。
特点:粒度大,加锁简单,容易冲突。
行锁:行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能被访问,其他记录可正常访问。
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高
记录锁:行锁的一种,记录锁只是表中的某一条记录。精准条件命中,并且命中的条件字段是唯一索引。
加了记录锁之后数据可以避免在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
页锁:粒度介于行锁和表锁之间的一种锁,一次锁定相邻的一组记录。
开销在表锁与行锁之间;会出现死锁
间隙锁:行锁的一种,锁定的是表记录的某一个区间,遵循左开右闭 (]
临键锁:行锁的一种,是InnoDB的行锁默认算法,总给来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有空隙空间也锁住。
如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排它锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,便面了整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。
- 意向共享锁:当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
- 意向排它锁:当一个事务试图对整个表进行加排他锁之前,首先需要获得这个表的意向排他锁。
MySQL锁升级
- MySQL 行锁只能加在索引上,如果操作不走索引,就会升级为表锁。因为 InnoDB 的行锁是加在索引上的,如果不走索引,自然就没法使用行锁了,原因是 InnoDB 是将 primary key index和相关的行数据共同放在 B+ 树的叶节点。InnoDB 一定会有一个 primary key,secondary index 查找的时候,也是通过找到对应的 primary,再找对应的数据行。
- 当非唯一索引上记录数超过一定数量时,行锁也会升级为表锁。测试发现当非唯一索引相同的内容不少于整个表记录的二分之一时会升级为表锁。因为当非唯一索引相同的内容达到整个记录的二分之一时,索引需要的性能比全文检索还要大,查询语句优化时会选择不走索引,造成索引失效,行锁自然就会升级为表锁。
隐式转换
隐式转换会导致索引不可用!
等号两边类型不一致会发生隐式转换, cast(index_filed as signed),然后和2进行比较。因为’2’,’2’,’2a’都会转化成2 ,故Mysql无法使用索引只能全表扫描。
发生隐式转换的情况:
- 如果字符串的第一个字符就是非数字的字符,那么转换为数字就是0
- 如果字符串以数字开头
- 如果字符串中都是数字,那么转换为数字就是整个字符串对应的数字
- 如果字符串中存在非数字,那么转换为的数字就是开头的那些数字对应的值
SQL分类
- 数据查询语言(DQL): 负责进行数据查询而不会对数据本身进行修改的语句,这是最基本的SQL语句。
- 数据定义语言(DDL): 负责数据结构定义与数据库对象定义的语言,由CREATE、ALTER与DROP三个语法所组成。
- 数据操纵语言(DML): 负责对数据库对象运行数据访问工作的指令集,以INSERT、UPDATE、DELETE三种指令为核心
- 事务处理语言(TPL): 它的语句能确保被DML语句影响的表的所有行及时得以更新。TPL语句包括BEGIN TRANSACTION,COMMIT和ROLLBACK。
- 数据控制语言(DCL): 一种可对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。 有GRANT和REVOKE两个指令组成。
- 指针控制语言(CCL): 用于对一个或多个表单独行的操作。
InnoDB是如何实现事务的
通过Buffer Poll,LogBuffer,RedoLog,UndoLog来实现事务
1、收到一个update语句后,会根据条件找到数据所在的页
2、执行update语句,修改BufferPoll中的数据,也就是内存中的数据
3、针对update语句生成一个RedoLog对象,并存入LogBuffer中
4、针对update语句生成undolog日志,用于事务回滚
5、事务提交,则把RedoLog对象进行持久化,后续还有其他机制将BufferPool中所修改的数据页持久化到磁盘中
6、如果事务回滚,则利用undolog日志进行回滚
索引下推
索引下推(index condition pushdown )简称ICP,在Mysql5.6的版本上推出,用于优化查询。
在不使用ICP的情况下,在使用非主键索引进行查询时,存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件 。
在使用ICP的情况下,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
怎么优化慢查询
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时。
查询慢的原因:1.是查询条件没有命中索引?2.是load了不需要的数据列?3.是数据量太大?
优化:
- 首先分析语句,看看是否load了额外的数据列,可能是查询了多余的行并且丢弃掉了,可能是加载了许多结果中不需要的列,对sql语句分析以及重写。
- 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
- 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。
事务的基本特性和隔离级别
基本特性:ACID,分别是:
原子性:一个事务中的操作要么全部成功,要么全部失败
一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态。如A给B转账100元,假设A只有90元,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证。
隔离性:一个事务的修改在最终提交之前,对其他事务是不可见的。
持久性:事务一旦提交,所做的修改就会永久保存到数据库中。
隔离性的4个隔离级别:
read uncommit: 读未提交,可能会督导其他事务未提交的数据,也叫脏读。
read commit:读已提交,两次读取的结果不一致,叫不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。
repeatable read: 可重复读,mysql默认,每次读取结果都一样,可能产生幻读。
serializable: 串行,一般是不会使用的,给每一行读取的数据加锁,会导致大量超时和锁竞争问题。
脏读:某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读:在一个事务的两次查询之中数据不一致,这可能是两次查询过程中插入了一个事务更新的原有的数据。
幻读:在一个事务的两次查询中数据不一致,例如有一个事务查询了几列数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前没有的。
ACID靠什么保证的
A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql。
C一致性由其他三大特性保证,程序代码要保证业务上的一致性
I隔离性由MVCC保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复。redo log 记录的是物理层面的数据页、偏移量。应对的问题是:MySQL异常宕机后,如何将没来得及提交的事物数据重作出来。
InnoDB redo log 写盘, InnoDB事务进入prepare状态
如果前面prepare成功,bin log写盘,再继续将事务日志持久化到bin log,如果持久化成功,那么InnoDB事务则进入commit状态(在redo log里面写一个commit记录)
redo log的刷盘会在系统空闲时进行
MVCC
推荐视频:
MVCC:多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链
MVCC只在READ COMMITTED和REPEATABLE READ两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
聚簇索引记录中有两个必要的隐藏列:
trx_id: 用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer:每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
已提交读和可重复读的区别就在于他们生成的ReadView的策略不同
开启事务时会创建ReadView,ReadView维护当前事务的事务id,即未提交的事务id,排序生成一个数组
访问数据,获取数据中的事务id(获取的是事务id最大的记录),对比readview
如果在ReadView的左边(比ReadView都小),可以访问(意味着该事务已经提交)
如果在ReadView的右边(比ReadView都大)或者就在ReadView中,不可以访问,获取roll_pointer,取上一个版本重新对比(在右边意味着,该事务在ReadView生成之后出现,在ReadView中意味着该事务还未提交)
已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView。
可重复读隔离级别则是在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
Redis
Redis事务实现
Redis单条命令保证原子性,但是无rollback机制,所以Redis的命令保证原子性,但是事务不保证原子性,Redis事务可以一次执行多个命令,并且带有以下三个重要保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存(顺序性)。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行 (一次性)
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中(排他性)
一个事务从开始到执行会经历三个阶段:
- 开启事务(multi)
- 命令入队
- 执行事务(exec, 放弃事务 discard)
异常的种类
- 编译异常,就是代码错误,没有这命令,这个事务会全部失败
- 运行时异常,语法没错误,但执行时出错,除了有异常的语句,其他的都可以继续执行。
Redis实现乐观锁
WATCH key [key …]监视一个(或多个)key,如果在事务执行之前这个key被其他命令改动,那么事务将被打断,整个事务都会失败。如果修改失败,使用UNWATCH取消监视,然后再使用watch获得最新的数据,再执行事务。
Redis内存淘汰机制
Redis 提供 6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
Redis持久化
RDB(Redis DataBase)
redis是内存型数据库,如果不持久化,根据内存断电即失的特点,我们就会失去我们保存好的数据
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快 照文件直接读到内存里。 Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程 都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。 这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
如何触发RDB快照:
1、配置文件中默认的快照配置,建议多用一台机子作为备份,复制一份dump.rdb
2、save和bgsave
Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。保存成功时返回 OK 。
BGSAVE 命令执行之后立即返回 OK , 然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
客户端可以通过 LASTSAVE 命令查看相关信息,判断 BGSAVE 命令是否执行成功。
3、执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
4、退出的时候也会产生dump.rdb文件
如何恢复
1、将备份文件移动到redis安装目录并启动服务即可
2、CONFIG GET dir获取目录
优点
1、适合大规模的数据恢复
2、对数据完整性和一致性要求不高
缺点
1、在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后所有的修改
2、Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
AOF(Append Only File)
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件 但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
恢复步骤
正常恢复:启动:设置yes,修改默认的appendonly no,改为yes,将有数据的aof文件复制一份保存到对应目录,恢复:重新启动redis然后重新加载。
异常恢复:启动:设置yes,故意破坏 appendonly.aof 文件! 修复:redis-check-aof –fix appendonly.aof进行修改恢复:重启redis然后重新加载
重写机制
AOF 文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再 rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,这点和快照有点类似! Redis会启动AOF文件的内容压缩
,只保留可以恢复数据的最小指令集。
触发机制
1、手动触发: 执行bgrewriteaof命令。
2、配置自动触发
auto-aof-rewrite-min-size 表示运行AOF重写是文件最小的大小。默认64M,小于64M就会不自动重写了。
auto-aof-rewrite-percentage 表示(aof_current_size- aof_base_size) / aof_base_size 的比值。 aof文件重写之后当前文件大小增长多少就触发重写
优缺点
优点:
1、每修改同步:appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差 但数据完整性比较好
2、每秒同步: appendfsync everysec 异步操作,每秒记录 ,如果一秒内宕机,有数据丢失
3、不同步: appendfsync no 从不同步
缺点:
1、相同数据集的数据而言,aof 文件要远大于 rdb文件,恢复速度慢于 rdb。
2、Aof 运行效率要慢于 rdb,每秒同步策略效率较好,不同步效率和rdb相同。
持久化总结
1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始 的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重 写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB文件保存的数据集要完整。 RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
5、性能建议 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够 了,只保留 save 900 1 这条规则。 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产 生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite 的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重 写可以改到适当的数值。 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也 减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据, 启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。
Redis发布和订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
下表列出了 redis 发布订阅常用命令:
序号 | 命令及描述 |
---|---|
1 | PSUBSCRIBE pattern [pattern …] 订阅一个或多个符合给定模式的频道。 |
2 | [PUBSUB subcommand [argument [argument …]]]( 查看订阅与发布系统状态。 |
3 | [PUBLISH channel message] 将信息发送到指定的频道。 |
4 | [PUNSUBSCRIBE [pattern [pattern …]]] 退订所有给定模式的频道。 |
5 | [SUBSCRIBE channel [channel …]] 订阅给定的一个或多个频道的信息。 |
6 | [UNSUBSCRIBE [channel [channel …]]] 指退订给定的频道。 |
Redis主从复制
主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力! 架构中经常使用! |
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点) ,但一个从节点只能有一个主节点。
作用:
1、 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点)
分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
4、高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
复制原理
- Slave启动成功连接到master后会发送一个sync同步命令,Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master将传送整个数据文件到slave ,并完成一次完全同步。
- 全量复制:而slave服务在接收到数据库文件数据后, 将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave ,完成同步
- 但是只要是重新连接master , 一次完全同步(全量复制)将被自动执行!我们的数据-定可以在从机中看到!
哨兵模式
哨兵模式是一种特殊的模式,首先Redis
提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis
服务器响应,从而监控运行的多个Redis
实例。
- 通过发送命令,让
Redis
服务器返回监控其运行状态,包括主服务器和从服务器。 - 当哨兵监测到
master
宕机,会自动将slave
切换成master
,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
故障切换(failover)的过程
- 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。
- 当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。
- 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
优点
1、 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2、 主从可以切换,故障可以转移,系统的可用性就会更好
3、 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点
1、 Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就很麻烦
2、 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
缓存穿透和雪崩
概念:
- 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
- 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
- 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,大量的请求就会集中到数据库里面这样就会造成数据库压力过大。
缓存击穿和缓存穿透的区别:
- 缓存击穿:指一个key是热点数据,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发就击穿缓存,直接请求数据库,数据库扛不住这么大并发量,可能会崩溃。
- 缓存穿透:key不存在,大量的请求在缓存中获取不到,数据库也获取不到,但请求都回到数据库,从而可能压垮数据库。
解决方案:
1、Redis高可用: 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
2、限流降级: 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
3、数据预热: 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
Redis实现分布式锁
1、利用setnx命令 + del key命令,加锁解锁。
问题:如果业务抛出异常没有及时释放锁,进程挂了,没有机会释放锁。
2、给锁设置一个过期时间,set key value ex 10 nx,在finally层释放锁。
问题: 业务执行的时间超过了锁的过期时间,导致锁被人拿走,然后自己执行到finally把别人锁释放掉。
3、加锁时,设置自己的唯一标识(UUID)。释放掉锁前判断(注意命令保证原子性)
问题: 锁过期时间不好判断
4、Redisson是一个Java语言实现的Redis SDK客户端,Redisson 封装了很多易用的功能 可重入锁、乐观锁、公平锁、读写锁、Redlock。 使用Redisson,它采用了自动续期的方案来避免锁过期,也就是看门狗线程。
问题: 以上的场景都是锁在单个Redis实例可能产生的问题,Redis集群是AP,在master-salve模式中异步赋值会造成信息丢失,锁丢失。
5、放弃master-salve模式,引入N个节点,官方建议是5个。客户端超过半数的redis实例才算获取到锁。
计算机网络
进程和线程、协程的区别
1、进程: 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
2、线程: 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
3、协程: 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
三次握手和四次挥手
SYN标志位:发起一个新的连接
ACK标志位:确认标志位,1位,表示确认序号是否有效
FIN标志位:结束一个连接
三次握手:
第一次握手:客户端发送,SYN位置1,生成序号(连接后,序号递增)
第二次握手:服务器发送,SYN位置1,生成序号(连接后,序号递增),ACK位置1,确认序号=客户端序号+1
第三次握手:客户端发送,ACK位置1,确认序号=服务器序号+1
四次挥手:
第一次挥手:客户端发送,FIN位置1,发送序号
第二次挥手:服务器发送,ACK位置1,确认序号=客户端序号+1
第三次挥手:服务器发送,FIN位置1,发送序号
第四次挥手:客户端发送,ACK位置1,确认序号=服务器序号+1
问:是否了解TCP/IP中三次握手和四次挥手机制吗?
答:TCP/IP协议是传输层的一个面向连接的安全可靠的传输协议
三次握手:
三次握手的机制是为了保证能建立一个安全可靠的连接。
第一次握手是由客户端发起,客户端会向服务端发送一个报文,在报文里面:SYN标志位置为1,表示发起新的连接。
当服务器收到这个报文之后就知道客户端要和我建立一个新的连接,于是服务端就向客户端发送一个确认消息包,在这个消息包里面:ACK标志位置1,表示确认客户端发起的第一次连接请求。
以上两次握手之后,对客户端而言:已经明确了我既能给服务端成功发送消息,也能收到服务端的响应,但对服务端而言:服务端只知道客户端给我发送的消息我能收到,但我相应给客户端的消息,客户端能否收到我是不知道的。所以还需要第三次握手。
第三次握手就是当客户端收到服务端发送的确认响应报文之后,还要继续给服务端进行回应,也是一个ACK标志位置1的确认消息。经过这三次连接,客户端和服务端都能确认我既能给对方发消息,也能收到对方发送的消息。那么这个连接就被安全的建立了。
四次挥手:
四挥挥手的机制是为了保证能安全的结束连接。
第一次挥手是客户端发起,客户端会向服务器端发送一个报文,在报文中:FIN标志位置1,表示客户端已准备结束当前连接。
第二次挥手是服务器发起,服务器会向客户端发送一个确认消息包,在这个消息包中,ACK标志位置1,表示确认接收到了客户端的请求。
第三次挥手是服务器发起,服务器会向客户端发送一个报文,报文中FIN标志位置1,表示服务器已准备结束当前连接。
第四次挥手是客户端发起,客户端会向服务器发送一个确认消息包,其中ACK标志位置1,表示确认收到了服务器的消息。
问:为什么是四次挥手,不能是三次?
答:第二次挥手后,服务器虽然收到了客户端的断开连接的请求,但是其仍可能需要向客户端发送一些消息,在这些消息发完之后才会断开连接,执行第三次挥手。所以接收断开连接请求和断开连接要分两次发送,第一次是确认收到消息,第二次才是执行断开连接,中间可能会继续向客户端发送一些消息。
TCP如何保证可靠传输
校验和:在发送端和接收端分别计算数据的校验和,如果两者不一致,则说明数据在传输过程中出现了差错,TCP将丢弃和不确认此报文段。
序列号:TCP会对每一个发送的字节进行编号,接收方接到数据后,会对发送方发送确认应答(ACK报文),并且这个ACK报文中带有相应的确认编号,告诉发送方,下一次发送的数据从编号多少开始发。如果发送方发送相同的数据,接收端也可以通过序列号判断出,直接将数据丢弃。
超时重传:在上面说了序列号的作用,但如果发送方在发送数据后一段时间内(可以设置重传计时器规定这段时间)没有收到确认序号ACK,那么发送方就会重新发送数据。
这里发送方没有收到ACK可以分两种情况,如果是发送方发送的数据包丢失了,接收方收到发送方重新发送的数据包后会马上给发送方发送ACK;如果是接收方之前接收到了发送方发送的数据包,而返回给发送方的ACK丢失了,这种情况,发送方重传后,接收方会直接丢弃发送方冲重传的数据包,然后再次发送ACK响应报文。
如果数据被重发之后还是没有收到接收方的确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长,直到最后关闭连接。
流量控制:如果发送端发送的数据太快,接收端来不及接收就会出现丢包问题。为了解决这个问题,TCP协议利用了滑动窗口进行了流量控制。在TCP首部有一个16位字段大小的窗口,窗口的大小就是接收端接收数据缓冲区的剩余大小。接收端会在收到数据包后发送ACK报文时,将自己的窗口大小填入ACK中,发送方会根据ACK报文中的窗口大小进而控制发送速度。如果窗口大小为零,发送方会停止发送数据。
拥塞控制:如果网络出现拥塞,则会产生丢包等问题,这时发送方会将丢失的数据包继续重传,网络拥塞会更加严重,所以在网络出现拥塞时应注意控制发送方的发送数据,降低整个网络的拥塞程度。拥塞控制主要有四部分组成:慢开始、拥塞避免、快重传、快恢复,如下图(图片来源于网络)。
这里的发送方会维护一个拥塞窗口的状态变量,它和流量控制的滑动窗口是不一样的,滑动窗口是根据接收方数据缓冲区大小确定的,而拥塞窗口是根据网络的拥塞情况动态确定的,一般来说发送方真实的发送窗口为滑动窗口和拥塞窗口中的最小值。
慢开始:为了避免一开始发送大量的数据而产生网络阻塞,会先初始化cwnd为1,当收到ACK后到下一个传输轮次,cwnd为2,以此类推成指数形式增长。
拥塞避免:因为cwnd的数量在慢开始是指数增长的,为了防止cwnd数量过大而导致网络阻塞,会设置一个慢开始的门限值ssthresh,当cwnd>=ssthresh时,进入到拥塞避免阶段,cwnd每个传输轮次加1。但网络出现超时,会将门限值ssthresh变为出现超时cwnd数值的一半,cwnd重新设置为1,如上图,在第12轮出现超时后,cwnd变为1,ssthresh变为12。
快重传:在网络中如果出现超时或者阻塞,则按慢开始和拥塞避免算法进行调整。但如果只是丢失某一个报文段,如下图(图片来源于网络),则使用快重传算法。
从上图可知,接收方正确地接收到M1和M2,而M3丢失,由于没有接收到M3,在接收方收到M5、M6和M7时,并不会进行确认,也就是不会发送ACK。这时根据前面说的保证TCP可靠性传输中的序列号的作用,接收方这时不会接收M5,M6,M7,接收方可以什么都不会,因为发送方长时间未收到M3的确认报文,会对M3进行重传。除了这样,接收方也可以重复发送M2的确认报文,这样发送端长时间未收到M3的确认报文也会继续发送M3报文。
但是根据快重传算法,要求在这种情况下,需要快速向发送端发送M2的确认报文,在发送方收到三个M2的确认报文后,无需等待重传计时器所设置的时间,可直接进行M3的重传,这就是快重传。(面试时说这一句就够了,前面是帮助理解)
快恢复:从上上图圈4可以看到,当发送收到三个重复的ACK,会进行快重传和快恢复。快恢复是指将ssthresh设置为发生快重传时的cwnd数量的一半,而cwnd不是设置为1而是设置为为门限值ssthresh,并开始拥塞避免阶段。
拥塞窗口和滑动窗口
滑动窗口:接受数据端使用的窗口大小,用来告知发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。
对于数据的发送端就是拥塞窗口了,拥塞窗口不代表缓存,拥塞窗口指某一源端数据流在一个RTT内可以最多发送的数据包数 。
RTT=发送到接收到ACK的时间
TCP沾包
- TCP传输数据基于字节流, 从应用层到 TCP 传输层的多个数据包是一连串的字节流是没有边界的,而且 TCP 首部并没有记录数据包的长度,所以 TCP 传输数据的时候可能会发送粘包和拆包的问题;
- 而 UDP 是基于数据报传输数据的,UDP 首部也记录了数据报的长度,可以轻易的区分出不同的数据包的边界。
沾包和拆包的原因:
- TCP 发送缓冲区剩余空间不足以发送一个完整的数据包,将发生拆包
- 要发送的数据超过了最大报文长度的限制,TCP 传输数据时进行拆包
- 要发送的数据包小于 TCP 发送缓冲区剩余空间,TCP 将多个数据包写满发送缓冲区一次发送出去,将发生粘包
- 接收端没有及时读取 TCP 发送缓冲区中的数据包,将会发生粘包
解决沾包:
1、每次发送设置带消息头的协议,存储开始的标志以及消息长度
2、每次发送定长的消息。
3、设置消息边界,结束的时候给标志位。
UDP
UDP
报头包括4个字段,每个字段占用2个字节(即16个二进制位)
- 源端口、目的端口、长度、校验和
特点:
- UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务
- 传输途中出现丢包,UDP 也不负责重发
- 当包的到达顺序出现乱序时,UDP没有纠正的功能
- 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为
如何让UDP实现高可靠
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
实现确认机制、重传机制、窗口确认机制。
发送: 包的分片、包确认、包的重发
接收: 包的调序、包的序号确认
UDP快在哪里
1、校验和: 对于 TCP 和 UDP,都实现了校验和算法,但二者的区别是,TCP 如果发现校验核对不上,也就是数据损坏,会主动丢失这个封包并且重发。而 UDP 什么都不会处理,UDP 把处理的权利交给使用它的程序员。
2、TCP三次握手建立连接,UDP不用建立连接。
3、超时重传、丢失重传等一系列重传机制,导致速度变慢。
IP生存周期
生存周期表示一个Internet报文生存期的上限,由报文的发送者来设置。可以把生存周 期看作是数据库包的寿命计数器。为了防止数据包在网络中无休止地被传递下去,或者由于 传输路径造成死循环,每个 IP 数据包中都包含一个寿命计数器。数据包在网络传输的过程 中,每经过一个路由器的处理,其中的寿命计数器就会递减1。如果寿命计数器的值等于0, 并且报文还没有到达目的地,则该报文将会被丢失。
寿命计数器TTL,默认为255。
路由器和交换机
工作层次不同:
路由器工作在网络层,根据IP地址转发数据,可以处理TCP/IP数据, 具有路由功能,用于连接内网和外网;
交换机工作在数据链路层,根据MAC地址转发数据帧,所连接的终端属于同一个网段,不用经过路由器就可以进行数据的转发。
用途不同:
- 路由器用于连接内网和外网,将内网的数据包通过路由功能转发到外网,实现内网和外网的互通
- 交换机用于连接内网的终端,使用了同一个网段,比如192.168.1.0,不同主机之间交换数据通过MAC地址识别。两个主机通信,首先发送ARP数据包,就IP地址转换为MAC地址才能进行相互通信。
组网位置不同
- 路由器部署在网络出口的位置,一般只有一个后者两个互为备份,用于连接内网和外网
- 交换机的数量就不确定了,根据用户的多少,可能由几百台交换机,完成用户终端的接入,是局域网组网的核心设备
Cookie和Session
Cookie 一般用来保存用户信息,存在浏览器中。再次访问自动登录
Session 的主要作用就是通过服务端记录用户的状态。购物车。
问:如果浏览器的cookie禁用,session还能使用吗?
答:不能使用. 因为session是基于cookie的. cookie存储着sessionid
OSI七层模型
- 物理层:通信信道上的原始比特流传输。
- 数据链路层:物理寻址,将原始比特流转变为逻辑传输线路。
- 网络层:控制子网的运行,如逻辑编址、分组传输、路由选择
- 传输层:接收上一层的数据,在必要的时候把数据进行分割,并将这些数据交给网络层,并保证这些数据段有效到达对端。
- 会话层:不同机器上的用户之间建立及管理会话。
- 表示层:信息的语法语义及其关联,如加密解密、转换翻译、压缩解压缩。
- 应用层:HTTP、FTP、POP3
HTTP请求的过程
1、域名解析
- 查询浏览器缓存(缓存只有一分钟)
- 查询路由器缓存
- 查询DNS缓存
2、向Web服务器发送一个HTTP请求
- 建立TCP,建立TCP时,需要发送数据,发送数据在网络层使用IP协议
- OSPF:IP数据包在路由器之间,路由选择使用OSPF (Open Shortest Path First,ospf)开放最短路径优先协议
- ARP:路由器在与服务器通信时,需要将ip地址转换为mac地址,用到了ARP协议。
- TCP建立完成之后,使用HTTP协议访问页面
3、服务器处理请求,返回一个HTML响应
4、浏览器解析HTML,渲染页面
- 解析HTML文件,创建DOM数
- 解析CSS,形成CSS对象模型
- 将CSS和DOM合并,构建渲染树
- 页面的布局和绘制
HTTP
HTTP是超文本传输协议, 是一种用于在Internet上传输超文本的传输协议。
请求报文的结构
- 第一行是包含了请求方法、URL、协议版本;
- 接下来的多行都是请求首部 Header,每个首部都有一个首部名称,以及对应的值。
- 一个空行用来分隔首部和内容主体 Body
- 最后是请求的内容主体
请求的头部信息
Accept:浏览器能够处理的内容类型
Accept-Charset:浏览器能够显示的字符集
Accept-Encoding:浏览器能够处理的压缩编码
Accept-Language:浏览器当前设置的语言
Connection:浏览器与服务器之间连接的类型
Cookie:当前页面设置的任何Cookie
Host:发出请求的页面所在的域
Referer:发出请求的页面的URL
User-Agent:浏览器的用户代理字符串
空行
请求体
请求数据
响应报文
- 第一行包含协议版本、状态码以及描述,最常见的是 200 OK 表示请求成功了
- 接下来多行也是首部内容
- 一个空行分隔首部和内容主体
- 最后是响应的内容主体
响应的头部信息
Age:推算资源创建经过时间
Cache-Control:控制HTTP缓存
Connection:浏览器与服务器之间连接的类型
Content-Encoding:适用的编码方式
Content-Type:表示后面的文档属于什么MIME类型
Date:表示消息发送的时间,时间的描述格式由rfc822定义
ETag:资源的匹配信息
Expires:提供一个日期和时间,响应在该日期和时间后被认为失效
Last-Modified:资源的最后修改日期时间
server:服务器名字
HTTP协议是基于TCP协议来实现的,简单来说http需要可靠的传输,而TCP是一个面向连接的、可靠的传输层协议,一般http默认使用的是TCP的80端口。
HTTP状态保持
1、基于Session实现会话保持
会话开始时,服务器会把会话状态保存起来(一般存在内存),然后给会话分配一个会话标识(SessionID)给客户端,会话标识一般保存在Cookie里,之后每次浏览器发请求时都会带上SessoinID,服务器拿到SessionID后就可以把之前存在服务端的状态信息与该会话联系起来。
若Cookie禁用,可通过url重写的方法把会话标识放在url参数里。
2、基于Cookie实现会话保持
完全将会话信息保存在浏览器Cookie中,每次浏览器发送HTTP请求的时候都会带上状态信息。
以购物车应用为例,服务端把商品信息加密后放在Cookie中,这样服务端就知道你在浏览的过程中添加了哪些商品到购物车。
HTTP各个版本的区别
HTTP1.0和1.1的区别
1、HTTP1.1默认使用长连接,HTTP1.1的持续连接有流水线方式和非流水线方式。流水线方式是客户端收到HTTP的响应报文之前就能接着发送新的请求报文。 与之相对应的非流水线方式是客户在收到前一个响应后才能发送下一个请求。
2、错误状态响应码: 在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
3、缓存处理: 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
4、 带宽优化及网络连接的使用 :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
HTTP和HTTPS的区别
1、端口:HTTP 的 URL 由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。
2、安全性和资源消耗: HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
HTTPS请求的过程
1、客户端向服务器发起HTTPS请求,连接到服务器的443端口
2、服务器将非对称加密的公钥传递给客户端,以证书的形式传到客户端
3、客户端接受到该公钥进行验证,就是验证证书,如果有问题,则HTTPS请求无法继续;如果没有问题,则上述公钥是合格的。(第一次HTTP请求)客户端这个时候随机生成一个私钥,成为client key,客户端私钥,用于对称加密数据的。使用前面的公钥对client key进行非对称加密
4、进行第二次HTTP请求,将加密之后的client key传递给服务器
5、服务器使用私钥进行解密,得到client key,使用client key对数据进行对称加密
6、将对称加密的数据传递给客户端,客户端使用非对称解密,得到服务器发送的数据,完成第二次HTTP请求
ARP
ARP(Address Resolution Protocol,地址解析协议)
每台主机都会在自己的ARP缓冲区中建立一个 ARP列表,以表示IP地址和MAC地址的对应关系。当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP列表中是否存在该 IP地址对应的MAC地址,如果有,就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。
此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个 ARP响应数据包,告诉对方自己是它需要查找的MAC地址;源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。
设计模式
设计模式六大原则
1、接口隔离原则(一个类对另一个类的依赖应该建立在最小的接口上,客户端不应该被迫依赖于它不使用的方法)
2、单一职责原则(一个类应该有且仅有一个引起它变化的原因)
3、依赖倒置原则(要面向接口编程,不要面向实现编程)
4、迪米特法则(只与直接朋友交谈,不与”陌生人”说话)
5、里氏替换原则(子类可以扩展父类的功能,但不能改变父类原有的功能)
6、开闭原则(对扩展开放,对修改关闭)
单例模式
1 | // 懒汉式单例模式 |
工厂模式
简单工厂模式
1 | // 简单工厂: |
工厂方法模式
1 | // 工厂方法模式 |
简单工厂与工厂方法结论
结构复杂度: simple
代码复杂度: simple
编程复杂度: simple
管理上的复杂度: simple
根据设计原则: 工厂方法模式
根据实际业务: 简单工厂模式
抽象工厂模式
1 | // 1. 定义产品接口 |
抽象工厂模式:提供了一个创建一些列相关或者相互依赖对象的接口,无需指定他们的实现类。
适用场景:
- 客户端(应用层)不依赖与产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品簇)一起使用创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖具体的实现。
优点:
- 具体产品在应用层的代码隔离,无需关心创建的细节
- 将一个系列的产品统一到一起创建。
缺点:
- 规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难(如加一个生产笔记本电脑,需要改动大量代码)
- 增加了系统的抽象性和理解难度
三种工厂模式比较
- 简单工厂模式(静态工厂模式):
- 虽然某种程度上不符合设计原则,但实际使用最多。
- 优点:解耦,能应对需求变更
- 缺点:扩展时需要修改工厂类,不符合开闭原则
- 工厂方法模式
- 不修改已有类的前提下,通过增加新的工厂类实现扩展
- 优点:解耦,能应对需求变更,扩展不需要修改旧的类
- 缺点:每次扩展的编码量提升,需要对应多写一个工厂类
- 抽象工厂模式
- 不可以增加产品,可以增加产品簇
- 优点:解耦,在现有系列之间切换灵活
- 缺点:扩展产品时很麻烦,修改从头到尾
建造者模式
- 介绍
- 使用多个简单的对象一步一步构建成一个复杂的对象,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 允许用户只通过指定复杂对象的类型和内容就可以构建它们,不需要知道内部的具体构建细节
- 场景举例
- KFC创建套餐:套餐是一个复杂对象,它一般包含主食如汉堡、烤翅等和饮料 如果汁、 可乐等组成部分,不同的套餐有不同的组合,而KFC的服务员可以根据顾客的要求,一步一步装配这些组成部分,构造一份完整的套餐
- 电脑有低配、高配,组装需要CPU、内存、电源、硬盘、主板等
- 核心组成
- Builder:抽象建造者,定义多个通用方法和构建方法
- ConcreteBuilder:具体建造者,可以有多个
- Director:指挥者,控制整个组合过程,将需求交给建造者,由建造者去创建对象
- Product:产品角色
1 | // 案例 |
- 建造者模式优点
- 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,更加精细地控制产品地创建过程
- 增加新的具体建造者无需修改原有库类的代码,符合开闭原则
- 建造者模式结合链式编程来使用,代码上更加美观
- 建造者模式缺点
- 建造者模式所创建的产品一般具有较多的共同点,如果产品差异大则不建议使用
- JDK中的使用
- tcp传输协议protobuf生成的api、java中的StringBuilder(不完全一样,思想一样)
- 建造者模式与抽象工厂模式的比较
- 建造者模式返回一个组装好的完整产品 , 抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族
- 建造者模式是把对象的创建分散开来,每个抽象方法负责其中的一部分。抽象工厂是每个方法负责一个产品族。
- 建造者模式所有函数加到一起才能生成一个对象。抽象工厂一个函数生成一个对象。
原型模式
- 介绍
- 是一种对象创建型模式,使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,主要用于创建重复的对象,同时又能保证性能
- 工作原理是将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程
- 应该是最简单的设计模式了,实现一个接口,重写一个方法即完成了原型模式
- 核心组成
- Prototype: 声明克隆方法的接口,是所有具体原型类的公共父类,Cloneable接口
- ConcretePrototype : 具体原型类
- Client: 让一个原型对象克隆自身从而创建一个新的对象
- 应用场景
- 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得
- 如果系统要保存对象的状态,做备份使用
1 | // 案例 |
注意:
- 当克隆执行完成后,实际上相当于新 new一个Person 对象并为其分配了新的存储地址及引用,因此person1 和 person2 的地址引用不同;
- 对于复杂对象,当进行克隆时,实际上是从被拷贝对象中拷贝了其引用地址,并非new一个出来
- 因此,二者是共享一个相同地址引用的复杂对象,这种情况被称为浅拷贝。
浅拷贝
- 如果原型对象的成员变量是基本数据类型,将复制一份给克隆对象。
- 如果原型对象的成员变量是引用类型,将引用对象的地址复制一份给克隆对象
- 也就是说 原型对象和克隆对象的成员变量指向相同的内存地址
- 通过覆盖Object类的clone()方法可以实现浅克隆
深拷贝
- 无论原型对象的成员变量是基本数据类型还是引用类型,都将复制一份给克隆对象,如果需要实现深克隆,可以通过序列化(Serializable)等方式来实现
原型模式优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,可以提高新实例的创建效率
- 可辅助实现撤销操作,使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用恢复到历史状态
原型模式缺点
- 需要为每一个类配备一个克隆方法,对已有的类进行改造时,需要修改源代码,违背了“开闭原则”
- 在实现深克隆时需要编写较为复杂的代码,且当对象之间存在多重的嵌套引用时,需要对每一层对象对应的类都必须支持深拷贝
深拷贝的实现
1 | //首先Person对象实现Serializable接口,然后自定义深拷贝方法 deepClone(): |
适配器模式
- 介绍
- 见名知意,是作为两个不兼容的接⼝之间的桥梁,属于结构型模式
- 适配器模式使得原本由于接⼝不兼容⽽不能⼀起⼯作的那些类可以⼀起⼯作
- 常见的几种适配器
- 类的适配器模式:想将一个类转换成满足另一个新接口的类时,剋使用类的适配器模式,创建一个新类, 继承原有的类,实现新的接⼝即可
- 对象的适配器模式:想将一个对象转换成满足另一个新接口的对象时,可以创建一个适配器类,持有原类的一个实例, 在适配器类的⽅法中,调⽤实例的⽅法就⾏
- 接口的适配器模式:不想实现一个接口中所有的方法时,可以创建一个Adapter,实现所有方法,在写别的类的时候,继承Adapter方法即可。
- 应用场景
- 电脑需要读取内存卡的数据,读卡器就是适配器
- 系统需要使⽤现有的类,⽽这些类的接⼝不符合系统的需要
- JDBC就是我们⽤的最多的适配器模式
- JDBC给出⼀个客户端通⽤的抽象接⼝,每⼀个具体数据库⼚商
- 如 SQL Server、Oracle、MySQL等,就会开发JDBC驱动
- 就是⼀个介于JDBC接⼝和数据库引擎接⼝之间的适配器软件
桥接模式
- 介绍
- 适配器模式类似,包括以后经常会遇到意思接近⼀样的设计模式,因为⼤神往往就是多个模式混⽤,且根据不 同的场景进⾏搭配,桥接设计模式也是结构型模式
- 将抽象部分与实现部分分离,使它们都可以独⽴的变化
- 通俗来说,是通过组合来桥接其它的⾏为/维度
- 应用场景
- 系统需要在构件的抽象化⻆⾊和具体化⻆⾊之间增加更多的灵活性
- 不想使⽤继承导致系统类的个数急剧增加的系统
- 有时候⼀个类,可能会拥有多个变化维度,⽐如啤酒, 有不同的容量和品牌,可以使⽤继承组合的⽅式进⾏开 发,假如维度很多,就容易出现类的膨胀,使⽤桥接模式就可以解决这个问题,且解耦
代理模式
- 代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处 是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
- 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
- 代理模式有不同的形式, 主要有三种 静态代理、理 动态代理 (JDK 代理、接口代理)和 Cglib 理 代理 (可以在内存动态的创建对象,而不需要实现接口, 他是属于动态代理的范畴) 。
静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
1 | // 定义手机的规则,不能你说是手机就是手机。 |
静态代理优缺点:
优点: 被代理对象只要和代理类实现了同一接口即可,代理类无须知道被代理对象具体是什么类、怎么做的, 而客户端只需知道代理即可,实现了类之间的解耦合。
缺点:
- 代理类和被代理类实现了相同的接口,代理类通过被代理类实现了相同的方法,这样就出现了大量的代码重复。 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。
- 每个代理类只能为一个接口服务,如果程序开发中要使用代理的接口很多的话,必然会产生许多的代理类,造成类膨胀。
动态代理
介绍:
- 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
- 代理对象的生成,是由java内部的反射机制来实现的。
1 | // 定义手机规则 |