admin管理员组文章数量:1589049
目录
- 一、Java基础
- 1、编译型与解释型
- 2、基本数据类型
- 3、==与equals
- 4、浅拷贝、深拷贝、引用拷贝
- 5、`StringBuilder` 与 `StringBuffer`
- 6、接口与抽象类
- 7、重载和重写
- 8、static和final
- 9、异常
- 10、`I/O `
- `I/O `流
- `I/O `模型:
- `BIO` (Blocking I/O): 同步阻塞
- `NIO` (Non-blocking/New I/O):同步非阻塞
- `AIO` (Asynchronous I/O):异步
- 11、反射
- 12、泛型
- 二、集合
- 1、List,Set,Queue,Map
- `List`(有序):
- `Set`(无重复):
- `Queue`(实现排队功能的叫号机):
- `Map`(键值对):
- 2、`HashMap` , `Hashtable`, `HashSet` ,`TreeMap` ,` ConcurrentHashMap`
- HashMap 和 Hashtable
- hashmap扩容
- HashMap 和 HashSet 区别
- HashMap 和 TreeMap 区别
- ConcurrentHashMap
- 3、解决哈希冲突
- 三、多线程
- 1、进程与线程
- 2、线程状态
- 3、进程线程的通信与调度
- 进程通信(IPC)与进程同步
- 进程调度
- 线程间的通信方式
- 线程同步
- 进程切换与线程切换
- 僵尸进程与孤儿进程
- 5、死锁
- 6、并发编程(原子、可见、有序)
- 7、内存模型
- CPU缓存模型
- Java内存模型
- 8、`synchronized`、`ReentrantLock`、`volatile`
- `synchronized`
- `synchronized`和`Lock`
- `synchronized`和`volatile` :
- 9、`Threadlocal`
- 10、线程池
- 简介
- 创建
- 11、JUC包
- ①Atomic 原子类
- ②locks
- ③并发容器
- ④线程
- 12、AQS
- 原理:
- AQS 定义两种资源共享方式 :
- AQS相关组件
- AQS设计模式
- 13、乐观锁与悲观锁
- 四、JVM
- 1、Java内存区域
- **堆和栈的区别?**
- java对象占用堆情况
- 2、OOM、FullGC排查
- 异常
- 解决
- 内存泄漏排查:
- 3、字符串常量
- 4、垃圾回收
- 发生情况
- 流程
- 内存分配策略
- 堆中回收对象
- 如何判断对象已经死亡
- `GC roots`
- 引用
- 方法区中回收废弃常量和无用的类
- 垃圾收集算法
- 垃圾回收器
- 5、JVM相关参数
- 6、类加载
- java文件运行过程
- new 一个对象的过程
- 对象创建过程
- 类加载器与双亲委派模型
- 五、计算机网络
- 1、`OSI`七层
- 2、`TCP`三次握手+四次挥手
- 3、`TCP`
- TCP与UDP
- `TCP`如何保证传输可靠:
- 查看TCP状态
- TCP头部报文
- 4、在浏览器中输入`url`地址显示主页的过程
- 过程:
- 用到的协议:
- DNS解析:
- URI 和 URL
- ping一个地址用到的协议
- 5、状态码
- 6、`HTTP`
- HTTP 1.0 和 HTTP 1.1
- HTTP 1.0 和 HTTP 2.0
- HTTP3.0(QUIC)
- HTTP 和 HTTPS
- HTTPS
- HTTP报文结构
- 请求报文
- 响应报文
- 7、`cookie`、`session`、`token`
- cookie、session
- token
- 8、GET和POST
- 六、操作系统
- 1、CPU
- 2、操作系统功能
- kernel 内核
- 功能
- 用户态与内核态
- 3、进程创建(用户程序-->在内存中执行)
- 4、内存管理
- 常见内存管理机制
- 虚拟地址空间
- 页面置换算法
- 5、linux文件系统
- inode
- 文件类型
- 权限
- 目录树
- 6、linux基本命令
- 目录
- 文件
- 用户管理
- 其他
- 查看日志
- 七、数据库
- 1、数据库设计
- 三大范式:
- 五大约束:
- 数据库设计通常分为哪几步
- 2、MySQL引擎
- 3、MySQL 高性能优化:
- 设计规范
- 大表优化
- 全局id 、分布式id
- 4、慢查询优化
- 5、`MySQL`事务的ACID
- `ACID`:
- `ACID`实现原理:
- `bin log`(归档日志)、`redo log`和`undo log`:
- 事务隔离级别:
- 6、锁定读与非锁定读
- 一致性非锁定读:**快照读**
- 锁定读:**当前读**
- Repeatable Read时解决幻读:
- 7、MVCC
- 原理
- 读已提交and可重复读:
- 8、一条 SQL 语句的被执行
- 基本架构
- 解决数据**一致性**的问题:
- 崩溃恢复
- 9、索引
- 索引类型
- 哈希索引
- B+Tree索引:聚簇和非聚簇
- 为什么是b+树:
- 索引失效
- 创建和使用索引
- 10、几种树
- 红黑树
- B树:
- B+树
- 11、Redis
- `MySQL`与`Redis`:
- 应用场景(分布式锁)
- 数据结构
- 12、redis单线程
- 处理多个连接:`select` / `poll` / `epoll`
- epoll水平触发LT与边缘触发ET
- 13、redis持久化
- 14、redis淘汰与删除
- 15、redis事务
- 16、缓存问题
- 缓存击穿
- 缓存穿透
- 缓存雪崩
- 数据库与缓存不一致
- 17、Redis主从复制
- 18、redis解决秒杀相关问题
- 19、sql语句
- 部门工资最高的员工
- 部门工资前三高
- 20、Elasticsearch
- 文档
- 索引
- 八、Spring
- 1、 IoC 和 AOP
- IoC
- AOP
- spring容器的启动流程
- 2、bean
- 将一个类声明为 bean 的注解
- bean生命周期
- bean的作用域
- bean自动装配
- 单例 bean 的线程安全问题
- 3、spring设计模式
- 4、常见注解
- 5、spring的循环依赖
- 6、spring事务
- 7、springboot
- 自动装配
- @Autowired
- 8、mybatis
- 原理
- 缓存
- 防止SQL注入
- 9、springcloud微服务
- nacos注册中心与openfeign远程调用:
- 分布式CAP+BASE
- Nginx与Ribbon 负载均衡:
- dubbo:
- rpc:远程调用
- 10、thrift
- 九、消息队列
- 1、好处与问题
- 2、常见MQ
- kafka
- RocketMQ
- RabbitMQ
- 3、RabbitMQ
- 组成
- 如何让保证不被重复消费
- 如何保证RabbitMQ消息的可靠传输?
- 4、RabbitMQ工作模式
- 5、RocketMQ
- 组成
- 十、设计模式
- 1、单例模式(Singleton Pattern)
- 懒汉模式,线程不安全
- 懒汉模式,线程安全
- 饿汉模式,线程安全
- 双重校验锁,线程安全
- 2、工厂模式(Factory Pattern)
- 3、原型模式(Prototype Pattern)
- 4、装饰器模式(Decorator Pattern)
- 5、代理模式(Proxy Pattern)
- 6、适配器模式(Adapter Pattern)
- 7、模板模式(Template Pattern)
- 智力题
一、Java基础
1、编译型与解释型
编译型 :编译型语言 会通过编译器 将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型 :解释型语言 会通过解释器 一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
Java:既是编译性语言(需要由编译器编译为.class字节码文件),又是解释性语言(需要由JVM读一行执行一行,由解释器解释为操作系统能执行的命令)
Java的编译器是javac.exe,解释器是java.exe
2、基本数据类型
引⽤数据类型:数组、类、接口
基本数据类型存放在 Java 虚拟机栈
中的局部变量表中,而包装类型属于对象类型,存在于堆
中。
所有整型包装类对象之间值的比较,全部使用 equals
方法比较。
3、==与equals
对象相等:比的是内存中存放的内容是否相等。
引用相等:比较的是他们指向的内存地址是否相等。
==:
基本数据类型(八种基本数据类型)比较的是值,引用数据类型(数组、类、接口)比较的是内存地址。
equals() :
只能判断引⽤数据类型,若没有覆盖equals,则等价于 ==
;若覆盖了,大多数情况比如string,integer,则是比较内容
string:
虚拟机在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true,引用相同
System.out.println(x==z); // false,==:string比较引用,开辟了新的堆内存空间,所以false
System.out.println(x.equals(y)); // true,equals:string:比较值,相同
System.out.println(x.equals(z)); // true,equals:string比较值,相同
hashcode
与equals:
获取哈希码(散列码),返回int,确定对象在哈希表中的索引位置,任何类都有hashcode()函数。
如果hashcode相同,再调用equals()检查两个对象是否真的相同,否则hashset不会让其加入成功,这样减少了equals使用次数,提高就执行速度。
两个对象相同,则有相同的hashcode;反之hashcode相同,对象不一定相等。所以要重写equals时必须重写hashcode()函数,如果没有重写 hashCode(),则该 class的两个对象⽆论如何都不会相等。
4、浅拷贝、深拷贝、引用拷贝
5、StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder 类,没有用 final 关键字修饰,所以这两种对象都是可变的。String 类中使用 final 关键字修饰,不可变。
StringBuffer 是线程安全的(加了同步锁)。 StringBuilder 是非线程安全的。
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 操作少量的数据: 适用 String
- 单线程大量数据: 适用 StringBuilder
- 多线程大量数据: 适用 StringBuffer
6、接口与抽象类
- 抽象类要被子类继承, 接口要被类实现,一个类可以实现多个接口,但只能实现一个抽象类。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
- 接口方法默认修饰符是 public,只能有static、 final 变量;抽象方法可以有 public、 protected 和 default 这些修饰符(为了被重写所以不能使用 private )
- 抽象类里可以没有抽象方法;如果一个类里有抽象方法, 那么这个类只能是抽象类。抽象方法只能申明,不能实现。abstract void abc(),不能写成abstract void abc(){}。
7、重载和重写
重写:
1.重写发生在父类与子类之间,是运行时的多态性
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.不可被重写:final,static,private
重载:
1.重载Overload发生在一个类中,是编译时的多态性
2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序),返回值类型可以相同也可以不相同。
8、static和final
static修饰的方法:
1、父类中的静态方法可以被继承、但不能被子类重写。
2、如果在子类中写一个和父类中一样的静态方法,那么该静态方法由该子类特有,两者不构成重写关系。
3、static修饰的属性的初始化在编译期(类加载的时候),初始化后能改变。跟该类的具体对象无关
final修饰:
1、修饰类表示不允许被继承。
2、修饰方法表示不允许被子类重写,但是可以被子类继承,不能修饰构造方法。一个类中的private方法会隐式地被指定为final方法。
3、修饰变量表示不允许被修改
a)方法内部的局部变量,使用前被赋值即可(只能赋值一次),没有必要非得初始化。
b)类中的成员变量(如果没有在定义时候初始化,那么只能在构造代码块中或者构造方法中赋值)
c)基本数据类型的变量(初始化赋值之后不能更改)
d)引用数据类型的变量(初始化之后不能再指向另外一个对象,但对象的内容是可以变的)
9、异常
Throwable
类有两个重要的子类 Exception
(异常)和 Error
(错误)。
Exception
:程序本身可以处理的异常,可以通过 try-catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理否则无法通过编译 ) 和 不受检查异常(可以不处理)。Error
属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
try-catch-finally:
无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行,且会覆盖之前的返回值。
除非: 在异常语句之前System.exit(int);线程死亡;关闭CPU
10、I/O
I/O
流
- 序列化: 将对象转换成可取用格式(例如二进制字节流,存成文件,存于缓冲,或经由网络中发送)的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成对象的过程
对于不想进行序列化的变量,使用 transient
关键字修饰。 当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
- static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
I/O
模型:
我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
Java 中 3 种常见 IO 模型:BIO,NIO,AIO
BIO
(Blocking I/O): 同步阻塞
应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。
NIO
(Non-blocking/New I/O):同步非阻塞
NIO 基于通道(Channel)和缓存区(Buffer),对于高负载、高并发的(网络)应用,应使用 NIO 。可以直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,显著提升性能。
NIO提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
NIO 通过轮询操作,避免了一直阻塞。应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
IO 多路复用模型中:线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。
在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。
AIO
(Asynchronous I/O):异步
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。
当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。
用户线程只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了
同步IO操作会引起进程阻塞直到IO操作完成。
异步IO操作不引起进程阻塞。
11、反射
指在运行状态中,动态获取信息以及动态调用对象方法的功能
- 对于任意一个类都能够知道这个类所有的属性和方法;
- 对于任意一个对象,都能够调用它的任意一个方法;
对象在运行是都会出现两种类型:编译时类型和运行时类型。编译时类型由声明对象时使用的类型来决定,运行时的类型由实际赋值给对象的类型决定 。比如编译时根本无法预知该对象和类属于哪些类,程序只能靠运行时信息来发现该对象和类的信息,那就要用到反射了。
反射最大的用途就是框架,比如:Spring中的Di/IoC、Hibernate中的find(Class clazz)、Jdbc中的classForName()、SpringBoot的Service注解等,很多开发框架都用到了反射机制
优点:
1)能够运行时动态获取类的实例,提高灵活性;
2)与动态编译结合
缺点:
1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
获取class对象的三种方式:
// 这一new 产生一个Student对象,一个Class对象。*
Student student = new Student();
// 调用某个类的 class 属性来获取该类对应的 Class 对象
Class studentClass2 = Student.class;
// 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 )
Class studentClass3 = Class.forName("com.reflect.Student")
12、泛型
泛型的原理就是“类型的参数化”,即把类型看作参数。也就是说把所有要操作的数据类型看作参数,就像方法的形式参数是运行时传递的值的占位符一样。
好处:
①类型安全。泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型
②消除了强制类型转换。使得代码可读性好,减少了很多出错的机会
③安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
原理:
泛型的实现是靠类型擦除技术。类型擦除是在编译期完成的,在编译期,编译器会将泛型的类型参数都擦除成它的限定类型,如果没有,则擦除为object类型,之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化。
二、集合
Collection
: List、Set
和 Queue
Map
: hashmap、hashtable、hashtree
1、List,Set,Queue,Map
List
(有序):
存储的元素是有序的、可重复的。(可以有多个元素引⽤相同的对象)
Arraylist
: Object[] 数组Vector
:Object[] 数组(线程安全)LinkedList
: 双向链表
Arraylist
线程不安全解决:
- 使用
Vector
(ArrayList
所有方法加synchronized
,太重)。 - 使用
java.concurrent.CopyOnWriteArrayList
(推荐)。是JUC的类,通过写时复制来实现读写分离。比如其add()
方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。
Arraylist
扩容:
当添加元素时,如果元素个数+1> 当前数组长度 【size + 1 > elementData.length】时,进行扩容, int newCapacity = oldCapacity + (oldCapacity >> 1)
,所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍,将旧数组内容通过Array.copyOf全部复制到新数组(如果5,就扩成5+2=7)
Arraylist
与 LinkedList
- 都不同步,不保证线程安全
Arraylist
底层是 object 动态数组,支持快速随机访问,适合频繁查找get(int index)
LinkedList
底层是双向链表,插入、添加、删除快,更占内存vector
是同步的,线程安全,但效率太低,尽量用 Arraylist
Set
(无重复):
不允许重复的集合。不会有多个元素引用相同的对象。无序,hashcode决定
HashSet
(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素 。只不过Value被写死了,是一个private static final Object
对象。用于不需要保证元素插入和取出顺序的场景LinkedHashSet
: LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。 底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树) ,用于支持对元素自定义排序规则的场景
Queue
(实现排队功能的叫号机):
按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
Queue
:是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则Deque
:是双端队列,在队列的两端均可以插入或删除元素PriorityQueue
: Object[] 数组来实现二叉堆,优先级最高的元素先出队,默认是小顶堆,通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。非线程安全的,不支持存储nullArrayQueue
: Object[] 数组 + 双指针- 阻塞队列:当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。不用手动控制什么时候该被阻塞,什么时候该被唤醒
阻塞队列的应用:
①生产者消费者模式,不用加锁
②线程池
Map
(键值对):
使用键值对存储。key 是无序的、不可重复的,value 是无序的、可重复的,两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象
HashMap
: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
(红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。)
LinkedHashMap
: LinkedHashMap 继承自 HashMap,它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
2、HashMap
, Hashtable
, HashSet
,TreeMap
, ConcurrentHashMap
HashMap 和 Hashtable
- HashMap 是非线程安全的,无并发控制,可能会链表成环; HashTable 是线程安全的(内部的方法基本都经过synchronized 修饰);
- HashMap 中, null 可以作为键,这样的键只有一个,在table[0]位置,可以有一个或多个键所对应的值为 null。但是在 HashTable 中会异常。
- ①创建时如果不指定容量初始值, Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。 HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方
(2的幂次方:Hash 值的范围值非常大,先做对数组的长度取模运算,得到的余数才是对应的数组下标。当length 是 2 的 幂次方,hash%length
等价于hash&(length-1)
, &
相比%
效率较高)
- HashMap 在解决哈希冲突时,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。
hashmap扩容
容量(初始16),加载因子(0.75),阈值(容量x加载因子)
如果加载因子比较大,扩容发生的频率比较低,浪费的空间比较小,发生hash冲突的几率比较大,底层的红黑树变得异常复杂。对于查询效率极其不利;
- JDK1.7中扩容的时候需要将所有的数据重新计算HashCode(
&
),然后赋给新的HashMap<K,V>,十分耗费性能;JDK1.8中不需要重新计算HashCode,经过rehash之后元素的位置,要么是在原位置,要么是在原位置再移动2次幂的位置。 - JDK1.7 链表采用头插法,多线程打乱插入新链表的顺序会造成环形链和数据丢失的情况。JDK1.8 采用尾插法。
- 1.7中是只要大于等于阈值就直接扩容2倍;而1.8中,当数组容量未达到64时,以2倍进行扩容,超过64之后,若桶中元素个数超过7,就将链表转换为红黑树(同时红黑树中的元素个数小于6就会还原为链表),当红黑树中元素超过32的时候才会再次扩容。
- 1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容
jdk1.7数据丢失与链表成环:
- 数据丢失:假设两个线程A、B都在进行put操作,当线程A判断是否出现hash碰撞后,由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全
- 链表成环:链表采用头插法,导致插入的顺序和原来链表的顺序相反的。两个线程同时插入 a 和 b ,指可能出现 a.next=b 同时b.next=a的情况。jdk1.8采用尾插法解决这个问题
hashmap的get操作:
- 当调用get方法时会调用hash函数,这个hash函数会将key的hashCode值返回,返回的hashcode与entry数组长度-1进行逻辑与运算得到一个index值,用这个index值来确定数据存储在entry数组当中的位置。
- 通过循环来遍历索引位置对应的链表
- 如果hash函数得到的hash值与entry对象key的hash值相等,并且entry对象当中的key值与get方法传进来的key值equals相同则返回entry对象的value值,否则返回null。
HashMap 和 HashSet 区别
HashSet
底层就是基于 HashMap 实现的,仅存储对象,使用成员对象计算hashcode,(hashmap存储键值对,使用key计算hashcode)
HashMap 和 TreeMap 区别
HashMap来说, TreeMap
主要多了对集合中的元素根据键排序(SortedMap 接口)的能力,以及对集合内元素的搜索(NavigableMap 接口)的能力。
ConcurrentHashMap
ConcurrentHashMap 解决多线程并发环境下 HashMap死循环问题,线程安全的,比Hashtable效率高。底层为数组+链表/红黑树。
- JDK1.7用数组+链表,并发控制segment分段锁(Segment 继承于 ReentrantLock)。value 以及链表都是 volatile 修饰的,保证了获取时的可见性,但不能保证并发原子性,所以还需要加锁
- JDK1.8用 Node 数组+链表+红黑树的数据结构,并发控制使用synchronized+CAS 代替 Segment。
synchronized
只锁定当前链表或红黑二叉树的首节点。先 CAS(UnsafepareAndSwapObject方法)尝试插入,如果有其它线程在该位置提前插入了节点,就自旋再次尝试,失败了再加锁。
(Hashtable使用synchronized,没有分段,效率低)
3、解决哈希冲突
产生原因:
由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)
解决方法:
①开放地址方法
(1)线性探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。
(2)再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
(3)伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
②链式地址法(HashMap的哈希冲突解决方法)
创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。
③建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据。
④再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
三、多线程
1、进程与线程
进程与线程:
- 线程是进程的一部分,一个进程拥有多个线程
- 进程是资源分配最小单位,线程是独立调度的最小单位
比如:启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
多进程与多线程:
- 多进程数据天然隔离,安全;多线程共享数据,方便。
- 进程间不会相互影响;线程一个挂掉可能导致整个进程挂掉
- 多进程开销大,创建销毁慢,切换复杂,CPU利用率低;多线程开销小,创建销毁快,切换简单,CPU利用率高;
- 多机分布的用进程,多核分布的用线程。
并行与并发:
- 并行–>单位时间内,多线程在多核CPU上就是并行
- 并发–>时间段内,多线程在单核CPU上就是并发
通信:
- 进程:管道,信号,信号量,消息队列,共享内存,套接字socket
- 线程:锁(互斥锁,共享锁);全局变量(用volatile关键字保证可见性);threadLocal;wait()+notify()
多线程的实现:
- Java:①继承自 Thread 类②实现Runnable接口
- python:①把一个函数传入并创建Thread实例,然后调用start()开始执行 ②定义一个类,继承自threading.Thread类,使用 init(self) 方法进行初始化,在 run(self)方法中写上该线程要执行的程序,然后调用 start() 方法执行
协程:
- 协程是一种用户态的轻量级线程,协程的调度完全由用户控制(线程由操作系统调度)。
- 协程位于同一个线程上,上下文切换开销非常小,可以不加锁。
- 遇到耗时的 IO 操作时,通过协程调度,执行下一个任务,避免阻塞在 IO 上,让 CPU 空等
2、线程状态
sleep()
和wait()
暂停线程的执行:
- sleep() 没有释放锁,而 wait() 释放了锁 。
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
- wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
start()
和run()
:
start()
:会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了,会自动执行 run()。run()
:main 线程下的普通方法去执行,不是多线程。
上下文切换:
如果线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,CPU 为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用。
发生线程切换,保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。
发生原因:
- 主动让出 CPU,比如调用了 sleep(), wait() 等。
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
3、进程线程的通信与调度
进程控制块PCB(process control block),程序段、相关数据段,三部分构成了进程
进程通信(IPC)与进程同步
- 进程同步:控制多个进程按一定顺序执行;
- 进程通信:进程间传输信息。
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息
临界区:对临界资源进行访问的那段代码称为临界区。需要互斥访问临界资源
- 同步:多个进程按一定顺序执行;
- 互斥:多个进程在同一时刻只有一个进程能进入临界区。
进程通信:管道(匿名/有名)、消息队列、信号、信号量、共享内存、套接字
- 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。只存在于内存中的文件
- 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 有名管道严格遵循先进先出(FIFO)。
- 消息队列(Message Queuing) :消息队列是消息的链表,与管道不同的是,消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以独立于读写进程存在,读进程可以根据消息类型有选择地接收消息,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流,以及缓冲区大小受限等缺点。
- 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;承载信息量少。
- 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。主要用于解决与同步相关的问题并避免竞争条件。
- 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
- 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
共享内存:
- 进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
- 进程之间在共享内存时,会保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
- mmap()系统调用实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
进程调度
为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率
- 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
- 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。UNIX 操作系统采取的便是这种调度算法。
- 优先级调度 :为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
线程间的通信方式
- wait() 和 notify()
- 管道流 PipeStream,一个线程发送数据到输出到管道,另一个线程从输出管道中读取
- ThreadLocal 主要解决为每个线程绑定自己的值
线程同步
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。有下面三种线程同步的方式:互斥量、信号量、事件
- 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种 Lock 都是这种机制。 - 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件(Event) :Wait/Notify,通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操
进程切换与线程切换
- 切换新的页表,然后使用新的虚拟地址空间。
- 切换内核栈,硬件上下文切换。(CPU的所有寄存器中的值、进程的状态以及堆栈上的内容)
线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
僵尸进程与孤儿进程
僵尸进程: 一个父进程利用fork创建子进程,如果子进程退出,而父进程没有利用wait 或者 waitpid 来获取子进程的状态信息,那么子进程的状态描述符依然保存在系统中。
孤儿进程:一个父进程退出, 而它的一个或几个子进程仍然还在运行,那么这些子进程就会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集的工作
5、死锁
- 互斥条件:该资源任意一个时刻只由一个线程占用。(无法破坏)
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(破坏:一次性申请所有资源)
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(破坏:申请不到别的,主动释放已有资源)
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(破坏:按一定顺序申请资源,反序释放)
死锁恢复:
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
- 建立有向图,拓扑排序,释放占用资源最少的线程
死锁避免:银行家算法。银行家算法的主要思想是避免系统进入不安全状态。在每次进行资源分配时,它首先检查系统是否有足够的资源满足要求,如果有,则先进行分配,并对分配后的新状态进行安全性检查。如果新状态安全,则正式分配上述资源,否则就拒绝分配上述资源。这样,它保证系统始终处于安全状态,从而避免死锁现象的发生。
找死锁的步骤:
- 通过
jps
确定当前执行任务的进程号 - 然后执行
jstack
命令查看当前进程堆栈信息 - 然后将会看到
Found a total of 1 deadlock
6、并发编程(原子、可见、有序)
- 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。
synchronized
可以保证代码片段的原子性。 - 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可见性。 - 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执行顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁止指令进行重排序优化。
7、内存模型
缓存一致性(Cache Coherence),解决是多个缓存副本之间的数据的一致性问题。
内存一致性(Memory Consistency),保证的是多线程程序访问内存时可以读到什么值。
CPU缓存模型
程序运行的时候我们把外存的数据复制到内存,解决硬盘访问速度过慢的问题。
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。
问题:内存缓存不一致
两个线程同时从Cache中读取i=1
进行i++
,返回Main Memory之后 i=2,而正确结果应该是 i=3
解决:
①加锁
②缓存一致性协议,保证每个缓存中使用的共享变量的副本是一致的。
Java内存模型
线程可以把变量保存本地内存中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取,即可见性。
load:主内存 read 后,把 read 的值放入工作内存的变量副本中
assign:把线程执行后的值赋给工作内存的变量
store:把工作内存的一个值送到主内存
8、synchronized
、ReentrantLock
、volatile
synchronized
可以保证被它修饰的普通方法(锁实例对象)、静态方法(锁class对象)、代码块在任意时刻只能有一个线程执行。通过对 对象监视器 monitor
的获取来实现。
JDK1.6 对锁的实现引入了大量的优化,无锁、偏向锁、轻量级锁、重量级锁,他们会随着竞争的激烈而逐渐升级(可以升级不可降级),偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
- 偏向锁:一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,当第一个线程来访问它的时候,它会偏向这个线程。偏向第一个线程,这个线程在修改对象头,成为偏向锁时,使用 CAS 操作,将对象头中 ThreadId 改成自己的 ID,之后再访问这个对象时,只需要对比 ID 即可。
- 轻量级锁:一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程;如果原来的线程依然存活,则偏向锁升级为轻量级锁。一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋),另外一个线程就会释放锁
- 重量级锁:当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞
MarkWord
:
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁;如果不是,则使用CAS将当前线程的ID替换Mard Word
- 如果成功则表示当前线程获得偏向锁,置偏向标志位1;如果失败,则说明发生竞争,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。如果自旋失败,则升级为重量级锁。
自旋:
就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取,好处是减少线程切换的上下文开销,缺点是会消耗CPU
synchronized
和Lock
synchronized
和Lock
-
synchronized
依赖于 JVM ,优化都是在虚拟机层面实现的,并没有直接暴露给我们。而Lock
是一个接口,依赖于API,是 JDK 层面实现的,需要 lock() 和 unlock() 方法配合try/finally语句块来完成,所以我们可以通过查看它的源代码,来看它是如何实现的。 -
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过
unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; -
ReentrantLock
和synchronized
都是可重入锁,自己可以再次获取自己的内部锁。同一个线程每次获取锁,锁的计数器都子增1,所以要等到锁的计数器下降为0时才能释放锁。 -
ReentrantLock
比synchronized
增加了⼀些⾼级功能:
①通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
②ReentrantLock提供了一种能够中断等待锁的线程的机制。sync
不可中断,除非抛出异常或者正常运行完成。Lock
是可中断的,通过调用interrupt()
方法。
③ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。公平锁就是先等待的线程先获得锁。
④可以实现“选择性通知”(锁可以绑定多个条件),可以通过一个或多个Condition来处理等待唤醒,一个Condition就是AQS中的一条等待队列。synchronized 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,
公平锁与非公平锁:
- 公平锁在获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的队首,则直接占有锁,否则插入到等待队列,FIFO原则。
- 非公平锁比较粗鲁,上来直接先尝试占有锁,失败则采用公平锁方式。非公平锁的优点是吞吐量比公平锁更大。
选择性通知condition:
- 一个Lock可以持有多个等待队列。condition内部维护了一个单向的 等待队列。
- 所有调用
condition.await
方法的线程会加入到等待队列中,并且线程状态转换为等待状态。 - 当前线程进入等待状态(进入等待队列),如果其他线程调用condition的
signal
或者signalAll
方法,如果该线程能够从await()方法返回的话,一定是该线程获取了与condition相关联的lock。如果在等待状态中被中断会抛出被中断异常。
synchronized
和volatile
:
volatile
只能用于变量,而synchronized
可以修饰⽅法以及代码块。volatile
关键字是线程同步的轻量级实现,所以volatile
性能比synchronized
要好。但synchronized
引入的偏向锁和轻量级锁优化之后,执行效率有了显著提升, 实际开发中使用synchronized
的场景还是更多一些。volatile
能保证数据的可见性,但不能保证数据的原子性。synchronized
两者都能保证。volatile
主要用于解决变量在多个线程之间的可见性、有序性,而synchronized
解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
9、Threadlocal
Threadlocal在线程内提供局部变量,让每个线程绑定自己的值,创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,避免线程竞争
底层是通过 ThreadLocalMap
进行存储键值,每个 Thread 都有一个 ThreadLocal.ThreadLocalMap
对象。每个ThreadLocal类创建一个Map,ThreadLocal(的弱引用)作为key, 实例对象作为value,通过Thread.currentThread()
获取到当前线程对象后,通过getMap(Thread t)
访问到该线程的ThreadLocalMap ,这样就能达到各个线程的值隔离的效果。
让每个线程可以关联多个 ThreadLocal变量。会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放多个 ThreadLocal变量, key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值
ThreadLocal key 为弱引用,而 value 是强引用。在垃圾回收时,一旦发现了只具有弱引用的对象(不一定会立即发现),都会回收它的内存。
- 如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。
- 这样 ThreadLocalMap 中就会出现key为null的Entry,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
- ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后,最好手动调用remove()方法。
10、线程池
简介
池化技术:线程池、数据库连接池、Http 连接池,为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了⼀种限制和管理资源。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
实现 Runnable
接口和 Callable
接口:
- Callable带返回值。
- Callable会抛出异常。
- Callable覆写
call()
方法,而不是run()
方法。
执行 execute()
方法和 submit()
方法:
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。
sleep
和wait
:
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会
创建
《阿里巴巴Java开发⼿册》中不允许使用 Executors 去创建,而是通过ThreadPoolExecutor
的方式。规避资源耗尽的风险
/**
* ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。
* 构造函数有四种,workQueue及以前必须都有,
* threadFactory和rejectHandler,全没有、有一个、全有,共四种
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize < 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler ==
null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
import java.util.concurrent.*;
public class Test {
private static ExecutorService pool;
public static void main( String[] args )
{
//maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.execute(new ThreadTask());
}
//获取返回值
//Future<?> future=threadPoolExecutor.submit(futureTask);
//Object value=future.get();
}
}
class ThreadTask implements Runnable{
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
就像一个银行。corePoolSize
就像银行的“当值窗口“,比如今天有2位柜员在受理客户请求(任务)。如果超过2个客户,那么新的客户就会在等候区(等待队列workQueue
)等待。当等候区也满了,这个时候就要开启“加班窗口”,让其它3位柜员来加班,此时达到最大窗口maximumPoolSize
,为5个。如果开启了所有窗口,等候区依然满员,此时就应该启动“拒绝策略” handler,告诉不断涌入的客户,叫他们不要进入,已经爆满了。由于不再涌入新客户,办完事的客户增多,窗口开始空闲,这个时候就通过
keepAlivetTime`将多余的3个“加班窗口”取消,恢复到2个“当值窗口”。
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor 其他常见参数:
- keepAliveTime : 空闲线程存活时间。当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory : executor 创建新线程的时候会用到。
- handler : 饱和策略。如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时, ThreadPoolTaskExecutor 定义⼀些策略:抛出异常拒绝新任务(AbortPolicy,Spring默认);直接丢弃新任务(DiscardPolicy);丢弃最早未处理任务(DiscardOledestPolicy);调用自己的线程运行任务(CallerRunsPolicy)
workQueue
的类型为BlockingQueue:
- 有界任务队列
ArrayBlockingQueue
:基于数组的先进先出队列,此队列创建时必须指定大小; - 无界任务队列
LinkedBlockingQueue
:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;吞吐量高,但会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。 - 直接提交队列
synchronousQueue
:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
常见线程池:
- FixedThreadPool:固定线程数的线程池。使用
LinkedBlockingQueue
实现。即线程池中没有可运行任务时,它也不会释放工作线程,需要shutdown - SingleThreadExecutor:是只有一个线程的线程池,corePoolSize 和 maximumPoolSize 都被设置为 1。使用
LinkedBlockingQueue
实现。在线程池中没有任务时可执行,也不会释放系统资源的,所以需要shutdown - CachedThreadPool:变长线程池,一个任务设置一个线程。使用
SynchronousQueue
实现。corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE。总会迫使线程池增加新的线程去执行新的任务。在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,不需要shutdown - ScheduledThreadPoolExecutor 主要用来以固定的频率或固定时延执行任务,使用的任务队列 DelayQueue 封装了一个 PriorityQueue
import java.util.concurrent.*;
public class Test {
private static Runnable getThread(final int i) {
return new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
// 1、newSingleThreadExecutor
ExecutorService singPool = Executors.newSingleThreadExecutor();
for (int i=0;i<10;i++){
singPool.execute(getThread(i));
}
singPool.shutdown();
// 2、newFixedThreadPool
// ExecutorService fixPool = Executors.newFixedThreadPool(5);
// for (int i = 0; i < 10; i++) {
// fixPool.execute(getThread(i));
// }
// fixPool.shutdown();
// 3、newCachedThreadPool
// ExecutorService cachePool = Executors.newCachedThreadPool();
// for (int i=0;i<10;i++){
// cachePool.execute(getThread(i));//不需要shutdown(),keepAliveTime后自动释放
// }
}
}
11、JUC包
①Atomic 原子类
基本类型( AtomicInteger、AtomicLong、 AtomicBoolean)、数组类型、引用类型、对象的属性修改类型
AtomicInteger 类主要利用 CAS
(compare and swap) + volatile
和 native 方法来保证原子操作,从而避免 synchronized 的高开销,不用加锁也可以保证线程安全。执行效率大为提升。
CAS
的原理是拿期望的值和原本的一个值作比较,如果相等,则证明共享数据没有被修改,替换成操作后的新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,用新的数重新执行刚才的操作。因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
CAS实际上是一种自旋锁,
- 一直循环,开销比较大。
- 只能保证一个变量的原子操作,多个变量依然要加锁。
- 引出了ABA问题。
使用AtomicStampedReference
类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。
②locks
主要提供了显示锁,如重入锁(ReentrantLock)和读写锁(ReadWriteLock)。核心是AQS这个抽象队列同步器框架。
③并发容器
④线程
12、AQS
是⼀个⽤来构建锁和同步器的框架,比如ReentrantLock
原理:
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
- 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中。
AQS 通过内置的虚拟的 双向队列来完成获取资源线程的排队工作;AQS 使用一个 private volatile int state
成员变量来表示同步状态;使用 CAS
对该同步状态进行原子操作实现对其值的修改。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。
AQS 定义两种资源共享方式 :
- Exclusive(独占):只有一个线程能执行,如
ReentrantLock
。又可分为公平锁和非公平锁。公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。 非公平锁:当线程要获取锁时,无视队列顺序,谁抢到就是谁的 - Share(共享):多个线程可同时执行,如CountDownLatch、Semaphore、 CyclicBarrier、ReadWriteLock 。
AQS相关组件
-
Semaphore
(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -
CountDownLatch
(倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。state
初始化为 N, N 个子线程是并行执行的,每个子线程执行完后countDown() 一次,state 会CAS
减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。 -
CyclicBarrier
(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
AQS设计模式
同步器的设计是基于模板方法模式的(这和我们以往通过实现接口的方式有很大区别)
如果需要自定义同步器一般的方式是这样:
- 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
13、乐观锁与悲观锁
- 悲观锁—少读多写情况下
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁用完后再把资源转让给其它线程)。在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。 - 乐观锁—少写多读情况下
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,CAS
算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS
实现的。MySQL
乐观锁通过MVCC
版本实现。
四、JVM
1、Java内存区域
栈管运行,堆管存储
-
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是本地(Native)方法,计数器的值为空(Undefined)。
-
虚拟机栈存储的都是局部变量,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、常量池引用等信息。变量一旦离开作用域就会被释放。栈内存的更新速度很快。
-
本地方法栈类似虚拟机栈,为虚拟机使用到的 Native 方法服务
-
堆:主要⽤于存放新创建的数组和对象,堆里的实体虽然不会被随时释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
-
方法区:主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(方法区也称永久代,JVM设置其大小固定,而元空间使用直接内存,只受系统内存限制,也可根据运行时需求动态调整大小。)
-
直接内存:这不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。新加入的 NIO(New Input/Output) 类,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。提高了性能
堆和栈的区别?
1)栈由系统自动分配,速度较快;而堆是人为申请开辟;一般速度比较慢
2)栈获得的空间较小,而堆获得的空间较大;
3)栈是连续的空间,而堆是不连续的空间。
java对象占用堆情况
public class Model {
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modela = new Model(2);
Model modelb = new Model(3);
}
Mark Word
:偏向锁的标识;通过cas操作竞争锁,如果竞争成功则操作Mark Word中线程ID设置为当前线程ID
metadata
:指向方法区中的类。JVM 默认开启指针压缩,一个指针用 4 字节表示
array length
:若当前对象不为数组,则对象头中不存在此项信息
填充字段:为满足 Java 对象所占内存必须为 8 字节的倍数
计算一个对象的大小:
参考
①java.lang.instrument.Instrumentation.getObjectSize()
此方法求出的值是一个近似值,并不准确
②java 中的 sun.misc.Unsafe类,有一个 objectFieldOffset(Field f) 方法,表示获取指定字段在所在实例中的起始地址偏移量。计算开头和结尾字段,就能知道该对象整体的大小。但只能是对象本身的大小,并没有计算对象中的引用类型所引用的对象的大小
2、OOM、FullGC排查
异常
程序计数器是唯一一个不会发生OutOfMemoryError的区域。
栈中异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
堆中异常:
- 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。(方法区中也会)
内存泄漏:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。
- 代码中存在死循环或循环产生过多重复的对象实体。
- 启动参数内存值设定的过小。
- 软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生
解决
- 堆溢出,堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
- 方法区溢出,一般出现于大量Class,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m或-XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
- 栈溢出StackOverflowError可以通过虚拟机参数-Xss来设置栈的大小。
内存泄漏排查:
- 确定频繁Full GC现象,找出进程唯一ID
使用jps(jps -l)
可以查看运行的Java进程;
(或ps(ps aux | grep tomat)找出这个进程在本地虚拟机的唯一ID;)
(或者top,可以看到 %MEM;) jstack pid
查看JVM中运行线程的状态,可以定位CPU占用过高位置,定位死锁位置。- 再使用“虚拟机统计信息监视工具:
jstat
”(jstat -gcutil 20954 1000)查看已使用空间站总空间的百分比,可以看到FGC的频次。 - 找出导致频繁Full GC的原因,找出出现问题的对象。
jvm必须配置
--XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath=/path/heap/dump
这样就是说OOM的时候自动导出一份内存快照,然后使用MAT打开刚刚导出的hprof文件,查看堆栈情况
补充: 线上出现OOM排查方法
3、字符串常量
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池(在堆中),避免字符串的重复创建。
- 对于
String str3 = "str" + "ing";
编译器会给你优化成String str3 = "string"
,创建str3
时会先在常量池中查找“string”
,所有两个实际时一样的 - 对于
String str4 = str1 + str2;
,实际上是通过 StringBuilder 调用 append() 方法,之后toString() 得到的,所以str4
并不是字符串常量池中存在的对象,属于堆上的新对象,所以不相等。 - 如果
str1
和str2
都被final
修饰,会被当做常量处理, 则str4 = str3;
String str2 = new String("abcd");
这种也是在堆中创建了新对象,不在常量池中
4、垃圾回收
发生情况
-
Java 虚拟机的堆内存设置不够。
比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小。我们可以通过参数 -Xms 、-Xmx 来调整。 -
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
在抛出 OutOfMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError。 -
内存泄漏
可达性分析一些引用链没有断开。比如,单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
还有一些提供 close 的资源未关闭导致内存泄漏。数据库连接,网络连接(Socket)和 IO 连接必须手动 close,否则是不能被回收的。
流程
Java堆:eden + survivor(from、to)+老年代
垃圾收集也主要在这个区域
- 对象都会首先在 Eden 区域分配,Eden区满了以后,将引发
minor GC
,清空Eden 区和"From"区,"From"和"To"会交换他们的角色,保证名为 To 的 Survivor 区域是空的。 - 在一次 Minor GC 后,如果对象还存活,则会进入 survivor区(s0 或者 s1),并且年龄加 1。当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代。
- 空间分配担保,检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC;如果小于,那这时就要改为进行一次 Full GC。(大对象直接进入老年代)
- 老年区满了会出发
full GC
,会触发整个堆的回收。
内存分配策略
- 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 - 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 - 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。 - 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 - 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC
发生Full GC的原因:
①调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
②老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
可以调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
③空间分配担保失败
堆中回收对象
如何判断对象已经死亡
- 引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。很难解决对象之间相互循环引用的问题 - 可达性分析算法: 这个算法的基本思想就是通过一系列的称为
GC Roots
的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链相连的话,则证明此对象是不可用的。
在可达性分析法中不可达的对象,也并非是“非死不可”的,要真正回收,至少要经历两次标记过程;
- 不可达的对象是第一次标记,
- 如果这个对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,那么这个对象将被放置到一个叫做F-Queue的队列中,队列中对象的finalize()方法将由一个虚拟机自动建立的、低优先级的Finalizer线程去执行,然后会被放在一个队列中进行第二次标记
- 过程中如果这个对象与引用链上的任何一个对象建立关联,在第二次标记在会被移出“即将回收”队列,否则就会被真的回收。
GC roots
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中Native 方法、JNI 本地接口引用的对象。
- 方法区中静态或常量引用的对象
引用
强引用、软引用、弱引用、虚引用
- 强引用
垃圾回收器绝不会回收它,宁可抛出异常也不回收。比如创建一个对象并把这个对象赋给一个引用变量,String str =“hello”;。 - 软引用
如果内存空间足够,就不回收,如果内存不足了,就会回收。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。 - 弱引用
垃圾回收器线程一旦发现了只具有弱引用的对象,都会回收它的内存。不过,垃圾回收器优先级很低, 不一定会很快发现它。 - 虚引用
仅持有虚引用,等于无引用。当垃圾回收器准备回收一个对象时发现虚引用,就会在回收之前,把这个虚引用加入到与之关联的引用队列中。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
软引用使用最多,可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。一般很少使用弱引用与虚引用
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
方法区中回收废弃常量和无用的类
垃圾回收主要发生在堆中,方法区主要回收的是废弃常量和无用的类:
①废弃常量
- 比如字符串常量池中存在字符串 “abc”,当前没有任何 String 对象引用该字符串常量。其他常量池中的接口,字段的符号引用也以此类似
②无用的类
- 所有实例都已被回收
- 该类对应的对象没有在任何地方被引用,也无法通过反射得到
- 加载该类的class loader已被回收
垃圾收集算法
- 标记-清除算法:
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点:①效率问题。②空间问题(标记清除后会产生大量不连续的碎片) - 标记-复制算法:
为了解决效率问题,它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 - 标记-整理算法:
标记后不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 - 分代收集算法:
当前虚拟机的垃圾收集都采用分代收集算法,根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,所以可以选择复制算法,而老年代的对象存活几率是比较高的,必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
(python使用引用计数
进行垃圾回收,使用标记清除
解决循环引用问题,用分代回收
提高效率)
垃圾回收器
- Serial :单线程,GC时其他工作线程都要停止
- ParNew :Serial 的多线程版本
- Parallel Scavenge(JDK1.8 默认):几乎和ParNew 一样。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。
- CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间,优先响应速度(提高用户体验)
- G1(JDK1.9 默认):回收器停顿时间最短而且没有明显缺点,代替cms
CMS:老年代
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。非常注重用户体验。它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
是⼀种 “标记-清除”算法实现的,整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;
- 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,用户线程可能会不断的更新引用域,这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启⽤户线程,同时GC线程开始对为标记的区域做清扫。
缺点:
- 对 CPU 资源敏感,吞吐量低;
- 无法处理浮动垃圾(并发清除阶段的垃圾);
- 它使用的回收算法----“标记-清除”算法会导致收集结束时会有大量空间碎片产生。老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC
G1:新生代+老年代
把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,每个小空间可以单独进行垃圾回收。通过预测每个 Region 垃圾回收时间
以及回收所获得的空间,并维护一个优先列表,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
- 初始标记
- 并发标记
- 最终标记:将在并发标记期间产生变动的那一部分标记记录,记录在线程的 Remembered Set Logs 里面,然后合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:用户可以指定一个GC停顿时间,
G1
收集器会尽量满足。
5、JVM相关参数
分类:
-XX:+某个属性
、-XX:-某个属性
,开启或关闭某个功能。比如-XX:+PrintGCDetails
,开启GC详细信息。-XX:属性key=值value
。比如-XX:Metaspace=128m
、-XX:MaxTenuringThreshold=15
。
①使用jps -l
配合jinfo -flags pid
可以查看所有参数。
② -Xms
和-Xmx
-Xms
等价于-XX:InitialHeapSize
,用于设置初始堆大小,初始默认为物理内存的1/64。
-Xmx
等价于-XX:MaxHeapSize
。用于设置最大堆大小,最大默认为物理内存的1/4
③-Xss
等价于-XX:ThresholdStackSize
。用于设置单个栈的大小,系统默认值是0,不代表栈大小为0。而是根据操作系统的不同,有不同的值。
④-Xmn
,新生代大小,一般不调。
⑤-XX:MetaspaceSize
,设置元空间大小。
⑥-XX:+PrintGCDetails
,输出GC收集信息,包含GC
和Full GC
信息。
⑦-XX:SurvivorRatio
,新生代中,Eden
区和两个Survivor
区的比例,默认是8:1:1
。通过-XX:SurvivorRatio=4
改成4:1:1
⑧-XX:NewRatio,老生代和新年代的比列,默认是2,即老年代占2,新生代占1。如果改成-XX:NewRatio=4
,则老年代占4,新生代占1。
⑨-XX:MaxTenuringThreshold
,新生代设置进入老年代的时间,默认是新生代逃过15次GC后,进入老年代。如果改成0,那么对象不会在新生代分配,直接进入老年代。
6、类加载
java文件运行过程
Java文件经过编译后变成 .class 字节码文件,字节码文件通过类加载器被搬运到 JVM 虚拟机中
new 一个对象的过程
- 编译得到 App.class,执行 App.class,系统会启动一个 JVM 进程,将 App 的类信息加载到运行时数据区的方法区内,即 App的 类加载
- JVM 执行main方法。 第一句需要创建一个Student对象,JVM 加载 Student 类,把 Student 类的信息放到方法区中
- 在堆中为一个新的 Student 实例分配内存,这个 Student 实例持有 指向方法区中的 Student 类的类型信息 的引用
- 执行 student.sayName() 方法时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址。
- 在栈中运行方法
对象实例初始化时会去方法区中找类信息,在方法区中【类的方法表】中找相应方法。然后再到栈那里去运行方法。
对象创建过程
- 类加载检查:遇到 new,检查对应类是否存在,是否已被加载过,如果没有,那必须先执行相应的类加载过程。
- 分配内存:虚拟机将为新生对象分配内存。分配方式有 “指针碰撞” 和 “空闲列表” 两种,取决于 GC 收集器的算法是"标记-整理",还是"标记-清除"。为了保证线程安全,虚拟机线先在 TLAB 分配内存,不够了再用 CAS 失败重试
- 初始化零值:虚拟机需要将分配到的内存空间都初始化为零值
- 设置对象头:例如指向它的类元数据的指针、对象的哈希码、对象的 GC 分代年龄、锁标志等信息
- 执行 init 方法:按程序员意愿初始化
类加载器与双亲委派模型
类的生命周期:(使用之前都属于类加载)
- 加载
①通过类的名称获取定义该类的二进制字节流;
②转化为方法区运行时存储结构;
③在内存中生成代表该类的class对象,作为方法区中该类各种数据的访问入口; - 验证
保证class文件字节流中的信息符合虚拟机要求,没有安全问题; - 准备
类变量被static修饰,再方法区中分配内存,初始化为0。(实例变量随着对象在堆中分配) - 解析
将常量池的符号引用替换为直接引用,也就是得到类或者字段、方法在内存中的指针或者偏移量。 - 初始化
根据程序员意愿初始化。
①初始化阶段是执行类构造器clinit
方法的过程。是所有类变量的赋值动作和静态语句块(static块)中的语句合并产生
②当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
类加载器ClassLoader
:
BootstrapClassLoader(启动类加载器) ,
ExtensionClassLoader(扩展类加载器) ,
AppClassLoader(应用程序类加载器)
ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候:
- 系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
- 加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。
- 当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用 BootstrapClassLoader 作为父类加载器。每个类加载都有一个父类加载器
双亲委派模型的好处:
- 防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重复加载。
- 保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全。
缺点:
- 顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
- 如果有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,即不走双亲委派,需要重写ClassLoader中的
loadClass
方法; - 如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的
findClass
() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
五、计算机网络
1、OSI
七层
物理(比特流)、数据链路(封装成“帧”)、网络(IP,ARP/RARP,“包”)、传输(TCP\UDP,“段”)、会话、表示、应用(HTTP、FTP、DNS、SMTP)
端到端:传输层
点到点:数据链路层、网络层
交换机工作在数据链路层,将MAC地址和端口对应,MAC地址的数据包将仅送往其对应的端口,而不是所有的端口
路由器工作在网络层。
2、TCP
三次握手+四次挥手
三次握手的目的:
建立可靠的通信信道,双方确认自己与对方的发送与接收都是正常的。
- 你吃饭了嘛?(seq=x),收到请回答(SYN=1)
- 收到(ACK=1),吃饭了(ack=x+1),你吃饭了吗?(seq=y),收到请回答(SYN=1)
- 收到(ACK=1),吃饭了(ack=y+1),那么我们聊一下接下里的事情(established)
四次挥手的目的:
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后(ACK)进⼊半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知(FIN),对方确认后就完全关闭了TCP连接。
- 被动关闭方接收到 FIN 信号,发送 ack 后,等待
CLOSE_WAIT
,再发出 FIN 信号 - 主动关闭方发出最后一条 ack 后,等待
TIME_WAIT
(2MSL)时间,约4分钟。主要是防止最后一个ACK丢失。
3、TCP
TCP与UDP
TCP
:面向连接;可靠;字节流;效率慢;所需资源多;用于文件传输、发送和接收邮件、远程登录等
UDP
:无连接;不可靠;数据报文段;效率快(没有拥塞控制);所需资源少(首部开销小);用于即时通信、视频通话、直播等
TCP
如何保证传输可靠:
- 有序:应用数据被分割成 TCP 认为最适合发送的数据块。 TCP 给发送的每一个包进行编号,接收方会缓存未按序到达的数据,重新排序后交给应用层。
- 校验和: TCP 将保持它首部和数据的检验和。如果收到段的检验和有差错, TCP 将丢弃这个报⽂段和不确认收到此报文段。
- 流量控制: TCP 利用滑动窗口(swnd)实现流量控制,每一方都有固定大小的缓冲空间,接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率。
- 拥塞控制: 发送方要维持一个 拥塞窗口(cwnd),让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。当网络拥塞时,减少数据的发送。慢开始、快重传与快恢复(FRR)、拥塞避免(拥塞窗口缓慢增大)
- ARQ协议(自动重传): 每发完一个分组就停止发送,等待对方确认,收到确认后再发下一个分组。如果一段时间之内没有收到确认帧,它通常会重新发送。停止等待 ARQ 或连续 ARQ 。
流量控制(滑动窗口):
- 发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
- 接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。
- 遵循快速重传、累计确认、选择确认等规则。
- 发送方发的window size = 8192;就是接收端最多发送8192字节,这个8192一般就是接收缓存的大小。
拥塞控制:
- 慢开始:拥塞窗口,开始时大小为1(发送一个报文),然后2,4,8,16指数增长,达到门限后改用拥塞避免算法
- 拥塞避免:达到门限后每次增加1,减少堵塞
- 快重传:一旦发现服务器向客户机发超过3次重复消息,判断消息丢失,快重传(而不是等待超时重传)
- 快恢复:一旦堵塞,降低快开始门限值,重新开始慢开始,拥塞避免。
ARQ - 停止等待 ARQ 协议:
每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果超时没有收到 ACK,则重新发送,直到收到确认后再发下一个分组。
确认迟到:A给B发消息,B的确认消息丢失了,A超时重传,B丢弃重复消息,再给A发送确认
确认丢失:A给B发消息,限时内没收到确认,重传,A丢弃重复的确认,B丢弃重复的消息
- 连续 ARQ 协议:
发送方维持一个发送窗口,其中的分组连续发送出去,。接收方累计确认,对最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
TCP粘包:
TCP报文长度大于缓冲区–>拆包
TCP报文长度小于缓冲区–>粘包
- 由TCP连接复用造成的粘包问题。
- 确认算法问题。
只有上一个分组得到确认,才会发送下一个分组;
收集多个小分组,在一个确认到来时一起发送。 - 数据包过大造成的粘包问题。
- 流量控制,拥塞控制也可能导致粘包。比如接收方不及时接收缓冲区的包,造成多个包接收
解决粘包:
①特定分隔符 ②特殊的头 ③划分为等长
查看TCP状态
cmd中输入:netstat
处理大量TIME_WAIT状态的连接
参考
- net.ipv4.tcp_syncookies = 1
表示开启SYN cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击 - net.ipv4.tcp_tw_reuse = 1
表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接 - net.ipv4.tcp_tw_recycle = 1
表示开启TCP连接中TIME-WAIT sockets的快速回收 - net.ipv4.tcp_fin_timeout
修改系統默认的 TIMEOUT 时间
TCP头部报文
- 源端口号、目的端口号。每个软件都对应一个端口,源端口+源IP+目的端口+目的IP = 套接字,socket 唯一标识每一个应用程序
- 序列号 Sequence Number。为了确保数据通信的有序性,接收端根据这个编号进行确认
- 确认序列号 Acknowledge Number 。是接收端所期望收到的下一序列号(上次成功收到的序号加1),主要用来解决不丢包的问题。
- TCP Flag 。6 个标志比特,URG,ACK,PSH,RST,SYN,FIN。
①ACK
:发送的时候 ACK 为 0,一旦接收端接收数据之后,就将 ACK 置为 1,发送端就知道了接收端已经接收了数据。
②SYN
:表示「同步序列号」,是 TCP 握手的发送的第一个数据包。当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;可用于端口扫描,扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口。
③FIN
:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,连接将被断开。
4、在浏览器中输入url
地址显示主页的过程
过程:
- 浏览器查找域名的IP地址,DNS解析
- 浏览器向服务器发送HTTP请求,TCP连接
(该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器) - 服务器处理请求并返回HTML响应
- 浏览器显示HTML
- 连接结束
用到的协议:
DNS(获取IP)、TCP、IP(发送数据)、OSPF(IP 数据路由选择)、ARP(IP地址与MAC地址转换)、HTTP (访问网页)
DNS解析:
一条域名的DNS记录会在本地有两种缓存:浏览器缓存和操作系统(OS)缓存。在浏览器中访问的时候,会优先访问浏览器缓存,如果未命中则访问OS缓存,最后再访问DNS服务器(一般是ISP提供),然后DNS服务器会递归式的查找域名记录,然后返回。
在修改hosts文件后,所有OS中DNS缓存会被清空,而浏览器缓存则不发生变化,并且浏览器缓存有过期时间,所以修改hosts文件之后,不一定会立刻生效。
递归查询:将待转换的域名放在 DNS 请求中,以 UDP 报文方式发给本地域名服务器。首先在本地域名服务器中查询 IP 地址,如果没有找到,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也没找到,本地域名会向 com 顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到目标 IP 地址并把它缓存到本地,供下次查询使用。
URI 和 URL
URI(Uniform Resource Identifier)
是统一资源标志符,可以唯一标识一个资源。URL(Uniform Resource Locator)
是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
ping一个地址用到的协议
ping命令基于网络层的命令,是基于ICMP协议工作的。
DNS
,将ping后接的域名转换为ip地址。(DNS使用的传输层协议是UDP
)- 通过
ARP
解析服务,由ip地址解析出MAC地址,以在数据链路层传输。 - ping是为了测试另一台主机是否可达,发送一份
ICMP
回显请求给目标主机,并等待ICMP回显应答。(ICMP用于在ip主机、路由器间传递网络是否通畅、主机是否可达等控制信息)
5、状态码
状态码:
2xx (3种)
- 200 OK:表示从客户端发送给服务器的请求被正常处理并返回;
- 204 No Content:表示客户端发送给客户端的请求得到了成功处理,但在返回的响应报文中不含实体的主体部分(没有资源可以返回);
- 206 Patial Content:表示客户端进行了范围请求,并且服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容。
3xx (5种)
-
301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL;
-
302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;
(301与302的区别:前者是永久移动,后者是临时移动(之后可能还会更改URL))
-
303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源;
(302与303的区别:后者明确表示客户端应当采用GET方式获取资源)
-
304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求为满足条件的情况下返回改状态码;
-
307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET;(不同浏览器可能会出现不同的情况);
4xx (4种)
- 400 Bad Request:表示请求报文中存在语法错误;
- 401 Unauthorized:未经许可,需要通过HTTP认证;
- 403 Forbidden:服务器拒绝该次访问(访问权限出现问题)
- 404 Not Found:禁止访问,表示服务器上无法找到请求的资源,除此之外,也可以在服务器拒绝请求但不想给拒绝原因时使用;
- 405:请求方法不对
5xx (2种)
- 500 Inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;
- 502:网关错误
- 503 Server Unavailable:表示服务器暂时处于超负载或正在进行停机维护,无法处理请求;
- 504:网关超时
6、HTTP
HTTP 1.0 和 HTTP 1.1
- HTTP 的长连接和短连接实质上是 TCP 协议的长连接和短连接。
- HTTP/1.0 中默认使用短连接。客户端和服务器每进行一次 HTTP 操作,就建立一次连接
- 从 HTTP/1.1 起,默认使用长连接:
Connection:keep-alive
,在一定的时间内TCP不会关闭。一个 TCP 连接是可以发送多个 HTTP 请求的,但两个请求的生命周期不能重叠
- 缓存处理 :在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
- 在HTTP1.1中新增了24个错误状态响应码
- 带宽优化及网络连接的使用 :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,这样就方便了开发者自由的选择以便于充分利用带宽和连接。
HTTP 1.0 和 HTTP 2.0
- 协议格式重新封装了,其实语义没变,压缩首部减少冗余
- HTTP1.x只能单向请求,只能由客户端发起;HTTP2.0允许服务端推送
- 连接共享 / 多路复用 / 并行执行。一个request对应一个stream id,这样一个连接上可以有多个stream,接收方可以根据stream id将frame再归属到各自不同的request里面,只通过一个 TCP连接就可以传输所有的请求数据。浏览器限制了同一个域名下的请求数量,多路复用可以绕过这个限制。
- 优先级。每个stream都可以设置又优先级和依赖。优先级高的stream会被server优先处理和返回给客户端, 优先级、依赖都可动态调整。比如商品页滑倒底端,先加载底部的图片,再加载上面的
- 重置连接。比如取消下载,对于http1.x来说,是通过设置tcp segment里的reset flag来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。http2.0引入RST_STREAM类型的frame,可以在不断开连接的前提下取消某个request的stream ,用于连接的复用
HTTP3.0(QUIC)
QUIC (Quick UDP Internet Connections), 快速 UDP 互联网连接。
QUIC是基于UDP
协议的。
- HTTP 2.0从逻辑上来说,不同的流之间相互独立,互不影响,但在实际传输方面,一旦某一个流的数据有丢包,则同样会阻塞在它之后传输的流数据传输。而基于QUIC则可以更为彻底地解决这样的问题,让不同的流之间真正的实现相互独立传输,互不干扰。
- 切换网络(比如wifi换流量)时,基于TCP的协议IP会改变,之前的连接不可能继续保持;而基于UDP的QUIC协议,使用64位的随机数作为连接的ID,并使用该ID表示连接。从而在网络完成切换之后,恢复之前与服务器的连接。
加密:首次连接时客户端和服务端的会密钥协商,服务端传递了config包,客户端会将config存储下来,后续再连接时可以直接使用。
前向安全:指的是密钥泄漏也不会让之前加密的数据被泄漏,影响的只有当前,对之前的数据无影响。
前向纠错:QUIC每发送一组数据就对这组数据进行异或运算,并将结果作为一个FEC包发送出去,接收方收到这一组数据后根据数据包和FEC包即可进行校验和纠错。
HTTP 和 HTTPS
- 端口 :HTTP 的 URL 由“http://”起始且默认使用端口
80
,而HTTPS的URL由“https://”起始且默认使用端口443
。 - HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。
- HTTPS 在传输数据之前需要客户端与服务器进行一个握手(
TLS/SSL
握手),在握手过程中将确立双方加密传输数据的密码信息。在HTTP报文进入TCP报文之前,先使用 TLS/SSL 对HTTP报文进行加密。
数据加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。
-
对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有 DES、AES 等;
-
非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有 RSA、DSA、MD5(签名算法) 等。
-
https:DES+RSA
HTTPS
HTTPS 经由 HTTP 进行通信,利用 TLS 来保证安全
加密过程:
- 客户端发起一个http请求,告诉服务器自己支持哪些加密算法,和一个随机数 a。
- 服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥,网站地址,服务器证书),和一个随机数 b。证书中有一个公钥来加密信息,私钥由服务器持有。
- 客户端验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。生成一个随机数 c,用公钥加密 c 得到 c1 发送。
- 生成随机密码(RSA签名)
如果验证通过,或用户接受了不受信任的证书,客户端就会从证书中取出服务器的公钥,用之前三个随机数,生成一个随机的对称密钥(session key),并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
服务器证书:
- 数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。数字证书的工作原理是公钥密码机制,简而言之就是通过“私钥签名、公钥验签”规则运行签名验签运算,保证电子文件内容不被篡改。
- 服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
- 进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
HTTP报文结构
请求报文
请求行 + head + body
- 请求行
请求方法(get、post)+ URL +协议版本 - header
User-Agent:产生请求的浏览器类型等
Accept:客户端可识别的响应内容类型列表。(Accept-Language、 Accept-Encoding、Accept-Charset)
Host:请求的主机名
connection:连接方式(close 或 keepalive);
Cookie - body
POST把提交的数据放置在<request-body>;
GET在请求行的url里
响应报文
状态行 + 响应头 + 响应体
- 状态行
协议版本 + 状态码 + 状态码描述 - 响应头
Server:服务器的软件信息及其版本。对应 User-Agent
Content-Length :给出文档长度
Content-type:给出媒体类型
Connection:连接方式
7、cookie
、session
、token
cookie、session
HTTP 是一种不保存状态,不对请求和响应之间的通信状态进行保存。Session 的主要作用就是通过服务端记录用户的状态,过时销毁。
通常 session 保存在服务端数据库或内存中,但服务器保存session开销过大,如果服务器做了负载均衡,下一个请求到了另外一台服务器的话,session会丢失。
通常通过在 Cookie 中附加一个 Session ID 来方式来跟踪用户。如果 cookie 被禁用,就加载 url 后面
Cookie 和 Session 都用来跟踪浏览器用户身份
- Cookie 由服务器生成,发给浏览器,以kv的形式保存在浏览器。我们在 Cookie中保存已经登录过得用户信息,下次访问网站的时候,连带cookie一起发给服务器,页面可以自动帮你登录的一些基本信息给填了;
- 网站有保持登录,这是因为用户登录的时候我们可以存放了一个
Token
在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写) - 登录一次网站后访问网站其他页面不需要重新登录。服务器使用Session把用户状态临时保存在服务端。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
cookie、session区别:
- Cookie 存储在客户端中(浏览器端),而Session存储在服务器上,相对来说 Session 安全性更高。
- Cookie保存字符串,不能超过4KB,session保存对象,无大小限制
- session借用cookie才能正常工作,把session_id储存在cookie中,如果禁用cookie,那就只能url重写,但不安全。
token
Token身份验证是无状态的,不需要存在服务端,一般浏览器本地存储。
服务器生成并发给用户一个token,包含了用户的数据,用密钥对数据做一个签名,把签名和数据一起作为token发给用户(这个token服务器不保存)。下次用户再访问服务端时,客户端每次请求都在HTTP header里面带上这个Token,服务器负责验证这个Token是不是合法的,有没有过期等;服务器用同样的密钥对数据计算签名,对比是否与token里的一致,如果相同就代表合法。(避免了攻击者通过 cookie 拿到用户的 session,进而伪造一些数据)
优点:
- JWT 的设计契合无状态原则:用户登录之后,服务器会返回一串 token 并保存在本地,在这之后的服务器访问都要带上这串 token,来获得访问相关路由、服务及资源的权限。
- 单点登录(在多个应用系统中,用户只需登陆一次,就可以访问所有相互信任的应用)就比较多地使用了 JWT,因为它的体积小,并且经过简单处理(使用 HTTP 头带上 token )就可以支持跨域操作。
为什么用JWT?
- HTTP协议是无状态的
- Cookie是服务器发送到用户浏览器并保存在本地的一小块数据,数据不能太大,并且明文容易被盗取。
- Session不能作为分布式系统,会出现session复制,影响效率。
- Token是在服务端将用户信息经过Base64Url编码过后传给在客户端。每次用户请求的时候都会带上这一段信息,因此服务端拿到此信息进行解密后就知道此用户是谁了。
- JWT三部分:header(什么算法)、payload(主题)和signature(对前两者加密)
项目里用到:
完成登录—>生成包含用户信息的字符串—>放到header请求头、或cookie、或url里传递—>设置有效时间(30min)
每次请求操作,判断如果登录—>检查是否包含token—>校验—>确定登陆状态
jwt 工具(Json Web Token)
8、GET和POST
(幂等:一个操作执行任意次对系统的影响跟一次是相同)
- GET - 从指定的资源请求数据。 POST - 向指定的资源提交要被处理的数据;PUT是幂等的,POST不是
- GET刷新前后数据一样,POST会丢失要提交的数据
- GET发送的数据写在URL里,对所有人可见,安全性很差,会保存在浏览器记录中,可以添加书签;POST数据不会出现在URL里,更安全,不会被保存在浏览器记录中
- GET数据长度受限,因为URL 的长度是受限制的;POST无限制
- GET只允许ASCII字符;POST无限制,二进制也可
PUT与PATCH:
- PUT是幂等的,PATCH不是
- PUT替代性的更新原资源,PATCH更新部分资源。
比如有一个UserInfo,里面有userId, userName,userGender等10个字段。可你的编辑功能因为需求,在某个特别的页面里只能修改userName,PUT把一个包含了修改后userName的完整userInfo对象传给后端,做完整更新,PATCH只传一个userName到指定资源去,表示该请求是一个局部更新,后端仅更新接收到的字段
六、操作系统
1、CPU
CPU 从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元
- 控制单元:由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和操作控制器OC(Operation Controller)等
- 运算单元:算术逻辑单元(ALU)、通用寄存器、数据缓冲寄存器DR和状态条件寄存器PSW组成
- 存储单元:CPU片内缓存和寄存器组
主要寄存器:
- 数据缓冲寄存器(DR):暂时存放ALU的运算结果,作为ALU运算结果和通用寄存器之间信息传送中时间上的缓冲;以及补偿CPU和内存、外围设备之间在操作速度上的差别。
- 指令寄存器(IR):保存当前正在执行的一条指令。
- 程序计数器(PC):确定下一条指令的地址。
- 数据地址寄存器(AR):保存当前CPU所访问的数据cache存储器中单元的地址。
- 通用寄存器(R0~R3):当算术逻辑单元(ALU)执行算术或逻辑运算时,为ALU提供一个工作区。
- 状态字寄存器(PSW):保存由算术指令和逻辑指令运算或测试结果建立的各种条件代码。
2、操作系统功能
kernel 内核
- 操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
- CPU 主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。
一些与硬件关联交紧密的模块,诸如时钟管理程序、中断处理程序、设备驱动程序等处于最底层。其次是运行频率较高的程序,诸如进程管理、存储器管理和设备管理等。这两部分内容构成了操作系统的内核。
大多数操作系统内核包括四个方面的内容。
- 时钟管理
- 中断机制
- 原语:接近硬件的公用小程序,设备驱动、比如CPU切换、进程通信等功能中的部分操作
- 系统控制的数据即处理:进程管理、存储器管理、设备管理(缓冲区管理、设备分配和回收等)。
功能
- 进程管理
进程控制、进程同步、进程通信、死锁处理、处理机调度等。 - 内存管理
内存分配、地址映射、内存保护与共享、虚拟内存等。 - 文件管理
文件存储空间的管理、目录管理、文件读写管理和保护等。 - 设备管理
完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。
主要包括缓冲管理、设备分配、设备处理、虛拟设备等。
用户态与内核态
中断(会发生用户态与内核态切换)
- 外中断
由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/ 输出请求。此外还有时钟中断、控制台中断等。 - 异常(内中断)
由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 - 陷入
在用户程序中使用系统调用。如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成
系统调用与函数调用
- 系统调用就是通过系统api操作由操作系统统一管理的资源,比如设备、文件、内存、进程、进程间通信
- 函数调用是在用户地址空间执行,而系统调用是在内核地址空间执行
- 函数调用不需要上下文切换,开销较小;系统调用需要切换到内核上下文环境然后切换回来,开销较大
linux系统调用
用户态转向核心态:
发生中断或系统调用可能会引起用户态转向核心态,所使用的堆栈也可能需要由用户堆栈切换为系统堆栈。
每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存 在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈。
- 进程陷入内核态后,先把【用户态堆栈的地址】保存在【内核栈】之中,然后设置【堆栈指针寄存器】的内容为【内核栈】的地址,这样就完成了用户栈向内核栈的转换;
- 当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在【内核栈】里面的【用户栈的地址】恢复到【堆栈指针寄存器】即可。
- 陷入内核态时如何知道内核栈的地址?
一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次陷入内核的时候得到的内核栈总是空的,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
3、进程创建(用户程序–>在内存中执行)
创建进程首先要将程序和数据装入内存。将用户原程序变成可在内存中执行的程序,通常需要三个步骤。
- 编译,由编译程序将用户源代码编译成若干个目标模块。
- 链接,由链接程序将编译后形成的一组目标模块,以及所需库函数链接,形成完整的装入模块。
- 装入,由装入程序将装入模块装入内存。
链接:
- 静态链接:在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的可执行程序,以后不再拆开。
- 装入时动态链接:将用户源程序编译后所得到的一组目标模块,再装入内存时,采用边装入变链接的方式。
- 运行时动态链接:对某些目标模块的连接,是在程序执行中需要该目标模块时,才对她进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。
比如,打印的库函数,可能很多程序都要用到,静态链接开销太大,要用动态链接,运行的时候再链接
装入:
- 绝对装入。在编译时,如果知道程序将驻留在内存的某个位置,编译程序将产生绝对地址的目标代码。由于程序中的逻辑地址与实际地址完全相同,装入内存后,不需对程序和数据的地址进行修改。绝对装入方式只适用于单道程序环境。
- 可重定位装入。在多道程序环境下,多个目标模块的起始地址通常都是从0开始,根据内存的当前情况,将装入模块装入到内存的适当位置。装入时对目标程序中指令和数据的修改过程称为重定位,地址变换通常是装入时一次完成,所以称为静态重定位。
其特点是在一个作业装入内存时,必须分配器要求的全部内存空间,如果没有足够的内存,就不能装入,此外一旦作业进入内存后,在整个运行期间,不能在内存中移动,也不能再申请内存空间。 - 动态运行时装入,也成为动态重定位,程序在内存中如果发生移动,就需要采用动态的装入方式。把装入模块装入内存时,仍为相对地址。等到程序真正要执行时才把相对地址转换为绝对地址
其特点是可以将程序分配到不连续的存储区中;在程序运行之前可以只装入它的部分代码即可运行,然后在程序运行期间,根据需要动态申请分配内存;便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。
4、内存管理
常见内存管理机制
- 块式管理(连续分配管理方式):将内存分为几个固定大小的块,如果程序运行需要内存的话,操作系统就分配给它一块,每个块中只包含一个进程。其中很大一部分几乎被浪费了。
- 页式管理(非连续分配管理方式):把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。将程序的逻辑地址划分为固定大小的页,而物理内存划分为同样大小的帧,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。页实际并无任何实际意义
- 段式管理(非连续分配管理方式):将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;段是有实际意义的,段式管理通过段表对应逻辑地址和物理地址。
- 段页式管理机制 。先把主存先分成若干段,每个段又分成若干页,也就是说,段与段之间,以及段的内部,都是离散的。
分页机制和分段机制
- 共同点 :
①都是为了提高内存利用率,较少内存碎片。
②页和段都是离散存储的,但是,每个页和段中的内存是连续的。 - 区别
①页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
②分页是系统管理的需要,是信息的物理单位;分段是满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息;
③页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。
虚拟地址空间
内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。
一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。
页表:
系统为每个进程建立一张页表,记录页面在内存中对应的物理块号,页表一般存放在内存中。
地址变换:
将逻辑地址中的页号,转换为内存中物理块号。地址分为页号和页内偏移量两部分,用页号去检索页表。在系统中通常设置一个页表寄存器PTR,存放页表在内存的初值和页表长度。
快表:
若页表全部放在内存中,则要存取一个数据至少要访问两次内存,一次是访问页表得到物理地址,第二次才根据该地址存取数据或指令。
地址变换机构中增设了一个具有并行查找能力的高速缓冲存储器——快表,又称联想寄存器TLB,用以存放当前访问的若干页表项。与此对应,主存中的页表也常称为慢表。
将虚拟地址转化后,送入块表查找页号,如果存在,直接找到对应地址;如果不存在,再去内存中查找页表并存在快表里
- 直接使用物理地址:
①容易破坏操作系统
②同时运行多个程序特别困难,多个程序想给同一个地址赋值,都会崩溃 - 虚拟地址:
①使用一系列相邻的虚拟地址,来访问物理内存中不相邻的大内存缓冲区,使得应用程序认为它拥有连续的可用的内存
②可以访问大于可用物理内存的内存缓冲区,数据或代码页会根据需要在物理内存与磁盘之间移动。
③不同进程使用的虚拟地址彼此隔离。
页面置换算法
虚拟内存的基本单位是页。当访问虚拟内存时,会通过MMU(内存管理单元)去匹配对应的物理地址,而如果虚拟内存的页并不存在于物理内存中,会产生缺页中断,从磁盘中取出缺的页放入内存,如果内存已满,还会根据某种置换算法将磁盘中淘汰一页,腾出空间。
当用户程序要访问的部分尚未调入内存,则产生中断。
页面置换算法:
FIFO先进先出算法、LRU最近最少使用算法、LFU最少使用次数算法、OPT最佳置换算法(保证置换出去的是未来不再或最晚被使用的页)
5、linux文件系统
inode
- block:存放实际文件的内容,硬盘的最小存储单位是扇区(Sector),块(block)由多个扇区组成
- inode:存储文件元信息。如某个文件被分成几块、每一块在的地址、文件拥有者,创建时间,权限,大小等。
文件类型
普通文件(-)、目录文件(d,directory file)、 符号链接文件(l,symbolic link,软链接,类似快捷方式)、 字符设备(c,char,访问字符设备比如键盘) 、 设备文件(b,block,访问块设备比如硬盘、软盘)、 管道文件(p,pipe)、 套接字(s,socket)
权限
- r:代表权限是可读,r 也可以用数字 4 表示
- w:代表权限是可写,w 也可以用数字 2 表示
- x:代表权限是可执行,x 也可以用数字 1 表示
目录树
- /bin: 存放二进制可执行文件(ls、cat、mkdir 等),常用命令一般都在这里;
- /etc: 存放系统管理和配置文件;
- /home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户 user 的主目录就是/home/user,可以用~user 表示;
- /usr : 用于存放系统应用程序;
- /opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把 tomcat 等都安装到这里;
- /proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
- /root: 超级用户(系统管理员)的主目录(特权阶级);
- /sbin: 存放二进制可执行文件,只有 root 才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如 ifconfig 等;
- /dev: 用于存放设备文件;
- /mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;
- /boot: 存放用于系统引导时使用的各种文件;
- /lib : 存放着和系统运行相关的库文件 ;
- /tmp: 用于存放各种临时文件,是公用的临时文件存储点;
- /var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等;
- /lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows 下叫什么.chk)就在这里。
6、linux基本命令
目录
cd usr
: 切换到该目录下 usr 目录
cd ..
: 切换到上一层目录
cd /
: 切换到系统根目录
cd ~
: 切换到用户主目录
cd -
: 切换到上一个操作所在目录
mkdir 目录名称
: 增加目录。
ls/ll
:查看目录信息。
(ll 命令可以看到该目录下的所有目录和文件的详细信息):
find 目录 参数
: 寻找目录(查)。
示例:① 列出当前目录及子目录下所有文件和文件夹: find .;② 在/home目录下查找以.txt 结尾的文件名:find /home -name “*.txt” ,
mv 目录名称 新目录名称
: 修改目录的名称(改)。
mv 目录名称 目录的新位置
: 移动目录的位置—剪切(改),cp 是复制
cp -r 目录名称 目录拷贝的目标位置
: 拷贝目录(改),-r 代表递归拷贝 。
rm [-rf] 目录
: 删除目录(删)。
文件
touch 文件名称
: 文件的创建(增)。
find 目录 -name 文件名
:文件查找,find /etc -name zhangsan,还可以-size,-group
head/cat/more/less/tail 文件名称
:文件的查看(查) 。head -n 文件名,显示文件的前n行内容。cat 显示文件全部内容。 tail -f 文件 可以对某个文件进行动态监控,tail -f 文件名。
vim 文件
: vim 文件—>进入文件—>命令模式—>按 i 进入编辑模式—>编辑文件 —>按Esc进入底行模式—>输入:wq/q! (输入 wq 代表写入内容并退出,即保存;输入 q!代表强制退出不保存)。
rm -rf 文件
: 删除文件(删)
tar -zcvf 打包压缩后的文件名 要打包压缩的文件或目录名
:打包并压缩文件,tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt 或 tar -zcvf test.tar.gz /test/
tar [-xvf] 压缩文件
:解压压缩包,tar -xvf test.tar.gz -C /usr
用户管理
每个用户必须属于一个组,每个文件有所有者(u)、所在组(g)、其它组(o)的概念
修改文件/目录的权限的命令:chmod
chmod u=rwx,g=rw,o=r aaa.txt
或者 chmod 764 aaa.txt
Linux 系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。每个用户都有一个用户组
useradd 选项 用户名
:添加用户账号
userdel 选项 用户名
:删除用户帐号
usermod 选项 用户名
:修改帐号
passwd 用户名
:更改或创建用户的密码
passwd -S 用户名
:显示用户账号密码信息
passwd -d 用户名
: 清除用户密码
groupadd 选项 用户组
:增加一个新的用户组
groupdel 用户组
:要删除一个已有的用户组
groupmod 选项 用户组
: 修改用户组的属性
其他
pwd
: 显示当前所在位置
sudo + 其他命令
:以系统管理者的身份执行指令
网络通信命令
: ifconfig 查看当前系统的网卡信息:ping查看与某台机器的连接情况: netstat -an查看当前系统的端口使用:
reboot
: 重开机
grep 要搜索的字符串 要搜索的文件 --color
: 搜索命令,–color 代表高亮显示
ps -ef/ps -aux
: 这两个命令都是查看当前系统正在运行进程。如果想要查看特定的进程:ps -ef|grep redis
。查看进程并排序:ps auxw --sort=%cpu
,%cpu按占用cpu排序,%mem按内存占用率,后面可以ps auxw --sort=%cpu | java
,然后 top 所显示的进程名
kill -9 进程的pid
: 杀死进程(-9 表示强制终止。)
CTRL+Z
:当前程序挂起进程并放入后台(暂停)。bg %N 使第N个任务在后台运行,fg %N 使第N个任务在前台运行,没有 N 默认最后一个
ctrl-c
:终止程序的执行;等于kill -2
,在结束之前,能够保存相关数据,然后再退出。
top
:命令主要用于查看进程的相关信息,cpu 信息和内存信息等。虚拟内存(预计用量)、常驻内存(实际用量)、共享内存、物理内存(常驻内存减去共享内存)
free
:命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。(swap页面置换的时候才用,一般都是0)
查看日志
tail -f <filename>
:在控制台上查看日志,他可以将新增的日志实时的打印出来
tail -n 行数 <filename> / tail -行数 <filename>
:这个展示的是文件最后一行倒数的行数,
cat -n <filename> | grep '条件'
:根据条件筛选出行号
sed -n '行1,行2p' <filename>
:常常需要查询这条记录的完整信息,这个时候可以使用
七、数据库
1、数据库设计
三大范式:
第一范式(1NF):(属性不可分)
- 1、数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性。
- 2、两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。
第二范式(2NF):
- 非主属性必须依赖主属性(主键),每一行只能与一列元素相关,消除部份依赖
第三范式(3NF):
- 表中的每一列都要与主键直接相关,而不是间接相关(表中的每一列只能依赖于主键),消除传递依赖。
五大约束:
- 主键约束(Primay Key Coustraint) 唯一性,非空性;
- 唯一约束 (Unique Counstraint)唯一性,可以空,但只能有一个;
- 默认约束 (Default Counstraint) 该数据的默认值;
- 外键约束 (Foreign Key Counstraint) 需要建立两表间的关系;
- 非空约束(Not Null Counstraint):设置非空约束,该字段不能为空。
数据库设计通常分为哪几步
- 需求分析 : 分析用户的需求,包括数据、功能和性能需求。
- 概念结构设计 : 主要采用 E-R 模型进行设计,包括画 E-R 图。
- 逻辑结构设计 : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。
- 物理结构设计 : 主要是为所设计的数据库选择合适的存储结构和存取路径。
- 数据库实施 : 包括编程、测试和试运行
- 数据库的运行和维护 : 系统的运行与数据库的日常维护。
2、MySQL引擎
数据类型:int
bigint
float
double
char
varchar
日期
默认存储引擎:InnoDB
(之前为MyISAM
)
支持事务、支持外键,支持行级锁,支持MVCC,有崩溃修复(redo log)
行级锁:
悲观锁,对当前操作加锁,能减少数据冲突,但开销大,会出现死锁。锁的是索引,分为共享锁与排他锁
InnoDB 存储引擎的锁的算法:
- Record lock:记录锁,单个行记录上的锁,锁住索引。(如果表中没有索引,那么就会锁整张表)
- Gap lock:间隙锁,锁定一个范围,不包括记录本身,锁住当前记录的前后不会有记录插进来
- Next-key lock:record+gap 临键锁,锁定一个范围,包含记录本身。比如一个事务读取一条数据期间,别的事务来操作这条数据,会造成幻读,所以需要锁住这个数据本身+间隙。
3、MySQL 高性能优化:
设计规范
- 基本设计规范
命名规范、全都使用 Innodb 存储引擎、字符集统一使用 UTF8、添加注释、控制单表数据量的大小、尽量做到冷热数据分离、尽量采用物理分表的方式管理大数据 - 字段设计规范
①优先选择符合存储需要的最小的数据类型;(列的字段越大,建立索引时所需要的空间也就越大)
②尽可能把所有列定义为 NOT NULL;(索引 NULL 列需要额外的空间来保存)
③使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间,不用字符串;
④Decimal 类型为精准浮点数,金额类数据在计算时不会丢失精度 - 索引设计规范
①限制每张表上的索引数量,索引可以增加查询效率,但同样也会降低插入和更新的效率
②每个表必须有个主键,不要使用更新频繁的列,不要使用UUID、MD5、HASH、字符串列等无法保证数据的顺序增长,建议使用自增 ID 值
③选择索引列的顺序,尽量把区分度最高的、字段长度小的、使用最频繁的列放在联合索引的最左侧
④避免建立冗余索引和重复索引,尽量避免使用外键约束 - 数据库操作行为规范
①超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作。大批量操作可能会造成严重的主从延迟,另外也要避免产生大事务操作,导致大量数据的阻塞
②对于大表使用 pt-online-schema-change 修改表结构,建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,把新表命名成原表,并把原来的表删除掉。把原来一个 DDL 操作,分解成多个小的批次进行。
大表优化
- 限定数据的范围
- 读/写分离:经典的数据库拆分方案,主库负责写,从库负责读;从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志(relay log)中。接着从库中有一个SQL线程会从中继日志读取binlog,再次执行一遍SQL。半同步复制用来解决主库数据丢失问题;并行复制(多线程)用来解决主从同步延时问题。
- 垂直分区:数据表列的拆分,把一张列比较多的表拆分为多张表。 表结构简单易于维护,但会让事务变得更复杂。解决表与表之间的io竞争
- 水平分区:保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。解决单表中数据量增长出现的压力
全局id 、分布式id
因为要是分成多个表之后,数据遍布在不同服务器上的数据库,我们需要一个全局唯一的 id来支持。生成全局 id 有下面这几种方式:
- 数据库自增 id : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
- UUID:通用唯一标识符,快,简单。不适合作为主键,因为太长了,查询效率低,并且无序不可读,可能造成安全问题和重复问题。比较适合用于生成唯一的名字的标示比如文件的名字。
- UUID是可以生成时间、空间上都独一无二的值;自增序列只能生成基于表内的唯一值
- 雪花算法,类似UUID,分布式 ID 生成算法
- Redis 集群:对 id 原子顺序递增
4、慢查询优化
开启 MySQL 慢查询日志功能,并设置时间,可以定位具体sql:
mysql> SET GLOBAL slow_query_log=ON;
Query OK, 0 rows affected (0.05 sec)
mysql> SET GLOBAL long_query_time=0.001;
Query OK, 0 rows affected (0.00 sec)
利用explain
关键字可以模拟优化器执行SQL查询语句
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| 1 | PRIMARY | cl | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8 | NULL | 379 | Using where; Using temporary |
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 63727 | Using where; Using join buffer |
| 2 | DERIVED | emp | ALL | NULL | NULL | NULL | NULL | 13317 | Using where |
| 2 | DERIVED | emp_cert | ref | emp_certificate_empid | emp_certificate_empid | 4 | meituanorg.emp.id | 1 | Using index |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
table | type | possible_keys | key |key_len | ref | rows | Extra :
- type:显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和ALL
- rows 显示需要扫描行数
- key 使用的索引
优化:
- 索引没用上。like、联合索引最左匹配问题
- 优化数据库结构。分表、经常联合查询的表建立中间表
- 将一个大的查询分解为多个小查询。对每一个表进行一次单表查询
- 当偏移量非常大的时候优化LIMIT分,比如
select id,title from collect limit 90000,10;
①虑筛选字段(title )上加索引
②先查询出主键id值select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;
③“关延迟联”inner join
。让MySQL扫描尽可能少的页面,获取需要的记录后,再根据关联列,回原表查询需要的所有列。如果这个表非常大,那么这个查询可以改写成如下的方式:Select news.id, news.description from news inner join (select id from news order by title limit 50000,5) as myNew using(id);
5、MySQL
事务的ACID
ACID
:
- 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
ACID
实现原理:
- 原子性:主要依靠undo.log日志实现,即在事务失败时执行回滚。undo.log日志会记录事务执行的sql,当事务需要回滚时,通过反向补偿回滚数据库状态
- 一致性:主要依靠undo.log日志实现,就是事务在执行的前和后数据库的状态都是正常的,表现为没有违反数据完整性,参照完整性和用户自定义完整性等等。而上面三种特性就是为了保证数据库的有一致性
- 隔离性:多线程时多事务之间互相产生了影响,需要加锁。写写操作通过加锁(next-key lock)实现隔离性,写读操作通过MVCC实现。默认支持的隔离级别是 REPEATABLE-READ (可重读),不保证避免幻读,需要应用使用加锁读Next-Key Locks来保证。在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
- 持久性:主要依靠redo.log日志实现,是 InnoDB 独有的,它让MySQL拥有了崩溃恢复能力。首先,在select时先查缓存池,再查磁盘;在update时先更新缓存,再更新磁盘。以减少磁盘IO次数,提高效率。但由于缓存断电就没了,所以需要redo.log日志。在执行修改操作时,sql会先写入到redo.log日志,再写入缓存中。这样即使断电,也能保证数据不丢失,达到持久性
bin log
(归档日志)、redo log
和undo log
:
-
redolog
保证InnoDB 的持久性,有数据崩溃恢复的能力;redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”。如果只修改了页面上一点内容,但把整个页面刷盘,是不合理的,所以用redo log。redo log体积小,只记录了哪一页修改了啥,因此体积小,刷盘快。并且 redo log是一直往末尾进行追加,属于顺序IO。效率显然比刷盘随机IO来的快。redolog作为异常宕机或者介质故障后的数据恢复使用。 -
binlog
保证了MySQL集群架构的数据一致性;binlog 是逻辑日志,记录是对应的SQL语句,属于MySQLServer 层
。 不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。 MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。 -
bin log 和 redo log 都保证了数据的持久化;redo log在事务执行过程中可以不断写入,属于InnoDB引擎层;而binlog只有在提交事务时才写入,属于server 层。
-
为了解决两份日志之间的一致问题,InnoDB存储引擎使用两阶段提交方案。原本的 redo --> bin , 变成redo(prepare) --> bin -->redo(commit)
-
binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。
-
undo
回滚行记录到特定版本,保证事务的原子性。回滚日志会先于数据持久化到磁盘上
事务隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更。—脏读
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据, 可以阻止脏读。但同一事务中,两次查询的信息可能不同(中间被另一事务改变了),即不可重复读。—不可重复读
- REPEATABLE-READ(可重复读): 自身事务没关闭的话,另一事务更改数据提交后也读不到,所以同一事务中对同一字段的多次读取结果都是一致的,除⾮数据是被本身事务自己所修改,可以阻止不可重复读。但幻读仍有可能发生,比如数据被删除了,那本事务读到的虽然一致,但都是幻象。—幻读
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说, 该级别可以防止脏读、不可重复读以及幻读。
MySQL InnoDB
存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ
(可重读)
6、锁定读与非锁定读
一致性非锁定读:快照读
- 通常加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,如果记录的版本小于可见版本,则表示该记录可见
- InnoDB 中,如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会去等待行锁的释放。而是会读一个快照
- 在 Repeatable Read 和 Read Committed 两个隔离级别下,如果是执行普通的 select 语句,则会使用 一致性非锁定读(MVCC)。并且在 Repeatable Read 下 MVCC 实现了可重复读,和防止“快照读”下的幻读
锁定读:当前读
如果执行的是下列语句,就是 锁定读:
select ... lock in share mode
(加 s 锁,其它事务也可以加 s 锁,如果加 x 锁则会被阻塞)
select ... for update
insert
、update
、delete
(加 x 锁,其它事务不能加锁)
每次读取的是数据的最新版本。会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。如果两次查询中间有其它事务插入数据,就会产生幻读
Repeatable Read时解决幻读:
- 快照读:通过MVCC来进行控制的,不用加锁。
- 当前读:通过next-key锁(行锁+gap锁),来防止其它事务在间隙间插入数据。
7、MVCC
(Multi-Version Concurrency Control
,多版本并发控制)
含义:为事务分配单向增长的时间戳。为每个数据修改保存一个版本。读操作只读取该事务开始前的数据库快照,使读写操作没有冲突
并发读-写时,可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题,还需添加乐观锁/悲观锁。
原理
实现:隐藏字段、Read View
、undo log
-
隐藏字段:
①DB_TRX_ID
:表示最后一次插入或更新该行的事务 id。
②DB_ROLL_PTR
: 回滚指针,指向该行的undo log
③DB_ROW_ID
:如果没有设置主键且该表没有唯一非空索引时,会使用该 id 来生成聚簇索引 -
Undo log
主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。
①当事务回滚时用于将数据恢复到修改前的样子,保证原子性和一致性
②MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读 -
Read View
主要是用来做可见性判断的, 创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他活跃未提交事务id列表。即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,需要判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
- 如果 trx_id 小于低水位,表示这个版本在事务启动前已经提交,可见;
- 如果 trx_id 大于高水位,表示这个版本在事务启动后生成,不可见;
- 如果 trx_id 大于低水位,小于高水位,分为两种情况:
①若 trx_id 在活跃事务数组中,表示这个版本在事务启动时还未提交,不可见;
②若 trx_id 不在活跃事务数组中,表示这个版本在事务启动时已经提交,可见。
读已提交and可重复读:
- RC隔离级别下,是每个快照读都会生成并获取最新的Read View (m_ids 列表);
- 而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。
8、一条 SQL 语句的被执行
基本架构
- 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
- 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除)。
- 分析器: 看你的 SQL 语句要干嘛,提取关键字,再检查你的 SQL 语句语法是否正确。
- 优化器: 按照 MySQL 认为最优的方案去执行。比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序,提前终止查询(limit),优化MIN和MAX(有索引的话只找最左端)函数等
- 执行器:执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
- 存储引擎 InnoDB
查询语句:权限校验—>分析器—>优化器—>权限校验—>执行器—>引擎
更新语句:查询—>执行器—>引擎—>redo log(prepare 状态)—>binlog—>redo log(commit状态)
解决数据一致性的问题:
- 判断 redo log 是否完整,如果判断是完整的,就立即提交。
- 如果 redo log 只是预提交但不是 commit 状态,去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。
崩溃恢复
在innodb层,prepare redo log中会记录一个trxid,宕机重新起来恢复时
- 先scan binlog,把所有的trxid拿出来做一个hash table(扫最后一个binlog文件,一个事务的日志是不能跨文件的)
- 去scan innodb redo log,扫cp开始往后的部分,也会产生trxid list
- 这时候去上面那个hash table中search,如果这个trxid在上面的hash表中,就是两个步骤都没问题,就commit,如果不在里面(第二步写binlog没成功),就rollback
9、索引
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
索引类型
- 主键索引:主键列使用的就是主键索引
- 二级索引:叶子节点存储的数据是主键。唯一(不重复+可null+可多个)、普通(可重复+可null+可多个)、前缀(字符串的前几个字符)、全文(用第三方软件)
- 聚簇索引
- 非聚簇索引(二级索引属于非聚集索引)
哈希索引
-哈希索引底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快,但不可排序,只能精确查找,无法部分查找;
InnoDB 和 MyISAM 都不支持 hash 索引
B+Tree索引:聚簇和非聚簇
⼤部分场景选择B+Tree索引,是B Tree(多路平衡查找树)的变种。对于主要的两种存储引擎的实现⽅式是不同的。
- MyISAM: 非叶子节点存储索引,叶子节点的 data 域存放索引+数据的地址, 索引文件和数据文件是分离的。以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
- InnoDB: 其数据文件本身就是索引⽂件。非叶子节点存储索引,叶子节点存储索引+索引对应的数据。,数据文件本身就是主索引,这被称为“聚簇索引 ”。
聚簇索引:
- 查询快,对于有序的自增主键很有优势
- 依赖于有序的数据,否则需要插入时排序,就很慢
- 修改数据,尤其是主键的代价很高
非聚簇索引:
- 叶子节点是不存放数据,更新代价比较小
- 如果是二级索引,叶子节点就存放的是主键,根据主键再回表查数据。
- 如果一个索引包含(或者说覆盖)所有需要查询的字段的值,就称之为“覆盖索引”,(select name from tb1 where name=‘wang’;)。无需回表查询
比如:
- id是主键,即主键索引,也是聚簇索引,where id=4 走的是聚簇索引,叶子节点存的是这一行数据;
- where name=‘wang’ 则是普通索引,非聚簇索引,叶子节点存的是主键,需要根据主键回表查询;(select name age from tb1 where name=‘wang’;)
- 但如果只需要查name,不需要别的信息,那也无须回表,走覆盖索引,非聚集索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作
为什么是b+树:
- 相对于b树 / 二叉树 / 红黑树来说,B+树每个节点能存储的节点数更多,层级更低。
- 相对于B树来说,B 树的所有节点都存放 key 和 data ,而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key每次查询是一定要到叶子节点,查询就更稳定,时间复杂度固定为
log n
。 B+树只存索引,一页能存更多的索引,IO次数更少 - 相对于b树/二叉树/Hash来说,叶子节点有双向链表指向与它相邻的叶子节点,便于范围查询。
- 二叉树在一般情况下查询性能非常好,但当数据非常大的时候,内存不够用,大部分数据只能存放在磁盘上,只有需要的数据才加载到内存中。磁盘读取时间远远超过了数据在内存中比较的时间。这说明程序大部分时间会阻塞在磁盘 IO 上,无法很好的利用磁盘预读(局部性原理)。B 树是专门为外部存储器设计的,如磁盘,它对于读取和写入大块数据有良好的性能,所以一般被用在文件系统及数据库中。
b+树一般只有1~3层:
- InnoDB 页的大小默认是 16k(16384)一页(一个叶子节点)代表一次IO
- 假设一行数据的大小是 1k,那么一个页可以存放 16 行这样的数据。
- 假设主键 ID 为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节,一页中能存放 16384/14=1170 个指针。
- 两层:1170 * 16=18720条记录
三层:1170 * 1170 * 16=21902400条记录
三层就可满足千万数据量,再高的话会增加IO次数
索引失效
1、要在where条件中(要有条件查询)
2、如果where条件中是OR
关系,加索引不起作用
3、like
“%aaa%” 不会使用索引;而like “aaa%”可以使用索引;
4、NOT IN
不会使用索引;NOT IN可以NOT EXISTS代替
5、索引列参与计算,使用了函数,会导致失效
6、符合最左原则。对于索引 (a,b,c),(b,c) 不起作用,但 (a,c)(a,c)(b,a,c)都可以走联合索引
7、当一个表有多条索引可走时, Mysql 根据查询语句的成本来选择走哪条索引, 可以用explain
预估,联合索引的话, 它往往计算的是最左边字段, 这样往往会走错索引。比如(b,a)索引和 a 索引,b 比 a 费时,很可能就走 a 索引了而不走联合索引了,就会降低效率,应当把(b,a)改为(a,b)
联合索引多种情况参考
- ①mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配;②= 和 in 可以乱序。
比如a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 - 联合索引查询,(A,B,C):
可以用上该组合索引查询:
A>5
;A=5 AND B>6
;A=5 AND B=6 AND C=7
;A=5 AND B IN (2,3) AND C>5
不能用上组合索引查询:
B>5
;B=6 AND C=7
——查询条件不包含组合索引首列字段
能用上部分组合索引查询:
A=5 AND B>6 AND C=2
——遇到大于停止,C没有用到索引 - 联合索引排序,(A,B):
可以用上组合索引排序:
ORDER BY A
——首列排序
A=5 ORDER BY B
——第一列过滤后第二列排序
ORDER BY A DESC, B DESC
——注意,此时两列以相同顺序排序
A>5 ORDER BY A
——数据检索和排序都在第一列
不能用上组合索引排序:
ORDER BY B
——排序在索引的第二列
A>5 ORDER BY B
——范围查询在第一列,排序在第二列
A IN(1,2) ORDER BY B
——理由同上
ORDER BY A ASC, B DESC
——注意,此时两列以不同顺序排序
创建和使用索引
- 选择合适的字段创建索引
区分度高,字段短,不为null;
被频繁查询、排序、用于连接;
不被频繁更新;
联合索引而非单索引
避免冗余 - 使用逻辑自增主键主键,而不要使用业务主键
- 删除长期未使用的索引
添加 PRIMARY KEY(主键索引)、UNIQUE(唯一索引)、INDEX(普通索引) 、
FULLTEXT(全文索引)
ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
多列索引
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )
联合索引:(a,b.c)只要有a,不管(c,b,a)(b,c,a)(a,c)都会自动优化,可以使用到联合索引!b、bc、c、cb不能用到
10、几种树
红黑树
为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树,这会使平衡树的性能大打折扣,
是一种自平衡的二叉查找树,在O(log n)时间内做查找,插入和删除。
为解决二叉查找树(BST)多次插入新节点导致不平衡,可能会退化成一个线性结构。
为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树,这会使平衡树的性能大打折扣
特点:
- 每个节点非红即黑;根节点总是黑色的;每个叶子节点都是黑色的空节点(NIL节点);
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
- 最长路径不超过最短路径的二倍
2-3树与红黑树:我们将3-结点表示为由一条左斜的红色链接相连的两个2-结点。红链接的头看作红节点,尾看作黑节点。
调整:变色+旋转(左旋/右旋)
变色:
左旋:
右旋:
AVL树:严格平衡的二叉树,要求每个节点左右子树高度不超过1,(红黑树只说路径长度不超过2倍),频繁查找使用AVL,频繁插入删除使用红黑树。
B树:
- 动态查找树主要有二叉查找树,平衡二叉查找树, 红黑树,查找的时间复杂度 O(log2-N)与树的深度相关,降低树的深度会提高查找效率,于是有了多路查找树B树、B+树、B*树,层级较低
- 二叉树在一般情况下查询性能非常好,但当数据非常大的时候,内存不够用,大部分数据只能存放在磁盘上,只有需要的数据才加载到内存中。磁盘读取时间远远超过了数据在内存中比较的时间。这说明程序大部分时间会阻塞在磁盘 IO 上,无法很好的利用磁盘预读(局部性原理)。B 树是专门为外部存储器设计的,如磁盘,它对于读取和写入大块数据有良好的性能,所以一般被用在文件系统及数据库中。
- 平衡二叉树是通过旋转来保持平衡的,而旋转是对整棵树的操作,若部分加载到内存中则无法完成旋转操作。
B+树
- 所有关键字存储在叶子节点处,非叶子节点并不存储真正的 data,所有 data 存储在叶节点导致查询时间复杂度固定为
log n
。而B 树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。 - B+树为所有叶子结点增加了一个链指针,叶节点两两相连可大大增加区间访问性,可使用在范围查询。
11、Redis
MySQL
与Redis
:
MySQL
是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢,用于持久化的存储数据到硬盘Redis
是NOSQL,即非关系型数据库,也是内存数据库,即将数据存储在内存中,读写速度非常快,被广泛应用于缓存方向,也经常用来做分布式锁,甚至是消息队列。
Redis
的好处:高性能+高并发
- 缓存数据,提高读取速度。
如果数据在缓存中就直接返回,不存在的话再查数据库,数据库中存在的话就更新缓存并返回数据。 - 高并发。 MySQL 这类的数据库的 QPS(每秒查询次数) 大概都在 1w 左右 ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
应用场景(分布式锁)
- 分布式锁
- Redis + Lua 脚本的方式来实现限流
消息队列
:Redis 自带的 list 或 Stream 数据结构可以作为一个简单的队列使用。- 复杂业务场景。比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
分布式锁:
条件:
- 互斥性:在任意时刻,只有一个客户端能持有锁 其他尝试获取锁的客户端都将失败而返回或阻塞等待
- 健壮性:一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功
- 唯一性:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给释放了,自己持有的锁也不能被其他客户端释放
- 高可用:不必依赖于全部Redis节点正常工作,只要大部分的Redis节点正常运行,客户端就可以进行加锁和解锁操作
①在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
②redis,使用SETNX key val加锁,value值为一个随机生成的UUID,并使用expire命令超时自动释放锁,释放锁的时候通过UUID判断是不是自己的锁,并执行delete进行锁释放。
③Redisson框架。客户端A要加锁,在redis集群中,用hash算法选择一台机器,检查锁是否存在,不存在就加锁,hset myLock
,会出现如下数据结构,表示“AAA”
这个客户端已经对“myLock”
这个锁Key完成了加锁,然后设置过期时间。(watch dog机制每隔一段时间查看,如果A还持有锁,就自动延长key的生存时间)
myLock
{
“AAA”:1
}
如果锁已经存在,就检查可重入,把“AAA”:1
变为“AAA”:2
,unlock的时候,锁次数减一,最后delete。
Redisson缺点:主从架构时,锁在主从之间复制是异步的,如果主突然宕机,可能会有别的客户端在新的主机上加锁成功
解决:利用RedLock——超过半数的Redis节点加锁成功才算成功
数据结构
- 普通字符串 key-value
常用命令:
set、get、mset、getm(批量)、strlen(长度)、
exists、decr、incr、expire(过期)、ttl(查看数据还有多久过期)
- list 双向链表
常用命令: rpush、rpop、lpush、lpop、lrange(对应下标范围的列表元素)、llen(链表长度) 等。
应用场景: 发布与订阅或者说消息队列、慢查询
- hash
类似HashMap,内部实现也差不多(数组 + 链表),适合用于存储对象。
- Java 中 HashMap 扩容每次需要去申请新的数组,将数据重新计算hash迁移到新数组中,插在链表的头部。(由于长度扩为原来2倍,所以rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置)
- Redis 使用了2个全局哈希表,①给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;②把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;③释放哈希表 1 的空间。 留作下一次 rehash 扩容备用。
- Redis 采用了渐进式 rehash 策略。由于是单线程,第二步如果一次性数据迁移会造成阻塞,所以迁移时仍然正常处理客户端请求。进行读操作,先去表1中找,找不到再去表2中找;进行写操作,直接写在表2中。
常用命令:
hset、hmset、hexists、hget、hgetall、hkeys、hvals 等。
应用场景: 系统中对象数据的存储。
- set 与 zset
压缩列表实际上类似于一个数组,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。所以查找头尾很快。时间复杂度 o(N)
跳表在链表的基础上,增加了多级索引,通过索引位置一次性多级跳转,实现数据的快速定位。时间复杂度 o(logN)
- bitmap
常用命令: setbit 、getbit 、bitcount(被被设置为 1 的位的数量)、bitop
BITOP
:BITOP operation destkey key [key ...]
,对保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。支持 AND 、 OR 、 NOT 、 XOR
应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)
eg:统计活跃用户
使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1
初始化数据:
统计 20210308~20210309 两天都在线的用户:
统计 20210308~20210309 至少一天在线的用户:
12、redis单线程
Redis为什么快:
- 首先,采用了多路复用 io 机制,同时监听多个套接字(即网络连接、TCP连接),复用单线程处理多个连接;
- 然后,数据结构简单,操作节省时间;
- 最后,运行在内存中,自然速度快
为什么单线程:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。单线程切换开销小,容易实现。
- Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。
套接字–>IO多路复用–>文件事件分派器–>事件处理器
处理多个连接:select
/ poll
/ epoll
- select机制
原理:select会阻塞监视3类文件描述符(fd),等有数据、可读、可写、出异常或超时,就会返回;返回后通过遍历 fdset 整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
缺点:轮询方式全盘扫描,每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历,fd增多性能下降;单个进程打开的 fd 有限制是1024个 - poll机制
原理:基本原理与select一致,也是轮询+遍历;唯一的区别就是 poll 没有限制 fd 个数(使用链表的方式存储fd)。 - epoll机制
原理:epoll 也没有限制fd个数,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。
优点:
效率提高,使用回调通知而不是轮询的方式,不会随着 fd 数目的增加效率下降
epoll水平触发LT与边缘触发ET
- 水平触发:只要这个fd还有数据可读,每次epoll_wait都会返回他的时间,提醒用户去操作
- 边缘触发:只会提示一次,无论fd是否还有数据可读,直到下次再有数据流入之前都不会再提示了。如果用户一次没读取完,只能等待下一次新数据来到时才会返回,
13、redis持久化
主要是为了故障备份
- 快照(snapshotting,
RDB
)默认方式:
save 900 1
:在900秒后,如果至少有1个key发生变化,则创建快照。
优点:性能好;恢复大数据集时的速度快,适合做冷备份;
缺点:实时性不好;容易丢失; - 只追加文件(append-only file,
AOF
)实时性更好:
appendonly yes
:开启AOF
appendfsync always/everysec/no
:每次有数据发生修改,先写入缓存,然后根据配置【每次有数据修改发生时 / 每秒 / 让操作系统决定】,写入AOF文件。
优点:实时性好;appen-only的模式写入性能非常高,没有磁盘寻址的开销;
缺点:AOF文件比RDB数据快照要大;性能差;数据恢复比较慢 - 混合:
AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复。
14、redis淘汰与删除
- redis内存满时–淘汰策略:
不删除(默认,返回错误信息)、
随机、
剩余最短时间、
最近最少使用(lru)、
整体最少使用 - key 过期了–删除策略:
定时删除(key过期立即删除该键)、
惰性删除(只有当获取键时,才检查获取的键是否过期,若过期删除该键)
定期删除(每隔一段时间(默认100ms),程序就对数据库进行一次检查,删除过期键)
Redis采用的是定期删除和惰性删除策略
15、redis事务
- 开始事务
MULTI
。 - 命令入队,先进先出(FIFO)的顺序执行。
- 执行事务(
EXEC
)。 DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令。WATCH
,监听指定的键,当调用 EXEC 执行事务时,如果一个被 WATCH 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。
16、缓存问题
缓存击穿
问题:针对缓存中没有但数据库有的数据。当Key失效后,假如瞬间突然涌入大量的请求,来请求同一个Key,这些请求不会命中Redis,都会请求到DB,导致数据库压力过大。
解决:
- 热点数据永不过期
- 布隆过滤器
- 限流
缓存穿透
问题:针对数据库和缓存中都没有的数据,如果从数据库查不到则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义,数据库容易崩溃
解决:
- 如果缓存和数据库都查不到某个 key 的数据就写一个 key-null 到 Redis 中去,并设置过期时间(短一点)
- 做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
- 可以把所有可能存在的请求的值都存放在布隆过滤器中,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,
布隆过滤器:
- 类似于一个hash set,用于快速判某个元素是否存在于集合中, “可能在集合中” 还是 “绝对不在集合中”。
- 本质上是由长度为 m 的位向量或位列表,比如
bitmap
- K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。
- 搜索一个值的时候,若该值经过 K 个哈希函数运算后,任何一个索引位为 0 ,那么该值肯定不在集合中。但如果所有哈希索引值均为 1,则该搜索的值可能存在集合中。
缓存雪崩
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。比如系统缓存模块崩溃;热点数据大面积失效
解决:
- 采用 Redis 主从架构或集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
- 设置不同的失效时间,如随机失效时间。热点数据永不过期
数据库与缓存不一致
旁路缓存协议:先更新完数据库,然后直接删除缓存
问题:仍会有时间差导致不一致
解决:①更新数据库和删除缓存操作加锁 ②给缓存设置一个比较短的过期时间
17、Redis主从复制
- 当从数据库启动时,会向主数据库发送sync命令
- 主数据库接收到sync后开始在后台保存快照rdb,在保存快照期间收到的命令缓存起来
- 当快照完成时,主数据库会将快照和缓存的命令一块发送给从数据库。复制初始化结束。
- 之后,主数据库每收到1个命令就同步发送给从数据库。 主从复制是乐观复制,当客户端发送写执行给主数据库,主数据库执行完立即将结果返回客户端,并异步的把命令发送给从数据库,从而不影响性能。
集群
Redis Sentinel
着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。Redis Cluster
着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
18、redis解决秒杀相关问题
重复购买:数据库加唯一索引防止用户重复购买
超卖:
- 乐观锁(悲观锁效率太低)。采用了版本号的方式,其实也就是CAS的原理。抢购成功之前再判断一下,库存和自己之前读取的是否一致,有没有被别的线程改过
- 在Redis存储一个K,V。例如 <商品1, 100>,每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。那么也就是只有100个线程会进入到后续操作。所以一定不会出现超卖的现象。之后异步的执行下单操作
- 开启 redis 事务,用watch监视,如果key变了,则这个事务失败。就保证在取得同一个key值得情况下,只有一个线程可以执行成功。
- redis分布式锁
19、sql语句
from >> on>> join >> where >> group by >>聚合函数(min, max)>> having >> select >> distinct >> order by >> limit
部门工资最高的员工
select d.name 'Department',e.name 'Employee',e.salary 'Salary'
from Employee as e,Department as d
where e.DepartmentId=d.id
and (e.Salary,e.DepartmentId) in
(select max(Salary),DepartmentId from Employee group by DepartmentId)
部门工资前三高
select d.Name as Department,e1.Name as Employee,e1.Salary as Salary
from Employee as e1
left join Department as d
on e1.DepartmentId=d.id
where 3>(
select count(distinct e2.salary)
from Employee e2
where e1.DepartmentId = e2.DepartmentId
and e1.salary<e2.salary
)
20、Elasticsearch
- 是一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎
- 【分布式】【实时】文件存储,并将每一个字段都编入索引,使其可以被搜索。
- 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
es提供了基于HTTP协议的RESTful APIS
,也就是说我们可以通过向es服务器发送curl HTTP请求来操作es服务器,如对文档读写、查询文档API、搜索API、索引的创建与删除,es默认使用9200端口接收HTTP请求
# 使用curl调用es,创建一个文档
curl http://localhost:9200/my_test/1 -H "Content-Type:application/json" \
-X POST -d '{"uid":1,"username":"test"}'
文档
es是面向文档的,文档是es中可搜索的最小单位,每个文档都有唯一的id
,这个id可以由我们自己指定,也可以由es自动生成。
es的文档由一个或多个字段组成,类似于关系型数据库中的一行记录,但es的文档是以JSON
进行序列化并保存的,每个JSON对象由一个或多个字段组成,字段类型可以是布尔,数值,字符串、二进制、日期等数据类型。
- 关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)
- Elasticsearch ⇒ 索引 ⇒ 类型 ⇒ 文档 ⇒ 字段(Fields)
PUT /megacorp/employee/1
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
往Elasticsearch里插入一条记录,其实就是直接PUT
一个json的对象,这个对象有多个fields,比如上面例子中的name, sex, age, about, interests,那么在插入的同时,Elasticsearch还为这些字段建立索引–倒排索引
,(为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,)
索引
ES索引原理
【文档】:
ID是Elasticsearch自建的文档id
【Posting List】:
Kate, John, 24, Female这些叫term
,而[1,2]就是Posting List
,就是一个int的数组,存储了所有符合某个term的文档id
。
【Term Dictionary】:
Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。
【Term Index】:
B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index
可以快速地定位到term dictionary
的某个offset
,然后从这个位置再往后顺序查找。
term index不需要存下所有的term,而仅仅是他们的一些【前缀】与【Term Dictionary的block】之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index
缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。
八、Spring
1、 IoC 和 AOP
IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)
IoC
IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,IoC的实现方式是依赖注入(Dependency Injection, DI)。当我们需要创建一个对象的时候,只需要配置好配置文件/注解,在需要的时候引入即可,完全不用考虑对象是如何被创建出来的,不需要考虑底层复杂的构造函数。
IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。
依赖注入:
在依赖注入中,不必创建对象,但必须描述如何创建它们。不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。
- setter 注入:部分注入;适用于设置少数属性;修改不会创建一个新实例
- 构造函数注入:没有部分注入;适用于设置很多属性;任意修改都会创建一个新实例
- 根据注解注入。
IOC核心容器的两个接口:BeanFactory
和ApplicationContext
ApplicationContext
创建容器,容器创建对象采取的策略是立即加载,也就是说只要读完配置文件,马上创建配置文件中的配置的对象(适用于单例对象)
Applicationcontext 有三个实现类:
1、ClassPathXmlApplicationContext,它可以加载类路径下的配置文件,
要求配置文件必须在类路径下,不在的话,加载不了
2、FileSystemXmlApplicationContext,它可以加载磁盘任意路径下的配置文件
3、读取注解创建容器BeanFactory
:在创建核心容器时,创建的对象采取的策略是延迟加载的方式。什么时候用到对象什么时候再创建对象。不支持基于依赖的注解。
应用启动的时候占用资源很少,但运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误。
AOP
AOP:面向切面编程。作为面向对象的一种补充,能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。依赖代理。通过代理原始类增加额外功能,我们可以将额外功能一次定义,然后配合切点达到多次使用的效果
- 静态代理:也就是自己手动创建的代理对象,在编译阶段就可生成 AOP 代理类代理类的.class文件。一个【代理对象】只能有一个【被代理对象】
- 动态代理:在运行时在内存中“临时”生成 AOP 动态代理类
①基于JDK的动态代理
②基于CGlib的动态代理
如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象。通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。 Proxy 类利用 InvocationHandler 接口,动态创建一个符合某一接口的实例,生成目标类的代理对象。
而对于没有实现接口的对象, 会使用 Cglib 生成一个被代理对象的子类来作为代理。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final ,那么它是无法使用 CGLIB 做动态代理的。
实现过程:
- 连接点(joinpoint) : 对一些方法实施拦截,被拦截到的点即连接点
- 通知(advice) : 所谓通知指的就是指拦截到连接点之后要执行的代码, 通知分为前置、后置、异常、最终、环绕通知五类(通知顺序可能会乱序,若对顺序性有要求的请使用环绕通知)。
- 织入(weave) : 将切面应用到目标对象并导致代理对象创建的过程
- 引入(introduction) : 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
简单写一个aop切面:
参考
基于AspectJ注解配置AOP
- 给配置类中加 @EnableAspectJAutoProxy 【开启基于注解的aop模式】
- 要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
- 当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
- 在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
@Before:前置通知,在方法执行之前执行
@After:后置通知,在方法执行之后执行
@AfterRunning:返回通知,在方法返回结果之后执行
@AfterThrowing:异常通知,在方法抛出异常之后执行
@Around:环绕通知,围绕着方法执行
@Controller
@Aspect//标注当前类为切面
public class MyLoggerAspect {
//@Before 将方法指定为前置通知
//必须设置value,其值为切入点表达式
@Before(value = "execution(public int com.atguigu.spring.day03_Spring.aop.MathImpl.add(int,int))")
public void beforeMethod(){
System.out.println("方法执行之前!");
}
}
spring容器的启动流程
- 初始化spring容器:
①实例化BeanFactory工厂,用于生成bean对象
②实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取并装换成BeanDefinition对象,该对象存储了bean对象的所有特征信息,如:是否单例、是否懒加载等
③实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定包目录进行路径扫描查找bean对象 - 将配置类的BeanDefinition注册到容器中,即通过配置读取器读取bean对象的所有特征信息生成BeanDefinition对象,注册到BeanDefinitionRegister中。
- 调用refresh方法刷新容器,刷新容器会实例化所有单例方法。
①prepareRefresh()刷新前预处理
②obtainFreshBeanFactory()获取在容器初始化时创建的BeanFactory
③prepareBeanFactory(beanFactory):beanfactory的预处理工作,向容器中添加一些组件。
④postProcessBeanFactory,子类若重写该方法可以实现BeanFactory创建以及预处理之后的事情
⑤invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器。
⑥registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
⑦initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
⑧initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到:
⑨onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
⑩registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件:
①①finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象;
①② finishRefresh():发布BeanFactory容器刷新完成事件:
2、bean
bean 代指的被 IoC 容器所管理的对象。
创建Bean对象的方式:
1、使用默认构造函数创建
2、使用普通工厂方法创建(类似@bean注解那样的形式)
3、使用工厂中的静态方法创建
Spring 启动时:
- 读取应用程序提供的 Bean 配置信息,并在 Spring 容器中生成一份相应的 Bean 配置注册表
- 然后根据这张注册表实例化 Bean,装配好 Bean 之间的依赖关系
- 将 Bean 放入 spring 容器中的 Bean 缓存池中,Bean 缓存池为 HashMap 实现
将一个类声明为 bean 的注解
我们一般使用 @Autowired
注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:
@Component
:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller
: 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
@Component和@bean的区别是什么
- 作用对象不同:@component注解作用于类,@bean注解作用于方法
- @component通常是通过类路径扫描来自动侦测以及自动装配到spring容器中。@bean注解通常是我们在标有该注解的方法中定义产生了bean。
Spring中出现同名bean怎么办:
- 同一个配置文件内同名的Bean,以最上面定义的为准
- 不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件
- 同文件中ComponentScan和@Bean出现同名Bean。同文件下@Bean的会生效,通过@ComponentScan扫描进来的优先级是最低的(因为是最先被注册的)
bean生命周期
实例化–>属性赋值–>初始化–>销毁
- 根据配置方法调用Bean构造方法或者工厂实例化bean对象。
- 利用依赖注入完成bean中所有属性值的配置注入。
- 如果通过Aware接口声明了依赖关系,根据其实现的Aware接口(比如 BeanNameAware,BeanClassLoaderAware)设置依赖信息。在某些情况下是需要在Bean中对IOC容器进行操作的。这时候需要在bean中设置对容器的感知。比如知道自己在容器中的名字。
- 把bean实例传递bean后置处理器的方法 postProcessBeforeInitialization
- 调用bean的初始化方法。如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
- 把bean实例传递bean后置处理器的方法postProcessAfterInitialization
- 程序使用bean
- 当容器关闭时,调用bean的销毁的方法。如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
bean的作用域
当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域
- singleton单例模式:唯一bean实例,spring中bean默认都是单例的,缺省是饿汉模式。启动容器时(即实例化容器时),为所有spring配置文件中定义的bean都生成一个实例。
- prototype原型模式:每次请求都会创建一个新的bean实例。在调用getbean的时候会创建实例对象。prototype作用域Bean的创建、销毁代价比较大。
(对于singleton作用域的Bean,每次请求该id的Bean,都将返回同一个实例,而prototype作用域的Bean, 每次请求都将产生全新的实例。) - request:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
- session:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。同一个HTTP session 共享一个bean,不同session使用不同的bean,session 过期后,bean 会随之失效。该作用域仅在基于web的Spring ApplicationContext情形下有效。
bean自动装配
当 bean 在 Spring 容器中组合在一起时, Spring 容器需要知道需要什么 bean 以及容器,应该如何使用依赖注入,来将 bean 绑定在一起,同时装配bean。
Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。
自动装配的不同模式:
- no:这是默认设置,表示没有自动装配。通过手工设置ref 属性来进行装配bean。
- byName:通过参数名自动装配,容器试图匹配、装配和XML配置文件中bean的属性具有相同名字的bean。
- byType:通过参数的数据类型自动自动装配,容器试图匹配和装配和该bean的属性类型一样的bean。如果有多个bean符合条件,则抛出错误。
- 构造函数:它通过调用类的构造函数来注入依赖项。同byType类似,不过是应用于构造函数的参数
- autodetect:首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。
自动装配有如下局限性:
- 重写:你仍然需要设置指明依赖,这意味着总要重写自动装配。
- 简单属性(如原数据类型,字符串和类)无法自动装配。
- 模糊特性:自动装配总是没有自定义装配精确,因此,如果可能尽量使用自定义装配。
单例 bean 的线程安全问题
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该方法,如果该方法中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
- 有状态bean:就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。适合用Prototype原型模式
- 无状态bean:就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 。不能保存数据,是不变类,是线程安全的。适合用就是单例模式,这样可以共享实例提高性能。
- Spring使用ThreadLocal解决线程安全问题。如果你的Bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全 。
3、spring设计模式
- 工厂设计模式 : Spring 使用工厂模式通过 BeanFactory(使用到某个 bean 的时候才会注入)、ApplicationContext(容器启动的时候,不管你用没用到,一次性创建所有 bean ) 创建 bean 对象。
- 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
- 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
4、常见注解
@component,@repository @controller @service @configration @bean 注册类
@configuration 作为配置类,替代xml配置文件
@autowired默认是按照类型装配注入的,默认情况下要求依赖对象必须存在。@resource 默认按照名称进行装配,名称 可以通过name属性进行指定。
-
声明bean的注解
@Component 组件,没有明确的角色
@Service 在业务逻辑层使用(service层)
@Repository 在数据访问层使用(dao层)
@Controller 在展现层使用,控制器的声明(C) -
注入bean的注解
@Autowired:由Spring提供
@Configuration 声明当前类为配置类,相当于xml形式的Spring配置(类上)
@Bean 注解在方法上,声明当前方法的返回值为一个bean,替代xml中的方式(方法上)
@Configuration 声明当前类为配置类,其中内部组合了@Component注解,表明这个类是一个bean(类上)
@ComponentScan 用于对Component进行扫描,相当于xml中的(类上) -
SpringMVC部分
@EnableWebMvc 在配置类中开启Web MVC的配置支持,如一些ViewResolver或者MessageConverter等,若无此句,重写WebMvcConfigurerAdapter方法(用于对SpringMVC的配置)。
@Controller 声明该类为SpringMVC中的Controller
@RequestMapping 用于映射Web请求,包括访问路径和参数(类或方法上)
@ResponseBody 支持将返回值放在response内,而不是一个页面,通常用户返回json数据(返回值旁或方法上)
@RequestBody 允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前)
@PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。
@RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
5、spring的循环依赖
构造器和非单例的循环依赖:无法解决
单例模式下的setter循环依赖解决方法:三级缓存
1、singletonObjects(一级缓存):存放完全初始化好的bean,可以直接使用
2、earlySingletonObjects(二级缓存):存放尚未填充属性的bean
3、singletonFactories(三级缓存):存放bean工厂对象
- spring允许在没初始化时提前暴露bean对象。首先实例化 A 对象,存放在
三级缓存
中 - 然后初始化 A 对象,发现 A 对象依赖 B 对象,①在
一级缓存
中寻找,没有找到(在这个步骤还有个判断对象是否正在创建,若是正在创建,基本是循环依赖),②在二级缓存
中找如果没有找到,③在三级缓存
中找,没有就实例化一个 B 对象,存放在三级缓存
中 - 同理在注入 B 对象 A 依赖的过程中,从
三级缓存
中发现有 A 对象,就把其提升到二级缓存
中,删除三级缓存
中的 A 对象, - 由于 B 对象初始化成功,将B对象存放到
一级缓存
中,其余的缓存中删除该对象,由于 B 对象创建完成,返回注入到 A 对象中,将 A 对象保存在一级缓存
中,删除二级缓存中的对象
6、spring事务
管理事务的方式:
- 编程式事务 : 在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
- 声明式事务 : 在 XML 配置文件中配置或者直接基于注解(
@Transactional
),实际是通过 AOP 实现。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的
@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,通过aop的方式进行管理
作用:
①根据配置,设置是否自动开启事务
②自动提交事务或者遇到异常自动回滚
原理:
- spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
- 每个带有@Transactional 注解的方法都会创建一个切面,所有的事务处理逻辑就是由这个切面完成的,这个切面的具体实现就是
TransactionInterceptor
类。 - 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
事务传播行为:
事务传播行为是为了解决业务层方法之间互相调用的事务问题。 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional
注解默认使用就是这个事务传播行为。如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行- TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
事务中的隔离级别:
Spring 也相应地定义了一个枚举类:Isolation
和MYSQL一样,READ_UNCOMMITTED
、READ_COMMITTED
、 REPEATABLE_READ
、 SERIALIZABLE
,默认使用后端数据库默认的隔离级别
回滚:
Exception 分为运行时异常 RuntimeException(数学错误、数组越界,空指针等),和非运行时异常 IOException(找不到文件等)。事务管理保证即使出现异常情况,它也可以保证数据的一致性。
@Transactional可以加在类或者方法上,只能应用到 public 方法才有效,(当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性)。这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
@Transactional(rollbackFor = Exception.class)
注解:
- 在 @Transactional 注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚(默认)
- 加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。
事务失效
- spring要求被代理方法必须是public的。如果不是public,而是protected、default或者private,spring则不会提供事务功能。
- 如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而无法添加事务功能。static的,同样无法通过动态代理,变成事务方法。
- 方法未被spring管理,没有@Controller、@Service、@Component、@Repository注解
- 在同一个类中的方法,直接内部调用另一个事务方法,解决:可以自己注入自己
- 捕获了异常,但没有正确的catch
7、springboot
- 很多Spring框架包的依赖配置都是重复添加的,而且需要做很多框架使用及环境参数的重复配置,如开启注解、配置日志等。Spring Boot 提供默认配置,快速搭建、开发和运行Spring应用。
- 自动装配,想引入第三方依赖比如redis,直接在依赖中引入一个
starter
即可,Spring Boot将为你管理依赖关系。Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可 - SpringBoot项目可以打包成jar文件。可以使用Java-jar命令从命令行将应用程序作为独立的Java应用程序运行。
- 在开发web应用程序时,springboot会配置一个嵌入式Tomcat服务器,以便它可以作为独立的应用程序运行。
自动装配
Spring Boot 通过@EnableAutoConfiguration
开启自动装配,在启动类 SpringApplication.run(…) 的内部就会执行,@Import 注解的 selectImports() 方法,在启动类所在包立找到所有 JavaConfig 形式的自动配置类(以AutoConfiguration结尾来命名),从META-INF/spring.factories中获取资源,筛选出以 EnableAutoConfiguration 为 key 的数据,加载到 IOC 容器中,
@SpringBootApplication
看作是以下三个的集合:
- @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
- @SpringBootConfiguration:允许在上下文中注册额外的 bean 或导入其他配置类
- @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。
@Autowired
- @Autowired注解的实现是通过后置处理器AutowiredAnnotationBeanPostProcessor类的postProcessPropertyValues()方法实现的。
- 自动装配时,从容器中如果发现有多个同类型的属性时,@Autowired注解会先根据类型判断,然后根据@Primary、@Priority注解判断,最后根据属性名与beanName是否相等来判断,如果还是不能决定注入哪一个bean时,就会抛出NoUniqueBeanDefinitionException异常。
- @Autowired自动装配中byName、byType与自动装配的模型中的byName、byTYpe没有任何关系,两者含义完全不一样,前者是实现技术的手段,后者是用来定义BeanDefiniton中autowireMode属性的值的类型。
@Resource和@Autowired:都是做bean的注入时使用
- 两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
- @Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在
- @Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。
public class TestServiceImpl {
// 下面两种@Autowired只要使用一种即可
@Autowired
private UserDao userDao; // 用于字段上
@Autowired
public void setUserDao(UserDao userDao) { // 用于属性的方法上
this.userDao = userDao;
}
// 下面两种@Resource只要使用一种即可
@Resource(name="userDao")
private UserDao userDao; // 用于字段上
@Resource(name="userDao")
public void setUserDao(UserDao userDao) { // 用于属性的setter方法上
this.userDao = userDao;
}
}
8、mybatis
原理
Mybatis底层封装了JDBC,使用了动态代理模式。
组件:
- SqlSessionFactoryBuilder (构造器):使用Builder模式根据mybatis-config.xml配置或者代码来生成SqISessionFactory。
- SqlSessionFactory (工厂接口):使用工厂模式生成SqlSession。
- SqlSession (会话): 一个既可以发送 SQL 执行返回结果,也可以获取Mapper的接口。
- SQL Mapper (映射器): 它由一个Java接口和XML文件(或注解)构成,需要给出对应的SQL和映射规则,它负责发送SQL去执行,并返回结果。
- Executor(执行器):负责SQL语句的生成和查询缓存的维护
- StatementHandler :封装了JDBC Statement操作,
工作流程:
- 通过SqlSessionFactoryBuilder创建SqlSessionFactory对象
- 通过SqlSessionFactory创建SqlSession对象
- 通过SqlSession拿到Mapper代理对象。
- 通过MapperProxy调用Mapper中增删改查的方法。spring 通过调用的mapper会在当前mapper所在的路径下找到与之同名的mapper.xml配置文件,在该配置文件中通过标签的 id 值与方法名的映射关系,找到对应需要执行的 sql语句了(对象中的字段可以和数据库中的字段名称不一致,可以自定义映射关系)
缓存
Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的
- 一级缓存是指 SqlSession 级别的缓存(相当于一个方法内的缓存),当在同一个 SqlSession 中进行相同的 SQL 语句查询时,第二次以后的查询直接从缓存中获取。Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句。Value为查询出来的结果集映射成的java对象
- 二级缓存
是指可以跨 SqlSession 的缓存。 是 mapper 级别的缓存,对于 mapper 级别的缓存不同的sqlsession 是可以共享的。
防止SQL注入
#{}
和 ${}
:
- #{} 是预编译处理,
${}
是字符串替换。 - Mybatis在处理#{}时,会将sql中的
#{}
替换为?
号,调用PreparedStatement的set方法来赋值;使用#{}可以有效的防止SQL注入 - Mybatis在处理
${}
时,就是把${}
替换成变量的值。
#{}
预编译防止SQL注入:
// 安全,预编译,执行sql前会预编译号该条语句,替换为?
String sql = "select id, username, password, role from user where id=?"; // 用set方法给?赋值
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
// 不安全,没有预编译,直接替换为id值
String sql = "select id,username,password,role from user where id=" + id;
//当id参数为"3;drop table user;"时,执行的sql语句如下:
//select id,username,password,role from user where id=3; drop table user;
PreparedStatement pstmt = conn.prepareStatement(sql);
9、springcloud微服务
springboot与springcloud:
- Spring Cloud 是完全基于 Spring Boot 而开发
- Spring Cloud 利用 Spring Boot 特性整合了开源行业中优秀的组件,整体对外提供了一套在微服务架构中服务治理的解决方案。;
Spring Cloud组件运行:
- 所有请求都统一通过 API 网关来访问内部服务。
- 网关接收到请求后,从注册中心获取可用服务。
- 由 Ribbon 进行均衡负载后,分发到后端的具体实例。
- 微服务之间通过 Feign 进行通信处理业务。
微服务优点:
- 将一个应用程序划分为小的服务,相互协调相互配合,共同提供最终的效果。
- 每个微服务专注自己的业务,可以分别独立的部署在平台上,松耦合
- 独立部署,容错性高;易于升级和扩展
nacos注册中心与openfeign远程调用:
- 生产者和消费者分别在nacos里注册,生产者的启动类要添加一个注解@EnableDiscoveryClient,实现负载均衡;消费者的启动类也需要添加一个注解@EnableFeignClients,这是为了使用feign实现远程调用
- 消费者中编写生产者的OpenFeigin接口,name属性是生产者注册到Nacos的name,Mapping地址要与生产者暴露的地址一样,除了没有具体的服务内容,实现了地址的映射,声明的接口名称、参数与服务者暴露的接口一致,完成了服务的引入,然后就可以在消费者端进行调用。
- 消费者的controller中,注入这个feignservice,即可使用
Nacos如果服务突然挂掉
- Nacos目前支持临时实例使用心跳上报方式维持活性。Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求,以确保自己处于活跃状态。Nacos服务端在15秒内如果没收到客户端的心跳请求,会将该实例设置为不健康,在30秒内没收到心跳,会将这个临时实例摘除。
- 在正常业务场景下,如果关闭掉一个服务实例,默认情况下会在关闭之前主动调用注销接口,将Nacos服务端注册的实例清除掉。
- 此时仍会有15秒的间隙,会有一部分请求被分配到异常的实例上。解决方法:
①自定义心跳周期
②springcloud有请求重试的机制 - Nacos中针对注册的服务实例有一个保护阈值的配置项。保护阈值是⼀个比例值(当前服务健康实例数 / 当前服务总实例数)。一般nacos只会返回给客户端健康的实例,但如果健康的太少了,低于阈值,为防止雪崩,nacos会提供所有实例
分布式CAP+BASE
CAP
一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项。
- 一致性:数据在多个副本之间能否保持一致的特性
- 可用性:系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果
- 分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
分区容错性是一个分布式系统必然需要面对和解决的问题(否则就成了单机分布)。因此需要在C(一致性)和A(可用性)之间寻求平衡。
BASE
Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)
- 基本可用:是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。比如,消费降级,响应变慢。注意,这绝不等价于系统不可用。
- 软状态:指允许系统中的数据存在中间状态
- 最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态,不需要实时
通过牺牲强一致性(ACID)来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态
Nginx与Ribbon 负载均衡:
- 本来使用nginx实现路由转发,最后用gateway做网关实现路由和过滤,
- gateway实现多个微服务内部负载均衡,nginx可以放在gateway前面,流量先经过nginx,再到gateway上。
Nginx是集中式负载均衡,请求是先进入负载均衡器,再发给客户端;
而在 Ribbon 中是先在客户端进行负载均衡,然后才进行请求的。
Nginx 使用的是 轮询和加权轮询算法。而在 Ribbon 中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule 轮询策略:若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
OpenFeign 直接内置了 Ribbon 进行负载均衡
dubbo:
- dubbo与springcloud都是微服务架构的方式,都需要服务提供方,消费方,注册中心;
- dubbo的注册中心选zookeeper,springcloud选nacos;
- dubbo服务调用使用RPC协议,springcloud使用 HTTP 协议的 REST API;
- Dubbo 只是实现了服务治理,而 Spring Cloud 覆盖了微服务架构下的众多部件,比如网关,过滤,熔断,服务治理只是其中的一个方面。Dubbo 需要额外集成这些
rpc:远程调用
如果不使用rpc框架,那么调用服务走http需要配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。Feign是一个http请求调用的轻量级框架,以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。
- 客户端通过网络通信将需要调用的接口信息(接口名,方法名,参数类型,参数)发送给服务端
- 服务端通过网络通信接受到信息之后,通过反射进行方法调用,将结果通过网络通信返回给客户端
RPC(Remote Procedure Call Protocol)——远程过程调用协议,在OSI网络通信模型中,RPC跨域了传输层和应用层。
RPC框架:
- 通信框架。它主要解决客户端和服务端如何建立连接(HTTP+TCP、socket)、以及服务端如何处理请求(BIO、NIO、AIO)的问题。
- 通信协议。它主要解决客户端和服务端采用哪种数据传输协议的问题。比如HTTP协议,dubbo自定义的协议。(RPC自定义TCP传输,根据需求精简传输需求,减少了http的无用数据传输,)
- 序列化和反序列化。它主要解决客户端和服务端采用哪种数据编解码的问题。比如 json
10、thrift
RPC框架原理到选型:gRPC、Thrift、Dubbo、Spring Cloud
Thrift:跨语言,协议层、传输层有多种控制要求,socket通信
Dubbo是rpc,不能很好跨语言,cloud基于http rest api,跨语言
序列化NIO netty
将自身token和appkey签名,加在请求协议里,发送给服务端,签名相同或在白名单内,鉴权通过,连接粒度/接口粒度
Octo:服务注册、服务自动发现、负载均衡、容错、灰度发布、数据可视化、监控告警等功能,类似
MTthrift :分布式服务通讯框架,致力于提供高性能和透明化的RPC远程服务调用方案,是 OCTO 服务治理方案的核心框架,支持服务注册、服务自动发现、分布式服务调用跟踪等,屏蔽了底层高性能网络通信的实现细节, 从而实现简单高效的服务开发,支持不同语言版本的代码实现, 保持通信协议的一致性,支持 Thrift/HTTP/pigeon 等协议
MNS 是 Meituan Naming Service 的缩写。 MNS 是服务注册路由中心,基于 ZooKeeper 构建,为公司各类分布式服务提供稳健可靠的命名服务管理组件, 快速实现服务注册、路由、服务自动发现。
MCC 是 Meituan Config Center 的缩写。 统一配置中心,提供统一配置管理服务, 实现配置与代码分离、配置信息实时更新、高可用性、版本控制, 提高服务开发效率,降低运维成本。 其原理是将 JSON 格式的配置文件存储在 ZooKeeper 目录下,当用户有需要时,将zk中的配置数据落地到本机的指定目录中。
使用thrift 提供的@ThriftService、@ThriftMethod、@ThriftStruct、@ThriftField等注解,注解于普通的Java类,使其成为thrift的数据模型(model)和服务接口(service)。其使用模式与 Dubbo 非常相似:服务的提供者和消费者基于共同的一套接口定义。
九、消息队列
1、好处与问题
好处:
- 通过异步处理提高系统性能(减少响应所需时间)。比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,再通知用户订单成功,
- 削峰/限流。将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息
- 降低系统耦合性。如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性就更好一些
问题:
- 系统可用性降低: 引入 MQ 之后,就需要去考虑消息丢失或者说 MQ 挂掉等等的情况
- 系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
- 一致性问题: 异步可以提高系统响应速度。但是也会导致数据不一致的情况了
2、常见MQ
Kafka、RocketMQ、RabbitMQ
- 吞吐量:RabbitMQ 比 Kafka、RocketMQ 低
- 可用性:RabbitMQ基于主从架构,Kafka 和 RocketMQ 基于分布式架构(少数机器挂掉不会导致不可用)
- RabbitMQ 并发能力最强,延时最低;RabbitMQ 稳定且开源
- Kafka 功能较为简单,只支持简单的 MQ 功能,在大数据领域的实时计算,日志采集被大规模使用
kafka
优点:
- 单机写入 TPS 约在百万条/秒,吞吐量高。时效性 ms 级可用性非常高;
- kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用;
- 消费者采用 Pull 方式获取消息,消息有序,通过控制能够保证所有消息被消费且仅被消费一次
- 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
缺点:
Kafka 单机超过 64 个队列/分区, Load 会发生明显的飙高现象,队列越多, load 越高,发送消息响应时间变长;
消费失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,
Kafka如何保证消息的有序性:
【前提条件:
生产者生产的消息是有序的,
kafka默认保证同一个partition分区内的消息是有序的】
- 方案一,kafka topic 只设置一个partition分区。kafka默认保证同一个partition分区内的消息是有序的,则可以设置topic只使用一个分区,这样消息就是全局有序,缺点是只能被consumer group里的一个消费者消费,降低了性能,不适用高并发的情况
- 方案二,producer将消息发送到指定同一个partition分区。producer可以在发送消息时可以指定需要保证顺序的几条消息发送到同一个分区,这样消费者消费时,消息就是有序。
RocketMQ
用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理, binglog 分发等场景。
优点:
- 单机吞吐量十万级,适合对于可靠性要求很高的场景
分布式
架构,消息可以做到 0 丢失- MQ 功能较为完善,扩展性好,支持 10 亿级别的消息堆积,不会因为堆积导致性能下降
RabbitMQ
由于 erlang 语言的高并发特性
,性能较好; 吞吐量到万级, MQ 功能比较完备,健壮、稳定、易用、跨平台、
AMQP,Advanced Message Queuing Protocol(高级消息队列协议),一个提供统一消息服务的应用层标准,是应用层协议的一个开放标准。
基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 RabbitMQ是该协议的典型实现。
3、RabbitMQ
组成
Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
如何让保证不被重复消费
原因:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决:
- 保证消息的唯一性。可以在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;也可以利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息
- 可以用单线程消费保证消息的顺序性;对消息进行编号,消费者处理消息是根据编号处理消息;
如何保证RabbitMQ消息的可靠传输?
原因:消息不可靠的情况可能是消息丢失,劫持等原因;
丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;
-
生产者丢失消息:RabbitMQ提供 transaction 和 confirm 模式来确保生产者不丢消息;
transaction机制:发送消息前,开启事务,然后发送消息,如果发送过程中出现什么异常,事务就会回滚,如果发送成功则提交事务。缺点:吞吐量下降;
confirm模式:一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个ACK
给生产者(包含消息的唯一ID),如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。 -
消息队列丢数据:消息持久化。
开启持久化磁盘的配置(将queue的持久化标识durable设置为true),可以和 confirm 机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。 -
消费者丢失消息:消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;
改为处理消息成功后,手动回复确认消息
4、RabbitMQ工作模式
①简单模式 / work模式
一个生产者 –> 一个队列 –> 一个或多个消费者。一条消息只能给一个消费者消费。
例子:发送一次短信
②发布订阅模式(广播)
每个消费者监听自己的队列;由交换机将消息转发到绑定此交换机的每个队列。不处理路由键routingkey。
例子:发送多种通知,当用户充值成功后,需要发送多种通知,比如短信、邮件。
③路由模式
每个消费者监听自己的队列,并且设置routingkey。
生产者将消息发给交换机,由交换机根据 routingkey 来转发消息到指定的队列。需要将一个队列绑定到交换机上,
④topic 主题模式
每个消费者监听自己的队列,并且设置带统配符的routing key。
发送消息时,由交换机根据routing key来转发消息到指定的队列。routing key使用通配符,和队列绑定的key匹配,该队列就行接收到消息。
5、RocketMQ
组成
- 消息(Message):每条消息必须属于一个主题
- 主题(Topic):一类消息的集合,是消息订阅的基本单位
- 标签(Tag):topic是一级分类,tag是二级分类,消费者可以根据Tag实现对不同子主题的不同消费逻辑
- 队列(Queue):一个Topic中可以包含多个Queue,称为一个Topic中消息的分区。一个分区只能被一个消费者消费。
- 消息标识(MessageId/Key):每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、offsetMsgId与key都称为消息标识
NameServer、Broker、Producer、Consumer
- 启动NameServer,NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
- 启动Broker时,Broker会与所有的NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。
- 发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。也可以在发送消息时自动创建Topic。
- Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息,即当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算法策略从队选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。(在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。)
- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
十、设计模式
设计模式分为三大类:
- 创建型模式:提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
共5种:工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式。 - 结构型模式:关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 - 行为型模式:特别关注对象之间的通信。
共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
1、单例模式(Singleton Pattern)
- 原则:
①单例类只能有一个实例。
②单例类必须自己创建自己的唯一实例。
③单例类必须给所有其他对象提供这一实例。 - 适用情况:
①一个全局使用的类频繁地创建与销毁
②当您想控制实例数目,节省系统资源的时候。 - 关键:
①判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
②构造函数是私有的。 - 应用:
Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
懒汉模式,线程不安全
第一次调用时才初始化
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
//用来测试
public void showMessage(){
System.out.println("Hello World!");
}
}
测试:
public class SingletonPatternDemo {
public static void main(String[] args) {
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
SingleObject object = SingleObject.getInstance();
object.showMessage();
}
}
懒汉模式,线程安全
加synchronized
锁
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
饿汉模式,线程安全
类加载时就初始化,浪费内存,容易产生垃圾对象。
没有加锁,执行效率会提高。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
双重校验锁,线程安全
懒加载,双锁机制,安全且在多线程情况下能保持高性能。
class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) singleton = new Singleton();
}
}
return singleton;
}
}
2、工厂模式(Factory Pattern)
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
何时使用:计划不同条件下创建不同实例时。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
//面条的抽象类
abstract class INoodles {
public abstract void desc();
}
//面条1
class Noodles1 extends INoodles {
@Override
public void desc() {
System.out.println("面条1");
}
}
//面条2
class Noodles2 extends INoodles {
@Override
public void desc() {
System.out.println("面条2");
}
}
//工厂(面馆)
class SimpleNoodlesFactory {
public static final int TYPE_1 = 1;
public static final int TYPE_2 = 2;
// 提供静态方法
public static INoodles createNoodles(int type) {
switch (type) {
case TYPE_1:return new Noodles1();
case TYPE_2:return new Noodles2();
default:return new Noodles1();//应该是Noodles3
}
}
}
//测试
public class FactoryMode {
public static void main(String[] args) {
INoodles noodles = SimpleNoodlesFactory.createNoodles(SimpleNoodlesFactory.TYPE_1);
noodles.desc();
}
}
3、原型模式(Prototype Pattern)
原型模式(Prototype Pattern)是用于创建重复的对象,这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。即利用已有的一个原型对象,快速地生成和原型对象一样的实例。
例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
4、装饰器模式(Decorator Pattern)
装饰器模式(Decorator Pattern)这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,添加新的功能。
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
5、代理模式(Proxy Pattern)
在代理模式(Proxy Pattern)中,用一个类代表另一个类的功能。我们创建具有现有对象的对象,以便向外界提供功能接口。为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因,直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时,加上一个对此对象的访问层。
import java.lang.reflect.Proxy;
//1、静态代理
//接口Subject
interface Subject {
void visit();
}
//实现Subject的目标对象类
class RealSubject implements Subject {
private String name = "dreamcat";
@Override
public void visit() {
System.out.println(name);
}
}
//目标代理对象
class ProxySubject implements Subject {
private Subject subject;
public ProxySubject(Subject subject) {
this.subject = subject;
}
@Override
public void visit() {
System.out.println("我是静态代理");
subject.visit();
System.out.println("静态代理结束了");
}
}
//2、动态代理
class DynamicProxy {
private Object target;
DynamicProxy (Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxy, method, args) -> {
System.out.println("我是动态代理");
Object value = method.invoke(target, args);
System.out.println("代理结束了");
return value;
});
}
}
//测试
public class ProxyMode {
public static void main(String[] args) {
// 静态代理
ProxySubject proxySubject = new ProxySubject(new RealSubject());
proxySubject.visit();
// 动态代理
Subject proxyInstance = (Subject)new DynamicProxy(new RealSubject()).getProxyInstance();
proxyInstance.visit();
}
}
6、适配器模式(Adapter Pattern)
适配器模式(Adapter Pattern)涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容
而不能一起工作的那些类可以一起工作。
比如读卡器是作为内存卡和笔记本之间的适配器。
适配器继承或依赖已有的对象,实现想要的目标接口。
spring MVC 中也是用到了适配器模式适配Controller。
7、模板模式(Template Pattern)
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
封装不变部分,扩展可变部分;行为由父类控制,子类实现。
智力题
一些智力题1
一些智力题2
9颗糖分给10个人,有几种分法
答:把糖看作1,人看作0,相当于这些 1 和 0 排列组合,永远把 1 分给后面紧接着的 0,由于最后一位必须是 0,否则最后一个糖没人给,所以就18个位置选出9个作为 1.
版权声明:本文标题:Java面试自用简洁版 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dongtai/1728050921a1143649.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论