java
基础
基础
特性
简单,面向对象,可移植,解释执行(先编译为class文件,然后jvm解释执行)
修饰符
作用域
private(同类)、protected(同包和子类)、default(同包)、public(所有)
static
static修饰的变量内存只有一份,static修饰的变量和方法可以直接通过类名访问。static修饰的代码块只会执行一次
final
final修饰的变量成为常量,final修饰的方法不能被覆盖,final修饰的类不能被继承
super
可以调用父类的方法和属性
数据类型
基本数据类型
整数型(byte,short,int,long)
浮点数型(float,double)单双精度
字符型(char 2)
布尔型(boolean 1)
引用数据类型
类和接口、数组
变量类型
常量(final 修饰,无默认值,存在常量池)
类变量/静态变量(static修饰,有默认值,存在方法区)
成员变量/实例变量(方法外,有默认值,存在堆中)
局部变量(方法内,无默认值,基本数据类型存在栈中,引用数据类型的变量在栈,但指向的对象在堆中)
面向对象
定义
面向过程注重事情的每个步骤及顺序,面向对象注重参与事情的对象有哪些,以及他们各自需要做什么
三大特性
封装
将类的某些信息隐藏在内部(private),对外通过方法调用,不用关心具体实现。减少耦合
继承
从已有的类中派生出新的类,能继承并扩充已有类的属性和方法(父类private不能继承)
多态
父类或接口定义的引用变量可以指向不同子类的实例对象,实现对引用变量的同一个方法调用,执行不同的逻辑。让代码更好的扩展和维护。
特殊类
抽象类(abstract)
“是不是”的概念,1.不能创建对象,只能被继承;2.可以定义抽象方法。
接口(interface)
“有不有”的概念,1. 方法都是抽象的,变量都是final修饰的常量。2. 不能创建对象,只能被类实现或者被接口继承。3. 1.8后新增default方法,不是抽象方法,不用实现。
内部类
作用
1. 内部类可以更方便访问外部类成员(否则只能通过外部类对象访问),2. 每个内部类都能独立的继承一个接口的实现,使多继承变得更加完善.
注意
1. 内部类和外部类可相互访问private属性。2. 非静态的内部类不能定义静态方法和变量。
调用内部类变量或方法
1. 间接调用(通过外部类方法调用)。2. 直接调用(定义内部类对象调用)Outer.Inter inter=new Outer.new Inter();
匿名内部类
new对象时,直接接创建类new X{public void f(){…}}
高阶
泛型
含义
类定义时不设置属性或方法具体类型,实例化的时候再指定具体类型。优点:代码重用、保护类型的安全以及提高性能。
泛型通配符
class A{}:可以定义任意类型
class A{}:只能定义B或B的子类
class A{}:只能定义B或B的父类
反射
具体:反射就是先得到Class类对象,再通过该对象获得它的成员变量/构造方法/成员方法,最后分别通过成员变量/构造方法/成员方法的对象调用对应的方法
作用:可以动态获取类的信息, 提高代码灵活度,框架中比较常见
具体流程:
准备阶段:编译期将每个类的元信息保存在Class类对象中
获取Class类对象:三种方法x.class/x.getClass()/Class.forName()
实际反射操作:根据获取的Class对象,来获取属性,方法,构造方法(Filed/Method/Constructor)
动态代理
作用:类似Spring的aop,可以动态的,不修改源代码的情况下为某个类增加功能,如在一个方法的前后添加一下功能。
异常
Throwable
Error(错误):程序无法处理
栈/内存溢出
虚拟机运行错误
Exception(异常):程序可以处理
RuntimeException及其子类:如下标越界
非RuntimeException异常:可查异常,需要try catch 或throws。如文件上传
IO
字节流:以字节为单位,可处理任意类型数据
字符流:以字符为单位,一次性可读多个字节,处理字符类型数据。
集合
collection(单列集合)
list(可重复,有序:元素存取顺序一样)
查快:ArrayList
底层数组,查快,增删慢,线程不安全,效率高
ArrayList的扩容:使用无参构造方法时,初始大小是0,当有数据插入时,扩展到10。每当容量到达最大量就会自动扩容原来容量的1.5倍(会把老数组元素重新拷贝一份到新数组,代价较高,所以知道初始容量,可以初始化时指定一个初始容量)。
线程安全
Vector:线程安全,结构跟ArrayList类似。内部实现直接使用synchronized 关键字对 每个方法加锁。性能很慢。现在淘汰(synchronizedList跟Vector类似,区别是使用synchronized 关键字对 每个方法内部加锁)
CopyOnWriteArrayList:线程安全,在写的时候加锁(ReentrantLock锁),读的时候不加锁,写操作是在副本进行,所以写入也不会阻塞读取操作,大大提升了读的速度。缺点:写操作是在副本上进行的,数组的复制比较耗时,且有一定的延迟。
删快:LinkedList
底层双向链表(所以可以作为栈和队列),查慢,增删快,线程不安全,效率高
set(唯一,无序:元素存取顺序不同)
未排序:HashSet
底层hashMap(对应value为object常量对象),先判断对象hashcode是否重复,如果不重复那肯定就没添加。如果重复再通过equals比较。
有一个子类LinkedHashSet,底层为链表和哈希表,依赖链表保证有序
排好序:TreeSet
底层红黑树(Compareable保证唯一)
map(双列集合,无序)
未排序:HashMap
线程不安全,底层数组+链表+红黑树(1.8之前为数组+链表)。(扩展:1.8后链表插入从头插法改为了尾插法,因为头插法在多线程中可能导致死循环:在扩容时,头插法打乱了链表的顺序,第一个线程扩容后,顺序相反。此时第二个线程再进行扩容时,就会出现死循环,本来a节点的next时b,单第一个线程扩容后b的next节点是a)
hashMap添加和扩容
put添加过程
1. 根据key获得哈希码,计算数组下标位置。
key通过hashcode获得hash码
位置计算:hash码&(length-1),比 hash值%数组长度 更快
2. 如果对应位置没有元素 就直接把封装好的对象放到该位置。如果对应位置是红黑树节点,就把新节点放到红黑树节点上(期间会判断是否存在key,存在就更新)。如果是链表节点,就通过尾插法插入链表节点,然后判断节点数大于等于8就转为红黑树。
3. 最后判断是可否需要扩容
扩容机制(默认大小16)
先生成新数组(默认原两倍),然后遍历老数组每个元素,计算对应新数组位置。如过某个位置元素大于等于8就转红黑树。
加载因子
默认0.75(hashmap默认大小16,乘以加载因子为12,所以达到12就进行扩容。)加载因子越大,空间利用率越高,但冲突机会增加。
线程安全的
线程安全的HashTable,底层数组+链表,通过Synchronized对整张表加锁保证安全,现已淘汰
线程安全的concurrentHashMap
1.7是segment数组+hashEntry实现。一个segment中包含一个hashEntry数组,hashEntr是链表结构。线程安全是通过ReentrantLock对Segment数组加锁实现,
1.8后是数组+链表+红黑树实现。线程安全是通过使用了CAS 加 volatile 或者 synchronized 的方式来保证线程安全
添加元素
判断容器是否为空
为空则使用volatile和cas来初始化。(初始化时,通过volatile修饰的变量来表示正在初始化,保证可见性和有序性,cas操作保证原子性)。
如果不为空则根据元素key计算存储位置是否为空
如果空就通过cas操作添加节点元素。(cas操作里还会判断一次是否为空)
如果不为空则通过Synchronized对节点加锁后,添加或更新节点元素。(hash值和key都相同就替换,或者就添加到红黑树或者链表节点中)
获取元素:通过volatile保证获取的元素是最新的。
扩容:扩容也是volatile和cas保证线程安全。扩容过程中通过Synchronized锁住正则迁移的一个 Node 节点减少了加锁粒度。(扩容过程中也可以get 和put)
根据key排好序:TreeMap
底层红黑树,各操作O(logn)
线程
状态
新建状态,new一个线程后
就绪状态,调用start后
运行状态,执行run方法
阻塞状态,如调用sleep,wait等方法后
终止状态,如线程正常结束,或者调用stop等
开启线程三种方式
继承Thread类,重写run方法。(代码:Thread t=new MyThread()😉
实现Runnable接口,在类中实现run()方法(代码:Thread t=new Thread(new MyThreadt())😉
实现Callable接口,实现call方法,再结合FutureTask 创建线程(可获取线程结束后的返回值)(代码:Thread t = new Thread(new FutureTask<>(new MyCallable));)
比较:接口实现优势:1.避免java中的单继承的限制,2. 使用接口的方式能放入线程池。(Callable较Runnable而言,线程可以有返回值)
execute和submit
execute只能提交Runnable类型的任务,无返回值。submit可以提交Runnable和Callable,有返回值
线程相关方法
Thread.sleep(long millis) :线程睡眠,线程转到阻塞状态(不释放锁)
Obj.wait() :线程等待,线程转到阻塞状态(释放对象锁,需要notify唤醒)
Obj.notify(): 线程唤醒,唤醒等待中的线程(注:它不是立即唤醒,notify()是在synchronized语句块内,在synchronized(){}语句块执行结束后当前线程才会释放对象锁,唤醒其他线程)
Thread.yield() :线程让步,让相同优先级的线程之间能适当的轮转执行
join(): 线程加入,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
interrupt():线程中断,向其他线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常。
注:线程优先级范围1-10,默认为5。main()本身也是一个线程,当main结束后,子线程仍会继续执行到结束。每个程序至少有两个线程,main和垃圾回收线程(每个程序启动都会启动一个jvm)
线程池
定义
可以容纳多个线程的容器,其中线程可以反复使用,避免反复创建线程的开销(同时新来任务也不用去创建线程比较快)。
创建线程池步骤
创建线程池:ExecutorService service = newFixedThreadPool(5);//(读音 e g z ki ter) 另一种创建方式: ExecutorService service = Executors.newFixedThreadPool(5);//
加入线程:service.submit(new MyCallable());//或者service.submit(new MyRunnable ());加入后会自动调用run方法
关闭线程池:service.shutdown();
线程池的七大参数
常驻核心线程数(corePoolSize):线程中长驻的核心线程
最大线程数(maximumPoolSize):大于等于1
多余的空闲线程存活时间(keepAliveTime):线程数超过常驻核心线程数时,多余线程的空闲时间达到keepAliveTime时就会销毁
unit:keepAliveTime的单位
任务队列(workQueue):被提交但是尚未被执行的任务。当任务达到核心线程数过后,就会放到任务队列中,任务调度时候再取出
线程工厂(threadFactory):用于创建线程
拒绝策略(handler):当工作线程大于等于最大线程数且任务队列也满了时,就会触发拒绝策略(4种:1. 异常策略(AbortPolicy):直接抛出异常。2. 丢弃策略DiscardPolicy:新来任务被提交后直接丢弃。3. 淘汰最久任务策略DiscardOldestPolicy:丢弃存活时间最长的任务。4. 执行策略(CallerRunsPolicy):让提交任务的线程自身去执行该任务)。
线程池执行流程
提交任务
判断核心线程是否满,没满创建线程执行提交任务(就算有空闲线程也会创建,保证创建满核心线程数)
核心线程满判断任务队列是否满,没满放任务队列,等核心线程空闲再去执行。
任务队列也满,判断最大线程数是否满,未满创建线程,满就触发拒绝策略
ThreadLocal
介绍:线程本地存储机制,它可以将数据缓存到某个线程内部,因为共享区的数据为了安全会使用加锁等机制,从而影响效率。
底层:每个线程都有一个ThreadLoacalMap:数据是以key为ThreadLocal对像,值为数据缓存到ThreadLocalMap中的,
场景:比如把用户信息存入Token中,当用户调用接口时,拦截器中解析Token(header中携带 Token),把用户信息存在ThreadLocal中,然后就可以在controller,service等多层方便的共享用户数据,并且能保证对每个请求,都只能访问当前请求的用户信息
注意:因为线程池中的线程不会销毁,所以ThreadLocal对应的值也不会被回收会导致内存泄漏,所以建议在使用完后remove掉。
和加锁对比:加锁用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
java中的四种线程池
CachedThreadPool-可缓存线程池:线程池中有空闲线程就利用,没有就创建(没有线程数量限制)。
FixedThreadPool-定长线程池:固定线程总数,如果没空闲任务则放到任务队列中(等待线程可以采用先进先出,或者后进先出等唤醒)
SingleThreadExecutor-单线程池:提交的线程按顺序一个个执行,就是一个线程执行完才执行下一个(最大线程数和核心线程数都为1)
ScheduledThreadPool-调度线程池:可以根据设定的时间间隔执行任务
锁(保证线程安全)
Synchronized和ReentranLock
不同点
Synchronized是一个关键词(jvm层面的锁),ReentranLock是一个类(API层面的锁)
Synchronized可以修饰方法和代码块(在方法前加static 可锁住类的所有对象),Jvm自动释放锁。ReentranLock只能修饰代码块。需要手动释放锁
Synchronized锁的是对象,锁信息保存在对象头中的Markword中(记录四个锁状态),ReentrantLock锁的是线程,锁的信息保存在AQS 中
相同点:Synchronized和ReentranLock都是可重入锁
就是在一个线程中允许你反复获得同一把锁,若多次加锁记需要多次解锁才能释放(实现:每个锁关联一个请求计数器和一个获得该锁的线程,加一次锁计数器加1)
为什么需要可重入锁:最大程度避免死锁(对象中加锁的A方法调用加锁的B方法,就会导致死锁,可重入锁允许一个线程反复或的一把锁,就不会导致死锁)
ReentrantLock 相对 synchronized 多了三种功能
1.等待可中断(正在等待的线程可以选择放弃等待,改为处理其他事情。)
2.可实现公平锁(默认非公平)
公平锁:在获取锁时,会先检查AQS同步队列是否有线程在排队,有就进行排队(AQS先进先出的双向队列)
非公平锁:获取锁时不会检查是否有线程在排队,而是直接竞争锁(如果没竞争到锁,后面就跟公平锁一样也会去排队,当锁释放只会唤醒AQS队列的首个线程,非公平只体现在加锁阶段,不体现在唤醒阶段)
3.可绑定多个条件,实现有选择的唤醒等待(synchronized结合notifyAll()会唤醒所有等待的线程。Reentrantlock中一个锁对象可以创建多个对象监视器,线程对象可以注册在指定的对象监视器中,从而可以有选择性的进行线程通知)
Synchronized相关
为什么说Synchronized是一个重量级锁
Synchronized 底层实现依赖于操作系统的互斥锁。而操作系统实现线程之间的切换需要从用户态转换到核心态,转换耗时,成本非常高。这种依赖于操作系统互斥锁的称为重量级锁。
锁机制如何升级
1.6之前就直接是重量级锁,但效率比较低,所以后面引入锁升级机制,用于平衡安全和效率的问题 。
偏向锁:在对象头中记录当前获得该锁的线程id,下次该线程就可以通过偏向锁直接获取到资源。(因为大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁)
轻量级锁:每个竞争线程通过自旋不停看被锁住资源是否释放。避免用户态到内核态的切换。
重量级锁:由操作系统来判断锁住资源是否释放,释放再通知其他线程。
过程
先尝试用偏向锁的方式去竞争资源。
如果失败就表示其他线程已经占用了偏向锁,此时升级成轻量级锁,通过自旋的方式尝试去加锁。
如过多次竞争失败就会升级成重量级锁(因为太多线程不断自旋竞争对效率影响也比较大),此时没竞争到锁的线程会被阻塞,
双重检查锁DCL(Double Check Lock)
作用:尽可能减少加锁的范围(锁如果加在第一重,初始化除了创建对象可能还涉及其他赋值操作,所以加锁范围太大。在初始化中创建 对象为什么还要二重检查?因为第一重检查未加锁,所以可能导致多个线程都判断对象为空,从而进入初始化方法,依次创建多个对象)
ReentrantLock相关
使用:先创建一个ReentranLock对象,然后通过lock()和unLock()方法加锁和释放锁。还有tryLock()尝试加锁,可以用于自旋。
加锁过程
ReentrantLock中包含一个AQS对象,这个对象中又三个核心变量:1. 加锁状态state(0表示未加锁)2. 加锁线程。3. 等待队列(Node双向链表)
加锁过程:线程尝试通过CAS操作将state值从0变为1,如果修改成功就把加锁线程设置为自己。修改失败就看看加锁线程是否为自己,不是就进入等待队列。当持有锁的线程释放后会唤醒队列首个线程
AQS
定义:AQS是java并发包的基础类,java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,如ReentrantLock。
volatile
作用:保证可见性(强制将修改的值立即写入主存)、有序性(禁止进行指令重排序)。
扩展:普通共享变量修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
指令重排:指令重排序是编译器处于性能考虑,源码顺序和程序顺序可能不一样。例子:比如new一个对象,底层是三步(1.分配内存空间。2. 执行构造方法,初始化对象。3. 把这个对象指向空间。)所以可能导致A线程执行了1,3。b线程判断此时对象为空,然后又执行初始化操作。
voalate和synchronized区别:
并发编程三个重要的特性是原子性、有序性、可见性。(事务的四个特性ACID:原子性一致性隔离性持久性)
原子性:synchronized通过互斥锁。voalate不保证原子性(可以和cas操作搭配保证原子性)
有序性:synchronized通过程序串行化执行,volatile通过禁止指令重排。
可见性:volatile通过强制将修改的值立即写入主存,synchronized通过jvm指令monitorexit把共享资源都刷新到内存保证
CAS操作:非阻塞原子性操作(通过硬件保证原子性),同一时刻只有一个线程可以修改,其他线程并不会阻塞而是重新尝试
JUC线程并发库
并发容器类:ConcurrentHashMap(线程安全)
锁相关类:ReentrantLock
线程池相关的类:Callable,Executor(创建线程池的)
面试题
比较
深拷贝和浅拷贝
引用拷贝:只复制对象地址,不会创建新对象(把一个对象直接赋值给另一个对象,两个变量指向的是内存中同一个地址)
浅拷贝:会创建对象,并对基本数据类型进行复制,对引用数据类型只会复制地址。
深拷贝:完全复制整个对象。
拷贝方法
浅拷贝方法:Object提供clone方法,可以实现Cloneable接口,然后重写clone方法,通过super调用clone方法获取拷贝对象(这里只会拷贝原对象的基本数据类型,称为浅拷贝)
深拷贝方法:重写clone方法,对其中引用变量再进行一次克隆(具体:通过上面浅拷贝拷贝对象后,可以在方法后面对拷贝对象的引用类型变量,比如数组变量,继续进行clone,这样就能实现深拷贝 ,对象中还有对象也可以使用递归的方式进行拷贝)
深拷贝:还可以用序列化的方式进行拷贝(先序列化成字节,然后反序列化)
重写和重载
重写是子类重写覆盖父类的方法(注意:重写方法的 修饰符范围不能小于父类,且private方法不能重写)
重载是同一个类中方法名相同,参数不同
序列化和反序列化
序列化:把对象拆解成字节碎片(可用于对象的持久化)
java实现:先把需要序列化的类实现Serializable接口(标志作用),通过对象输出流ObjectOutputStream把对象转换成字节碎片,然后通过文件输出流FileOutputStream把字节碎片写入文件。(还可以指定一个序列化id号,防止进出版本不一致)反序列化同理:通过FileInputStream从文件中读取字节碎片,然后通过ObjectInputStream把字节碎片转换为对象
jdk、jre和jvm
jdk:它是java开发工具包。包含:java编译器,Java运行环境jre,java常用类库
jre:它是Java运行环境,包括jvm和jvm所需类库
jvm:java虚拟机,用于运行字节码文件(会将字节码文件解释为机器指令,不同操作系统机器指令不同,所以不同操作系统的jvm也不同,但都运行字节码文件)
、equals()和hashCode()
:如果是基本类型,则比较的是值,引用类型比较的是引用地址。
equals:具体比较什么是看类中equals方法重写逻辑(默认是Object中的equals跟是一样的)
hashcode:hashcode是获取一个对象的hash码,可以根据hash码找到对象在堆中的位置(堆中有一个hash表与之对应)。
hashcode扩展
两个对象相等,hashcode一定相等。hashcode相等,对象不一定相等。
为什么重写equals时也要重写hashcode方法:因为java中一些集合类比如hashset判断两个对象是否相等会先判断hashcode,所以如果不重写hashcode,就会导致该类与其他集合类一起工作时出问题。
hashcode意义:HashSet集合添加元素,需要判断是否添加重复元素。如果用equals一个个比较太慢了,所以可以先判断对象hashcode是否重复,如果不重复那肯定就没添加。如果重复再通过equals比较。
String、StringBuffer和StringBuilder区别
String:底层时final修饰的byte数组(所以不可被继承)。若频繁修改字符串时,会产生很多无用的中间对象,效率很低。
StringBuilder :底层也是一个byte数组,但不是常量,当容量不足时会进行扩容。线程不安全。
StringBuffer:在StringBuilder基础上考虑了线程安全。通过synchronized为每个方法加锁保证线程安全。效率较低
int和Integer
区别
Integer:Integer是int提供的封装类;Integer对象需要实例化,默认值为null。
int:int是基本数据类型,直接存储数值,默认值是0;
数值比较
Integer和int比较:Integer是int的封装类,int与Integer比较时,Integer会自动拆箱,无论怎么比,int与Integer都相等
Integer和Integer比较:通过equals比较,但对于数值在-128与127之间的Integer对象,会缓存在内存中。所以会直接从内存取,不会创建新的对象,所以也可以通过比较。
拆箱装箱原理:装箱是编译的时候自动调用vulueOf(),拆箱是调用intValue()方法。
run()和start()
run方法就是一个普通的方法,而start方法会创建一个新线程去执行run()的代码。
取模取余:
符号相同没区别。符号不同时,取模结果的符号和除数一致,取余结果的符号和被除数一致。
注:%号在Java中计算负数时是取余而不是取模,如果要对负数取模,要用Math.floorMod( )方法。
概念
new对象初始化顺序
父类静态代码块,子类静态代码块,父类构造方法,子类构造方法
构造方法有哪些特性
名字与类名相同,没有返回值,但不能用 void 声明构造函数,创建对象时自动执行
一致性hash算法
优化分布式环境下,主机增加或减少情况下hash映射的问题。普通hash会全部重新隐射。一致性hash通过hash环的数据结构来优化。把主机iphash后放到hash环上,然后对key hash后,放到顺时针方向离自己最近的主机上。所以主机增加或减少时只需少部分数据需要重新映射
具体:环范围0-2^(32-1),主机ip hash后放到环上,节点key hash后放到顺时针最近最近上。(数据倾斜可以通过虚拟节点解决——每个主机创建多个虚拟节点,放到环上)
java8新特性
lambda表达式
定义:语法更简单,本质就是创建一个接口实现类的具体对象。(对应接口只能有一个抽象方法,可以在接口加一个注解,限制其只能有一个抽象方法)
例子:X x = ()->System.out.print(“1”);//()对应的就为接口中无参方法,箭头后就为实现的内容。
作用:简化代码,适用于代码不复用的场景
函数式接口
可以在接口加一个注解,对应接口只能有一个抽象方法,与lambda连用
方法引用
lambda深层引用,如果在lambda表达式中,函数式接口的参数和返回值 和 方法体实现中的方法的的参数和返回值一样就可以使用
三种使用情况:
对象::实例方法名(非静态方法)例子:X x = y::方法名; x.方法名(参数)(y为方法体中的方法对应的对象)
类::静态方法名
类::实例方法名(还要满足条件:两个参数,第一个参数为方法调用者,第二个参数为方法的实际参数)
接口新增default方法
不是抽象方法,不用重写
修改一些底层实现
如hashmap,底层新增红黑树结构
新增一些api(日期相关)
为什么数组具有快速查找的能力
数组是连续的内存地址,且元素都是同一类型,占用空间大小一样,所以通过下标就可以计算出元素的位置。
浮点数
IEEE754标准
float占4字节,32位(1个符号位,8个指数位,23个尾数位)指数对应表示大小范围:8位指数大小范围是[-127,128]),所以表示数值范围是-2128到2128。尾数对应精度:23位尾数,能表示最大十进制为2的23次方为7位数,所以完整精确表示的为6位。(省略位为1,所以不算进去)
double占8字节,64位(1个符号位,11个指数位,52个尾数位)表示10进制精度位15位。表示数值范围为-21024到21024
十进制数值存储过程(以float为例)
先把10进制转换为2进制,然后规范化(科学计数法)。对于符号位:正数存0,负数存1。对于指数位:因为指数位8位范围是[-127,128],转二进制存计算机时,为了避免负数,加上一个固定偏移量127,然后转二进制存入8位指数位。对于尾数:由于规范化后首位都为1,所以省略首尾,将小数点后的位数放入尾数部分,不足补0。
例子:10.75转为二进制为1010.11,规范化为1.0101110^3。 正数符号位为0。尾数为省略首位后为01011。指数为3+127=130,换算为二进制为10000010。所以最终10.75存在计算机就为:0 10000010 01011000000000000000000
扩展:十进制转二进制
正数:除2取余,倒叙排列。如2:2/2得1取0;1/2得0余1取1。最后得10
小数:乘2取整,正序排列。如0.25:0.252=0.5取0,0.5*2=1.0取1。最后得到01
注意事项
1. 比较两个浮点不要用==,而是做差是否在一定范围(一般差小于10的-6次方)。2. 尽量使用double而不是float(因为float精度太低 )。3.金融场景一定要使用BigDecimal(无精度损失)
扩展
int占4字节,32位,除去符号位还剩31位。范围为[-231,231-1]。(因为31可以表示231个正负数。分别为[0,231-1]和[-231+1,-0]。因为只需要一个0,而且-0的原码加上符号位,恰好等于-0的补码,所以负数要多一个,最后范围为[-231,2^31-1])
正数:原码=反码=补码。负数:反码为原码符号位不变,其他位相反;补码为反码加1。(计算机都存的补码)JVM
介绍
jvm是程序虚拟机,运行字节码,主要功能是内存管理和垃圾回收。
目前主要使用的JVM为HotSpot,它采用解释器和即时编译器并存的架构
翻译字节码(解释执行):读一行执行一行,响应快,速度慢,类似走路
Jit编译器(翻译执行):将热点代码编译成机器码,并进行优化,下次执行就很快,响应慢,速度快,类似等公交车。
jvm采用的是基于栈的指令架构
基于栈的指令架构:零地址指令,完成一个功能需要更多指令,性能较低,不与硬件直接打交道,可移植,跨平台。
基于寄存器指令架构:多地址指令,完成一个功能花费更少指令,性能较高,指令集架构依赖硬件,可移植性差,设计实现也较复杂。
JVM调优
尽量减少减少GC的频率和Full GC的次数。分析dump文件,看GC频率和次数。然后调整内存比例和大小来找到合适参数。
结构
类加载器
定义
类加载就是将磁盘上的class文件加载到内存中。类加载器记录了加载类的集合,就是哪些类是由它加载的。
分类
引导类加载器(BootstrapClassLoader):又称启动类加载器,使用c/c++实现,加载Java核心类库的类
自定义类加载器:所有引导类加载器派生出的子类(主要包括:扩展类加载器,系统类加载器,用户自定义类加载器)
用户自定义类加载器
用途
扩展加载源
隔离加载类(如使用不同中间件时,防止加载的类重名冲突)
防止源码篡改(对字节码加密,加载到内存时自定义类加载器解密)
除了引导类加载器,其他加载器都不是必须的,可以使用自定义在需要的时候动态加载。
实现方式
继承ClassLoader或者URLClassLoader()
继承关系
引导类加载器->扩展类加载器->系统类加载器->用户自定义加载器
类加载过程
加载
在内存中生成一个该类的Class对象(用于创建实例对象)
链接
验证:确保Class文件合法。
准备:为类变量分配内存,设置默认初始值,如0,false…(类变量为static修饰的,这里不包含final static ,因为final static变量在编译时已经分配)
解析:将常量池的符号引用转换为直接引用
初始化
执行类构造器方法 clinit() 的过程,该方法由编译器收集类中所有类变量赋值动作和静态代码块中的语句合并而来
双亲委派机制
原理
类加载器收到加载请求后会把请求委托给父类加载器去加载,父类加载器不能加载,再由子类加载。
加载过程
启动类加载器看是否能加载这个类(在rt.jar中找是否有这个类,有就加载然后结束),不能加载就抛出异常,通知子类加载器加载,也就是看扩展类加载器是否能加载这个类(在ext目录下是否有这个类,有就加载结束),不能加载就继续通知子类加载器加载不断重复。
优点
避免类的重复加载
防止核心API被篡改(沙箱安全机制)
运行时数据区
相关
jvm内存结构:程序计数器,方法区,本地方法栈,虚拟机栈,堆
一个进程对应一个方法区和堆,一个线程对应一个程序计数器,虚拟机栈,本地方法栈。
GC一般在方法区和堆中,OOM不会在程序计数器中
OOM故障排除(调优)
尝试扩大堆内存看看结果
用内存快照分析工具分析内存(如Jprofiler,MAT)
通过对程序设置参数(-XX:+HeapDumpOnOutOfMemoryError),当JVM发生OOM时,自动生成DUMP文件。用jprofiler打开,一是可以看哪些对象占了空间,二是可以通过查看各个线程看问题出在哪行
程序计数器
用于存储下一条执行指令的地址,多个线程情况下,当切换回某个线程的时候,就需要通过pc寄存器知道当前线程执行的位置。
本地方法栈
作用
管理本地方法调用。
本地方法
带native关键字的方法,主要由c编写,本地方法可以直接调用处理器中的寄存器,分配本地内存等。
虚拟机栈
作用
参与方法调用和返回,保存局部变量和部分结果。扩展:每个线程创建时会创建一个虚拟机栈,虚拟机栈中保存的是多个栈帧(内存区块),一个栈帧对应一个方法,虚拟机栈大小可以固定或者动态变化。(通过-Xss 设置)
栈帧组成
局部变量表
它是一个数子类型的数组(定义的所有类型都能转换为数字),其存储单元为变量槽(Slot),32位以内类型变量占一个槽)若变量作用域失效,变量槽便可以回收重用。局部变量表中直接或间接引用的对象不会被回收,随方法调用结束销毁。局部变量表是性能调优的重要部分。
操作数栈
用于保存计算的中间结果(比如下一条字节码为加操作,通过局部变量表是不知道哪两个数相加。但通过操作数栈弹出两个栈顶数就可以。)
动态链接
作用
每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 作用就是: 当前方法中如果需要调用其他方法的时候, 通过运行时常量池,就可以将符号引用转换为直接引用,然后就能直接调用对应方法。
常量池
常量池:存放final常量,字符串和基本数据类型的值,符号引用(方法的名称和描述符,字段的名称和描述符)。它是字节码文件中的一部分 ,本质就是符号地址和真实地址的对照表。
运行时常量池:当类的字节码被加载到内存中后,它的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址变为真实地址(如#2会被转化为内存中的地址)。位置:1.6之前在方法区,1.7在堆,1.8后在元空间
两类方法调用
静态链接:目标方法在编译期可知(对应方法早期绑定非虚方法)
动态链接:目标方法在运行时才可知(对应方法晚期绑定和虚方法)比如多态中,animal类可能时狗猫,所以编译时不知道调用狗的eat()还是猫的eat();所以为晚期绑定。
不同方法调用指令不同,java7出现动态调用指令invokedynamic。它是为了实现 动态类型语言支持而做的一种改进。(静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息)
虚方法表
调用虚方法时,在运行过程中根据调用者的动态类型来决定具体的目标方法(多态),效率就很低,所以引入虚方法表。每个类中都有一个虚方法表,存储各方法实际入口
方法返回地址
用于存放pc寄存器的值 ,在该方法结束后返回到被调用的位置。如果正常退出就到调用位置的下一条语句,异常退出根据异常表确定(比如try catch捕获了就会接着执行,未捕获就会向上抛 )
一些附加信息(了解)
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如对程序调试提供支持的信息。
方法区
存放已被加载的类信息(构造方法/接口定义)、常量、静态变量。(一个java进程对应一个方法区,1.8以前又叫永久代)
元空间:1.8后方法区被元空间替代,元空间使用本地内存。(不存在垃圾回收,如果加载太多的第三方jar包,可能导致内存溢出)
堆
介绍
1. 一个java进程对应一个堆内存,堆内存大小在创建时确定(可设置)堆可以在物理上不连续,但必须在逻辑上连续。2. 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
结构
新生代:伊甸园区和幸存区(from 和 to)+老年代(伊甸园和from,to比例8:1:1,新生代老年代比例1:2)
TLAB(ThreadLocal)
定义
从堆的新生代区划分出一部分空间,为每个线程分配一个私有缓存区。这个缓存区被称为TLAB(Thread Local Allocation Buffer)。
优点
因为共享区的数据为了安全会使用加锁等机制,从而影响效率,所以产生TLAB,当线程数据在私有缓存区放不下才放到共享数据区。
逃逸分析
定义
逃逸主要是指new的对象是否在方法外使用,如果在方法外使用了就是发生了逃逸,如果没发生逃逸就可以做一些优化。如:(java7之后,默认开启了逃逸分析,java7之前需要设置参数开启。)注意:只有在server模式下,才可以开启逃逸分析。
开启后编译器可对代码优化
栈上分配对象
除了堆中分配对象外,在栈上也可以分配对象,不过有条件,经过逃逸分析后发现一个对象如果没有发生逃逸,就可以在栈上分配,如果发生逃逸就不能栈上分配。(栈帧弹出栈,变量就自动被回收,就不用GC。)
同步省略(锁消除)
JIT编译器可以借助逃逸分析来判断一个对象是否只被一个线程访问,如果是,JIT在编译这个同步块的时候,会取消这部分代码的同步,这样能提高并发性和性能
分离对象(标量替换)
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。(无法分解成更小数据的数据,如java的基本数据类型)
堆设置参数
-Xms:设置堆初始内存大小(默认电脑内存大小1/64)
-Xmx:设置堆的最大内存大小(默认电脑内存大小1/4)
-Xmn:设置新生代大小(可设置初始值和最大值)
-XX:NewRatio=2:设置新生代和老年代占比为1:2(默认就是2)
-XX:ServivorRatio=8:设置伊甸园区和幸存区比例为8:1:1(默认就是8)
-XXHandlePromotionFailure:设置空间分配担保(1.7之前有效,1.7之后固定为true)
垃圾回收
新对象申请内存过程
1. 先判断伊甸园是否能放下,2. 不能就YGC然后判断伊甸园是否能放,3. 还不能就判断是否Old区能否放下, 4. 还不能就FGC然后判断是否能放下,不能就OOM。(能就分配内存)
YGC
新生代范围的gc。伊甸园区空间不足时触发,频率比较高(常用复制算法)
YGC前(空间分配担保):步骤:1. 老年代最大连续可用空间是否大于新生代所有对象的总空间,是的话YGC,2. 不是则判断老年代最大连续可用空间是否大于之前晋升的平均大小,是的话YGC,否则FGC。(jdk1.7前:在2步骤前,如果参数HandlePromoionFailure为true才进行2步骤,为false就直接FGC)
FullGC
FGC:全堆范围的gc,老年代空间不足或者对空间使用达到80%触发(可调整)或者调用System.gc() (常用分代收集算法,新生代和老年代采取不同收集算法)
常用GC算法
复制算法
每次ygc,将伊甸园区活下来的对象和from区寿命未超阈值的对象复制到to区,然后交换from和to。(每次from区对象复制到to对象寿命+1,超阈值15的放old区。如果to区放不下也放old区)
优点:没有碎片空间,缺点浪费空间,因为始终有一个幸存区为空。适用对象存活度较低的场景
标记清除法
扫描两次,第一次标记要清除的对象,第二次清除标记的对象
优点不需要额外空间,缺点产生内存碎片
标记压缩法(标记整理算法)
三次扫描,在标记清除法的基础上,第三次是向一端移动对象,消除内存碎片
优点无内存碎片,缺点三次扫描效率低
标记清除压缩
与标记压缩法不同在于多进行几次标记清除,产生较多的碎片后再进行压缩
垃圾回收器
新生代
Serial: 单线程收集器,采用复制算法,优点:对垃圾回收来说简单高效。缺点:会造成STW(就是垃圾回收时用户线程停止))
ParNew: 多线程收集器,相对Serial是多线程去执行垃圾回收。(只是多线程进行垃圾回收,也会造成STW)
Parallel Scavenge: 多线程收集器,相对ParNew,可实现用户线程可控的吞吐量,就是可以设置吞吐量大小和gc停顿时间(吞吐量:CPU运行用户程序时间除以CPU运行总时间)。
具体:有个参数(UseAdaptiveSizePolicy)打开之后,就不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行情况,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
老年代
Serial Old
Serial收集器的老年代版本,单线程收集器,采用标记压缩算法 。
Parallel Old
Parallel Scavenge 的老年代版本,多线程收集器,采用标记压缩算法,能实现可控吞吐量 。
CMS
并发收集器,垃圾回收线程和用户线程同时执行(在并发标记阶段不会造成STW),采用标记清除算法。
优缺:优点:低停顿。缺点:1. 无法清除浮动垃圾(主要在并发清除阶段,因为并行,所以在回收过程,用户程序还会产生垃圾)。2. 因为采用标记清除法,所以会产生内存碎片。3. 对cpu敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,如果cpu数量越少,处理用户线程的cpu资源就越少
过程
初始标记:标记 GCRoots 能直接关联到的对象,速度很快(为了防止程序继续产生GC Root对象,所以需要STW)
并发标记:基于初始标记的GC Root对象,进行可达性分析,标记存活对象,它在整个回收过程中耗时最长。(不会造成STW)
重新标记:并发标记期间,因用户程序的运行产生一些新的垃圾,很少所以快(为了防止继续产生垃圾所以需要STW)。
并发清除:并发的清除没有标记的垃圾对象。(不会造成STW)
新一代收集器G1
采用标记压缩算法,不区分新生代和老年代,直接把堆空间划分固定大小的region,在后台维护一个set集合指向这些region,根据筛选决定回收哪些region的垃圾(回收运行的时间、垃圾堆积程度以及region的优先级)。
优缺:优点:不会产生内存碎片,能精确控制垃圾回收时间。缺点:维护指向每个region区域的指针,占用额外内存。
过程
初始标记:标记 GCRoots 能直接关联到的对象,速度很快(为了防止程序继续产生GC Root对象,所以需要STW)
并发标记:基于初始标记的GC Root对象,进行可达性分析,找到存活对象,它在整个回收过程中耗时最长。(不会造成STW)
最终标记:并发标记期间,因用户程序的运行产生一些新的垃圾,很少所以快(为了防止继续产生垃圾所以需要STW)。
筛选回收:根据回收运行的时间、垃圾堆积程度以及region的优先级来回收对应region。(会造成STW)
相关问题
新生代为什么用复制算法:因为复制只需要一次扫描,效率高。新生代存活对象少,所以复制所需的额外空间小。
G1和CMS对比:1. G1采用标记压缩不会产生内存碎片。2. G1具有筛选回收,所以对回收时间可预测(也可自己设置)3. G1在筛选回收时候不是并行所以不会产生浮动垃圾,CMS是并发清除会产生浮动垃圾。4. G1内存占用更多(因为后台需要维护指向每个区域的Set集合)
GCRoot对象
比如方法中定义了一个对象,那个对象就是GCRoot对象。主要用于判断哪些对象可以被回收:通过可达性分析:通过GC Root对象向下搜索(G Croot对象中可能引用了其他对象),形成的路径为引用链。对访问过的对象打上标记,没有标记的对象等待被回收。
面试题
内存溢出举例?
JVM栈溢出:局部数组过大,递归调用层数太多,大量循环
JVM堆溢出:创建对象太多
为什么CMS会将serial old作为后备垃圾回收器?
因为CMS采用标记清除算法,会产生内存碎片。serial old采用标记压缩算法,能避免产生内存碎片
重点
秒杀系统
系统核心
解决并发读写问题
项目技术
SpringBoot ,Thymeleaf模板和前端layui框架,mysql存储,redis缓存
功能开发
登录模块
流程:两次加密,前端校验信息,后端校验信息,然后查数据库是否存在用户,存在再校验密码
为什么两次md5加密:第一次在前端加密,防止明文传输被截获。第二次在后端生产随机盐然后进行加密(并把盐写道数据库),第二次加密是为了防止数据库泄漏,通过密文和前台盐反编译。(数据库存了每个用户固定盐,验证的时候直接在后台用前端加密后密码再与数据库盐加密,然后与数据库密码对比)
判断用户是否登录:在登录成功后,生成一个uuid存到cookie中,然后session存uuid和对应用户。当用户访问网址时,从cookie获取对应uuid,再获取session中是否有对应用户,没有就跳转到登录页面,但每个页面都判断太麻烦了,所以所有的请求在访问前先通过拦截器判断是否登录,然后再执行对应方法。(shiro是通过拦截器,拦截请求判断是否登录)
扩展:禁用session时怎么记录登录用户
1. 可以通过url重写的方式,把登录用户信息拼接到url地址
2. 隐藏表单的方式记录用户信息
秒杀模块
流程
到秒杀时间,就可以秒杀,点击秒杀后,先判断库存是否还有,然后判断用户是否已经成功抢过,都没问题就开始抢购(执行:库存减1和创建订单 ,通过事务来约束,如果异常就回滚。)
优化后的流程
整个流程:请求来先通过内存标记(变量)判断是否还有库存,没有直接返回。有再通过redis判断是否有库存,有再判断redis中是否重复抢购,没有再redis预减库存,然后把是否真正抢购逻辑放到mq中异步处理,然后可以直接返回一个状态(排队中),实现流量削峰。
并发情况下问题解决
原始设计
读订单看是否抢购过,没有再读库存是否还有,没有才减1,然后创建订单
同一用户秒杀多件商品
原因:同一用户同时发送两个秒杀请求,并发情况下,第一个请求订单还没创建,第二个秒杀请求判断此时还没创建订单,又可以秒杀。
解决:在订单表中,通过用户id和商品id结合建唯一索引,保证一个用户一个订单(多次就会抛出异常)。
库存超卖(秒杀库存为负数)
原因:并发情况下,多个请求开始判断库存还有,然后跳转到减库存和创建订单,但这个时候可能已经没有了
解决:在进行减库存的时候,库存大于0才减少,根据是否减少来判是否真正抢购成功,没抢购成功就不会创建订单。
优化
为什么:因为每次从数据库来进行判断是否重复抢购和是否还有库存比较耗时,所以采用redis缓存来进行优化
redis数据缓存
redis判断是否重复抢购:把抢购成功的 用户id和商品id 的组合存到redis中,秒杀时先通过缓存判断是否抢够成功过。
Redis预减库存:
因为抢购前判断是否还有库存,系统启动时,把库存放redis中,来一个请求时通过redis预减库存(这样就不用访问数据库了),当redis中库存为0时就返回已抢完。
并发情况:判断是否为0和减库存不是原子操作,并发情况可能出问题,解决:redis分布式锁解决(lua脚本)
redis分布式锁
通过lua脚本实现:多个命令(判断库存大于0和减库存)写在lua脚本中,它是原子性的,加上redis是单线程的,所以能实现分布式锁的功能
实现方式有两种,1.在redis服务器写好,然后java中执行。2.java写好,然后执行时传到redis服务器执行
内存标记
当redis中库存没有了,如果还有大量请求,每次还要获取redis来判断。所以通过建一个hashmap的成员变量,记录商品和 是否有库存,每次请求如果商品无库存就不用查redis了。
优化
缓存
对象缓存
实现:所有登录用户序列化后存到redis中,同时也能解决分布式session的问题(用的是json序列化。redis存的是:(uuid,序列化用户))
分布式session
为什么用?:对于分布式的服务器,如一个用户登录时存在a服务器的session中,过一会访问请求可能被nigix分发到其他服务器,但其他服务器session中没有存该用户,所以又会跳到登录页面。
解决方案
redis缓存(第一种就是把所有登录用户序列化后存到redis中,第二种就是通过引入Spring-session-redis组件,存到session中的值都会在redis中
前端存储:把用户信息存cookie中,缺点就是不安全,数据受cookie大小限制。
session复制,多台服务器session同步复制,缺点就是性能有影响,session占内存,无法水平扩展。
数据缓存(上面提到的)
1.用户id和抢购成功的商品id,放reids判断重复抢购。
2. 把库存放redis, 预减库存
页面优化
页面缓存
为什么用:对于一些公共页面,如商品详情页面,每次都需要后台thymeleaf和数据渲染后再传回前台,需要读数据库和渲染,比较耗时。
实现:把一些公共页面在后台渲染放到redis中,下次访问就从redis中获取。(用的thymeleafViewResolver)
数据更新:当数据进行修改后更新缓存,或根据业务设置过期时间
动静分离(页面静态化)
为什么用:页面缓存每次还是会返回整个页面,前后端分离的话,只需要从后台获取数据,然后在前台进行渲染——vue, react等
实现:把thymeleaf转成普通html文件,然后放到项目static中(浏览器对static静态资源的缓存)。访问页面时,ajax从后台请求数据,然后根据后台返回的数据渲染到页面(js渲染:如根据id赋值到页面上)
原理:浏览器对static静态资源的缓存,这样我们就可以不用返回静态资源,只需要返回对应页面数据即可
304涉及页面缓存
对于一些静态资源,浏览器一般会做缓存,客户端在请求文件的时候,请求中会包含If Modified Since(这是个时间)这个时间之后如果文件发生了修改才返回200和具体文件内容,否则返回304.
对于动态资源,浏览器一般不会缓存
或者把页面放到静态资源中,利用动静分离,每次只请求数据部分,然后渲染到静态页面上
如何需要缓存整个动态页面(不经常更改的):我们可以在Response的HTTP的header中添加一个last Modified 的定义(静态资源默认会有,所以会缓存),添加后浏览器就会对页面进行缓存。动态页面请求时,在判断文件是否修改还是会查数据库,但如果没有修改的话,就不用返回页面的内容,只是返回一个304(HTTP header),从而减少带宽
接口优化
Rabbit流量削峰和异步处理:
把用户和商品ID组合成实体类,生产者就是传入的json序列化后的对象。消费者收到信息后把消息转换为对象,再次数据库判断是否还有库存和是否重复抢购,再执行库存减一和创建订单的操作。
我做的单机版本,是同一个服务器,所以实现的一个异步功能。用户点击秒杀后,直接返回在排队中,然后服务器从mq中消费消息,抢购成功的发送邮件(记录所有抢购用户,没抢购成功的也会发送邮件)。
扩展:可以把消息放到多个队列,让多个消费者去处理
RabbitMQ
RabbitMQ介绍
一个消息队列。可以异步处理消息。生产者把消息发给交换机,交换机可以根据规则把消息推送到不同消息队列,最后由消费者处理队列的消息
场景:
1. 流量削峰:多个服务器根据自身能力去处理请求
2. 解耦:把业务逻辑拆分,比如下订单和发邮件通过不同队去处理
3. 异步:把消息放到消息队列后,就直接进行返回或者执行其他过程
安装
rabbitMQ是由erLang编写,所以安装前需要下载erlang环境。RabbitMQ包里自带了可视化管理插件,安装后,通过ip加端口访问。默认只能本地访问,配置后可以远程访问。15672(内存运行)
交换机类型
Fanout模式:广播模式(类似发布订阅模式),生产者把消息发送到fanout交换机,然后交换机把消息发送到所有绑定该交换机的队列
direct模式:生产者发送消息时携带一个路由键,交换机根据消息的路由键发送到对应的队列
topic模式(常用):相比direct模式,引入通配符更好管理路由键。引入通配符:* 和# ,*匹配一个,#匹配0个或多个。
相关问题
MQ挂掉怎么处理?:通过集群方式
消费者获取消息:1.消费者去轮询。2.消息队列有消息就通知消费者。
数据丢失:
生产者端丢失:回调确认,消息队列确认收到消息会发送确认
消息队列丢失:消息持久化,存到磁盘
消费者端丢失:回调确认(收到消息发送给消息队列,消息队列才删除消息)或者让生产者重发(比如生产者给每个消息一个编号,如果消费数量少了,查少了哪个就重发)
保证数据有序:把需要有序的消息放到同一个队列通过一个消费者去消费。
安全优化
接口限流
为什么用:1.防止恶意脚本来刷秒杀接口(防止dos攻击)。2. 防止请求超载。(一般限流为系统最大qps的70%-80%)
具体实现:
在需要限流的方法上添加自定义注解,然后拦截器里拦截请求后判断方法上是否有限流注解,有的话进行限流逻辑。
限流逻辑:通过令牌桶的方式限流,创建一个大小固定的令牌桶,然后通过定时器定时向桶内放入令牌,每个请求都会尝试获取一个令牌,没获取到就不允许访问。(不同接口限流用不同令牌桶)
防止恶意消耗令牌:
实现:对用户IP校验限制次数,同一个ip5s内只能访问10次。redis生成一个计数器(ip和次数),5秒后过期
为什么用计数器:通过计数器的方式,因为如果对每个用户创建一个令牌桶不太合理,限流不采用计数器是因为它有临界问题和浪费资源问题
具体代码实现:
自定义注解通过@interface关键字实现
分支主题
令牌增减都通过sychronized加锁,通过@Scheduled(fixedRate = 1000)这个注解来执行增加令牌的操作
令牌桶容量实际情况肯定是根据接口访量动态扩容嘛,我就是实现并测试了一下这个功能,容量2,1秒生成一个令牌。一秒内发送多次请求就拒绝访问量。
计数器方式
设计:如一个用户5秒内只能访问5次。为用户访问时,redis生成一个计数器,访问一次计数器加1,超过5次就不允许访问,计数器5秒后失效,所以5秒后又可以继续访问了。
计数器方式存在的问题:
临界问题:比如系统每分钟能承受最大请求是100,然后通过计数器限流每分钟70。但如果在前一分钟快结束和后一分钟刚开始时,同时有70的请求来,就会导致请求超过最大请求。
浪费资源:比如系统每分钟能承受最大请求是100,然后通过计数器限流每分钟70。如果前一分钟前30秒后后一分钟后三十秒就把请求处理完了,这两分钟中间空出来的60秒服务器就是空闲的。
四种主流的方式:计数器,漏桶,令牌桶,滑动窗口
漏桶
类似漏斗,入水速率可以任意。出水速率恒定,缺点就是:因为出水速率恒定,所以不能处理瞬时突发流量
场景:当调用的第三方系统本身没有保护机制,由于我们不能更改第三方系统,不能控制它的消费速度,所以只有在调用时控制调用速度。
滑动窗口
跟计数器类似,只是限流控制得更精细。通过把时间间隔划分成更小的粒度,比如计数器是1分钟限流多少,滑动窗口就把一分钟划分成10段,所以每过6秒,滑动窗口就会移动,并减去前六秒的访问。(缺点:越精细需要的存储空间越大)
隐藏秒杀接口地址
为什么用:防止黄牛或者不法分子,用脚本来不停访问秒杀接口,速度比普通用户快且导致服务器压力大。所以点击秒杀时,暴露的地址是获取部分秒杀地址,然后前台拼接后,通过秒杀地址去执行秒杀。
具体设计
以点击秒杀时,暴露的地址是获取部分秒杀地址,获取部分秒杀地址是通过uuid生成,然后存到redis中,(用户和商品作为key,uuid作为value)。后面抢购时需要做地址校验,最后在返回到前台拼接成完整秒杀地址进行秒杀。
不同用户不同秒杀地址:这样做是为了防止黄牛得到秒杀地址后,用多个账户去秒杀商品
秒杀地址在redis中有过期时间的,防止黄牛实现通过获取秒杀地址接口,然后拼接成秒杀地址,根据这个秒杀地址进行不停访问。
缺点:黄牛可以写一个脚本:先根据获取秒杀地址的接口获取秒杀地址,然后拼接成真正秒杀地址,然后访问秒杀接口。不断执行这个脚本。
复杂验证码
为什么用:点击秒杀时,需要输入验证码,防止脚本和拉长请求时间
实现:通过第三方验证码工具,生成时存入redis(key为:用户id商品id组合,value为验证码计算结果)校验时直接根据redis获取即可。
压力测试
具体执行
公共页面(不用登录就能查看):在Jmeter中配置好ip,端口,路径,请求方式。设置测试的线程数和循环次数。运行然后查看聚合报告(里面记录了请求总数,平均响应时间,吞吐量等信息)
秒杀测试(需要不同用户,去访问):先生成n个用户存到数据库,然后循环去登录用户(用代码去模拟登录HttpURLConnection),登录了session中才有对应已用户,然后把用户名,和uuid存到csv文件中(压力测试时做参数,模拟cookie中的uuid)。最后进行压力测试。
测试工具JMeter相关概念
JMeter:java编写的,多线程的,可以对http, 数据库,tcp等进行压力测试
Qps:每秒查询数:服务器每秒查询次数
Tps:每秒事务数:每秒完成(请求服务器,服务器查询,响应)的个数 。
Qps和Tps区别:一个tps可能包含多个qps(如一访问一个页面会请求服务器3次,对应一个TPS,3个QPS)
吞吐量:是指系统在单位时间内处理请求的数量(与tps区别在于,tps强调时刻:每秒处理事务,吞吐量强调的是时间段内:每秒处理事务。像跑步,一秒跑10米,但如果10秒,我可能就只能跑80米,每秒就只有8米,10米对应tps,8米就对应吞吐量)
使用
windows操作:通过可视化工具,先填写你要测试的链接信息(ip,端口,路径,参数(如登录过的页面需要传入cookie中的uuid)),然后设置测试的线程数和循环次数。再添加需要查看的信息(聚合报告(多次测试的累计),结果树等。),最后运行就可以了(测试链接信息可以保存)。
linux测试:在linux上测试:先安装,然后在window上配置好的测试连接信息,再在linux上通过JMeter执行,最后生成的结果拷贝回window可视化软件中查看。
配置不同用户进行压力测试:先用一个csv文件中保存用户id和参数,然后导入项目做一些配置(就不用单独写参数了),最后运行
表设计
用户表:用户名,密码,盐,手机号,地址
商品表:id,名称,详情,图片,价格(类型用decimal,java是BigDecimal),库存
订单表:id,用户id,商品id,商品数量,商品单价,状态(0:新建,1:已支付,3:已退款,4:已完成),创建时间,支付时间,用户手机号,用户地址
主流方案
先进行预约,给预约用户发token,没有token的用户就抢购时就单机展示没抢到
token:相当于一个令牌,是服务端生成的一串字符串(根据用户信息加密生成),作为客户端进行请求的一个标识。下次请求就不用验证用户密码。(比通过cookie存一个uuid,再与服务器端session获取更好,服务器不用存相关信息)
通过网关对ip进行限流和拒绝重复请求
真正入库操作也非常耗时,所以可以把订单放redis中。同时用户抢购后也能快速查看订单。真正入库就通过mq去消费。
redis压力可以通过分片和二级缓存来处理
hash分片算法:把数据分为k个槽位,各个服务器负责处理对应槽位段的数据,然后把数据的key进行 hash 后对k取模落在哪个槽位就由哪个服务器处理
代码简化
Lombok组件
不需要再写getter、setter或equals方法,只要有一个注解@DATA即可,还能加一些构造方法的注解。
validation组件(校验工具)
可以通过加注解来省略代码判断,如不能为空,字符串最少几个等等(也可以自定义组件)
逆向工程
可以通过表直接生成controller ,service每一层的,一套代码(这里使用mybatis自带的逆向生成工具,一般是新建一个逆向工程项目,然后生成需要的代码,拷贝到项目中去)
分布式
发布订阅
含义:是一种消息通信模式,通过一个字典实现,字典键就是一个个频道,字典值就是一个链表(保存所有订阅了该频道的客户端)。比如在某个频道发布一个消息,频道所对应链表的所以客户端都能收到消息。
主从复制
含义
一个主节点,多个从节点,数据复制只能从主节点到从节点,主节点master写为主,从节点slave读为主。可用于数据的备份和恢复,负载均衡(读写分离)。
相关说明
复制原理(数据同步):每次从机重新连接主机都会进行一次全量复制,正常情况都是进行增量复制(只复制主机中新增加的数据)
如果主机宕机了,可以手动使用slaveof no one让自己变成主机(手动太麻烦,后面引入哨兵模式)
哨兵模式
原理
主机宕机后,自动从从机中选择主机(哨兵是独立的进程,用于监控每个reids服务器是否正常运行。如果发现主机宕机后,就会选择一个新的主机,然后通过发布订阅模式通知其他的从机让他们切换主机)
多哨兵模式
哨兵也可能宕机,所以一般也有多个哨兵,当一个哨兵认为主机服务器不可用时,成为主观下线。当发现主机不可用的哨兵到达一定值时,就会进行failover故障转移操作,通过投票选择一个新的主机。
优缺点
优点:主从复制的优点他都有,自动切换主机更加健壮。
缺点:在线扩容。多哨兵配置麻烦。
数据一致性(持久层和缓存数据同步)
定义
如正常情况:A先修改完数据库-->A修改缓存-->B修改数据-->B修改缓存。(可能发生:A先修改完数据库-->B修改数据-->B修改缓存-->A修改缓存)
解决
加锁(影响效率)
同步删除:修改数据库后删除缓存(问题:1. 并发场景(a线程查,初始缓存空,然后从数据库查,还没来得及写缓存,此时b线程更新数据,然后删除缓存过后a线程才写入缓存,这时缓存为脏数据。)。2. 缓存删除失败的时候)
延迟双删:删除缓存,修改数据库,延迟一会再删除缓存(问题:1.延迟需要时间,高并发场景性能较低。2. 在读写分离情况下,由于数据同步需要时间,从库可能读到脏数据)
利用MQ异步重试机制:保证缓存删除成功,实现最终一致性即可。(问题:耦合性比较高,每个修改数据地方都需要发消息到MQ。解决:通过主从复制机制,mysql从机监听binlog日志,发生修改时再发消息到MQ)
TCP 协议如何保证可靠传输
滑动窗口负责数据的可靠传输
把要发送的数据分为4各部分,分为已发送并确认,已发送未确认,即将发送,等待发送。发送窗口由已经发送未确认和即将发送组成,当收到ACK确认后,窗口收缩,当窗口大于容纳数据时就会进行窗口扩张。
丢包、乱序、重传解决:一个数据包 包含起始序列号,长度,数据内容(数据包里可以包含多个字节),收到数据后回复ack(下一包的起始序列号)和接收窗口大小(根据网络情况调节 )。最后接收端组装,丢失的进行重传,或者发送端长时间没收到ack也会进行超时重传。
流量控制:协调端到端的数据传输接收能力(通过滑动窗口实现,让发送方发送速率不要太快,接收端好接收。通过接收方回复确认的时候会发送接收窗口大小来实现)
拥塞控制
避免发送方注入到网络过多的数据, 防止网络负载过大(具体实现:发送方维护一个拥塞窗口(也就是发送窗口),一开始发送方发送一个字节,在收到接收方的确认后,拥塞窗口就呈指数增加(慢开始算法),当拥塞窗口超过慢开始阈值的时候,拥塞窗口就呈线性增加(拥塞避免算法),直到出现超时后,把慢开始阈值设置为出现拥塞时拥塞窗口的一半,再重复慢开始的操作)
快重传和快恢复:当收到3个对同一个报文段的重复确认,就代表数据丢失,慢开始阈值降为当前窗口一半,窗口不重新慢开始,而是直接从阈值处线性增加。
超时重传(ARQ 协议):
停止等待 ARQ 协议:每发完一个分组就等待,在规定时间内没收到确认就重发。(信道利用率低,等待时间长)
连续 ARQ 协议:发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,不需要等待对方确认。接收方收到窗口所有数据后再确认(接收端组装,丢失的进行重传)
校验和: TCP 数据包中有校验和字段,接收端可以根据它来确认数据包是否有损坏,损坏则丢弃
有序编号:每个tcp数据包都有编号,接收端可以把数据有序的组装。
内存管理
名词解释
物理地址:物理设备上真正的地址。
逻辑地址:是指计算机用户看到的地址,操作系统可以将不连续的物理地址隐射成连续的逻辑地址。
虚拟内存:把磁盘空间作为虚拟内存使用。不用把程序的全部加载到内存,可以把暂时不需要的放到虚拟内存中,在需要时进行数据交换
虚拟地址空间:虚拟地址空间是映射到物理内存和虚拟内存上
内存管理方式
分页存储管理
把进程分为大小固定的页,内存分为同样大小的块。优点:内存空间利用率高。(最后一页装不满也会有内存碎片)
页表:存储页号和内存块号的映射(页号从0开始,所以省略)。
地址转换:逻辑地址除以每页大小可以得到页号和页内偏移,根据页表可以找到页号对应的块号,块号加页内偏移就为物理内存地址。例子:如一页为1k,1023逻辑地址转物理地址步骤:1.1023除以1k 的0余1023,再通过页表查0对应的内存块号,最后内存块号乘以1024+1023就为物理地址。
多级页表作用:1. 可用于减少页表占用的连续空间。2. 可以节约内存(相同内存多级可以隐射更多的内存地址,可以不用把页表全部加载到内存)
分段存储管理
把进程按模块分为大小不固定段,逻辑地址为二维(段号和段内偏移组成) 。优点:方便按照逻辑模块实现信息的共享和保护。缺点:因为段内地址必须连续,所以容易产生内存碎片
段表:由存储段号,起始地址,和段长组成。
地址转换:根据段表找到段号对应的起始地址,起始地址加上段内偏移就为对应物理内存地址(段内偏移超过段长就会越界中断)。例子:如将逻辑地址(1,103)转物理地址步骤:先根据1找到段表中的段号,然后判断103是否小于段长,小于物理地址就为起始地址+103。大于就越界中断
段页式存储管理
将进程按逻辑模块分段,再将各段分页。
请求分页
请求分页是在分页的基础上实现。区别在于是否将作业的全部地址空间同时装入主存。请求分页存储管理只需把当前需要的页面装入内存。所以请求分页存储管理可以提供虚存。(请求分段同理)
地址映射及变换
全相联映射:主存任一一块都可以放到cache的任一一行中.
直接映射:将主存按cache大小分成若干区,区内的块与cache行一一对应
组相联映射:将cache分组,主存分区,每个区包含的块数等于cache组数。组内全相联映射,组间直接映射。
页面置换算法
最佳置换算法OPT:每次选择淘汰的页面都是在未来最长时间不在访问的页面。(拥有最好的性能,实际无法达到,可用于评价其他算法的优劣)
先进先出置换算法FIFO
最少使用置换算法LFU
最近最久未使用置换算法LRU
时钟置换算法CLOCK(每个页面设置一个访问位,访问:访问位设为1,需要淘汰时循环扫描访问位:1变为0,0淘汰)
抖动(颠簸现象)
页面在内存和外存间,短时间频繁调度称为抖动。主要原因是分配给进程的内存不够。
结构型模式
适配器模式
通过引入适配器将原本接口不兼容的类能一起工作。(一般采用对象适配器,不采用类适配器,对象适配器可以通过多态实现多种类型的适配)
例子:如电脑上只有usb接口,插不上网线接口,通过引入一个转接器(适配器)将电脑接上网线。
桥接模式
适用于多维度的场景,将每个维度分离,使它们都可以独立的变化,再通过桥接把不同维度组装成起来。
例子:比如品牌和电脑的组合,新增一个品牌时,电脑和品牌的组合就会多很多,所以把品牌和电脑分离。通过桥接的方法把它们再组合起来(在电脑中通过一个品牌的成员变量,创建电脑时,传入具体品牌)
代理模式
访问某个对象时,是通过代理对象间接访问
Spring中的AOP
索引
介绍
定义:MySQL索引的建立对于MySQL的高效运行是最重要的,索引可以大大提高MySQL的查询速度。
分类
逻辑分类
普通索引(KEY/INDEX)
普通索引是最基本的索引,它没有任何限制,值可以为空,仅加速查询。
唯一索引(UNIQUE KEY)
索引列的值不同(可以有一个空值,空值跟其他值也不同)。
主键索引(PRIMARY KEY)
主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。
联合索引
组合索引指在多个字段上创建的索引,只有在查询条件中包含了创建索引时的第一个字段,索引才会被使用(遵循最左前缀原则)
全文索引(FULLTEXT)
全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较(特定的数据库引擎才有,如MyISAM)
普通索引和唯一索引选择:优先选择普通索引,普通索引可以配合changebuffer,在更新时速度较快(唯一索引不能更新到changebuffer是因为它值必须唯一,所以需要判断是否有冲突,没冲突再插入)
按存储形式分类
聚集索引:数据页和索引页放一起(InnoDB)。注意:除了主键索引外,其他索引的叶子节点没有存实际值,是因为这样每建一个索引都复制一份数据非常占空间。还有就是更新某值的时候所有索引对应的数据都要修改值。所以实际存的为索引的值和对应的主键,当找到某索引时,再通过其主键去主键索引找对应的全部值(这就称为:回表)
非聚集索引:数据页和索引页分开存放,叶子节点存的是数据的地址信息(MyIsam)。非聚集索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同
按数据结构分类
Hash类型
速度快,但是不支持范围查询,所以不常用
b+树
b+树一般三层,每个节点16kb,对应一次IO。数据只存在叶子节点。非叶子节点存放索引。叶子节点间通过双向链表连接。
为什么用b+树不用b树:1.非叶子节点不存数据,能存更多的索引,对应读写磁盘代价更低。2. 非叶子节点不存数据,所以查询比b树更稳定。3. 叶子节点双向链表连接,所以支持范围查询
结构具体为:叶子节点为数据页,其他节点为索引页。数据页中有页目录和对应数据,数据按索引顺序存放(数据页间双向链表,页中数据单向链表),每个二层索引页中存放的是若干页地址,一个页地址对应一个数据页中页目录的起始地址。一层索引页中存放的若干页地址,一个页地址对应二层索引页中一个索引页中的第一个页地址。
可存放千万行的数据:假如索引为int类型占4字节,指针占6字节,两层可以指向的数据页就是(16*1024/10 )^2=2683044页。每页16kB,如果一条记录占1kb,那就能存2683044*16=42,928,704(四千多万条数据)
各种树
二叉树:缺点:树的高度不稳定,可能变成链表,影响效率。
平衡二叉树:左右子树高度差不超过1
平衡二叉查找树:平衡二叉树基础上保证,中序遍历为递增。(平衡算法有:红黑树,AVL树)
红黑树:增删差时间复杂度为logn(子树高度差可能大于1,不是严格平衡二叉树)。特征:1.红节点的孩子是黑节点,2. 叶子节点不存储数据空节点,3. 叶节点和根节点为黑色。4.从任一节点出发到任意叶子节点的路径,黑色节点的数量是相等的。(所以去掉红色节点后长度不超过log(n+1),又因为两个红色节点间一定有黑色节点,所以总高度不超过2log(n+1))。
AVL树:相对红黑树高度控制更严格,查找效率小于logn。但增加删除的调整都相对复杂(所以一般用红黑树)
B树:一个节点有两个或多个孩子节点,且一个节包含多个元素(可设置),节点间和节点内部元素都是排好序的(可设置)。
B+树:相对于B树,叶子节点包含所有元素,且叶子节点间有指针。
引入:1. 为了保证节点均匀分布,就有了平衡二叉树。2. 为了减少树的高度和一次IO读取更多的数据引入B树。3. B+树在B树的基础上,在查询的稳定性 和排序方面进行了优化,因为B+树所有数据都会保存到叶子节点,且叶子节点有序。
索引失效
查不到造成
最左前缀原则:联合索引必须遵循最左前缀原则,是因为比如联合索引字段为a,b,c。它是在a排好序的基础上再排b,所以如果查询条件没有a的话,是不能通过b来走索引的
索引查询更慢造成
范围查询可能走索引也可能不走索引。(因为如果查询范围数据量大,而且索引中不包含需要查询的字段。会导致大量的回表操作,速度还不如全表扫描。)(但是如果查询的字段在索引中,这个时候就可以走索引了,因为不用回表操作了。这个称为覆盖索引)
索引下推
MySQL 5.6 提出,目的是减少回表次数(是联合索引的情况下),具体做法是:可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,再进行回表,从而减少回表次数。
如联合索引为name 和age两个字段, 5.6之前根据最左匹配原则,是在联合索引中找到符合name的主键,然后回表,server层再对符合age的进行过滤。5.6过后是回表前就先对age进行过滤
回表
非主键索引叶子节点只存了索引字段和主键字段,所以如果查询的字段不在索引字段中就需要通过主键去查需要的字段,这个就称为回表
核心日志(WAL)
binlog
binlog 是server层的日志,用于恢复和同步数据,但不支持崩溃恢复。(三种记录形式:记录改变的行,记录sql语句,或者混合模式)写入策略为追加。
redolog
redo log是引擎层(innodb)的日志,物理日志,记录的是在数据页上做了什么修改,所以支持崩溃恢复。写入策略是循环写
undolog
undolog属于引擎层(innodb)的日志,记录数据的历史版本,用于事务失败回滚和MVCC版本控制
日志生成顺序与数据恢复
一条语句执行流程
查询语句:1.通过连接器处理客户端连接和授权认证等 2.mysql8.0前先会去缓存区找。没有就先进行词法分析,语法分析,优化sql语句。3. 执行器调用引擎接口,返回执行结果(从磁盘读取)。
更新语句:相对查询语句,1. 在读到数据后记录undolog日志.2. 修改内存数据,然后记录redolog(prepare状态),然后记录binlog,3. 最后提交数据,同时记录redolog(commit状态)
执行顺序(sql解析步骤)
先有表(from ,join ,on)然后过滤(where),然后分组(group by ,having)然后选择显示字段和去重(select distinct),然后排序(order by),最后分页(limit)
redolog为什么需要两阶段提交
redo log日志在事务提交后会持久化到磁盘,如果先写redolog再写binlog,如果一条语句在redolog后崩溃,此时本机有数据,但由于未写binlog,所以从节点会少一条数据,会造成主从不一致。
如果先写binlog再写redolog,如果一条语句写binlog之后崩溃了,事务未提交本机无数据,且因为本机未写入redolog,所以本机数据不能回复。但从服务器可能根据同步的binlog日志已经进行数据修改,从而造成主从不一致。
所以引出两阶段提交:在写binlog前先写一次redolog(prepare状态,此时事务未提交),这样如果是在binlog之后崩溃,从服务器有数据,本机也可以根据redolog重做数据,保证主从数据一致性。
数据库崩溃恢复:
redolog和binlog都无记录,未修改不操作
redolog为prepare状态,binlog无记录,通过undolog回滚
redolog为prepare状态,binlog有记录。根据redo log对数据进行重做(相当于提交事务)。(这里不回滚是因为binlog日志可能已经同步到从服务器了,所以回滚可能导致主从数据不一致)
redolog为commit状态,binlog有记录。正常完成的事务,不需要恢复。
Springboot
SpringBoot启动过程
创建springApplication对象,然后执行该对象的run方法。run方法核心功能:创建spring容器和创建相关的bean对象。(这个就是spring中处理的事,springboot还需要做一些整合的工作,比如一些初始化和自动装配的操作)
具体
构造springApplication对象
判断应用类型(如servlet,reactive ,如何判断?:类加载器是否加载成功)
通过spring.factories 加载应用上下文初始器和应用事件监听器(实际就是一个类路径,可以通过这个创建对应的监听器。)
给它的一个属性赋值传入的启动类(因为后面run方法中会对启动类的注解进行解析)
调用springApplication的run方法
创建监听器(用于监听run方法的执行。)
根据应用类型创建上下文(上下文包含Spring容器)
执行上下文初始化器的初始化方法(主要就是识别启动类,方便后续对其注解进行解析,以及注册spring的核心组件类)
执行refresh方法(主要就是自动装配和启动spring容器)
SpringBoot自动配置原理
自动装配就是从自动配置类(如spring.factories)中获取到对应的bean对象,然后由spring容器来帮我们进行管理。
比如:引入reids的maven后,就会有对应jar包,然后里面有对应的自动配置类,里面就会根据条件装配生成bean对象放到spring容器中
条件装配底层原理?
条件装配:根据是否存在某些类或bean对象,或者根据配置文件中的一些属性来进行装配。如:@ConditionalOnClass({a.class}):通过类加载器去加载a这个类,能加载才执行)
原理:ASM 是一个 Java 字节码操控框架 1. 先通过ASM工具获取字节码文件中@ConditionalOnClass后的字符串“a.class”,2. 再判断这个类是否能加载,3. 最后决定是否装载。
扩展:Springboot启动时扫描哪些需要创建bean也是通过ASM来判断扫描路径下的类是否包含@component或者@bean。而不是把扫描路径下所有类字节码文件都加载到jvm中,通过反射来判断是否存在@component或者@bean(这样浪费内存空间)
Springboot底层如何判断选择tomcat 还是jett?
Springboot 是先创建Spring容器,再启动tomcat或者jett,具体选择哪个是根据项目中有哪个相关的依赖。Spring默认为tomcat是因为pom.xml中spring-boot-starter-web中默认有tomcat的依赖。
拦截器
底层是AOP,使用:
1. 先创建拦截器(实现 HandlerInterceptor 接口,实现一些方法:如方法执行前或执行后执行一些逻辑),
2. 然后配置拦截器,指定拦截规则(实现WebMvcConfigurer,然后定义拦截规则 )
stater组件
目的是方便开发,引入相关stater组件后,可以导入对应功能的jar包和维护jar包版本间的依赖。 还有就是stater组件内部集成了自动装配的机制,对于它所需要的维护的外部化配置可以在配置文件中配置就可以了(如reids的接口等)
跳转
重定向(客户端跳转,地址栏变化):return “redirect:+地址”;
内部转发(服务器跳转,地址栏不变,可携带参数和request范围值):return “forword:+地址”;
过滤器和拦截器
过滤器:它是针对tomcat 中servlet来说,它依赖于servlet容器,过滤器可以对几乎所有的请求起作用(因为它是针对tomcat)。它是基于函数回调实现。
拦截器:它是针对Spring,所有请求都要经过前端控制器DispatcherServlet,所以拦截器只针对经过DispatcherServlet路径的请求生效。基于java反射机制实现。
IO模型(网络编程)
IO模型
BIO:阻塞IO,如accept和recv都是阻塞的,要有连接或者数据才会继续执行。
NIO:非阻塞IO,如accept和recv都是非阻塞的,没有连接或数据直接返回-1。
AIO:异步IO,跟非阻塞IO不同的是,当有连接或数据时,会通过回调函数通知。(linux没用AIO是因为如果大量IO时,太多回调函数会造成触发不过来。AIO使用少量IO情况下,比如磁盘,文件可以采用AIO)
IO多路复用
产生
网络服务器可以与大量客户端建立tcp连接,如果每个连接都用一个线程,CPU上下文切换消耗大,所以引出单线程IO多路复用的方式(单线程如何在处理A客户时,接收其他客户请求,答:通过DMA实现,DMA控制器是独立CPU专门处理IO的)。
原始IO多路复用
while(true)中遍历fd,如果某个fd有数据就处理。这样就能处理每个网络连接。但如果需要程序来轮询判断fd是否有数据程序效率很低,所以可以考虑使用内核来轮询判断。(文件描述符fd:linux一切都是文件,每一个网络连接在内核中都是以文件描述符fd的形式存在。文件描述符存储的是一个数字)
三种IO多路复用
select
select函数过程:1. 将文件描述符集合拷贝到内核。2. 通过内核态监听哪些fd有数据(有数据对应bitmap置位)3.把文件描述符集合拷贝到用户态,对有数据的进行处理。
select五个参数:文件描述符最大值+1,读文件描述符集合,写文件描述符集合,异常文件描述符集合,超时时间。(读写文件描述符集合中为一个bitmap,共1024位,根据已建立连接的文件描述符fd决定哪位为1,其余为0。第一个参数的作用在于内核遍历bitmap的范围)
select函数流程:先接收连接,得到已建立连接的文件描述符数组集合fds,同时得到文件描述符最大值max,然后while循环(先通过fds初始化读写文件描述符集合set,然后执行select(max+1,rset,wset,null,null)函数,最后处理有数据的fd )
优点:通过内核态监听哪个fd有数据,速度快
缺点:1. bitmap有1024的限制,fd大于1024就不能用select了。2. bitmap不能重用,下次while循环需要重新初始化。3. 执行select函数会用用户态到内核态的数据拷贝开销。4. select返回时,并不知道哪个fd有数据,需要通过遍历bitmap得知,遍历时间复杂度o(n)。
poll
特点:与select不同在于没有用bitmap,而是把fd封装成一个结构体pollfd,在结构体中包含fd,events(表示读还是写),revents(标志位,表示是否有数据),poll函数传输结构体数组。
优点:自定义数组,没有fd大小限制。使用结构体所以能解决select中bitmap不可重用的问题。
缺点:依然存在select中3.4问题(数据拷贝和轮询判断)。
epoll
优点
1. 不用把fd从用户态拷贝到内核态,而是通过内存映射(mmap)来实现与内核消息传递。(mmap:可以将普通文件映射到内存中,对内存修改直接映射到文件本身。传统读文件时,需要先把数据从磁盘拷贝到内核态,内核态再拷贝到用户态,非常影响性能,)
2. epoll不采用轮询的方式判断是否有数据,而采用事件通知的形式(有数据就通过回调函数通知)
缺点
当事件触发比较频繁时,回调函数也会被频繁触发
三个重要方法
epoll_create 在内核空间中创建一个红黑树根节点。
epoll_ctl 每个连接建立时(accept),就把fd加入根节点。(添加删除节点)
epoll_wait 有数据的fd会通知内核,然后会把它放到就绪链表中,最后返回有数据fd的个数。
两种触发方式
水平触发:有数据但没有处理的fd,会在下一次调用epoll_wait的时候再次放到就绪链表中(缺点就是如果一直不处理就重复返回,耗费性能)
边缘触发:只放一次,不处理就不会通知了
场景选择
select场景:适用连接数量少或者活跃的连接数量多的情况。
epoll场景:适用连接数量多,但活跃的连接数量少的情况。
三种方式如果传入超时时间为0就是非阻塞。否则就是阻塞
reactor
reactor模型:通俗讲就是将对io处理转换为对事件的处理,通过reactor来进行管理。它对epoll进行封装,然后根据epoll_wait返回的fd判断可读还是可写,然后进行相应处理。(它由非阻塞IO和IO多路复用组成)
单reactor模型:网络处理(比如判断哪些fd可读)跟具体的业务逻辑处理(处理可读fd)都在一个线程中(缺点就是不适用高并发情况)。例子:redis采用的就是单reactor模型,因为redis是内存数据库,操作其中数据非常快,所以可以采用这种方式。
单reactor模型+任务队列+线程池:与单reactor相比,它把具体的业务逻辑处理交给其他线程去处理。(缺点很大流量情况下不适用,就需要采用多reactor模型)例子:skynet采用单reactor模型+任务队列+线程池
多reactor模型:一个reactor专门负责接收网络连接,然后把连接分配给多个reactor。每个reactor都是一个线程或进程。例子:nginx采用多reactor模型,每个reactor是一个进程。(通过master接收网络连接,然后fork出多个子进程worker,子进程都监听一个端口,共享一块内存,跟据锁机制来判断那个worker处理连接)
多reactor+消息队列+线程池:对于大流量且业务密集的情况下可以采用这种模式。
Spring
Spring AOP
含义
面向切面编程,说的通俗易懂就是可以动态的,不修改源代码的情况下为程序添加功能,如在一个方法的前后添加一下功能。具体实现:在beans.xml中定义一个切面(包含新功能的类)和一个切点(表示在哪个类的哪个方法添加切面)
AOP底层
底层实现是通过代理对象,代理对象类继承普通对象类,代理对象中有一个target属性(它就是依赖注入后的普通对象),在代理对象中,会重写切点方法(在重写的方法里面加入切面逻辑以及调用target对象的切点方法)
Spring事务底层原理
Spring事务底层也是基于AOP。如果类中加了@Transactional,在创建该类bean的 初始化后 这个过程,就会生成代理对象。通过代理对象执行某个方法时,先在代理对象中判断改方法是否有@Transactional,有就重写这个方法,1. 先通过事务管理器新建一个数据库连接,2. 然后关闭数据库自动提交,3. 再通过target执行普通对象的方法。4. 最后数据库提交或者回滚。
事务传播机制(事务嵌套)
默认是将内层事务合并到外层一起提交,有异常一起回滚。
其他事务传播:1. @Transactional(propagation=Propagation.NEVER):表示如果已经存在事务的话,会抛出异常。2. propagation=mandatory:外层没有事务,抛出异常。3. propagation=support:内层事务是否生效依赖外层,外层是事务才生效。
注意:事务方法只能通过代理对像调用,才会生效(如果在一个类中一个普通方法调用一个有事务注解的方法,是不会生效的,因为普通方法调用相当于是普通对象调用的事务方法,解决办法,增加一个属性,自己注入自己,然后在普通方法中调用事务方法)。
Spring IOC
Spring IOC
控制反转(依赖注入),一种面向对象编程的思想,通过引入IOC容器,利用依赖注入的方式,实现对象之间的解耦。(联想齿轮。bean对象创建通过反射实现)
IOC容器:管理bean对象,负责创建对象和建立对象间的依赖。
Bean之间的关系
1. 继承。2. 依赖(如果某个bean依赖其他bean,创建该bean时会先创建依赖的那个bean)。3. 引用(通过property对bean起别名)
创建bean
Spring如何创建bean对象
1. 通过构造方法创建一个普通对象。2. 对普通对象进行依赖注入。3. 进行初始化(包括初始化前,初始化,初始化后)。4. 将完整的bean对象放入map单例池中
初始化前过程(通过@PostConstruct注解,也称后置处理器)
作用于bean对象创建过程中的初始化前。原理就是:通过反射判断创建的对象中是否有带有这个注解的方法,有的话就执行(通过这种方式,程序员可以对对象里的属性赋值等操作)
初始化过程
如果创建的对象实现了InitializingBean这个接口,就会在该类中实现一个初始化方法(程序员可以做一些初始化工作),在bean初始化的时候就会判断该对象是否实现了这个接口,如果实现了就执行该初始化方法
初始化后过程
初始化后可以进行AOP,会产生代理对象,如果有代理对象,就把代理对象放单例池中。
推断构造方法是什么
在创建bean对象时,前面会先创建普通对象,如果类中有多个构造方法,spring会选择无参的构造方法,如果没有就会报错,也可以加@Autowired来告诉spring选择哪个构造方法。
依赖注入先bytype再byName是什么
创建bean时,如果类属性中有@Autowired(或者构造方法需要传某个对象),就会给它注入值,先去map单例池中找是否存在,先通过byType找,如果只有一个直接赋值,如果有多个再byName找(如果bytype找到多个,且byName没找到则报错)。如果map单例池中没有就直接创建一个bean再赋值。
普通对象和bean对象区别
Bean对象就是在普通对象的基础上进行依赖注入,放入map单例池过后的对象
配置bean
@Configuration
@Configuration用于定义配置类,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被用于构建bean对象。@Configuration注解的类本身也是一个bean对象。
@Component
可以用于注册所有bean((@Controller和@Service和@Repository,分别用于组测controller,service,dao层))
为什么有了@Compent,还需要@Bean呢?
如果想将第三方的类变成组件,你又没有没有源代码,也就没办法使用@Component进行自动配置,这种时候使用@Bean就比较合适了。
注入bean
@Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上
@Autowired:默认按类型装配(这个注解是属业spring的)有三种方法注入:1. 属性注入,2. 构造方法注入,3. set方法注入
@Resource:默认按照名称进行装配(这个注解属于J2EE的)
循环依赖
定义
创建bean a时,需要b,创建b时需要a
具体解决方案:三级缓存
一级缓存:就是单例池,存放完整的单例bean
二级缓存:1.用于存放还没有经过完整bean生命周期步骤的单例bean。2.保证不会重复创建多个不完整的bean
三级缓存:存放的lambda表达式(其中包含对应的普通对象和是否需要aop的一些信息)
创建过程
1. 开始创建A,先把A名称放入creatingSet中,表示A正在创建。2. 创建A普通对象后,将lambda表达式存到三级缓存中(lambda表达式包含A的普通对及相关信息,如是否需要AOP)3. 对A中属性B进行填充时,先到单例池找是否有B,这时没有,就开始创建B。4. 创建B普通对象后开始填充A,现在单例池中找,没找到。5. 然后判断是否在creatingSet中,找到则出现了循环依赖。6. 然后到二级缓存中找是否存在A,此时没找到。7. 然后通过三级缓存获取A的lambda,根据是否需要AOP,创建A代理对象或者普通对象。8. 最后将创建的A对象放入二级缓存。9. 继续后续操作完成B的创建。10. 继续完成A的后续创建。
导致循环依赖失效原因及解决
情况1:构造方法导致循环依赖失效
原因:a构造方法需要b,b构造方法需要a,这样连普通对象都生成不了
解决:在构造方法前加@lazy,通过这种方式不会产生循环依赖(原理是先直接给该类属性生成一个代理对象,并未真正生成对应的bean,而是在具体调用该属性的时候再去生成真正对应的单例bean,此时当前类已经生成了单例bean,所以不会出现循环依赖。)
情况2:@Async 会导致循环依赖失效
原因:因为如果类本身有aop的话,在循环依赖时会提前生成代理对象,而提前生成的代理对象只会处理aop, 在创建bean对象最后步骤里会判断二级缓存是否已经存在该代理对象,如果存在,就不会创建新的代理对象,但是@Async并没有处理,所以又会创建一个处理@Async的代理对象,这样就会存在两个不同的代理对象,所以会抛出异常。
解决方法:可以通过在会导致循环依赖的类属性前加@lazy,通过这种方式不会产生循环依赖(原理是先直接给该类属性生成一个代理对象,并未真正生成对应的bean,而是在具体调用该属性的时候再去生成真正对应的单例bean,此时当前类已经生成了单例bean,所以不会出现循环依赖。)
单例bean和单例模式区别
单例bean:可以一个类对应不同名称的多个bean
单例模式:一个类对应的bean只能有一个