文章目录
- 一、JVM
- 1、Java虚拟机
- 1、常见的JVM
- 1、JVM执行流程
- 1、类加载过程
- 1、类加载器分类
- 1、双亲委派机制
- 1、运行时数据区
- 1、对象实例化、内存布局、访问定位
- 1、解释器&即时JIT编译器
- 1、StringTable
- 1、触发垃圾回收方式
- 1、垃圾标记算法
- 1、Finalization机制
- 1、垃圾清除算法
- 1、引用(强软弱虚)
- 1、常见的垃圾回收器
- 1、Serial 串行垃圾回收器
- 1、ParNew 并行垃圾回收器
- 1、Parallel 垃圾回收器(吞吐量优先)
- 1、CMS垃圾回收器(低延迟)
- 1、G1垃圾回收器(区域化分代式)
- 1、垃圾回收器总结
- 1、JVM 调优的参数
- 1、内存溢出问题
- 1、内存溢出排查
- 1、CPU飙高排查方案与思路
- 1、
- 二、多线程和并发编程
- 1、基本概念
- 1、线程创建的几种方式
- 1、常见的API
- 1、停止线程(打断机制)
- 1、线程状态(生命周期)
- 1、Synchronized简介
- 1、Monitor(监视器或管程)
- 1、Synchronized锁升级
- 1、Wait & Sleep
- 1、join原理
- 1、生产者/消费者(模拟)
- 1、JMM内存模型
- 1、三大特性
- 1、内存屏障(volatile)
- 1、缓存一致性
- 1、happens-before
- 1、CAS
- 1、CAS & synchronized
- 1、Atomic
- 1、Unsafe类
- 1、AQS
- 1、ReentrantLock实现原理
- 1、ReentrantLock & Sysnchronized
- 1、死锁产生的条件
- 1、线程池
- 1、线程池状态
- 1、如何确定核心线程数
- 1、线程池使用场景
- 1、悲观锁 & 乐观锁
- 1、HashTable & ConcurrentHashMap
- 1、currentHashMap 1.8优化
- 1、初始化数组流程
- 1、ThreadLocal
- 1、ThreadLocal内存泄漏问题
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 1、
- 三、Java基础
- 四、Java web
- 五、Mysql
- 1、sql语句执行顺序
- 1、视图、存储过程和函数、触发器
- 1、Innodb和myIsam对比
- 1、请求执行流程
- 1、缓存池buffer_pool
- 1、InnoDB存储结构(页,区,段,表空间)
- 1、索引分类
- 1、hash、AVL、B、B+对比
- 1、索引设计原则
- 1、分析查询语句:EXPLAIN
- 1、索引失效场景
- 1、关联查询优化
- 1、count(*)和select( *)
- 1、主键设计
- 1、三大范式
- 1、水平切分、垂直切分
- 1、事务四大特性
- 1、事务隔离级别
- 1、事务日志(redo/undo log)
- 1、锁
- 1、表锁
- 1、行锁
- 1、MVCC-快照读&当前读
- 1、MVCC-Undo Log版本链
- 1、MVCC-ReadView
- 1、定位慢查询
- 1、超大分页处理
- 1、sql优化
- 1、主从复制原理
- 1、分库分表
- 六、Spring
- 七、Spring MVC
- 八、Mybatis
- 九、Redis
- 十、springboot
- 十一、SpringCloud
- 十二、排序算法
- 十三、集合原理
- 十四、设计模式
- 十五、RabbitMQ
- 十六、Kafka
- 十七、技术场景
- 十五、RabbitMQ
- 十六、Kafka
- 十七、技术场景
一、JVM
1、Java虚拟机
#1)是一台虚拟的计算机【软件】,用来执行一系列虚拟【计算机指令】,分为【系统虚拟机】和【程序虚拟机】
①系统虚拟机:VMware【Linux虚拟机】,对物理计算机的仿真,
②程序虚拟机:Java虚拟机【JVM】,专门用来执行单个计算机程序而设计
#作用
1、JVM是二进制字节码的运行环境。
负责加载到字节码内部,解释、编译为对应平台的机器指令执行。
#特点
1、一次编译,到处运行
2、自动内存管理
3、自动垃圾回收机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9OaihiM7-1689324564341)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220716105459.png)]
1、常见的JVM
#1)三大常见虚拟器
1、HotSpot VM 【默认Java虚拟机】
2、Jrockit VM 【专注与服务器端--快】
3、J9【IBM】
#2)其他虚拟机
Sun Classic VM【第一款java虚拟机】
Taobao JVM【阿里基于openJDK开发定制的AlibabaJDK】
1、JVM执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmRKWv0F-1689324564342)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220716115347.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYg8E7tj-1689324564342)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220716172425.png)]
1、类加载过程
#1)加载
将.class字节码所代表的【静态存储结构】转化为【方法区】的【运行时数据结构】
内存中生成了一个java.lang.Class对象,作为方法区这个类的各种数据的访问入口
#2)链接
① 验证
确保Class文件的字节流中包含【符合】当前虚拟机的要求,确保正确性,不会危机自身安全
例如:文件格式校验、元数据校验、字节码校验、符号引用验证【以CA FE BA BE开头】
② 准备
为【类变量】分配内存,设置其【默认初始值】
不包含final修饰的static变量【编译时候就会分配,准备阶段会显示初始化】
不会为实例变量分配初始化【实例变量会随着对象一起分配到堆中】
③ 解析
将常量池中的【符号引用】改用【直接引用】的过程
#3)初始化
# 执行类构造器方法的过程-----类构造器方法【<clinit>()】
1、此方法不需要被定义,是javac编译器【自动收集】类中的所有
【类变量赋值动作】和【静态代码块中的语句】合并而来
2、构造器方法中指令按语句在源文件的【顺序执行】
#注意:<init>()不同于类的构造器-----类的构造器函数【<init>()】
1、JVM会保证子类的<clinit>()执行前,会先执行父类的<clinit>()
2、必须保证一个类<clinit>()在多线程下被同步加锁---只加载一次,放在方法区
1、类加载器分类
#1)启动类加载器【引导类加载器】
1、使用C/C++实现的,嵌套到JVM内部
2、用来加载核心类库
JAVA_HOME/jre/lib/rt.jar、resource.jar
sun.boot.class.path路径下内容
3、不是继承java.lang.ClassLoader,没有父类加载器
4、加载扩展类和应用类加载器,并为他们指定父类加载器
5、处于安全考虑,bootstrap只会加载包名为.java、.javax、sun开头的类
#2)扩展类加载器【Extension ClassLoader】
1、Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
2、派生于ClassLoader类
3、父类加载器为启动类加载器
4、从java.ext.dirs系统属性所指定的目录加载类库
或从JDk安装目录下的jre/lib/ext子目录下加载类库
【用户自定义在该目录下创建jar,也会被加载】
#3)应用程序类加载器【系统类加载器 AppClassLoader】
1、Java语言编写,由sun.misc.Launcher$AppClassLoader实现
2、派生于ClassLoader类
3、父类加载器为扩展类加载器
4、从java.class.path指定路径加载类库
或从环境变量classpath路径
5、该类加载的是程序中默认的类加载器【我们自己定义的类】
6、通过ClassLoader#getSystemClassLoader()方法获取该类的加载器
#4)自定义类加载器
1、双亲委派机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oscp7TMQ-1689324564342)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220717123225.png)]
#1)定义
JVM对class文件采用【按需加载】的方法,也就是说需要使用该类时,才会将他的.class文件加载到内存中生成Class对象
在加载某个类的class文件时,采用【双亲委派机制】----把请求交由父类处理、是一种任务委派模式
#2)原理
1、如果一个类加载器收到类加载请求,他并不会自己去加载,而是【委托父类】加载器去执行
2、如果还有父类加载器,则上一步委托,【依次递归】,最终到达【启动类加载器】
3、如果【可以完成】类的加载,则【结果返回】
如果【无法完成】,【子加载器】才会尝试自己加载---双亲委派模式
#沙箱安全机制
保证对Java核心源代码的保护,就是沙箱安全机制
类加载时候,先使用引导类加载器进行加载,先加载JDK自带的文件【rt.jar或java.lang.String.class】
1、运行时数据区
1.1 PC程序计数器
#1)作用
PC寄存器是用来存储指向【下一个指令的地址】,由执行引擎读取下一条指令
#2)特点
1、占内存空间很小,几乎可以忽略不记【运行速度最快的存储区域】
2、JVM规范中,每个线程都有它的程序计数器,是【线程私有】的
生命周期与线程的生命周期一致
3、任何时间一个线程只有一个方法执行,所谓【当前方法】
存储Java方法的JVM指令地址
或,在执行native方法时,时未指定值【undefined】
4、分支。循环、跳转、异常处理、线程恢复等功能都需要依赖计数器完成【程序控制流的指示器】
5、字节码解释器就是通过改变计数器的值来选取下一条需要执行的字节码指令
6、他是JVM【唯一一个】没有规定OutOtMemoryError情况的区域
1.1 虚拟机栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bZ6yJQAd-1689324564343)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220719224958.png)]
#1)定义
Java的指令都是根据【栈】来设计的
每个线程创建时都会创建一个虚拟机栈,其内部保存一个个栈帧【一个方法对应一个栈帧】,对应一次次方法调用
是线程私有的
#2)局部变量(结构1)
0.非静态方法,存放this指向当前对象
1.方法入参
2.局部变量【基本数据类型、对象的引用】
3.return Address地址
#3)操作数栈(结构2)
1、用于【保存】计算过程中的【中间结果】,同时可以作为其【临时】的存储空间
2、方法开始执行,栈帧随之被创建,其会创建一个【空的操作数栈】
#4)动态链接(结构3)
1、每个【栈帧】内部都包含一个【运行时常量池】中栈帧所属【方法的引用】
为了支持当前方法的代码能够实现【动态链接】,例如:invokedynamic指令
2、所有的【变量】和【方法的引用】都作为符号引用,保存在class文件的【常量池】中
一个方法调用另一个方法,通过常量池中指向方法的符号引用来表示
#【动态链接】的作用就是为了将这些引用,【转换】为调用方法的【直接引用】
#5)方法返回地址(结构4)
存放【调用】该方法的pc寄存器的值-----【方法调用的地址】
# 对于上面4补充
#1)概念1
#静态链接、早期绑定
在编译器确定,运行期保持不变
#动态链接、晚期绑定
在编译器无法确定
#2)非虚方法----早期绑定
【静态方法、私有方法、final方法、实例构造器、父类方法】都是非虚方法
其他都是【虚方法】
#3)方法重写的本质
1、找到操作数【栈顶】第一个元素的对象的【实际类型】,记作C
2、【常量】中找【名称符合】的方法,
进行【权限访问】:通过---返回方法的直接引用
不通过---java.lang.IllegaAccessRrror异常
3、【常量中没找】按继承关系,依次从下往上查找
没有---java.lang.AbstractMethodRrror异常
1.1 本地方法栈
# 定义
调用本地C/C++方法,使用native关键字
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
1.1 堆区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3wZZhwZ-1689324564343)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220725221706.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5HNEnjn-1689324564344)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220725222346.png)]
# 定义
#1)一个JVM实例 ---> 一个堆空间
#2)JVM启动时,被创建,即空间大小也被确定【可以调节】
#3)逻辑连续,物理不连续
#4)线程共享【除了线程私有的缓冲区】
#5)所有的实例变量和数据【几乎都是】在堆中分配内存的
#6)方法执行结束,对象不会立刻被清除,在垃圾收集的时候才会被移除
# Java7 和 Java8 区别
#1)JDK7
新生代+养老区+【永久区】
#2)JDK8
新生代+养老区+【元空间】
# Minor、Major、Full GC
部分收集(Partial GC):
新生代收集(Minor GC / Young GC):Eden区满时触发
老年代收集(Major GC / Old GC):Major GC速度比Minor GC慢10倍以上
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
整堆收集(Full GC):
收集整个Java堆和方法区的垃圾收集
#逃逸分析
随着JIT编译器的发展与逃逸分析技术逐渐成熟,【栈上分配、标量替换】优化技术将会导致一些微妙的变化,所以的对象都分配到堆上也渐渐变得不那么“绝对了”
如果经过逃逸分析后发 现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vMSev1er-1689324564344)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220726213025.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qd462rvc-1689324564344)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220726221817.png)]
1.1 方法区
#1)类型信息【类、接口、枚举、注解】
1、全类名【包名.类名】
2、直接父类的全类名【接口,Object没有父类】
3、类型修饰符【public、abstart、final子集】
4、【实现直接接口】的有序列表
#2)域(属性)信息
1、按声明顺序,保存在方法区
2、名称、类型、修饰符
#3)方法信息
1、方法名称
2、返回类型
3、修饰符
4、方法字节码、操作数栈、局部变量大小及大小
5、异常表(abstart和native除外)
异常处理开始位置、结束位置、代码处理程序计数器的偏移地址、被捕获异常的常量池索引
#4)non-final类变量
1、随着类的加载而加载
2、类的所有实例共享【null.hello()】
#5)全局常量(static final)
1、在编译的时候被分配
方法区演变
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vfqdLFjL-1689324564345)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220730111534.png)]
#1)永久代为什么被元空间替换?
1、永久代设置空间的大小难以确定【容易出现OOM】
2、对永久代新能调优很困难
#2)StringTable调整?
因为永久代回收概率很低,在full gc的时候才会触发【在老年代、永久代空间不足才会被触发】
放入堆中,可以即时回收
#3)位置
1、静态static------方法区【静态空间】
2、成员变量-------堆中
3、局部变量---------栈帧的局部变量表中
#注意:只要是【对象】都会在【堆中分配空间】
1、对象实例化、内存布局、访问定位
1、解释器&即时JIT编译器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kRVcofXo-1689324564345)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220730220550.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cUoWND8l-1689324564345)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220730220758.png)]
#1)解释器
对【字节码】文件进行【逐行解释】的方式执行,将其翻译为对应平台的的本地【机器指令】执行
#2)JIT编译器
将虚拟源码直接编译成本地机器平台的机器语言【热点数据】
1、StringTable
#字符串拼接
#1)常量与常量的拼接结果在常量池中【原理:编译器优化.class】
#2)常量池中不会由相同内容的常量
#3)只要其中有一个变量,结果就在堆中【原理:StringBuilder】---final 属于常量
#4)拼接结果调用intern()方法
如果常量池中有,返回地址引用
如果常量池中没有,将新的字符串对象放入常量池中,返回对象地址的引用
#intern()方法
jdk6:
【已有】返回常量池中地址的引用
【没有】复制一份,放入串池,返回常量池中的【对象地址】
jdk7之后:
【已有】返回常量池中地址的引用
【没有】复制一份,放入串池,返回常量池中的【引用地址】
1、触发垃圾回收方式
#1)Eden区或S区不够
#2)老年代不够
#3)方法区不够
#4)System.gc()
1、垃圾标记算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9S6wf6iO-1689324564346)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220801204058.png)]
#1、引用计数算法
对应一个对象A
【引用】了A,引用计数器就加1;【失效】时,引用计数器就减1
当引用计数器【为0】时,可以进行垃圾回收
#问题:
无法解决【循环引用】问题,致命缺陷-----没有采用此算法
#2、可达性分析算法
#1)解决问题
解决在引用计数算法中循环引用的问题,防止内存泄露【也叫跟踪性垃圾收集】
#2)思路
1、以根对象集合(GC Roots)为起始点,按照自上向下搜索根对象集合所连接的目录对象【是否可达】
2、内存中的存活对象,都会被根对象集合【直接或间接】连接着,搜索所走过的路径称为【引用链】
3、如果没有任何引用链相连,则不可达,意味着对象已死亡,可以标记为垃圾对象
4、只有被根对象集合【直接或间接连接】的对象才是存活对象
#3)可以作为GC Roots的几类元素
1、虚拟机栈引用的对象【每个线程中调用方法的参数、局部变量】
2、本地方法栈JIT引用的对象
3、方法区中静态属性引用的对象
4、方法区常量引用的对象【字符串常量池StringTable里的引用】
......
1、Finalization机制
#1)定义
1、finalization机制允许开发人员提供对象被【销毁前自定义处理逻辑】
2、垃圾回收此对象时,会先调用这个对象的finalize()方法
3、finalize()方法允许子类对象【重写】,用于对象被回收时,进行资源回收【资源释放、关闭文件、套接字、数据库连接信息】
#2)对象的三种状态
1、可触及的【根对象集合,可以到达的对象】
2、可复活的【对象的引用被释放,但在finalize()方法中被复活】
3、不可触及的【finalize()被调用,并且没有被复活,进入不可触及的状态】 --- finalize()只会被调用一次
#3)具体过程【经历两次标记】
1、对象到GC Roots没有引用链,则进行【第一次标记】
2、判断是否进行finalize()方法
1)对象没有重写finalize()方法,或者已经被调用【判定为不可触及的】
2)重写方法,且没有被调用,会被插入F-Queue队列中,由虚拟机自动创建、低优先级Finalizer线程触发finalize()方法执行
3)finalize()是对象【逃脱死亡的最后机会】,如果该方法的引用链建立了链接,会被移出’即将回收‘集合
1、垃圾清除算法
1.1 标记-清除算法(Mark-sweep)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B1VJw0ti-1689324564346)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220801222432.png)]
#1)执行过程
有效内存空间被耗尽时,就会停止整个线程(stop the world),在进行【标记】,【清除】操作
标记:从引用根节点开始遍历,标记所有被引用的对象【可达对象】
清除:堆内存进行线性遍历,【没有标记的可达对象】,将会被回收
#2)缺点
1、效率不算高
2、GC时,需要停止整个用户线程,用户体验差
3、清理处理的内存空间不是连续的,产生内存碎片,需维护一个【空间列表】
#3)何为清除?
清除不是真正的置空,把需要清除的对象【地址保存】在空闲的地址列表中,
新对象加载时,【替换】原来需要清除的对象地址
1.2 复制算法(Copying)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Li09YNEC-1689324564346)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220801223043.png)]
#1)核心思想
将内存分为两份,每次只用一份,垃圾回收时,存活对象【复制】到另一份,【清除原有】内存所有对象,交换完成垃圾回收
#2)优点
实现简单,运行高效
复制保证空间的连续性,不会出现’碎片‘问题
#3)缺点
需要两倍的内存空间
需要维护对象中引用关系
适合垃圾对象很多,存活数量不会太大的情况【Young区的Survivor0区和Survivor1区】
1.3 标记-整理(压缩)算法(mark-compact)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJ4dgeRS-1689324564347)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220801224903.png)]
#1)背景
基于老年代回收的特性【大部分是存活的对象】
#2)优点
消除标记-清除算法,内存区域分散的特性,重新分配对象时,只需持有一个内存的起始地址即可
消除复制算法, 内存减半的高额代价
#3)缺点
效率低
移动对象的同时,如果对象被其他对象引用,需调整引用的地址
移动过程中,暂时用户程序【STW】
1、引用(强软弱虚)
1.1 定义(依次减弱)
#1)【强引用】new Object()---只要引用关系还在,就会不被回收
#2)【软引用】内存不足,回收
#3)【弱引用】无论内存是否足够,都会被回收
#4)【虚引用】用于对象追踪,收集器回收会收到一个系统通知
1.2 强引用(Strong Reference)
#1)一般99%都是强引用,也是默认的引用类型
#2)对象是可触及的,不能被回收
#3)造成内存泄露的主要原因
#4)例子
StringBuffer s = new StringBuffer()
1.3 软引用(Soft Reference)
#1)内存充足,不会被回收
内存不足,会被回收【在系统发生OOM异常之前执行】
#2)高速缓存会用到软引用【mybatis的内部类】
#3)例子
Object o = new Object();
SoftReference<Object> sf = new SoftReference<>(o);
o = null;
1.4 弱引用(Weak Reference)
#1)发现,即被回收
#2)弱引用可能存在较长时间【不能很快发现】
#3)用来缓存可有可无的数据
#4)例子【WeakHashMap】
WeakReference<Object> sf = new WeakReference<Object>(new object());
1.5 虚引用(Phantom Reference)
#1)不会决定对象的生命周期,随时可能被回收
#2)不能单独使用,使用get()方法,总是null
#3)跟踪垃圾回收过程,会收到一个系统通知
#4)必须和引用队列一起使用,通过引用队列发送通知
#5)可以跟踪对象回收时间
#6)例子
ReferenceQueue phantomQueue = new ReferenceQueue(); #引用队列
PhantomReference<Object> sf = new PhantomReference<Object>(new object(),phantomQueue);
1.6 终结器引用(Final Reference)【了解】
#1)实现对象的finalize()方法
#2)无需手动编码,配合引用队列使用
#3)GC时,终结器队列入队。由Finalizer线程通过终结器引用找到被引用对象,调用其finalize()方法,第2次GC被回收
1、常见的垃圾回收器
#1)新生代和老年代垃圾回收器搭配
①Serial --- Serial Old
|--- CMS
②ParNew --- Serial Old
|--- CMS
③Pararllel --- Serial Old
|---Parallel Old
④G1
1、Serial 串行垃圾回收器
#1)定义
只会使用【一个CPU】或者一条收集线程去完成垃圾收集工作,
且必须【停止】所有的【用户线程(STW)】,直到收集结束
#2)特点
简单而高效,没有多线程线程切换带来的开销,对于单线程收集效率最高
可以用 —XX:+UseSerialGC 指定年轻代和老年代使用串行收集器
#3)总结
现在使用较少,以前用于单核CPU,现在都不是单核了
对于交互较强的应用,是不能接收的
1、ParNew 并行垃圾回收器
#1)定义(处理新生代)
采用【并行回收】的方式执行内存回收
与Serial串行收集器一样,采用【复制算法】、【Stop—The-World】机制
#2)特点
在多核CPU的环境下,可以充分利用CPU,提升系统吞吐量
但是,在单核CPU,不必Serial收集器高效--频繁切换线程,产生额外开销
可以用—XX:UserParNewGC 来指定收集器
—XX:ParallelGCThreads 来限制线程数量,默认开启和CPU数据相同的线程数
#3)总结
对于新生代,回收次数频繁,使用并行方式高效
对于老年代,回收次数少,使用串行方式节约资源(线程切换,带来额外开销)
1、Parallel 垃圾回收器(吞吐量优先)
#1)定义
同样采用【复制算法、并行回收、STW机制】
与ParaNew收集器不同,其目标是达到一个可控制的吞吐量,也被称为【吞吐量优先】的垃圾回收器
#2)特点
更适合后台运算,而不需要太多交互的任务。类似批量处理、订单支付、科学计算等等
JDK1.6老年代使用Parallel Old收集器,代替原有的Serial Old收集器
老年代次采用【标记-压缩算法】,也是基于【并行回收、STW机制】
可以用—XX:UserParallelGC 和 —XX:UserParallelOldGC 来指定收集器
—XX:ParallelGCThreads 来限制线程数量,默认开启和CPU数据相同的线程数
—XX:MaxGCParallelMillis 指定暂停时间
-XX:GCTimeRatio 衡量吞吐量(取值0-100)
#3)总结
适合吞吐量优先的场景中,Parallel和Parallel Old组合,在Server模式下性能不错
Java8中,默认使用该垃圾收集器
1、CMS垃圾回收器(低延迟)
#1)定义(Concurrent-Mark—Sweep)
JDK1.5推出认为是划时代意义的垃圾回收器CMS
真正意义上的【并发】收集器,第一次实现了垃圾收集线程和用户线程【同时进行】工作
#2)特点
尽可能缩短垃圾收集用户线程的停顿时间(低延迟),适合于用户交互的程序,良好的响应速度能提升用户体验
使用【标记-清除算法】,也会有STW
#问题:
无法于Parallel 配合使用,在JDK1.5中使用Serial和ParNew配合使用
#3)工作原理
初始标记:仅仅标记GC Roots能【直接关联】到的对象,所以【速度快】(STW)
并发标记:从GC Roots的直接关联对象开始【遍历所有】可达的对象,耗时长(不需STW)
重新标记:因上一阶段【并发导致】标记产生变动的一部分对象进行【修正标记】
并发清除:判断已经死亡的对象,释放内存空间
#4)总结
尽管使用并发回收,但是【初始标记和再次标记】任然需要执行STW机制,但是暂停时间不会太长,可以说明目前所有的垃圾收集器都完全不能做到不需要STW,只是【尽可能缩短】暂停时间
另外,CMS回收过程中,还要确保用户线程有给足够的内存,
当堆内存达到一定阈值,便开始回收
要是预留空间无法满足用户线程的执行,就会启动后备方案,临时使用Serial Old重新对老年代垃圾收集
#问题:为啥不使用标记-压缩算法?
因为当并发清除的时候,用户线程会并发执行,无法整理压缩
1、G1垃圾回收器(区域化分代式)
1)新生代回收
2)年轻代垃圾回收+并发标记
3)Mixed Collection (混合垃圾回收)
#1)定义(并行)--JDK9使用的默认垃圾收集器
延迟可控的情况下,尽可能提高吞吐量
把堆内存分为很多不相关的区域Region(物理上不连续)
可以避免全区域的垃圾收集,G1跟踪各个Region的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),后台维护一个优先列表,每次【根据允许的收集时间】, 【优先回收价值最大】的Region
#2)特点
Region之间是【复制算法】,整体上可以看成是【标记-压缩算法】
#设置H区的原因:
堆中的大对象,默认会直接分配到老年代,如果存在短期的大对象,会对垃圾收集器造成负面影响。G1划分了一个Humongous区,专门存大对象,【如果一个H区存不下,找连续的H区来存储】
#3)回收过程
年轻代GC:当Eden区用尽,开始年轻代的回收;暂停所有的用户线程,从【Eden区】移动到【Survivor区或老年区】
老年代并发标记:堆内存使用达到了一定值(默认45%),开始老年带的并发标记过程
混合回收:老年代存活对象到空闲区间,一次回收一部分的Region区
Full GC:上面无法正常工作,就会触发Full GC(G1初衷就是为了避免其出现)
1、垃圾回收器总结
1、JVM 调优的参数
#1)设置堆空间大小
-Xms:设置堆的初始化大小
-Xmx:设置堆的最大大小
#2)虚拟机栈的设置
Xss 对每个线程stack大小的调整,-Xss128k
每个线程默认会开启1M的内存
#3)年轻代中Eden区和两个Survivor区的大小比例
-XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden = 2:8
#4)年轻代晋升老年代阈值
-XX:MaxTenuringThreshold=threshold
默认为15
取值范围0-15
#5)设置垃圾回收收集器
-XX:+UseParallelGC
-XX:+UseParallelOldGC
1、内存溢出问题
#1)误用线程池
例如:固定线程数线程池Executors.newFixedThreadPool(2)
用的阻塞队列LinkedBlockingQueue,没有上限,大量线程堆积导致内存溢出
#2)查询数据量过大
#3)动态生成类
1、内存溢出排查
1)出现的主要原因
#1.对象创建过多:
当程序中创建了大量的对象,但是没有及时地释放它们,导致内存空间不足。
#2.内存泄漏:
内存泄漏是指已经分配的内存空间,由于某些原因得不到释放,导致内存使用率逐渐增加。
1、static字段引起的内存泄露
2、未关闭的资源导致内存泄露
3、使用ThreadLocal造成内存泄露
#3.数据处理量过大:
如果程序需要处理大量的数据,比如大文件或者大图片,将导致内存空间迅速占满。
#4.堆栈溢出:
调用层次过深,方法调用栈太大,超出 JVM 栈的最大深度限制,导致堆栈溢出。
#5.使用了过多的第三方库或工具:
如果程序中引入了过多的第三方库或工具,可能会产生一定的内存开销,导致内存不足。
2)排查思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xpmm9Gvo-1689324564350)(http://43.143.239.200:9000/monkey/bookImages/image-20230607150845609.png)]
#1)获取堆内存快照dump
1、运行dump快照
jmap -dump:format=b,file=heap.hprof 进程id
2、启动添加参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/app/dumps/
#2)VisualVM去分析dump文件
#3)通过查看堆信息的情况,定位内存溢出问题
1、CPU飙高排查方案与思路
1)出现的主要原因
#1)频繁的GC: 如果访问量很高,内存分配太快,可能会导致频繁的GC甚至FGC,从而导致CPU飙升
#2)线程上下文切换:大量线程的状态在 Blocked(锁定等待,IO等待等)和 Running 之间发生变化。当锁争用激烈时,这种情况很容易发生。
#3)有些线程正在执行非阻塞操作,如死循环
#4)死锁现象
#5)正则表达式
2)排查思路
#1)使用top命令查看占用cpu的情况
使用‘shift+m’快捷键,按cpu使用率排列,找到对应的进程id
#2)查看进程中的线程信息
ps H -eo pid,tid,%cpu | grep 进程id
#3)jstack命令
jstack 进程号 > 指定文件
#4)将第三步查询的线程号,转16进制
找到文件对应nid=对应的线程
1、
二、多线程和并发编程
1、基本概念
#1)进程:
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存
程序是静止的,进程实体的运行过程就是进程,是系统进行【资源分配的基本单位】
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
#2)线程:
线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统【独立调度的基本单位】,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源
#关系:
一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程
#作用:
使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能
#3)并行:
在同一时刻,有多个线程在多个 CPU 上同时执行
#4)并发:
在同一时刻,有多个线程在单个 CPU 上交替执行
#5)同步:
需要等待结果返回,才能继续运行就是同步
#6)异步:
不需要等待结果返回,就能继续运行就是异步
1、线程创建的几种方式
1)继承Thread类
#1)继承Thread类,重写run方法,start方法运行线程
#2)优缺点
优点:编码简单
缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性
#3)代码示例
public static void main(String[] args) {
Thread thread = new Thread("t1") {
@Override
public void run() {
System.out.println("执行线程逻辑....");
}
};
thread.start();
}
#4)本质【Thread 类本身也是实现了 Runnable 接口,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用 Runnable#run】
public class Thread implements Runnable {
private Runnable target;
public void run() {
if (target != null) {
// 底层调用的是 Runnable 的 run 方法
target.run();
}
}
}
2)实现Runnable接口
#1)实现Runnable接口,重写run方法,创建Thread对象,start方法运行线程
#2)优缺点
缺点:代码复杂一点。
优点:
线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
同一个线程任务对象可以被包装成多个线程对象
适合多个多个线程去共享同一个资源
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
线程池可以放入实现 Runnable 或 Callable 线程任务对象
#3)代码
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程逻辑....");
}
}, "t1");
thread.start();
}
3)实现 Callable 接口
#1)过程
定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
创建一个 Callable 的线程任务对象
把 Callable 的线程任务对象包装成一个未来任务对象
把未来任务对象包装成线程对象
调用线程的 start() 方法启动线程
#2)优缺点
优点:同 Runnable,并且能得到线程执行的结果
缺点:编码复杂
#3)代码示例
public static void main(String[] args) {
FutureTask<String> task = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "执行结果";
}
});
Thread thread = new Thread(task);
thread.start();
try {
//获取结果
String res = task.get();
} catch (Exception e) {
e.printStackTrace();
}
}
4)线程池(后续说明)
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
1、常见的API
方法 | 说明 |
---|---|
void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法【执行】 |
void run() | 线程启动后调用该方法【执行调用run方法】 |
void setName(String name) | 给当前线程取名字【设置名字】 |
void getName() | 获取当前线程的名字 线程存在默认名称:子线程是 Thread-索引,主线程是 main【获取名字】 |
static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行【得到当前线程】 |
static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争【休眠指定时间】 |
static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用【让出执行权,可能再次抢到】 |
final int getPriority() | 返回此线程的优先级【获取优先级】 |
final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10【设置优先级-操作系统控制】 |
void interrupt() | 中断这个线程,异常处理机制【中断线程】 |
static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
final void join() | 等待这个线程结束【等待这个执行结束,在执行当前线程】 |
final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
final native boolean isAlive() | 线程是否存活(还没有运行完毕)【是否存活】 |
final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程【设置守护线程】 |
#1)run方法 && start方法
run:包含这个线程要执行的业务逻辑【直接调用run方法,执行的是主线程中的run方法】
start:启动新线程,处于【就绪】状态,通过这个新线程去执行对应的run方法中的代码
#说明:
run方法中不能抛出异常,只能try/catch【父类没有抛出异常,子类不能多于父类抛出的异常】
#2)sleep方法 && yield方法
sleep:
由【Running】状态变成【Timed Waiting】状态----阻塞
线程不会释放对象锁
其他线程使用interrupt方法打断正在睡眠的线程,会抛出异常InterruptedException
睡眠结束后,未必会得到立即执行,需要重新抢占CPU
yield:
让出当前线程的CPU使用权,重新抢占CPU
具体实现依赖于操作系统的任务调度器
锁资源不会释放
#3)join方法
等待当前线程执行完成之后,继续向下执行
#4)interrupt
public void interrupt():打断这个线程,异常处理机制
public static boolean interrupted():判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted():判断当前线程是否被打断,不清除打断标记
#打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
#注意
sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
打断正常运行的线程:不会清空打断状态(true)
#5)不建议
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
stop()、suspend()、resume()
1、停止线程(打断机制)
1、线程状态(生命周期)
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) |
Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 |
Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |
1、Synchronized简介
#1)可重入、不公平的重量级锁
#2)原则
锁对象建议【共享资源】
成员方法中的使用【this】作为锁对象
静态方法中的使用类对象【类名.class】作为锁对象
#3)同步代码块
synchronized(锁对象){
// 访问共享资源的核心代码
}
#4)同步方法
//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
1、Monitor(监视器或管程)
#1)定义
每个Java对象都可以关联一个Monitor对象(也属于class,实例存储在堆中),
当使用Synchronized给对象上锁(重量级锁),该对象头的Mark Word中就会被设置指向Monitor对象的指针
【这就是重量级锁】
#2)对象内存布局
指针头:
Mark Word:Hash值,分代年龄,锁信息
Klass:指向类元信息
实例数据:真正的数据
对齐方式:为了提高运行效率(64位虚拟机,8字节)
#3)工作流程【见下面工作流程图】
1、初始Monitor中的Owner位null
2、当Thread-2执行synchronized(obj)就会把Monitor的所有者Owner置为Tread-2(Monitor中只能有一个Owner)【obj对象中的Mark Word指向Monitor,把原有的MarkWord存入线程栈中的锁记录中--轻量级锁】
3、在Thread-2上锁的过程中,其他线程来执行synchronized,就会进入EntryList(双向链表-阻塞队列)
4、Thread-2执行同步代码块的内容,会将Owner置为空,把之前保存的信息设回MarkWord
5、唤醒EntryList中的线程来竞争锁【非公平锁】,放入Owner中
6、以前获取过锁,使用【wait-notify机制】,使线程进入Waiting状态【WaitSet】
#注意:
synchronized必须进入同一个对象的Monitor才有效
不加synchronized的对象不会关联监视器,不遵守以上规则
try-catch机制,一定会释放锁对象
32位虚拟机 Mark Word
64位虚拟机 Mark Word
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l017LR8e-1689324564353)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220810234240.png)]
对象的内存布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHgtsSD5-1689324564353)(http://43.143.239.200:9000/monkey/bookImages/%E6%88%AA%E5%9B%BE20220810233005.png)]
工作流程
1、Synchronized锁升级
1)偏向锁
#1)定义
当线程第一次获取锁对象,进偏向状态【标记101】,同时将使用的ThreadID记录到Mark Word中
【当前线程再次获取锁对象】:不需要竞争
【当另一个线程获取锁对象】:锁升级为轻量级锁
#注意:
默认开启偏向锁,Mark Word后三位为101
JDK8中,偏向锁是延迟生效的【默认4s】,刚启动时,很多线程竞争锁,效率反而会降低
一旦调用hashcode方法,就无法进入偏向状态【MarkWord存不下31的hashcode】
#2)偏向锁撤销
调用hashcode:导致偏向锁被撤销(存的时ThreadID,存不下hashcode)
其他线程使用偏向锁对象【发生竞争】,会升级为轻量锁
调用wait/notify,需要申请Monitor,进入WaitSet,重量级锁
#3)批量撤销
批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
2)轻量级锁
#1)定义(JDK6引入)
一个对象有多个线程要加锁,但是加锁时间是错开的【没有竞争】
在尝试加轻量级锁的过程中,CAS操作无法成功,会变成重量级锁
#可重入锁:
线程可以进入任何一个他已经拥有的锁所同步的代码块【可以避免死锁】
#2)执行过程
创建锁记录(lock record)对象,每个线程的栈帧都会包含一个锁记录的结构,指向对象
尝试使用CAS来交换LockRecord和MarkWord
【成功】:对象头存储00(轻量级锁),给该对象加锁
【失败】:
已经持有该对象的轻量级锁,有竞争,进入锁膨胀过程
锁重入,添加一条Lock Record作为重入的计数
#3)解锁时
锁记录为null:表示有重入,重入计数减一
锁记录不为null:CAS将Mark Word的值恢复给对象头
【成功】解锁成功
【失败】说明已经进入重量级锁,使用其解锁过程
3)重量级锁
#1)定义
见上面Monitor使用
#2)过程
线程0对该对象加了轻量级锁,线程1来获取锁资源,会升级会重量级锁
申请Monitor锁,将Owner指向线程0的lock Record,线程1进入EntryList阻塞对象
#3)解锁
CAS将Mark Word中的值恢复失败,进入重量级锁解锁流程
找到Monitor对象,将Owner设为null,唤醒EntryList的阻塞线程
4)锁优化
#1)自旋锁
#定义
重量级锁竞争时,不会立即阻塞,先使用自旋(默认10次)来进行优化,循环方式去获取锁
#注意:
自旋占有CPU资源,多核CPU才有优势
自旋失败进入阻塞状态
#优缺点
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:自旋线程越来越多,会不断消耗CPU资源
#2)锁消除
代码中不可能存在竞争的共享数据的锁进行消除【JIT即时编译器的优化】
逃逸分析来判断
#3)锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
1、Wait & Sleep
#1)sleep()方法是Thread类的静态方法,用来控制自身流程的
wait()方法是Object类的方法,用于线程间的通信
#2)sleep()方法不会释放锁资源
wait()方法会释放锁资源
#3)sleep()方法可以在任何地方使用
wait()方法必须在同步方法或者同步代码块中使用
#4)sleep()方法等待指定时间后唤醒
wait()方法需要用notify()方法唤醒
1、join原理
#1)定义
等待当前线程执行结束,才接着往下执行
#2)源码
public final synchronized void join(long var1) throws InterruptedException {
long var3 = System.currentTimeMillis();
long var5 = 0L;
//时间小于0,直接抛异常
if (var1 < 0L) {
throw new IllegalArgumentException("timeout value is negative");
} else {
//等待时间0,会一直处于等待状态,直至线程死亡
if (var1 == 0L) {
while(this.isAlive()) {
this.wait(0L);
}
//指定超时时间,时间到了退出循环
} else {
while(this.isAlive()) {
long var7 = var1 - var5;
if (var7 <= 0L) {
break;
}
this.wait(var7);
var5 = System.currentTimeMillis() - var3;
}
}
}
}
1、生产者/消费者(模拟)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtEZfm4I-1689324564355)(http://43.143.239.200:9000/monkey/bookImages/image-20230419231758255.png)]
//1、消息队列
class MessageQueue{
public final LinkedList<Message> queue = new LinkedList<>();
private final int capacity;
MessageQueue(int capacity) {
this.capacity = capacity;
}
//消费消息
public Message get(){
synchronized (queue){
while (queue.isEmpty()){
log.info("获取消息为空,等待生产!!!");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();
log.info("消费消息:{}", message);
queue.notifyAll();
return message;
}
}
//生产消息
public void send(Message message){
synchronized (queue){
while (queue.size() == capacity){
try {
log.info("消息容量超出,等待消费!!!");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(message);
log.info("生产消息");
queue.notifyAll();
}
}
}
//2、消息体
class Message{
private int id;
private Object body;
public Message(int id, Object body) {
this.id = id;
this.body = body;
}
public int getId() {
return id;
}
public Object getBody() {
return body;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", body=" + body +
'}';
}
}
//测试
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(3);
for (int i = 0; i < 10; i++) {
final int id = i;
new Thread(() -> {
messageQueue.send(new Message(id,"消息数据" + id));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true){
Message message = messageQueue.get();
}
}, "消费者").start();
}
1、JMM内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CT0kyuYe-1689324564355)(http://43.143.239.200:9000/monkey/bookImages/image-20230606152433399.png)]
#1)定义
一种抽象的概念,定义了程序中各个变量(包含实例变量、静态变量、数组对象元素)的访问方式。
#2)作用
屏蔽各种硬件和操作系统的内存访问差异,让java程序在各个平台都能达到一致的内存访问效果
规定了线程和内存之间的一些关系
#3)主内存和工作内存
主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值
工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝【相当于主存的缓存】
1、三大特性
#1)可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程可以立即看到
#处理方式
volatile、final修饰、锁
#2)原子性
不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响
#处理方式
synchronized、CAS、Lock、ThreadLocal
#3)有序性
在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序
1、内存屏障(volatile)
#1)保证可见性:
写屏障:保证在该屏障【之前】的,对共享变量的改动,都同步到主存当中
读屏障:保证在该屏障【之后】的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据
#2)保证有序性:
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障【之后】
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障【之前】
#3)不能解决指令交错
1、缓存一致性
#1)定义
使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据
#2)原理
lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
#3)作用
确保对内存的读-改-写操作原子执行
阻止屏障两侧的指令重排序
强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效
1、happens-before
#1)定义【先行发生】
Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结
不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性
#2)例子
传递规则:A先行发生于B操作,B又先行发生于C操作,即A肯定先行发生于C操作
线程启动规则:Thread对象的start()方法,先行发生于此线程中的每一个操作
......
1、CAS
#1)定义(Compare-and-Swap)
是sun.misc.Unsafe类中的,完全依赖于硬件的功能,实现的原子操作
#2)原理(lock cmpxchg指令,保证单核和多核CPU下都能保证交换的原子性)
单核:会省略lock,单处理器自身会维护处理器的顺序一致性,不需要用lock的内存屏障效果
多核:需要加lock,执行带lock执行时,CPU会执行【总线锁或缓存锁定】,将修改的变量写到内存中,而且是原子操作
#3)优点
CAS 体现的是【无锁并发、无阻塞并发】,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
CAS 是基于乐观锁的思想
#4)缺点
1)执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,使用 CAS 线程数不要超过 CPU 的核心数,采用分段 CAS 和自动迁移机制
2)只能保证一个共享变量的原子操作
对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
3)引出来 ABA 问题
1、CAS & synchronized
#1)synchronized 是从悲观的角度出发:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差
#2)CAS 是从乐观的角度出发:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值,CAS 这种机制也称之为乐观锁,综合性能较好
1、Atomic
方法 | 作用 |
---|---|
public final int get() | 获取 AtomicInteger 的值 |
public final int getAndIncrement() | 以原子方式将当前值加 1,返回的是自增前的值 |
public final int incrementAndGet() | 以原子方式将当前值加 1,返回的是自增后的值 |
public final int getAndSet(int value) | 以原子方式设置为 newValue 的值,返回旧值 |
public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回 实例:AtomicInteger 里的 value |
#1)构造方法
public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer
#2)AtomicInteger 原理【自旋锁 + CAS算法】
旧值 == 内存中值 【可以修改】
旧值 != 内存中值 【不能修改,重新获取最新值,自旋动作】
#注意:
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果
#4)代码(unsafe类)
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
// var5: 用 var1 和 var2 找到的内存中的真实值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
#5)其他
原子引用:AtomicReference、AtomicStampedReference、AtomicMarkableReference(指定版本号,解决ABA问题)
原子数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
原子更新器:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
原子累加器:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator(分段执行,最后在合起来)
1、Unsafe类
**1)定义
Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问
Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接【调用操作系统底层资源】执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针
**2)代码
public static void main(String[] args) {
MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
if (atomicInteger.compareAndSwap(20)) {
System.out.println(atomicInteger.getValue());
}
}
class MyAtomicInteger {
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
private volatile int value;
static {
try {
//Unsafe unsafe = Unsafe.getUnsafe()这样会报错,需要反射获取
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
// 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值
VALUE_OFFSET = UNSAFE.objectFieldOffset(
MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public MyAtomicInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public boolean compareAndSwap(int update) {
while (true) {
int prev = this.value;
int next = update;
// 当前对象 内存偏移量 期望值 更新值
if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
System.out.println("CAS成功");
return true;
}
}
}
}
1、AQS
1)工作机制
2)如何保证原子性
3)公平锁还是非公平锁
1、ReentrantLock实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
1、ReentrantLock & Sysnchronized
#1)可中断
使用lock.lockInterruptibly()方法加锁,可以使用interrupt()方法打断,并释放锁
#2)可设置超时时间
使用trylock()方法,设置锁的超时时间
#3)可设置为公平锁
构造方法传true代表公平锁,排队等待其他锁释放在执行,并发低,一般不使用
#4)支持多个条件变量(相当于多个WaitSet)
static Lock lock = new ReentrantLock();
//条件变量(waitSet)
static Condition condition = lock.newCondition();
public static void main(String[] args) {
lock.lock();
try {
condition.wait();
...
condition.signal();
condition.signalAll();
}finally {
lock.unlock();
}
}
#相同点:
都是可重入锁
补充:
1)用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
2)获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
3)锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
4)响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
5)底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
1、死锁产生的条件
#1)原因
线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。
#2)如何排查诊断
jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息
#3)
1、线程池
#1)定义
一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作
#2)作用
1、降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2、提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死
3、提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
#3)常见阻塞队列
ArrayBlockQueue:由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列
PriorityBlockQueue:支持优先级排序的无界阻塞队列
DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止
LinkedTransferQueue:由链表结构组成的无界阻塞队列
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
#4)常见线程池
①public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
②创建一个拥有 n 个线程的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
③创建一个可扩容的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
④创建一个只有 1 个线程的单线程池【优点:异常不会停止所有,包装只能使用部分接口,向不能修改线程数】
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
#5)拒绝策略
AbortPolicy:让调用者抛出 RejectedExecutionException 异常,默认策略
CallerRunsPolicy:让调用者运行的调节机制,将某些任务回退到调用者,从而降低新任务的流量
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常
DiscardOldestPolicy:放弃队列中最早的任务,把当前任务加入队列中尝试再次提交当前任务
1、线程池状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ro0PWNEq-1689324564358)(http://43.143.239.200:9000/monkey/bookImages/image-20230710230135341.png)]
1、如何确定核心线程数
1、线程池使用场景
#1)批量导入:
使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM
#2)数据汇总:
调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
#3)异步线程(线程池):
为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间
1、悲观锁 & 乐观锁
#1)悲观锁【synchronized & lock】
1、只有线程占有了锁,才能去操作共享变量,,每次只有一个线程获取锁,获取失败都会停下来等待
2、线程从阻塞到唤醒、到阻塞,如果频繁的切换上下文,影响性能
3、优化:发现锁被占用,都会重试几次,减少阻塞
#2)乐观锁【AtomicInteger】--CAS
1、无需加锁,每次只有一个线程修改共享变量成功,其他线程不需要停止,不断重试直至成功
2、一直运行,不需要阻塞,因此不需要切换上下文
3、需要多核CPU支持,且线程数不因该超过CPU核数
1、HashTable & ConcurrentHashMap
#1)都是线程安全的map集合
#2)HashTable并发度低,整个hashTable对应一把锁,同一时刻,只有一个线程操作他(方法用synchronized修饰)
#3)【1.7】使用Segment+数组+链表的结构,,每个segment对应一把锁,
不同线程访问不同的segment,就不会发生冲突
#4)【1.8】将数组每个头节点作为锁,不同线程访问的头节点不同,就不会冲突
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0viHj2v-1689324564359)(http://43.143.239.200:9000/monkey/bookImages/image-20230606164305020.png)]
1、currentHashMap 1.8优化
#1)存储结构
1.7:数组+链表
1.8:数字+链表/红黑树
#2)读写操作
1.7:分段锁segement
1.8:CAS+synchronized锁头节点的方式
#3)扩容
1.7:扩容等待
1.8:会协助扩容
#4)计数器
1.7:
1.8:类似LonAdder,分区计数,最后合并
#注意:
弱一致性
1、初始化数组流程
sizeCtl是数组初始化和扩容的一个控制变量
#1)为-1时:
正在初始化
#2)小于-1(低16位表示):
一个线程在扩容-2
两个线程在扩容-3
....
#3)为0:
数据没有初始化
#4)大于0:
数组的下一次扩容阈值,或数组初始化的大小
1、ThreadLocal
#1)定义
线程隔离,每个线程持有各自的资源,避免引起线程安全问题
实现线程内的资源共享
#2)原理
每个线程内部都有一个ThreadLocalMap类型的成员变量,用来存储资源对象
【set方法】将ThreadLocal作为key,资源作为value,存入ThreadLocalMap中去
【get方法】根据ThreadLocal,查询对应的资源
【remove方法】根据ThreadLocal,删除对应的资源
#3)问题:ThreadLocal中的key为啥设计为弱引用?
线程可能会长时间运行(线程池中线程),如果key不在使用,触发GC回收key
但GC只会让key的内存释放,后续还会根据key是否为null进一步释放对应的value
1、调用get方法,发现key为null,让value为null,但key重新赋值
2、调用set方法,会清除临近的null对应的value
3、调用remove方法【推荐】,一般使用作为静态变量,属于强引用,因此GC无法删除
1、ThreadLocal内存泄漏问题
#1)弱引用
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本
#2)ThreadLocalMap 中的 key 是弱引用,值为强引用;
key 会被GC 释放内存,关联 value 的内存并不会释放。建议主动 remove 释放 key,value
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
三、Java基础
1、基础知识
1)switch-case支持数据类型(整形常量)
- 支持数据类型(整形常量)
1)只能是如下的六种数据类型之一:`byte`、`short`、`char`、`int`、`枚举类型`(JDK5.0)、`String类型`(JDK7.0)
2)不能是:long,float,double,boolean。
- case穿透
在switch语句中,如果case的后面不写break,将出现穿透现象,也就是不会在判断下一个case的值,直接向后运 行,直到遇到break,或者整体switch结束。
2)成员变量&局部变量&静态变量*
成员变量 | 局部变量 | 静态变量 | |
---|---|---|---|
定义 | 方法外,类中 | 方法内,形参,代码块中 | 方法外,类中 |
调用方式 | 对象调用 | 局部范围内(函数,语句内) | 对象,类型调用 |
生命周期 | 与对象共存亡 | 与方法共存亡 | 与类共存亡 |
存储位置 | 堆内存 | 栈内存 | 方法区 |
初始值 | 有默认初始化值 | 没有,先赋值后使用 | 有默认初始化值 |
别名 | 实例变量 | – | 类变量 |
修饰符 | 可以使用 | 不能使用 | – |
遵循的原则为:就近原则
首先在局部范围找,有就使用;接着在成员位置找。
3)类&对象*
类 | 对象 | |
---|---|---|
定义 | 一类事物的描述,是抽象的 | 是一类事物的实例,是具体的 |
区别 | 类是一个静态的概念,类本身不携带任何数据。当没有为类创建任何对象时,类本身不存在于内存空间中。 | 对象是一个动态的概念。每一个对象都存在着有别于其它对象的属于自己的独特的属性和行为。对象的属性可以随着它自己的行为而发生改变。 |
具体的:类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。
4)值传递*
值传递 | 引用传递 | |
---|---|---|
定义 | 在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 | 在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 |
区别 | 会创建副本。不能改变原始对象 | 不会创建副本。可以改变原始对象 |
注意:
1.Java都是值传递:
1)当基本数据类型时,是将变量值拷贝一份给方法的形参,不会改变原有变量的值。
2)当引用数据类型时,是将对象引用的地址值拷贝一份给形参,对对应地址值上的数据操作,原来的也会改变。
5)封装继承多态**
①封装
- 高内聚&低耦合
1.高内聚:类的内部数据操作细节自己完成,不允许外部干涉
2.低耦合:仅对外暴露少量的方法用于使用
- 介绍
将类的某些信息隐藏在类的内部,不允许外部程序直接访问,并通过该类提供的方法来实现对隐藏信息的操作和访问。(简单的说就是隐藏对象的信息,留出访问的接口)。
特点:
1.只能通过规定的方法访问数据;
2.隐藏类的实例细节,方便修改和实现。
- 四种权限修饰符
内部类 | 同一个包 | 不同包的子类 | 同一个工程 | |
---|---|---|---|---|
private | yes | |||
缺省 | yes | yes | ||
protect | yes | yes | yes | |
public | yes | yes | yes | yes |
②继承
- 继承性的好处
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
①# 减少了代码的冗余,提高了代码的复用性;
②# 便于功能的扩展;
③# 为之后多态性的使用,提供了前提。
- 体现
①一旦子类 A 继承父类以后,子类 A 中就获取了父类 B 中声明的结构:属性、方法
特别的,父类中声明为 private 的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构。
只有因为封装性的影响,使得子类不能直接调用父类的结构而已。
②子类继承父类以后,还可以声明自己特有的属性或方法,实现功能的拓展。
子类和父类的关系:不同于子集与集合的关系。
extends:延展、扩展
③规定
1.一个类可以被多个类继承
2.Java 中类的单继承性:一个类只能有一个父类
3.子父类是相对的概念。
4.子类直接继承的父类,称为:直接父类。间接继承的父类,称为,间接父类。
5.子类继承父类后,就获取了直接父类以及所有间接父类中声明的属性和方法。
6.如果我们没有显式的声明一个类的父类的话,则此类继承于 java.lang.Object 类
③多态
- 父类引用指向子类对象
Father father=new Son() | Son son=(Son)father | |
---|---|---|
相同成员变量 | 父类 | 子类 |
相同static成员变量 | 父类 | 子类 |
重写方法 | 子类 | 子类 |
static方法 | 父类 | 子类 |
只有父类有 | 父类 | 父类 |
- 解释
-
因为子类是对父类的一个改进和扩充,所以一般子类在功能上较父类更强大,属性较父类更独特, 定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。
-
所以,父类类型的引用可以调用父类中定义的所有属性和方法,而对于子类中定义而父类中没有的方法,它是无可奈何的;
-
同时,父类中的一个方法只有在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用;
-
对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。也可以叫做动态绑定。
-
6)重写&重载
重写(overriding) | 重载(overloading) | |
---|---|---|
多态体现 | 父类与子类之间多态性的表现 | 一个类中多态性的表现 |
定义 | 子类中定义某方法与其父类有相同的名称和参数 | 同名方法,不同参数个数或有不同的参数类型 |
- 重写规定
1)子类与父类重写的方法名,方法形参列表相同
2)子类重写方法的修饰权限不小于父类被重写的方法(向上转型时,调用方法矛盾)
子类不能重写父类修饰符为private的方法
3)返回值类型:
父类方法返回值类型void,则子类一定为void
父类方法返回值A类,则子类一定为A类或者A类的子类(例如,int和double不能被重写)
4)子类重写的方法抛出异常不大于父类被重写的方法抛出的异常类型
5)要非static修饰都一样,要么都是static修饰(此时不是方法的重写,不能被覆盖)
6)重写以后,当创建子类对象以后,通过子类对象去调用子父类中同名同参数方法时,执行的是子类重写父类的方法。即在程序执行时,子类的方法将覆盖父类的方法。
7)static修饰的方法不能被重写
7)static&final
static | final | |
---|---|---|
修饰内容 | 属性、方法、代码块、内部类 | 类、变量、方法 |
定义 | 创建多个对象,多个对象共享个静态变量或方法 | 最终的、不能被修改。被修改的类不能被继承 |
使用 | ① 静态变量随着类的加载而加载。可以通过"类.静态变量"的方式进行调用。 ② 静态变量的加载要早于对象的创建。 ③ 由于类只会加载一次,则静态变量在内存中也只会存在一次。存在方法区的静态域中。 | ①修饰属性,可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化 ⑤修饰局部变量,尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。 一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。 |
用途 | 工具类(Math,Arrays、Collections)、共享的数据 | 常量、不可改变的类(String、System、StringBuffer) |
常量: static + final
- 单例模式(饿汉式&懒汉式)
//饿汉式
class Single{
private static Single single = new Single();
private Single(){
}
public static Single getSingle(){
return single;
}
}
//懒汉式--线程安全
class Single{
private static Single single = null;
private Single(){
}
public static Single getSingle(){
if (single == null){
synchronized(Single.class){
if (single == null){
single = new Single();
}
}
}
return single;
}
}
8)代码块
静态代码块 | 非静态代码块 | |
---|---|---|
作用 | 初始化类 | 初始化对象 |
创建 | 类的加载而执行(执行一次) | 对象的创建而执行 |
顺序 | 优于非静态代码块 | 优于构造器 |
调用 | 只能调用静态的属性、方法 | 可以调用静态的,也可以调用非静态的 |
执行:
#先父后子、静态先行
1.随着类的加载,先执行静态代码块(只执行一次)
2.随着对象的创建,先执行父类的代码块、构造器,后执行子类的
9)抽象类&接口
区别点 | 抽象类 | 接口 | |
---|---|---|---|
1 | 定义 | 既可以有抽象方法,也可以有普通方法 | 只能定义方法,不能实现 |
2 | 组成 | 构造方法、抽象方法、普通方法、常量、变量 | 常量、抽象方法、(jdk8.0:默认方法、静态方法) |
3 | 使用 | 子类继承抽象类(extends) | 子类实现接口(implements) |
4 | 构造器 | 有构造器,不能实例化 | 没有构造器,不能实例化 |
5 | 常见设计模式 | 模板方法 | 简单工厂、工厂方法、代理模式 |
6 | 访问权限 | public、protected和缺省 | public |
7 | 局限 | 抽象类有单继承的局限 | 接口没有此局限 |
8 | 优点 | 可以有方法的实现,便于扩展 | 可以同时被多个类实现, |
说明:
也就是说在层次结构中,Java 接口在最上面,然后紧跟着抽象类,这下两个的最大优点都能发挥到极至了。这个模式就是缺省适配模式
A extends AbstractB implements interfaceC,那么A即可以选择实现(@Override)接口interfaceC中的方法,也可以选择不实现;A即可以选择实现(@Override)抽象类AbstractB中的方法,也可以选择不实现
- 静态代理
public class Test {
public static void main(String[] args) {
ProxyServer proxyServer = new ProxyServer(new Server());
proxyServer.show();
}
}
interface Work{
void show();
}
class Server implements Work{
@Override
public void show(){
System.out.println("真实的服务器");
}
}
class ProxyServer implements Work{
private Work work;
public ProxyServer(Work work){
this.work = work;
}
@Override
public void show() {
System.out.println("准备中。。。。");
work.show();
System.out.println("销毁中。。。。");
}
}
1、高级知识
1)多线程
a.概念
1.程序:一组指令的集合(静态代码)
2.进程:正在运行的程序,或者程序的一次执行过程(动态过程)
3.线程:进程执行的一条路径
1.并行:多个cpu同时执行多个任务(多人做多事)
2.并发:一个cpu同时执行多个任务(多人做一事)
b.创建线程的四种方式
①继承Thread类
public class Test extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("a" + i);
}
}
public static void main(String[] args) {
Test test = new Test();
//线程test
test.start();
//线程main
for (int i = 0; i < 100; i++) {
System.out.println("b" + i);
}
}
}
- 相关的方法
* 1.start():启动当前线程,执行当前线程的run()
* 2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
* 3.currentThread(): 静态方法,返回当前代码执行的线程
* 4.getName():获取当前线程的名字
* 5.setName():设置当前线程的名字
* 6.yield():释放当前CPU的执行权
* 7.join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才
结束阻塞状态。
* 8.stop():已过时。当执行此方法时,强制结束当前线程。
* 9.sleep(long millitime):让当前线程“睡眠”指定时间的millitime毫秒)。在指定的millitime毫秒时间内,当前线程是阻塞状态的。
* 10.isAlive():返回boolean,判断线程是否还活着
* - 线程的优先级等级
* - MAX_PRIORITY:10
* - MIN _PRIORITY:1
* - NORM_PRIORITY:5 --->默认优先级
* - 涉及的方法
* - getPriority() :返回线程优先值
* - setPriority(intnewPriority) :改变线程的优先级
②实现Runable接口
public class Test implements Runnable{
public static void main(String[] args) {
Thread a = new Thread(new Test());
a.start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
③实现Callable接口
public class Work implements Callable<Integer> {
private int ticket = 500;
public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(new Work());
Thread thread1 = new Thread(task, "线程1:");
thread1.start();
try {
System.out.println(task.get()); //获取返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
while (true){
Thread.sleep(10);
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ticket--);
}else {
System.out.println("没票了!!");
return 0;
}
}
}
}
④创建线程池
1)优势
描述 | |
---|---|
(1)降低资源消耗 | 通过重复利用已创建的线程降低线程创建和销毁造成的消耗 |
(2)提高响应速度 | 重复利用线程池中线程,不需要每次都创建 |
(3)提高线程的可管理性 | 线程池可以进行统一的分配,调优和监控 |
2)使用
线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {}
//最终都调用构造器
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;
}
参数介绍:
参数名 | 说明 |
---|---|
corePoolSize(必需) | 核心线程数。默认情况下,核心线程会一直存活。 但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 |
maxnumPoolSize(必需) | 线程池所能容纳的最大线程数。 当活跃线程数达到该数值后,后续的新任务将会阻塞。 |
keppAliveTime(必需) | 线程闲置超时时长。如果超过该时长,非核心线程就会被回收。 如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 |
unit(必需) | 指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。 |
workQueue(必需) | 任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。 |
threadFactory(可选) | 线程工厂。用于指定为线程池创建新线程的方式。 |
handler(可选) | 拒绝策略。当达到最大线程数时需要执行的饱和策略。 |
任务队列(workQueue):
参数 | 说明 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列) |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。 |
DelayQueue | 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。 |
SynchronousQueue | 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。 |
LinkedBlockingDeque | 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出) |
LinkedTransferQueue | 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。 |
拒绝策略(handler):
拒绝策略(handler) | 描述 |
---|---|
AbortPolicy(默认) | 丢弃任务并抛出 RejectedExecutionException 异常 |
CallerRunsPolicy | 由调用线程处理该任务 |
DiscardPolicy | 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式 |
DiscardOldestPolicy | 丢弃队列最早的未处理任务,然后重新尝试执行任务 |
3)4 种常见的功能线程池
4)总结
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
其实 Executors 的 4 个功能线程有如下弊端:
- FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
- CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
c.生命周期
1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
2.就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
3.运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
4.阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
5.死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
d.线程同步
①同步代码块
synchronized (同步监视器){
同步代码
}
**说明**:
1.操作**共享数据**的代码(同步代码块)
共享数据:多个线程**共同**操作的变量
2.**同步监视器**,俗称锁。任何一个类的的对象,都可以充当一个锁。
要求:多个线程必须公用一个锁
1)this当前对象 2)对象.class 3)Object
3.操作共享数据的代码,即为需要被同步的代码 --->不能包含代码多了,也不能包含代码少了
4.实现Runable接口,可以用this,而继承Thread谨慎使用this
②同步方法
1.如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
* 1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2)非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
③lock锁(JDK5.0)
1.死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
public class Work extends Thread implements Runnable{
private static int ticket = 100;
//1.实例化lock,参数true表示按顺序轮流执行线程
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
lock.lock();//2.枷锁
try { //必须放在try-finally里面
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ticket--);
}else {
break;
}
}finally {
lock.unlock();//3.释放锁
}
}
}
public static void main(String[] args) {
Work work = new Work();
Thread a = new Thread(work, "a: ");
Thread b = new Thread(work, "b: ");
Thread c = new Thread(work, "c: ");
a.start();
b.start();
c.start();
}
}
e.线程的通信
- sleep()和wait()的异同
sleep() | wait() | |
---|---|---|
声明位置 | Thread类中声明 | Object类中声明 |
调用要求 | 任何需要的场景下 | 同步代码块或同步方法中 |
否释放同步监视器 | 不会释放锁 | 会释放锁 |
- wait(),notify(),notifyAll()
wait() | notify() | notifyAll() | |
---|---|---|---|
执行结果 | 进入阻塞状态,并释放同步监视器 | 会唤醒被wait的一个线程 | 唤醒所有被wait的线程 |
说明 | |
---|---|
1.使用位置 | 必须使用在同步代码块或同步方法中 |
2.调用者要求 | 调用者必须是同步代码块或同步方法中的同步监视器 |
3.定义位置 | 定义在java.lang.Object类中 |
2)String、StringBuffer和StringBuilder
①String
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char[] value;
private int hash;
}
1.Serializable 序列化接口,表示对象可以被序列化。
2.Comparable 可比较接口,提供了一个compareTo()方法,用来比较两个对象的大小。
3.CharSequence 字符序列接口,提供了几个对字符序列进行只读访问的方法,比如:length()、charAt()、subSequence()、toString()方法等。
4.value 用来存储字符串中的字符。因此,String类的底层实现是把字符存储在一个char类型的数组中。value数组被 final 所修饰,所以String对象创建之后就不能被修改了。
5.hash 用来缓存计算之后的哈希值。这样就不用每次都重新计算哈希值了。
②StringBuffer
- 定义
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
private transient char[] toStringCache;
//继承AbstractStringBuilder的属性
char[] value;
int count;
}
1.Serializable 序列化接口,表示对象可以被序列化。
2.CharSequence 字符序列接口,提供了几个对字符序列进行只读访问的方法,比如:length()、charAt()、subSequence()、toString()方法等。
3.toStringCache 用来缓存toString()方法返回的最近一次的value数组中的字符。当修改StringBuffer对象时会被清除。
4.value 用来存储字符序列中的字符。value是一个动态的数组,当存储容量不足时,会对它进行扩容。
5.count 表示value数组中已存储的字符数。
- append源码分析
public synchronized StringBuffer append(String var1) {
this.toStringCache = null; //清除上一次toString,保存下的字符数组(字符串值)
super.append(var1); //拼接
return this;
}
//扩容方式(append)--- 初始容量16
1.①ensureCapacityInternal(拼接之后的总长度); //判断容量是否需要扩容
②this.value = Arrays.copyOf(原字符数组, this.newCapacity(需拼接的字符串))//如果容量不足,数组容量扩大
③newCapacity ——> (this.value.length << 1) + 2; //容量扩大为原来的2倍+2
2.①getChars(首索引位置,末索引位置,目标数组,目标数组开始偏移量) //将新字符串拼接
②System.arraycopy(原数组, 原数组起始位置,新数组,新数组起始位置,复制多长的原数组) //
3.this.count += var2 //字符数组长度,增加
③StringBuilder
类似Stringbuffer
- 注意:StringBuilder没有成员变量toStringCache
1.存储的是value的一个副本,只要value值发生变化,toStringCache就会置为空。
使得每次调用toString,产生一个新的toStringCache数组副本,保证引用旧的toStringCache字符串对象不会改变
2.优点:
1)调用toString时,共享一个toStringCache数据,提高了创建String对象的速度
2)连续调用不会,创建内容一样的String多个对象
④区别与联系
StringBuffer | StringBuilder | |
---|---|---|
线程安全 | 是,速度慢 | 否,速度块 |
继承关系 | 都是继承AbstractStringBuilder | 同 |
toStringCache | 有。解决线程安全问题 | 没有 |
3)Comparable和Comparator
Comparable | Comparator | |
---|---|---|
实现类 | String、包装类 | |
重写的方法 | compareTo(obj) | compare(Object o1,Object o2) |
规则 | 如果当前对象this大于形参对象obj,则返回正整数, 如果当前对象this小于形参对象obj,则返回负整数 如果当前对象this等于形参对象obj,则返回零。 | 如果方法返回正整数,则表示o1大于o2 如果返回0,表示相等 返回负整数,表示o1小于o2 |
对比 | 一旦一定,按这种排序 | 临时性的比较 |
使用原因 | 1、当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码 2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式 | |
优点 | 个性化比较、解耦 | |
Collections | Collections.sort(T[]) | Collections.sort(T[], Comparator<? super T>) |
java8新特性对象排序:
先按照age倒序排,然后按照name正序排
//①集合方法排序(改变原list) list.sort(Comparator.comparing(Man::getAge).reversed().thenComparing(Man::getName))
//②stream排序 (不改变list,生产新的) list.stream().sorted(Comparator.comparing(Man::getAge).reversed().thenComparing(Man::getName)).forEach(System.out::println);
4)注解
①介绍
1.定义新的Annotation类型使用@interface关键字
2.自定义注解自动继承了java.lang.annotation.Annotation接口
3.Annotation的成员变量在Annotation定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
4.可以在定义Annotation的成员变量时为其指定初始值,指定成员变量的初始值可使用**default**关键字
5.如果只有一个参数成员,建议使用参数名为value
6.如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。格式是“参数名=参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
7.没有成员定义的Annotation称为标记;包含成员变量的Annotation称为元数据Annotation
注意:自定义注解必须配上注解的信息处理流程才有意义
②四个元注解
- @Retention—>生命周期
@Retention: 只能用于修饰一个Annotation定义, 用于指定该Annotation 的生命周期, @Rentention包含一个RetentionPolicy类型的成员变量, 使用@Rentention时必须为该value 成员变量指定值:
1)RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
2)RetentionPolicy.CLASS:在class文件中有效(即class保留),当运行Java 程序时, JVM 不会保留注解。这是默认值
3)RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行Java 程序时, JVM 会保留注释。程序可以通过反射获取该注释。
- @Target—>修饰范围
用于修饰Annotation 定义, 用于指定被修饰的Annotation 能用于修饰哪些程序元素。@Target 也包含一个名为value 的成员变量。
@Target(ElementType.TYPE) //接口、类、枚举、注解
@Target(ElementType.FIELD) //字段、枚举的常量
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR) //构造函数
@Target(ElementType.LOCAL_VARIABLE)//局部变量
@Target(ElementType.ANNOTATION_TYPE)//注解
@Target(ElementType.PACKAGE) ///包
- @Documented—>是否在.class文件包含
用于指定被该元Annotation 修饰的Annotation 类将被javadoc工具提取成文档。默认情况下,javadoc是不包括注解的。
- @Inherited—>子类会继承这个注解
被它修饰的Annotation 将具有继承性。如果某个类使用了被@Inherited 修饰的Annotation, 则其子类将自动具有该注解。
③Java8新特性
- 可重复注解
@Repeatable(MyAnnotations.class)
@MyAnnotation(value = "hi")
@MyAnnotation(value = "abc")
//jdk 8之前的写法:
//@MyAnnotations({@MyAnnotation(value="hi"),@MyAnnotation(value="hi")})
- 类型注解
ElementType.TYPE_PARAMETER表示该注解能写在类型变量的声明语句中(如:泛型声明)。
ElementType.TYPE_USE表示该注解能写在使用类型的任何语句中。
④反射获取注解信息(需补)
5)集合
①介绍
1)继承关系
2)Collection接口
增删改查等等
小知识点(Arrays.asList):
public static <T> List<T> asList(T... var0) {
return new Arrays.ArrayList(var0);
}
//可以将一个变长参数或者数组转换成List
/**
*1.可变参数为基本数据类型数组时,由于基本数据类型不能泛型化,会当作一个数组对象传进去,size为1
*2.。。。。。包装类数组。。。。。。,可以泛型化,会当作多个参数传进去,size就是多个
*3.地层实现Arrays下的内部类ArrayList,没有重写add,remove,而且以final修饰变量。因此不能改变,会报异常。
*4.底层数组作为其物理实现,只要执行操作修改这个list就会修改原来的数组。要想不改变原来数组,就要在另一个容器中创建一个副本,写法如下:new ArrayList<String>(Arrays.asList(test));
3)Iterator迭代器接口
1.Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
2.Iterator 仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
3.集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
- 错误写法
//错误方式一:
Iterator iterator = coll.iterator();
while(iterator.next() != null){
System.out.println(iterator.next());
}
//错误方式二:
//集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
while(coll.iterator().hasNext()){
System.out.println(coll.iterator().next());
}
//如果还未调用next()或在上一次调用next方法之后已经调用了remove方法,再调用remove都会报IllegalStateException。
②List
1. List接口框架
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的、可重复的数据。 -->“动态”数组,替换原有的数组
|----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[]elementData存储
|----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
|----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[]elementData存储
1)ArrayList
public class ArrayList<E>
//让*通用*的方法在继承的抽象类AbstractList中实现
extends AbstractList<E>
//通用抽象方法
implements List<E>,
//支持快速(通常是固定时间)随机访问
RandomAccess,
//可以使用Object.Clone()方法
Cloneable,
//(序列化)从类变成字节流传输,然后还能从字节流变成原来的类。
java.io.Serializable{
//1.序列化id
private static final long serialVersionUID = 8683452581122892189L;
//2.默认的初始化容量
private static final int DEFAULT_CAPACITY = 10;
//3.指定该ArrayList容量为0时,返回该空数组。
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
//4.当调用无参构造方法,返回的是该数组。刚创建一个ArrayList 时,其内数据量为0。
//它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而EMPTY_ELEMENTDATA是在用户指定容量为0时返回。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// 5.保存添加到ArrayList中的元素。
// ArrayList的容量就是该数组的长度。
// 该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入ArrayList中时,数组将扩容值DEFAULT_CAPACITY。
// 被标记为transient,在对象被序列化的时候不会被序列化。
transient Object[] elementData;
//6.实际大小(数组包含的元素个数/实际数据的数量)默认为0
private int size;
//7.分派给arrays的最大容量(Integer.MAX_VALUE - 8) , 为什么要减去8呢?
//因为某些VM会在数组中保留一些头字,尝试分配这个最大存储容量,可能会导致array容量大于VM的limit,最终导致OutOfMemoryError。
private static final int MAX_ARRAY_SIZE = 2147483639;
//1.指定容量
public ArrayList(int initialCapacity)
//2.空List(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
public ArrayList(int initialCapacity)
//3.将collection对象转换成数组,然后将数组的地址的赋给elementData
public ArrayList(Collection<? extends E> var1)
}
- add()方法
public boolean add(E var1) {
//确保容量是否足够
this.ensureCapacityInternal(this.size + 1);
//添加元素,长度size自增
this.elementData[this.size++] = var1;
return true;
}
【扩容方式】
this.ensureExplicitCapacity(
calculateCapacity(this.elementData, var1))
/*
*1. elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? Math.max(10, size+1) : size+1
*【计算所需容量】若elementData空,默认10容量;第二次及之后扩容,所需容量size+1
*2. ensureExplicitCapacity(第一步 计算出来所需的容量)
*【扩容】++this.modCount,修改次数自增;grow(第一步 计算出来所需的容量),扩容
*第一步 计算出来所需的容量 - this.elementData.length > 0 容量不足
*3. this.grow(第一步 计算出来所需的容量)
*【扩容方式】var2 + (var2 >> 1)先扩容至1.5倍;
如果不够,直接扩容到所需容量
如果超出最大容量,只能扩容到最大容量
Arrays.copyOf(this.elementData, 所需容量)
2) LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>,
//可以作为一个双端队列
Deque<E>,
Cloneable,
Serializable {
//元素实际个数
transient int size;
//头节点
transient LinkedList.Node<E> first;
//尾节点
transient LinkedList.Node<E> last;
//内部类Node
private static class Node<E> {
E item;// node存储的元素
Node<E> next;// 前驱
Node<E> prev;// 后驱
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
//无参构造函数
public LinkedList() {this.size = 0;}
//带参构造函数
public LinkedList(Collection<? extends E> var1) {
this();
this.addAll(var1);
}
}
- 指定索引添加元素add(int index, E element)
public void add(int var1, E var2) {
this.checkPositionIndex(var1);
if (var1 == this.size) {
this.linkLast(var2); //尾节点插入
} else {
//其他节点插入(node()方法:会判断在前一半,还是后一半去遍历)
this.linkBefore(var2, this.node(var1));
}
}
void linkLast(E var1) {
//当前最后一个节点
LinkedList.Node var2 = this.last;
//创建节点,并使得其前驱节点指向原尾节点
LinkedList.Node var3 = new LinkedList.Node(var2, var1, (LinkedList.Node)null);
//新创建的尾节点
this.last = var3;
//如果空链表,添加元素,即首节点也是尾节点
if (var2 == null) {
this.first = var3;
//否则,原尾节点的next指向新尾节点
} else {
var2.next = var3;
}
++this.size;
++this.modCount;
}
void linkBefore(E var1, LinkedList.Node<E> var2) {
LinkedList.Node var3 = var2.prev;
//插入节点指定前驱和后继
LinkedList.Node var4 = new LinkedList.Node(var3, var1, var2);
var2.prev = var4;
//判断是否空链表插入
if (var3 == null) {
this.first = var4;
} else {
var3.next = var4;
}
++this.size;
++this.modCount;
}
3)Vector
1.Vector是线程安全的, ArrayList不是线程安全的, 这是最主要的
2.ArrayList不可以设置扩展的容量, 默认1.5倍; Vector可以设置, 默认2倍
3.ArrayList无参构造函数中初始量为0; Vector的无参构造函数初始容量为10
- Vector与Collections.synchronizedList
1.SynchronizedList有很好的扩展和兼容功能, 可以将所有的List子类转成线程安全的类
2.使用SynchronizedList在遍历的时候要手动进行同步处理
3.SynchronizedList可以指定锁对象
4)总结
ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储
③Set(建议先看Map)
Set接口的框架:
|----Collection接口:单列集合,用来存储一个一个的对象
|----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”
|----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
|----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
|----TreeSet:可以按照添加对象的指定属性,进行排序。
1)HashSet
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, Serializable {
//hashSet基于HashMap实现,使用HashMap存储数据
private transient HashMap<E, Object> map;
//用于充当map的value,用于判断删除是否成功
private static final Object PRESENT = new Object();
//无参构造函数
public HashSet() {this.map = new HashMap();}
//指定初始值,
public HashSet(Collection<? extends E> var1) {
this.map = new HashMap(Math.max((int)((float)var1.size() / 0.75F) + 1, 16));
this.addAll(var1);
}
//指定初始化容量,加载因子
public HashSet(int initialCapacity, float loadFactor) {
this.map = new HashMap(initialCapacity, loadFactor);
}
//指定容量
public HashSet(int initialCapacity) {
this.map = new HashMap(initialCapacity);
}
2)LinkedHashSet
- 使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
3)TreeSet
- 自然排序和定制排序
④Map
* 一、Map的实现类的结构:
* |----Map:双列数据,存储key-value对的数据 ---类似于高中的函数:y = f(x)
* |----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
* |----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
* 原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
* 对于频繁的遍历操作,此类执行效率高于HashMap。
* |----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
* |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
* |----Properties:常用来处理配置文件。key和value都是String类型
*
*
* HashMap的底层:数组+链表 (jdk7及之前)
* 数组+链表+红黑树 (jdk 8)
1)HashMap
- JDK7实现原理
- JDK8实现原理
- 实现原理
1.HashMap的内部存储结构其实是【数组+链表+红黑树】的结合
2.当实例化一个HashMap时,会初始化initialCapacity【初始容量16】和loadFactor【加载因子默认0.75】
3.在put第一对映射关系时,系统会创建一个长度为initialCapacity【初始容量16】的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素
4.每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
5.那么HashMap什么时候进行扩容和树形化呢?
当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成红黑树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把红黑树再转为链表。
注意:关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
- 成员变量及构造器
public class HashMap<K, V>
extends AbstractMap<K, V>
implements Map<K, V>, Cloneable, Serializable {
//HashMap的默认容量,16
static final int DEFAULT_INITIAL_CAPACITY = 16;
//HashMap的默认加载因子:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75F;
//Bucket中链表长度大于该默认值,转化为红黑树:8
static final int TREEIFY_THRESHOLD = 8;
//Bucket中红黑树节点小于该默认值,转化为链表:6
static final int UNTREEIFY_THRESHOLD = 6;
//桶中的Node被树化时最小的hash表容量:64
static final int MIN_TREEIFY_CAPACITY = 64;
//Node数组
transient HashMap.Node<K, V>[] table;
//所有数据
transient Set<Entry<K, V>> entrySet;
transient int size;
transient int modCount;
//扩容的临界值,= 容量*填充因子:16 * 0.75 => 12
int threshold;
//实际加载因子
final float loadFactor;
- 遍历3种方式
//方式 1.Map.Entry
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
//【1.1 for】
for (Map.Entry<Integer, Integer> entry : entries) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
//【1.2 itertor】
Iterator<Map.Entry<Integer, Integer>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Integer, Integer> next = iterator.next();
System.out.println(next.getKey() + " : " + next.getValue());
}
//方式 2.map.keySet()
Set<Integer> keySet = map.keySet();
//【2.1 for】
for (Integer key : keySet) {
System.out.println(key + " : " + map.get(key));
}
//【2.2 itertor】
Iterator<Integer> iterator1 = keySet.iterator();
while (iterator1.hasNext()) {
Integer next = iterator1.next();
System.out.println(next + " : " + map.get(next));
}
//方式 3.map.values()
Collection<Integer> values = map.values();
Iterator<Integer> iterator2 = values.iterator();
while (iterator2.hasNext()) {
System.out.println(iterator2.next());
}
2)LinkedMap
- 在
HashMap
存储结构的基础上,使用了一对双向链表来记录添加元素的顺序 - HashMap中的内部类:Node
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
HashMap.Node<K, V> next;
- LinkedHashMap中的内部类:Entry
static class Entry<K, V> extends HashMap.Node<K, V> {
LinkedHashMap.Entry<K, V> before;
LinkedHashMap.Entry<K, V> after;
3)TreeMap
-
TreeMap
存储Key-Value
对时,需要根据key-value
对进行排序。TreeMap
可以保证所有的Key-Value
对处于有序状态。 -
TreeSet
底层使用红黑树结构存储数据 -
TreeMap
的Key
的排序:- 自然排序:TreeMap的所有的Key 必须实现Comparable接口,而且所有的Key应该是同一个类的对象,否则将会抛出ClasssCastException
- 定制排序:创建TreeMap时,传入一个Comparator 对象,该对象负责对TreeMap中的所有key 进行排序。此时不需要Map 的Key实现Comparable 接口
-
TreeMap
判断两个key
相等的标准:两个key
通过compareTo()
方法或者compare()
方法返回0
。
4)HashTable
Hashtable
是线程安全的。- 不允许使用
null
作为key
和value
⑤补充:cloneable接口
//cloneable其实就是一个【标记接口】,只有实现这个接口后,然后在类中重写Object中的clone方法,然后通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException(克隆不被支持)异常。Object中clone方法:
protected native Object clone() throws CloneNotSupportedException;
//1.调用jvm中的实现体时进行判断的,调用的是本地在jvm中编写的C的接口
//2.clone先分配对象,再将值填充到对象中。和原对象相同,但是【地址值不同】。
- 浅拷贝&深拷贝
//实现Cloneable接口,重写clone()方法
@Override
protected School clone() throws CloneNotSupportedException {
School clone = (School) super.clone(); //浅拷贝,只拷贝一层
clone.student = student.clone(); //深拷贝,拷贝学校类下的学生类
return clone;
}
//注意:
浅拷贝,拷贝的对象只是引用他的地址,改变其值,拷贝对象的值也随之改变
⑥红黑树
1)红黑树的5个性质
定义:通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL
1 每个结点要么是红的要么是黑的。
2 根结点是黑的。
3 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
4 如果一个结点是红的,那么它的两个儿子都是黑的。
5 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
(注:上述第3、5点性质中所说的NULL结点,包括wikipedia.算法导论上所认为的叶子结点即为树尾端的NIL指针,或者说NULL结点。然百度百科以及网上一些其它博文直接说的叶结点,则易引起误会,因,此叶结点非子结点)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwr3lBpO-1689324564360)(http://43.143.239.200:9000/monkey/bookImages/image-20220331105710362.png)]
⑦ hashCode和equals
所有处理的根本目的,都是为了提高 存储key-value
的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突。即:对于不同key
,存储的数组下标位置要尽可能不一样
6)反射
/**
* 关于java.lang.Class类的理解
* 1.类的加载过程:
* 程序经过Javac.exe命令后,会生成一个或多个字节码文件(.class结尾)。
* 接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件
* 【加载到内存中】。此过程就称为【类的加载】。加载到内存中的类,我们就称为【运行时类】,此
* 运行时类,就作为Class的一个实例。
*
* 2.换句话说,Class的实例就对应着一个运行时类。
*/
①获取Class实例的四种方式
//1. 类名.class
Class<Person> clazz1 = Person.class;
//2.运行时对象.getClass()
Person per = new Person();
Class clazz2 = per.getClass();
//3.Class静态方法 forName(String classPath)
Class clazz3 = Class.forName("com.example.servlet.Person");
//4.使用类的加载器:ClassLoader
ClassLoader classLoader = this.getClass().getClassLoader(); //获取当前的类加载器
Class<?> clazz4 = classLoader.loadClass("com.example.servlet.Person"); //获取Person实例
②常见使用
一、获取属性数组:
//1.getFields() 获取当前【运行时类及其父类】中声明为【public】访问权限的属性
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
//2.getDeclaredFields() 获取当前运行时类中声明的所有属性。(不包含父类中声明的属性)
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
//权限修饰符+变量类型+变量名
System.out.println(Modifier.toString(declaredField.getModifiers())+" "+
declaredField.getType()+" "+
declaredField.getName());
}
二、获取方法数组:
//3.getMethods() 获取当前【运行时类及其所有父类】中声明为【public】权限的方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
System.out.println(method);
}
//4.getDeclaredMethods() 获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
Method[] decMethods = clazz.getDeclaredMethods();
for (Method decMethod : decMethods) {
System.out.println(decMethod);
//注解
System.out.println(Arrays.toString(decMethod.getAnnotations()));
//权限修饰符+返回值类型+方法名+形参列表
System.out.println(Modifier.toString(decMethod.getModifiers())+" "+
decMethod.getReturnType()+" "+
decMethod.getName()+" "+
Arrays.toString(decMethod.getParameterTypes()));
}
三、获取构造器数组
//5.获取当前运行时类中声明为public的构造器
System.out.println(Arrays.toString(clazz.getConstructors()));
//6.获取当前运行时类中声明的所有的构造器
System.out.println(Arrays.toString(clazz.getDeclaredConstructors()));
四、获取父类运行时类
//7.获取运行时类的父类
System.out.println(clazz.getSuperclass());
//8.获取运行时类的带泛型的父类
System.out.println(clazz.getGenericSuperclass());
//9.获取运行时类的父类实现的接口
System.out.println(Arrays.toString(clazz.getSuperclass().getInterfaces()));
//10.获取运行时类所在的包
System.out.println(clazz.getPackage());
五、创建对象,读写属性
//11.创建对象
Person person = clazz.newInstance();
//方式一:获取属性,读写属性
Field id = clazz.getField("id");
id.set(person, 111); //设值
System.out.println(id.get(person)); //取值
//方式二:获取属性,读写属性(常用)
Field id1 = clazz.getDeclaredField("id");
id1.setAccessible(true); //确保可以访问
id1.set(person, 222);
System.out.println(id1.get(person));
六、获取指定方法及构造器
//12.指定的方法
Method show = clazz.getDeclaredMethod("show", String.class);
show.setAccessible(true);
show.invoke(person, "SHOW!!");
//13.指定构造器
Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class);
System.out.println(constructor.newInstance("wwwwwww"));
③动态代理
public class Test {
public static void main(String[] args) throws Exception {
ProxyServer proxyServer = new ProxyServer();
Works works = (Works) proxyServer.getInstance(new Server());
works.show();
}
}
interface Works{
void show();
}
class Server implements Works{
@Override
public void show() {
System.out.println("真实服务器执行。。。");
}
}
class ProxyServer {
//根据加载到内存中的被代理类,动态的创建一个代理类及其对象
public Object getInstance(Object obj){
Handler handler = new Handler(obj);
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
}
}
class Handler implements InvocationHandler{
//代理类对象进行赋值
private Object obj;
public Handler(Object obj){
this.obj = obj;
}
//调用方法时,自动执行下面方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理类实现业务
return method.invoke(obj, args);
}
}
7)java8新特性
①Lamdba表达式
直接访问
1)标记了【final】的外层【局部变量】
2)实例的字段
3)静态变量
②Optional(验空)
//1.获取Optional对象
// Optional<Object> o1 = Optional.of(null); //报错:NullPointerException
Optional<Object> o2 = Optional.ofNullable(null); //允许为空
Optional<Object> o3 = Optional.empty(); //与ofNullable传null效果一致
//2.访问 Optional 对象的值
// Object getVal = o2.get(); //Optional对象不能为空(报错!!)
String getStr = Optional.of("Java").get(); //得到Java字符串
//3.判断是否为空
boolean isEmpty = o2.isPresent(); //空返回false,非空返回true
o2.ifPresent(System.out::println); //为空,后面不执行
Optional.of("not is empty!").ifPresent(System.out::println); //不为空,执行lamdba表达式
//4.对象为空时,返回指定默认值
Object orElse1 = Optional.ofNullable(null).orElse("执行了orElse");//空值执行orElse操作
Object orElseGet2 = Optional.ofNullable(null).orElseGet(()->"执行了orElse");//空值执行orElse操作
//区别:对象不为空时,orElseGet() 方法不创建 User 对象,影响性能
Object orElse3 = Optional.of("null").orElse(show());//有值返回,后面操作【会执行】
Object orElseGet4 = Optional.of("null").orElseGet(()->show());//有值返回,后面操作【不会执行】
//5.map(): 获取用户姓名,没有返回【"姓名为空!!"】
String name = Optional.of(new Person("小明", 7)).map(t -> t.getName()).orElse("姓名为空!!");
Object faltMap = Optional.of(new Person("小明", 7)).flatMap((t) -> Optional.of(t.getName() + ",你好!")).orElse("姓名为空!!"); //输出:【小明,你好!】
//6.过滤得到, 年龄大于六的人
Optional<Person> person = Optional.of(new Person("小明", 7)).filter(t -> t.getAge() > 6);
③Stream
①Stream 自己不会存储元素。Stream关注的是对数据的运算,与CPU打交道
集合关注的是数据的存储,与内存打交道
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
- 创建
//Stream创建
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream(); //创建顺序流
Stream<String> parallel = stream.parallel(); //顺序流转化为并行流
Stream<String> parallelStream = list.parallelStream(); //创建并行流
IntStream arrStream = Arrays.stream(new int[]{1, 2, 3}); //创建数组流
//静态方法
Stream<Integer> intStream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> iterateStream = Stream.iterate(10, t -> t * t).limit(3);//输出:10 100 10000
Stream<Double> genStream = Stream.generate(Math::random).limit(3);//获取三个[0,1)随机数
- 遍历/匹配(foreach/find/match)
List<Integer> num = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
num.stream().forEach(System.out::println); //foreach遍历
Integer integer = num.stream().findFirst().get(); //获取第一个元素
Integer integer1 = num.stream().parallel().findAny().get(); //随机获取一个(适用于并行)
boolean isHave = num.stream().anyMatch(t -> t > 5); //是否包含符合特定条件的元素
- 过滤filter
Stream<Integer> filterNum = num.stream().filter(t -> t > 5);//数字大于5的
- 聚合(max/min/count)
List<String> strList = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
String maxStr = strList.stream().max(Comparator.comparing(String::length)).get();//得到:"weoujgsd"
Integer maxInt = num.stream().max(Comparator.comparingInt(t -> t)).get(); //int比较
long count = strList.stream().count(); //获取个数
- 映射(map/flatMap)
//map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
//flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
Stream<String> strStream = Arrays.stream(strArr).map(String::toUpperCase);//字符串转大写
//将两个字符数组合并成一个新的字符数组
List<String> list1 = Arrays.asList("m,k,l,a", "1,3,5,7");
Stream<String> stringStream = list1.stream().flatMap(t -> Stream.of(t.split(",")));//[m, k, l, a, 1, 3, 5, 7]
- 归约(reduce)
//归约,也称缩减,顾名思义,是把一个流缩【减成】一个值,能实现对集合【求和】【求乘积和求】【最值】操作。
List<Integer> list2 = Arrays.asList(1, 3, 2, 8, 11, 4);
Integer sum1 = list2.stream().reduce((t1, t2) -> t1 + t2).get();//1.求和方式1
Integer sum2 = list2.stream().reduce(Integer::sum).get();//2.求和方式2
Integer sum3 = list2.stream().reduce(10, Integer::sum);//3.求和方式3【设置初始值10】
Integer mul = list2.stream().reduce((t1, t2) -> t1 * t2).get();//求乘积和
Integer max1 = list2.stream().reduce((t1, t2) -> t1 > t2 ? t1 : t2).get();//最大值方式1
Integer max2 = list2.stream().reduce(Integer::max).get();//求最大值方式2
- 收集collect
//collect从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。
//collect主要依赖java.util.stream.Collectors类内置的静态方法。
1. 归集(toList/toSet/toMap)
List<Integer> list3 = Arrays.asList(1, 2, 3, 3, 5, 2);
List<Integer> toList = list3.stream().collect(Collectors.toList());//数组输出
Set<Integer> toSet = list3.stream().collect(Collectors.toSet());//去重set
Map<String, Person> toMap = person.stream().collect(Collectors.toMap(Person::getName, t -> t));//map输出
2. 统计(count/averaging)
计数:count
平均值:averagingInt、averagingLong、averagingDouble
最值:maxBy、minBy
求和:summingInt、summingLong、summingDouble
统计以上所有:summarizingInt、summarizingLong、summarizingDouble
//统计数量
Long counts = person.stream().collect(Collectors.counting());
//平均工资
Double avgSalary = person.stream().collect(Collectors.averagingDouble(Person::getSalary));
//最低工资
Integer minSalary = person.stream().map(Person::getSalary).collect(Collectors.minBy(Integer::compare)).get();
//工资总和
Integer sumSalary = person.stream().collect(Collectors.summingInt(Person::getSalary));
//统计所有工资,输出IntSummaryStatistics{count=6, sum=49300, min=7000, average=8216.666667, max=9500}
IntSummaryStatistics allSalary = person.stream().collect(Collectors.summarizingInt(Person::getSalary));
3. 分组(partitioningBy/groupingBy)
分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。
分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。
//工资高于7000分组【只能2组,是、否】
Map<Boolean, List<Person>> partition = person.stream().collect(Collectors.partitioningBy(t -> t.getSalary() > 7000));
//性别分组【多组】
Map<String, List<Person>> group = person.stream().collect(Collectors.groupingBy(Person::getSex));
//将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> groups = person.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
4.接合(joining)
List<String> list4 = Arrays.asList("A", "B", "C");
String join = list4.stream().collect(Collectors.joining(","));//输出: A,B,C
5.归约(reducing)
//年龄求和加上1000
Integer sumAge = person.stream().collect(Collectors.reducing(1000, Person::getAge, Integer::sum));
- 排序sorted
// 先按工资升序再按年龄降序排序
List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).reversed().map(Person::getName).collect(Collectors.toList());
- 提取组合
String[] arr1 = { "a", "b", "c", "d" };
String[] arr2 = { "d", "e", "f", "g" };
//合并去重
Stream<String> distinct = Stream.concat(Arrays.stream(arr1), Arrays.stream(arr2)).distinct();
//获取1~6元素
Stream<Integer> limit = Stream.iterate(1, t -> t + 1).skip(1).limit(6);//输出:2~7
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
四、Java web
1、web容器
1)Tomcat
1.封装HTTP协议操作,简化开发
2.可以将Web项目部署到服务器中,对外提供网上浏览服务
#①:Tomcat将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,所有的HTTP头数据读可以通过request对象调用对应的方法查询到。
#②:Tomcat同时会要响应的信息封装为HttpServletResponse类型的response对象,通过设置response属性就可以控制要输出到浏览器的内容,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器
- 1.真正管理Servlet的容器是Context容器
一个Context【对应】一个Web工程;新建一个servlet时【就会新建】一个context,同时【加载】它需要的config,容器的配置属性由应用的web.xml指定;
- 2.ServletConfig、ServletContext、ServletRequest和ServletResponse通过容器传递给Servlet
a.启动Tomcat时,Servlet容器被创建,每个web应用都对应于一个context容器;
b.客户端发起一次请求时,请求根据url地址指定的ip和端口号就能找到tomcat服务器,再根据工程名找到对应的web服务;
c.此时创建一个线程,根据ServletRequest发起请求,Servlet作为控制器根据页面的请求内容查找相应的服务,并将结果通过ServletResponse返回给客户端。
//注意:每个请求(而非每个用户)对应一个线程,Servlet一般只会存在一个实例
2)servlet
①介绍
Servlet(Server Applet),全称Java Servlet,未有中文译文。是【用Java编写的服务器端程序】。其主要功能在于【交互式地浏览和修改数据】,【生成动态Web】内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。
Servlet API 包含以下4个Java包:
1.javax.servlet 其中包含定义servlet和servlet容器之间契约的类和接口。
2.javax.servlet.http 其中包含定义HTTP Servlet 和Servlet容器之间的关系。
3.javax.servlet.annotation 其中包含标注servlet,Filter,Listener的标注。它还为被标注元件定义元数据。
4.javax.servlet.descriptor,其中包含提供程序化登录Web应用程序的配置信息的类型。
②执行流程
Servlet类加载 -> 实例化 -> 服务 -> 销毁
1.Web Client向Servlet容器(Tomcat)发出Http请求;
2.Servlet容器接收Web Client的请求;
3.Servlet容器创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中;
4.Servlet容器创建一个HttpResponse对象;
5.Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给HttpServlet对象;
6.HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息;
7.HttpServlet调用HttpResponse对象的有关方法,生成相应数据;
8.Servlet容器把HttpServlet的响应结果传给Web Client。
【执行过程】
#1.浏览器发出http://localhost:8080/web-demo/demo1请求,从请求中可以解析出三部分内容,分别是localhost:8080、web-demo、demo1
根据localhost:8080可以找到要访问的Tomcat Web服务器
根据web-demo可以找到部署在Tomcat服务器上的web-demo项目
根据demo1可以找到要访问的是项目中的哪个Servlet类,根据@WebServlet后面的值进行匹配
#2.找到ServletDemo1这个类后,Tomcat Web服务器就会为ServletDemo1这个类创建一个对象,然后调用对象中的service方法
ServletDemo1实现了Servlet接口,所以类中必然会重写service方法供Tomcat Web服务器进行调用
service方法中有ServletRequest和ServletResponse两个参数,ServletRequest封装的是请求数据, ServletResponse封装的是响应数据,后期我们可以通过这两个参数实现前后端的数据交互
生命周期
#1)加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
默认情况,Servlet会在【第一次访问被容器创建】,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,那么我们能不能把Servlet的创建放到服务器启动的时候来创建,具体如何来配置?
urlPatterns和value都可以
@WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高
#2)初始化:在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次
#3)请求处理:每次请求Servlet(也就是访问浏览器)时,Servlet容器都会调用Servlet的service()方法对请求进行处理
#4)服务终止:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的destroy()方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收。
③javax.servlet包内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jOCQSzzp-1689324564362)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fp-blog.csdn.net%2Fimages%2Fp_blog_csdn_net%2FLhyUp%2FEntryImages%2F20091226%2Fservlet.jpg&refer=http%3A%2F%2Fp-blog.csdn.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651981906&t=74a6baf1c966130779e7d17ee8835a18)]
1.Servlet 接口
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
//这个方法会返回由Servlet容器传给init( )方法的ServletConfig对象
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
//这个方法会返回Servlet的一段描述,可以返回一段字符串。
String getServletInfo();
void destroy();
}
生命周期:
1.init( ) //初始化信息,只执行一次
当Servlet第一次被请求时,Servlet容器就会开始调用这个方法来初始化一个Servlet对象出来,但是这个方法在后续请求中不会在被Servlet容器调用,就像人只能“出生”一次一样。我们可以利用init( )方法来执行相应的初始化工作。调用这个方法时,Servlet容器会传入一个ServletConfig对象进来从而对Servlet对象进行初始化。
2.service( )
每当请求Servlet时,Servlet容器就会调用这个方法。就像人一样,需要不停的接受老板的指令并且“工作”。第一次请求时,Servlet容器会先调用init( )方法初始化一个Servlet对象出来,然后会调用它的service( )方法进行工作,但在后续的请求中,Servlet容器只会调用service方法了。
3.destory
当要销毁Servlet时,Servlet容器就会调用这个方法,就如人一样,到时期了就得死亡。在卸载应用程序或者关闭Servlet容器时,就会发生这种情况,一般在这个方法中会写一些清除代码。
2.ServletRequset接口
说明:Servlet容器对于接受到的每一个Http请求,都会创建一个ServletRequest对象,并把这个对象传递给Servlet的Sevice( )方法
public interface ServletRequest {
//返回请求主体的字节数
int getContentLength();
//返回主体的MIME类型
String getContentType();
//返回请求参数的值【最常用的方法,可用于获取查询字符串的值】
String getParameter(String var1);
3.ServletResponse接口
说明: javax.servlet.ServletResponse接口表示一个Servlet响应,在调用Servlet的Service( )方法前,Servlet容器会先创建一个ServletResponse对象,并把它作为第二个参数传给Service( )方法。ServletResponse隐藏了向浏览器发送响应的复杂过程。
public interface ServletResponse {
String getCharacterEncoding();
String getContentType();
//发送二进制数据
ServletOutputStream getOutputStream() throws IOException;
//发送文本PrintWriter对象,使用ISO-8859-1编码(该编码在输入中文时会发生乱码)
PrintWriter getWriter() throws IOException;
void setCharacterEncoding(String var1);
void setContentLength(int var1);
void setContentLengthLong(long var1);
//在发送任何HTML之前,应该先调用setContentType()方法,设置响应的内容类型,并将“text/html”作为一个参数传入,这是在告诉浏览器响应的内容类型为HTML,需要以HTML的方法解释响应内容而不是普通的文本,或者也可以加上“charset=UTF-8”改变响应的编码方式以防止发生中文乱码现象。
void setContentType(String var1);
void setBufferSize(int var1);
int getBufferSize();
void flushBuffer() throws IOException;
void resetBuffer();
boolean isCommitted();
void reset();
void setLocale(Locale var1);
Locale getLocale();
}
4.ServletConfig接口
说明:当Servlet容器初始化Servlet时,Servlet容器会给Servlet的init( )方式传入一个ServletConfig对象
public interface ServletConfig {
String getServletName();//获取servlet在web.xml中配置的name值
ServletContext getServletContext();//获取servlet上下文配置
String getInitParameter(String var1);//获取servlet初始化参数
Enumeration<String> getInitParameterNames();//获取servlet所有初始化参数的name
}
//例如:
String paramVal = this.config.getInitParameter("name");//获取指定的初始化参数
response.getWriter().print(paramVal); //页面输出:name对应的value
5.ServletContext对象
说明:
WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用。
ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,可以通过ServletConfig.getServletContext方法获得ServletContext对象。
由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。ServletContext对象通常也被称之为context域对象。
public interface ServletContext {
Object getAttribute(String var1);
Enumeration<String> getAttributeNames();
void setAttribute(String var1, Object var2);
void removeAttribute(String var1);
6.GenericServlet抽象类
好处:
1.为Servlet接口中的所有方法【提供了默认】的实现,则程序员需要什么就直接改什么,不再需要把所有的方法都自己实现了。
2.提供方法,包围ServletConfig对象中的方法。
3.将init( )方法中的ServletConfig参数赋给了一个内部的ServletConfig引用从而来保存ServletConfig对象,【不需要】程序员自己去【维护】ServletConfig了。
④javax.servlet.http包内容
1.HttpServlet抽象类
public abstract class HttpServlet extends GenericServlet {
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
2.HttpServletRequest接口
- 1获取请求行
//获取请求方式
String method = request.getMethod(); //【GET】
//获取请求资源相关
String uri = request.getRequestURI(); // 【/WEBpro/line】
StringBuffer url = request.getRequestURL(); //【http://localhost:8080/WEBpro/line】
//获取web应用名称
String name = request.getContextPath(); //【/WEBpro】
//获取地址后参数
String parms = request.getQueryString(); //【null】
//获得客户机IP地址
String ip = request.getRemoteAddr(); //【127.0.0.1】
- 2获取请求头
//获取指定的头
String user = request.getHeader("User-Agent");//【Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36】
System.out.println(user);
//获得所有头的名字
Enumeration<String> em = request.getHeaderNames();//【host、cookie 等等】
- 3获取请求体
//获得单个参数的值
String par = request.getParameter("username");
//获得多个参数的值
String[] hobby = request.getParameterValues("hobby");
//获得所有请求参数名称
Enumeration<String> en = request.getParameterNames();
//获取所有参数,参数封装到MAP<String,String[]>
Map<String, String[]> map = request.getParameterMap();
- 乱码问题
//此为post与get通用方式
String username = request.getParameter("username");
username = new String(username.getBytes("iso8859-1"),"UTF-8");
//Post可以直接这么写
request.setCharacterEncoding("UTF-8");
- 请求转发
request.getRequestDispatcher("login.jsp").forward(request, response);
3.HttpServletReaponse接口
- 响应数据
// 字符输出流
PrintWriter writer = response.getWriter();
writer.write("<h2>Hello</h2>");
// 字节输出流
ServletOutputStream out = response.getOutputStream();
out.write("<h2>Hello</h2>".getBytes());
//乱码问题
1.getWriter()
// 设置服务端的编码
response.setCharacterEncoding("UTF-8");
// 设置客户端的响应类型及编码
response.setHeader("content-type","text/html;charset=UTF-8");
2.getOutputStream()
response.setHeader("content-type","text/html;charset=UTF-8");
//同时指定服务器和客户端【简单】【替换上面】
response.setContentType("text/html;charset=UTF-8");
- 重定向
// 重定向跳转到index.jsp
response.sendRedirect("index.jsp");
3)Cookies&Session
- Cookies
//Cookies获取
Cookie[] cookies = req.getCookies();
//Cookie值的修改
方案一(直接写新的盖上去)
Cookie cookie = new Cookie("key1","newValue1");
resp.addCookie(cookie);
方案二(找到要改的,再用Cookie对象的方法改)
Cookie[] cookies = request.getCookies();
for (Cookie cookie: cookies){
if (name.equals(cookie.getName())){
myCookie.setValue("NewValue");
}
}
//生命周期
cookie.setMaxAge(10*60); //单位秒
Cookie在生成时就会被指定一个Expire值,这就是Cookie的生存周期,在这个周期内Cookie有效,超出周期Cookie就会被清除。
*正数:超出失效
*负数:关闭浏览器失效
*0:立即失效【删除已经存在客户端的此Cookie(我们可以通过此方法删除某个Cookie)】
*不设置:一次会话范围内
1)会话级Cookie
服务器端并【没有】明确指定Cookie的存在时间
在浏览器端,Cookie数据存在于【内存】中
只要浏览器还开着,Cookie数据就一直都在
【浏览器关闭】,内存中的Cookie数据就会被【释放】
2)持久化Cookie
服务器端明确【设置】了Cookie的存在时间
在浏览器端,Cookie数据会被保存到【硬盘】上
Cookie在硬盘上存在的时间根据服务器端限定的时间来管控,【不受浏览器关闭的影响】
持久化Cookie到达了预设的时间会被释放
- Session
session是服务器端的技术。服务器为每一个浏览器开辟一块内存空间,其本质就是要给大的Map。一般情况下,服务器会在一定时间内(默认30分钟)保存这个 Session,过了时间限制,就会销毁这个Session。在销毁之前,程序员可以将用户的一些数据以Key和Value的形式暂时存放在这个 Session中。
// 1.调用request对象的方法尝试获取HttpSession对象
HttpSession session = request.getSession();
// 2.调用HttpSession对象的isNew()方法
boolean wetherNew = session.isNew();
// 3.打印HttpSession对象是否为新对象
System.out.println("wetherNew = " + (wetherNew?"HttpSession对象是新的":"HttpSession对象是旧的"));
// 4.调用HttpSession对象的getId()方法
String id = session.getId();
// 5.打印JSESSIONID的值
System.out.println("JSESSIONID = " + id);
- 区别
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗
考虑到安全应当使用session。
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能
考虑到减轻服务器性能方面,应当使用COOKIE。
4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、所以个人建议:
将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中
1、http
#1)请求数据格式
【请求行】: HTTP请求中的第一行数据,请求行包含三块内容,分别是 GET[请求方式] /[请求URL路径] HTTP/1.1[HTTP协议及版本]
请求方式有七种,最常用的是GET和POST
【请求头】: 第二行开始,格式为key: value形式
请求头中会包含若干个属性,常见的HTTP请求头有:
Host: 表示请求的主机名
User-Agent: 浏览器版本,译为用户代理。例如Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79,
IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko;
Accept:表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有;
Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。
【请求体】: POST请求的最后一部分,存储请求参数
#2)响应数据格式
【响应行】:响应数据的第一行。响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
【响应头】:第二行开始,格式为key:value形式
响应头中会包含若干个属性,常见的HTTP响应头有:
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg;
Content-Length:表示该响应内容的长度(字节数);
Content-Encoding:表示该响应压缩算法,例如gzip;
Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒
【响应体】: 最后一部分。存放响应数据
#状态码
状态码分类 说明
1xx 响应中——临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx 成功——表示请求已经被成功接收,处理已完成
3xx 重定向——重定向到其它地方:它让客户端再发起一个请求以完成整个处理。
4xx 客户端错误——处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等
5xx 服务器端错误——处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等
1、request和response
#1)request:获取请求数据
浏览器会发送HTTP请求到后台服务器[Tomcat]
HTTP的请求中会包含很多请求数据[请求行+请求头+请求体]
后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到request对象中
我们可以从request对象中获取请求的相关参数
获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
#2)response:设置响应数据
业务处理完后,后台就需要给前端返回业务处理的结果即响应数据
把响应数据封装到response对象中
后台服务器[Tomcat]会解析response对象,按照[响应行+响应头+响应体]格式拼接结果
浏览器最终解析结果,把内容展示在浏览器给用户浏览
1、请求转发&重定向
1、Cookie&Session
Cookie和Session小结
Cookie 和 Session 都是来完成一次会话内多次请求间数据共享的。
#1)区别:
存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端。关闭浏览器,cookie看setMaxAge,session销毁。关闭服务器后两者都依然存在没销毁。
安全性:Cookie不安全,Session安全
数据大小:Cookie最大3KB,Session无大小限制
存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
服务器性能:Cookie不占服务器资源,Session占用服务器资源
#2)应用场景:
购物车:使用Cookie来存储,因为要长期存储,且数据不敏感。
已登录用户的个人信息展示:使用Session来存储,因为数据敏感
记住我功能:使用Cookie来存储,因为要长期存储,牺牲安全提升体验
验证码:使用session来存储,因为安全重要。发验证码请求和提交验证码请求的验证码只需要短期存储、且安全。
#3)结论
Cookie是用来保证用户在未登录情况下的身份识别
Session是用来保存用户登录后的数据
1、过滤器
@WebFilter("/*")
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter demo...");
//放行
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
1、
五、Mysql
1、sql语句执行顺序
SELECT ...,....,... 【2】
FROM ...
JOIN ... ON 多表的连接条件
JOIN ... ON ...
WHERE 不包含组函数的过滤条件 【1】
AND/OR 不包含组函数的过滤条件
GROUP BY ...,...
HAVING 包含组函数的过滤条件
ORDER BY ... ASC/DESC 【3】
LIMIT ...,..
1、视图、存储过程和函数、触发器
#1)视图
视图是一种【虚拟表】,本身是【不具有数据】的,占用很少的内存空间
视图建立在【已有表】的基础上, 视图赖以建立的这些表称为基表
#本质:可以看作是存储的select语句
增删改操作-----相互影响
删除视图操作-----不影响基表
#sql:
CREATE VIEW 视图名称
AS 查询语句
#2)存储过程&存储函数
存储过程的命令,服务器端就可以把预先存储好的这一系列 SQL 语句全部执行
存储函数就是指定一段特定的逻辑,得到一个结果
| | 存储过程 | 存储函数 |
| :------: | :--------------: | :--------------------------: |
| 关键字 | procedure | function |
| 调用 | call 过程名() | selelct 函数名() |
| 返回值 | 0个或多个 | 只能一个 |
| 应用场景 | **一般用于更新** | **一般用于查询且返回一个值** |
#缺点
可移植性差、调式维护困难、不适合高并发场景
#3)触发器
触发相对应的增删改事件、就会自动执行触发器对应的操作
#sql:
CREATE TRIGGER 触发器名称
{BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON 表名
FOR EACH ROW
触发器执行的语句块;
1、Innodb和myIsam对比
#1)事务
InnoDB支持事务,MyISAM不支持。
对于InnoDB每一条SQL语言都【默认封装成事务】,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
#2)外键
InnoDB支持外键,而MyISAM不支持。
对一个包含外键的InnoDB表转为MYISAM会失败;
#3)聚簇索引
InnoDB是聚集索引,MyISAM是非聚集索引。
InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针
#4)count(*)
InnoDB不保存表的具体行数,执行需要全表扫描;而MyISAM用一个变量保存了整个表的行数,速度快。
#原因:
事务特性,同一时刻,不同事务对应的行数可能不同,会根据非空索引得到对应的行数
#5)锁
InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁
InnoDB的行锁是实现在索引上的,而【不是锁在物理行记录】上。潜台词是,如果访问【没有命中索引】,也【无法使用行锁】,将要退化为表锁。
#6)主键问题
InnoDB表必须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键),而Myisam可以没有
#7)文件结构
innodb:数据和索引都是存储在.ibd文件中
myIsam:数据和索引分开存储【数据---.MYD;索引---.MYI】
MyISAM 在某些情况下可能比 InnoDB 查询更快,主要有以下几个原因:
#1. 索引结构:
MyISAM 使用的是非聚集索引,而 InnoDB 使用的是聚集索引。非聚集索引存储的是索引字段和对应的行指针,而聚集索引存储的是完整的数据行。在某些查询场景下,MyISAM 可以通过非聚集索引直接定位到需要的数据,而不需要额外的访问磁盘读取数据行,从而提高查询速度。
#2. 缓存机制:
MyISAM 使用了基于文件的缓存机制,将数据行缓存在操作系统的文件缓存中。这样在查询时可以直接从内存中读取数据,避免了磁盘的访问延迟,提高了查询速度。而 InnoDB 使用的是基于页的缓存机制,数据存储在 InnoDB 缓冲池中,需要额外的管理和调度
#3. 事务和锁:
MyISAM 不支持事务和行级锁,而 InnoDB 支持。事务和锁对并发访问的控制带来了一定的开销,因此在某些情况下,MyISAM 可能由于没有这些额外的开销而表现更快。
1、请求执行流程
#1)查询缓存--8.0舍弃
缓存中有,结果直接返回客户端
没有,进入下一步-->解析器
#舍弃原因:
1、命中率低。
必须一摸一样,包括空格、注释、大小写等
使用函数,系统表也会导致不命中
2、会检测每张表,相关的增删改操作都会影响缓存,数据库压力变大
#2)解析器
语义(词法)解析
语法解析
预处理器--检查生成的解析树
#3)优化器
确定 SQL 语句的执行路径【连接顺序,条件顺序,索引使用】
#4)执行器
先判断是否有权限,然后执行(查询执行引擎,执行对应API)
1、缓存池buffer_pool
#1)为什么要有缓存池
1、Mysql 的 innodb 存储引擎是基于磁盘存储的,并且是按照页的方式进行管理的。
2、在数据库系统中,CPU 速度与磁盘速度之间的差距是非常大的,为了最大可能的弥补之间的差距,提出了缓存池的概念。
3、所以缓存池,简单来说就是一块「内存区域」,通过内存的速度来弥补磁盘速度较慢,导致对数据库造成性能的影响。
#2)原理
#「读操作」:
在数据库中进行读取页的操作,首先把从磁盘读到的页存放在缓存池中,下一次读取相同的页时,首先判断该页是不是在缓存池中。
若在,称该页在缓存池中被命中,则直接读取该页,否则,还是去读取磁盘上的页。
#「写操作」:
对于数据库中页的修改操作,首先修改在缓存池中的页,然后在以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘,而是通过 checkpoint 的机制把页刷新回磁盘。
可以看到,无论是读操作还是写操纵,都是对缓存池进行操作,而不是直接对磁盘进行操纵。
1、InnoDB存储结构(页,区,段,表空间)
#1)行
【行格式Compact简介】
1、【record_type】记录类型。0-普通记录;1-目录项记录;2-最小记录;3-最大记录
2、【next_record】下一条记录地址。相对本条记录的地址偏移量
3、各个列对应的数据
4、其他信息。包括其他隐藏列的值以及记录的额外信息
#种类
1、compact
2、Dynamic(默认)【行溢出。一页存不下一行数据,分开存】
3、Compressed【上一基础上做了压缩】
#2)页【16KB】
1、默认大小为16KB
2、【页和页】之间通过【双向链表】连接的,他的物理结构上不相邻的
3、【页内行】之间通过【单向列表】连接的,所有记录按照从小到大排序的
4、每个页都会生成一个【页目录】,查找记录时,会使用【二分法】快速定位对应的槽
#3)区【64页=1M】
1、一个区由一个或多个页组成
2、在InnoDB引擎中,一个区会分配【64个连续的页】,即区的大小【64*16kb = 1M】
#为啥有?
由于【页与页】之间【双向链表】连接,这就导致【相邻页】在磁盘上的【物理位置】可能离的比较【远】
B+树在使用【范围查询】时,需要定位最大与最小记录,可能需要加【载多个页到内存】中,
即需要多次【磁盘随机IO】,且磁盘速度相对于内存相差好几个量级
#4)段
1、一个段由一个或多个区组成,但【不要求】段中的区时相邻的
2、段是数据库的【分配单位】,不同类型对象以不同的段形式存在
例如:创建表---->表段
创建索引--->索引段
#为什么有?
在进行【范围查询】时,B+树【数据只存储在叶子节点】,从而要【区分】是否非叶子节点,导致效率变低,
所以InnnoDB对B+树的叶子节点和非叶子节点做了区分,分段存储,
即【一个索引】会生成【两个段】,【叶子节点段】和【非叶子节点段]
其他段:数据段、索引段、回滚段
#5)表空间
1、是逻辑容器,【一个表空间】对应【一个或多个段】,
但【一个段】只能属于【一个表空间】
2、从管理上划分:
系统表空间
用户表空间
撤销表空间
临时表空间
1、索引分类
#1)聚簇索引
聚簇索引【叶子节点】存放完整的【用户记录数据】
#2)二级(辅助、非聚簇)索引
聚簇索引【叶子节点】存放【主键】,不存数据
#3)联合索引(随a1、a2创建联合索引)
先对a1列排序,a1相等的情况下,在对a2列排序
#补充【B+创建过程】
#1)创建【非聚簇索引】,在一个页中【插入】用户记录(此页一直作为根节点)
#2)当页中【空间用完】时,将根节点的页,【复制一份】
【原来的页】充当【目录项的页】
【复制的页】充当【叶子节点的页】
#3)以此类推,生成三层、四层的B+树。
注意:最上层根节点一直固定不变【方便使用】
#所有分类
#1)功能逻辑
普通索引
唯一索引
主键索引
全文索引
#2)物理实现
聚簇索引【主键索引】
非聚簇索引【非主键索引】
#3)作用字段个数
单列索引
联合索引
#补充
#【回表操作】
因为叶子节点【没有】存放真实数据,
通过列找到对应的【主键】,
【根据主键】到聚簇索引找到对应的【数据】
#【索引下推】
1、SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';
2、不使用:
查找索引记录 --> 回表 --> 得到结果
3、使用:
查找索引记录 --> 根据其他索引过滤部分数据 --> 回表【数据变少】 --> 得到结果
4、好处:可以【减少】存储引擎访问基表【次数】,但效率取决于【过滤】掉的数据【比例】
1、hash、AVL、B、B+对比
#1)Hash结构
等值查询快,不适合范围查询
#2)AVL树(平衡二叉树)
最多两个子节点;左子节点、根节点、右子节点依次变大
所有节点的左右子树的【高度差不大于1】
#缺点:
数据量大,对应树【高度高】,意味着【磁盘IO】操作次数【多】,会影响效率
#3)B树(多路平衡查找树)
平衡二叉树演变而来【变成多叉树】
#缺点
叶子节点和非叶子节点【都存放数据】,导致【范围查询】效率变【慢】
#4)B+
相对B树,只有叶子节点存数据
#好处
#1)【查询效率更稳定】。B+树IO次数一定,只有叶子节点放数据;B树可能一次就找到,也可能多次。
#2)【查询效率更高】。由于B+树目录页不存放数据,即可以存放更多的数据,IO次数也会减少
#3)【范围查询效率更高】。由于B树非叶子节点也放数据,范围查找时,会返回上一次,效率会贬低
1、索引设计原则
------------【适合】-------------
#1)字段唯一的,创建唯一索引
【Alibaba】唯一性的字段,即使是组合字段,也要建成唯一索引
#2) 频繁作为 WHERE 查询条件的字段【√】
#3)经常 GROUP BY 和 ORDER BY 的列【√】
分组和排序都是【按照某种顺序】检索或者存储的,提过效率。
如果【排序有多个】,可以建立【组合索引】
#4) UPDATE、DELETE 的 WHERE 条件列
因为更新、删除操作,需要【先检索】出来对应的数据,在执行
如果【更新】的是【非索引字段】也会提高效率,因为【不需要对索引维护】
#5)DISTINCT 字段需要创建索引
#6)多表 JOIN 连接操作时,创建索引注意事项【√】
1、连接表的数量尽量不要超过3张,每增加一张表就相当于增加了一次嵌套的循环,数量级增长非常快
2、对 WHERE 条件创建索引
3、对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致。
#8)使用列的类型小的创建索引【表示的数据范围的大小】
1、数据类型越小,在查询时进行的【比较操作越快】
2、数据类型越小,索引【占用的存储空间就越少】,在一个数据页内就可以【放下更多的记录】,
从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
#8)使用字符串前缀创建索引
1、区分度计算公式:
count(distinct left(列名, 索引长度))/count(*)
2、【Alibaba】在 varchar 字段上建立索引时,必须指定索引长度,
没必要对全字段建立索引,根据实际文本区分度决定索引长度。
#说明:
索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上。
#9) 区分度高(散列性高)的列适合作为索引
1、列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有【9条记录】,但该列的【基数却是3】。
2、也就是说,在记录行数一定的情况下,列的【基数越大】,该列中的值【越分散】;列的【基数越小】,该列中的值【越集中】。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小的列建立索引效果可能不好。
可以使用公式【select count(distinct a)/count(*) from t1】计算区分度,【越接近1越好】,一般【超过33%】就算是比较【高效】的索引了。
#10)使用最频繁的列放到联合索引的左侧
#11)在多个字段都要创建索引的情况下,联合索引优于单值索引
##【限制】单张表的索引不超过6个
1、每个索引都需要【占用磁盘空间】
2、索引会【影响INSERT、DELETE、UPDATE】等语句的性能,
因为表中的数据更改的同时,索引也会进行调整和更新
3、会增加MySQL优化器生成执行计划时间,需判断使用那个索引
---------【不适合】-----------
#1)在where中使用不到的字段,不要设置索引
#2)数据量小的表最好不要使用索引
#3)有大量重复数据的列上不要建立索引
#2)避免对经常更新的表创建过多的索引
#4) 不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等
#5)删除不再使用或者很少使用的索引
#6)不要定义冗余或重复的索引
1、分析查询语句:EXPLAIN
列名 | 值 | 描述 |
---|---|---|
id | 1、2、3 | 每个select对应一个或多个 唯一id |
select_type | SIMPLE、PRIMARY、SUBQUERY | 对应的查询类型 |
table | 表名、<union1,2> | 表名 |
partitions | 分区信息 | |
type | system、const、ref | 针对单表的访问方法(重要) |
possible_keys | 可能用到的索引 | |
key | 实际上使用的索引 | |
key_len | 实际使用到的索引长度(主要联合索引) | |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 | |
rows | 预估的需要读取的记录条数 | |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 | |
Extra | 一些额外的信息 |
#1)id
查询中【每一个select】关键字,mysql都会为他【分配一个唯一的id值】
相同级别查询id相等,子查询id变大
#2)type
结果值从最好到最坏依次是(至少range级别):
system > const > eq_ref > ref >
fulltext > ref_or_null > index_merge > unique_subquery >
index_subquery > range > index > ALL
#3)key
1、表示【实际用到】的索引有哪些,
2、如果为NULL,则没有使用索引
#4)key_len
1、【实际】使用到的【索引长度】(即:字节数)
2、越小索引效果越好
3、但是在【联合索引】里面,命中一次key_len加一次长度。【越长代表精度越高】,效果越好
#5)rows
1、预估的需要读取的记录条数
2、值越小,代表数据越有可能在一个页里面,这样IO就会更小。
3、通常与filtered一起使用
#6)filtered
1、越大越好!
2、指【返回结果的行】占需要【读到的行】(rows 列的值)的百分比
3、对于【单表查询】来说,这个filtered列的值【没什么意义】,
我们更关注在连接查询中驱动表对应的执行计划记录的filtered值,它决定了【被驱动表】要执行的【次数】(即:rows * filtered)
#7)extra
一些【额外信息】的,包含不适合在其他列中显示但十分重要的额外信息
1、索引失效场景
#1)全值匹配我最爱
select * from student WHERE age = 1 and name = 'a' and classId = 1;#时间: 0.503s
CREATE INDEX idx_age ON student(age ) ; #单个索引
select * from student WHERE age = 1 and name = 'a' and classId = 1;#时间: 0.136s
CREATE INDEX idx_age_classid ON student( age , classId);#两个联合索引
select * from student WHERE age = 1 and name = 'a' and classId = 1;#时间: 0.037s
CREATE INDEX idx_age_classid_name ON student( age , classId , name) ;#三个联合索引
select * from student WHERE age = 1 and name = 'a' and classId = 1;#时间: 0.037s
#2)最左匹配原则【索引顺序age、classId、name】
EXPLAIN SELECT * FROM student WHERE age=30 AND name = 'abcd';# 走idx_age索引
EXPLAIN SELECT * FROM student WHERE classid=1 AND name = 'abcd' ;# ALL
# 走idx_age_classid_name索引
EXPLAIN SELECT * FROM student WHERE classid=4 and age=30 AND name = 'abcd' ;
说明:【过滤条件】使用的索引必须【按照索引创建的顺序】,依次满足
一旦【跳过】那个索引字段,【后面的索引】字段都会【失效】
#3)计算、函数、类型转换(自动或手动)导致索引失效
CREATE INDEX idx_name ON student (NAME) ;
#使用函数【无法确定返回值,即没法确定使用那个索引】
EXPLAIN SELECT * FROM student WHERE LEFT(student.name,3) = 'abc';
CREATE INDEX idx_sno ON student(stuno);
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;#做计算
# name字符串类型,需要隐式函数转换
EXPLAIN SELECT * FROM student WHERE name=123;
#4)范围查询,右侧索引列失效【联合索引】
#范围查询。第三个索引失效
EXPLAIN SELECT * FROM student
WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
#5)不等于(!= 或者<>)索引失效
#6)is null可以使用索引,is not null无法使用索引
#7)like以通配符%开头索引失效
#8)OR 前后存在非索引的列,索引失效
原因:取并集,由于最少有一个全表扫描,索引就没有意义
EXPLAIN SELECT * FROM student WHERE age = 10 OR classid = 108;
1、关联查询优化
#1)join原理
#本质
join方式连接多个表,本质就是各个表之间数据的【循环匹配】
#内连接&&外连接
内连接:
优化器会根据你查询语句做【优化】,决定先查哪张表
外连接:
通常【主表】是【驱动表】,【连接表】是【被驱动表】
#注意:有时候会【优化为内连接】
#2)索引原则
1、内连接的【主被驱动表】是由优化器决定的。优化器认为哪个【成本比较小】,就采用哪种作为【驱动表】。
2、如果两张表只有一个有索引,那有索引的表作为被驱动表。
原因:驱动表要全查出来。有没有索引你都得全查出来。
3、两个索引都存在的情况下, 数据量大的作为被驱动表(小表驱动大表)
原因:驱动表要全部查出来,而大表可以通过索引加快查找
#3)join执行方式
1、Simple Nested-Loop Join(简单嵌套循环连接)
驱动表去一条数据,遍历被驱动表
2、Index Nested-Loop Join(索引嵌套循环连接)
优化的思路主要是为了【减少内层表数据的匹配次数】,所以要求【被驱动表】上必须【有索引】才行
3、Block Nested-Loop Join(块嵌套循环连接)
#1)原:
每次访问被驱动表,加载到内存-->匹配——>清除-->再次访问,加载到内容。。。。
# 大大增加了IO的次数
#2)现:【缓存区,批量匹配】
一块数据-->join buffer-->批量匹配-->下一块。。。。
#一块一块获取,引入【join buffer缓冲区】
4、Hash Join(8.0)
建立hash表,首次会比较慢
1、count(*)和select( *)
#1) COUNT(*)与COUNT(具体字段)效率【前提,非空字段】
1、SELECT COUNT(*)和SELECT COUNT(1)
InnoDB:系统自动采用占空间最少的二级索引,没有索引采用主键统计
MyISAM:数据表的mate信息存储row_count值,一致性由表级锁保证
2、SELECT COUNT(具体字段)
InnoDB:【尽量使用二级索引】,因为主键索引包含真实数据,【信息多】,效率慢
MyISAM:同上
#2) SELECT(*)
1、MySQL 在【解析、的过程中,会通过 【查询数据字典】 将【*】按序【转换】成【所有列名】,这会大大的耗费资源和时间。
2、无法使用 覆盖索引
1、主键设计
#1)自增主键问题
1、可靠性不高。自增回溯,8.0版本解决
2、安全性不高。对外暴露的接口容易猜测对应的信息,例【/uesr/1】,猜测用户id,进行数据爬取
3、性能差。需要在服务端生成
4、交互多。额外执行last_insert_id(),去获取自增的id
5、局部唯一性。只对当前数据库唯一,分布式系统来说,是噩梦
#2)业务字段作为左键
建议尽量不要用跟业务有关的字段做主键。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。
#3)淘宝订单
订单ID = 时间 + 去重字段 + 用户ID后6位尾号
#4)UUID
1.组成【UUID = 时间+UUID版本(16字节)- 时钟序列(4字节) - MAC地址(12字节)】
2.全局唯一
时间:从1582-10-15 00:00:00.00到现在的100ns的计数【重复率1/100ns】
时钟序列:避免时钟回拨导致产生的时间重复
MAC地址:全局唯一
3、36字节【32字符 + 4个短红线】
4、无序【时间最低为放在前面】
#改造UUID:
将时间高低位互换,时间递增,即UUID递增【8.0】
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
# uuid_to_bin(@uuid) 转成16进制存储
# uuid_to_bin(@uuid,TRUE); 修改成先高位 中位 地位,就可以保证uuid递增了
1、三大范式
#1)概念
1、超键:能唯─标识元组的属性集叫做超键。【非空唯一字段和其他字段组合】
2、候选键:如果超键不包括多余的属性,那么这个超键就是候选键。【特殊超键】
3、【重】主键:用户可以从候选键中选择一个作为主键。
#最少字段数,去唯一确定一条数据---可以为多个字段
4、外键:如果数据表R1中的某属性集不是R1的主键,而是另一个数据表R2的主键,那么这个属性集就是数据表R1的外键。
5、主属性:包含在任一候选键中的属性称为主属性。【候选键属性】
6、非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性。
#候选键 --> 码;主键 --> 主码
#2)第1范式(原子性)
确保数据表中每个字段的值必须具有【原子性】,
也就是说数据表中每个字段的值为【不可再次拆分】的最小数据单元。
#3)第2范式
1、满足第一范式(1NF)
2、满足数据表里的每一条数据记录,都是可【唯一标识】的。
而且所有【非主键字段】,都必须完全【依赖主键】,不能只【依赖主键的一部分】
#例子:
比赛表(球员编号、姓名、年龄、比赛编号、时间和场地)
# 姓名和年龄部分依赖球员编号。
(球员编号) → (姓名,年龄)
# 比赛时间, 比赛场地部分依赖(球员编号, 比赛编号)。
(比赛编号) → (比赛时间, 比赛场地)
#4)第3范式
1、满足第二范式(2NF)
2、要求数据表中的所有【非主键字段】不能依赖于【其他非主键字段】
#例子
商品表(商品主键id、类比id、类别名称、商品名称、价格)
#非主键类别名称依赖与类别id【违反3NF】
#改进:
商品表(商品主键id、类比id、商品名称、价格)
分类表(类比id、类别名称)
1、水平切分、垂直切分
#1)垂直切分
按照业务将表分类,分布到不同的数据库上面
#优点:
拆分后业务清晰,拆分规则明确
系统之间整合或者扩展容易
数据维护简单
#缺点:
部分业务表无法join,只能通过接口方式解决,提高了系统复杂度
受每种业务不同的限制存在单裤性能瓶颈,不易数据扩展跟性能提高。
事务处理复杂
#2)水平切分
同一个表拆到不同的数据库中
1、事务四大特性
#1)原子性(atomicity)
要么全部提交
要么全部失败回滚
【不存在中间状态】
#2)一致性(consistency)
事务执行前后,数据从一个【合法性状态】变换到另外一个【合法性状态】---业务合法
#例子
1、账户余额200,转出300
2、A给B转账,A和B的总数要保持一致
3、唯一约束
#3)隔离型(isolation)
一个事务的执行【不能被其他事务干扰】
即一个事务内部的操作及使用的数据对【并发】的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
#4)持久性(durability)
一个事务一旦被提交,它对数据库中数据的改变就是【永久性的】
持久性是通过【事务日志】来保证的。日志包括了【重做日志】和【回滚日志】
1、事务隔离级别
#1)脏写
事务Session A【修改】了另一个【未提交】事务Session B【修改】过的数据
#2) 脏读
事务Session A【读取】了Session B【已更新】但是【未提交】的数据
#3)不可重复读
事务Session A【读取】数据
事务Session B【更新】数据
然后,Session A【读取】数据----读取数据不同
#4)幻读
事务Session A【读取】数据
事务Session B【插入】数据
然后,事务Session A【读取】数据 -- 幻读
1、事务日志(redo/undo log)
#1)定义:事务的【原子性、一致性、持久性】是由【redo log、undo log】实现的
1、REDO LOG 称为【重做日志】,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的【持久性】----物理级别
2、UNDO LOG 称为【回滚日志】,回滚行记录到某个特定版本,用来保证事务的【原子性、一致性】---逻辑操作
#2)redolog【重做日志】
#执行流程
1、先将【磁盘上】的数据页加载到内存的【buffer pool】中
2、所有【数据变更】,【先更新buffer pool】数据,
3、在将【脏页】数据以一定频率刷新到磁盘【checkPoint机制】
#刷盘策略
1、设为0:commit时,不进行刷盘【系统默认master thread每1s进行一次同步】
后台线程每1s同步一次数据,有丢失数据风险
2、设为1:commit时,都进行同步,刷盘操作【默认】
只要事务提交成功,redo log记录一定在磁盘中,数据不会丢失
3、设为2:commit时,只把redo buffer写入page cache中【os自己决定同步磁盘时间】
事务提交后,写入page cache中
数据库挂了不会丢失数据,os挂了可能会丢失1s内数据
#checkPoint检查点机制
write pos:当前位置【一边写一边后移】
checkpoint:要擦除的位置【循环后移】
#注意:
如果 write pos 追上 checkpoint ,表示日志文件组满了
需要等待最前事务提交,清除部分记录
#3)undolog【回滚日志】
#作用
1、回滚数据:逻辑操作【做相反的操作】,不是恢复原来状态【例数据结果、页回滚后大不相同】
2、MVCC:主要实现读不加锁,写加锁【提高性能】
1、锁
X锁 | S锁 | |
---|---|---|
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
1、表锁
表锁
锁类型 | 自己可读 | 自己可写 | 自己可操作其他表 | 他人可读 | 他人可写 |
---|---|---|---|---|---|
读锁 | 是 | 否 | 否 | 是 | 否,等 |
写锁 | 是 | 是 | 否 | 否,等 | 否,等 |
意向锁
意向共享锁(lS) | 意向排他锁(IX) | |
---|---|---|
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
意向共享锁(lS) | 意向排他锁(IX) | |
---|---|---|
共享锁(S)表 | 兼容 | 互斥 |
排他锁(X)表 | 互斥 | 互斥 |
#1)表锁
#说明
1、增删改查【DQL】时,【不会】添加表级锁
2、alter、drop【DDL】时,其它事务【并发执行增删改查】语句会发生【阻塞】
#2)意向锁-存储引擎自己维护
#定义
在加入行锁时,会在加一个表锁叫意向锁
原因:因为在【获取表锁】时,需要【判断】表中有没有存在锁,
如果没有意向锁,需要【遍历】所有数据去看【有没有行锁】,效率低
#总结:
1、【多粒度锁】,支持行级锁和表级锁共存
2、【意向锁之间】互补【排斥】
【IS锁】和【S锁】兼容之外,【其它】意向锁和表级锁【排斥】
3、提高了【并发】性能,同时实现表锁和行锁【并存】,满足事务【隔离性】的要求
#3)自增锁
AUTO-INC锁是当向使用含有【AUTO_INCREMENT列】的表中插入数据时需要获取的一种【特殊】的【表级锁】
【插入】数据时,获取一个【自增锁】,每条记录的auto_increment【递增】,结束后所【释放】
#在事务执行中,其它事务插入要阻塞,每条语句都要对表锁进行竞争【并发低】
#4)元数据锁-自动控制
#定义
当对一个表做【增删改查操作】的时候,加 MDL【读锁】
当要对表做【结构变更】操作的时候,加 MDL 【写锁】
解决了DML和DDL操作之间的一致性问题。
不需要显式使用,在访问一个表的时候会被自动加上。
1、行锁
#1)定义
行锁(ROW LOCK),又叫记录锁。就是锁住一行数据【在存储引擎层实现的】
#2)优缺点
#优:力度小,降低锁冲突概率,提高并发性能
#缺:锁开销表较大,枷锁会很慢,容易出现死锁
#1)记录锁
对某条记录加锁
#2)间隙锁
id=8数据加间隙锁,不允许别的事务在id值为 8 的记录【前边的间隙】插入新记录,【即3-8之间的数据】
虽然有【共享gap锁】和【独占gap锁】这样的说法,但是它们起到的【作用是相同的】
#例子
事务1查询id=5记录,没有记录,加间隙锁(3,8)
事务2再给间隙锁加锁,【不会阻塞】
#注意:
插入数据时,在这条记录前面的间隙,会阻塞
#3)临建锁
既要【锁住该记录】,又要阻止其他事务在记录前面【间隙插入数据】,产生Next-Key Locks的锁
select * from student where id <= 8 and id > 3 for update;
#是一个【记录锁】和一个【gap锁】的合体
#4)插入意向锁
事务【插入】记录时,判断是否加了【gap间隙锁】,有需要等待
innoDB在【等待的时候会】在内存中【生成一个锁结构】,即插入意向锁
1、MVCC-快照读&当前读
#1)快照读
又叫一致读,读取的时快照数据【读历史版本】
#2)当前读
读取的是记录的最新版本【最新数据,而不是历史版本的数据】
读取时还要保证其他并发事务不能修改当前记录,会对【读】取的记录进行【加锁】
1、MVCC-Undo Log版本链
#1))聚簇索引记录中两个隐藏列
1.trx_id:每次一个事务对某条聚簇索引记录进行【改动】时,都会把该事务的事务id【赋值】给trx_id隐藏列。 #----事务id
2.roll_pointer:每次对某条聚簇索引记录进行改动时,都会把【旧的版本】写入到【undo】日志中,然后这个隐藏列就相当于一个指针,可以通过它来【找到】该记录【修改前的信息】。 #----旧版本undo log地址指针
#2)原理
每次对记录改动时,都会产生一个undo日志,每个日志都有一个roll_pointer
把这些undo日志连起来,形成一个链表【版本链】
1、MVCC-ReadView
#1)由来
在MVCC机制中,多个事务对【同一条记录】进行【改动】,会产生多个【历史快照】
#如果一个事务想要查询这个记录,需要读取那个版本记录?
ReadView,解决行可见性
#2)定义
事务在使用MVCC机制进行【快照读】产生的【视图读】。
#事务启动时,回生成当前的快照。每个事务构建一个数组,用来记录维护当前活跃事务的ID
#3)判断规则
#1)当前trx_id值 = ReadView中creator_trx_id
访问自己修改的记录【可以】
#2)当前trx_id值 < up_limit_id【最小活跃事务id】
说明事务都已提交【可以】
#3)当前trx_id值 >= low_limit_id
所有的trx_ids都未提交提交【不能】
#4)当前trx_id值在up_limit_id和low_limit_id之间
1、存在:事务在活跃【不能】
2、不存在:事务已提交【可以】
#4)说明
lnnoDB中,MVCC是通过Undo Log + Read View进行数据读取,Undo Log保存了历史快照,而Read View规则帮我们判断当前版本的数据是否可见。
#5)总结
# RC 和 RR
READ COMMITTD【RC】在每一次进行普通SELECT操作前都会生成一个ReadView
REPEATABLE READ【RR】只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
1、定位慢查询
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9iOQkbV-1689324564366)(http://43.143.239.200:9000/monkey/bookImages/image-20230529215254805.png)]
1、超大分页处理
1、sql优化
1、主从复制原理
1、分库分表
#1)
#2)
#3)
#4)
六、Spring
1、实例化(创建)bean的三种方式
1.构造方法(常用)
<!--name:为bean指定别名,别名可以有多个,使用逗号,分号,空格进行分隔-->
<bean id="bookService" name="service1 service2 service3" class="com.blog.service.impl.BookServiceImpl">
</bean>
2.静态工厂(了解)
<bean id="bookDao" class="com.blog.factory.BookDaoFactory" factory-method="getBookDaoImpl"/>
3.实例工厂(了解)
<bean id="bookDaoFactory" class="com.blog.factory.BookDaoFactory"/>
<bean id="bookDao" factory-bean="bookDaoFactory" factory-method="getBookDaoImpl"/>
【作用:】第三方库、创建过程比较繁琐时(例如:mybatis中的SqlSessionFactory)
4.FactoryBean(实用)
<bean id="bookDao" class="com.blog.factory.BookDaoFactoryBean"></bean>
实现:创建一个BookDaoFactoryBean类,实现FactoryBean接口,重写接口方法
public class BookDaoFactoryBean implements FactoryBean<BookDao> {
public BookDao getObject() throws Exception {
return new BookDaoImpl();
}
public Class<?> getObjectType() {
return BookDao.class;
}
}
5.使用@Component,@Service,@Controler,@Repository注解
6、@Configuration + @Bean
7、注解@Import创建单个bean或者配置类
8、使用ImportSelector或者ImportBeanDefinitionRegistrar接口,配合@Import实现
1)实现ImportSelector接口,重写selectImports方法,返回需要创建的bean数组全类名
2)实现ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法,注册到BeanDefinition中
#1)创建规则
只有无参构造方法-----默认执行
有参、无参都有----执行无参
只有一个有参---执行(参数必须由spring管理)
多个有参----加一个Autowired---执行
|---都没有加----报错
|---加多个------报错
1、bean生命周期
#1)初始化容器
1.创建对象(内存分配)
2.执行构造方法
3.执行属性注入(set操作)(set ...)
4.执行bean初始化方法(service init ...)
#2)使用bean
执行业务操作(book dao save ...)
#3)关闭/销毁容器
执行bean销毁方法(service destroy ...)
关闭两种方式:
ConfigurableApplicationContext是ApplicationContext的子类,子类才有下面两种方法
①close()方法
②registerShutdownHook()方法
-------------------------------
初始化阶段:
#1)Bean实例的属性注入
①普通属性注入(反射set注入)
②单向引用属性注入(有,反射set注入;没有,先创建需注入的属性)
③双向引用属性注入(循环依赖)
#2)Aware接口属性注入
#3)BeanPostProcessor的before()方法回调
#4)实现InitializingBean接口的初始化方法
#5)自定义初始化方法的回调
#6)BeanPostProcessor的after()方法回调
1、依赖注入DI
1.setter方式----基本数据类型、引用数据类型
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
<property name="dataBaseName" value="mysql"></property>
<property name="connectionCount" value="100"></property>
</bean>
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"></property>
<property name="userDao" ref="userDao"></property>
</bean>
2.构造器方式----基本数据类型、引用数据类型
<!-- 方式1:标准注入-->
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"></constructor-arg>
<constructor-arg name="connectionNum" value="100"></constructor-arg>
</bean>
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>
<!-- 方式2:参数类型注入-->
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
<constructor-arg type="java.lang.String" value="mysql"></constructor-arg>
<constructor-arg type="int" value="9421"></constructor-arg>
</bean>
<!-- 方式3:参数index注入-->
<bean id="bookDao" class="com.blog.dao.impl.BookDaoImpl">
<constructor-arg index="0" value="mysql"></constructor-arg>
<constructor-arg index="1" value="9421"></constructor-arg>
</bean>
3.注解注入@Autowire、@Resource
1、自动装配
1、按类型
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl" autowire="byType"/>
2.按name
<bean id="bookService" class="com.blog.service.impl.BookServiceImpl" autowire="byName"/>
#注意:
1、自动装配用于引用类型依赖注入,不能对简单类型进行操作
2、使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
3、使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
4、自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效
1、循环依赖
//DefaultListableBeanFactory的四级父类DefaultSingletonBeanRegistry:
#1)源码
#【一级缓存】成品 -- 实例化和初始化都完成
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
#【三级缓存】半成品 -- 实例化,但没有被引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
#【二级缓存】半成品 -- 实例化,已经被引用
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
#2)【假设】加入缓存Map(假设A、B形成循环依赖)
1、A实例化时,先存缓存中(没有属性注入)
2、A属性注入时,依赖B--->即实例化B对象
2.1 B实例化--->依赖注入--->先去一级缓存中找--->缓存找--->完成属性注入
2.2 。。。。。
2.3 B放入单例池
3、。。。。
4、AOP
5、A放入单例池
#只有二级缓存问题:
第4步时:aop会生成一个【A的代理对象】,但是B对象在属性注入已经注入【A的普通对象】
单例池只有有一个----出现问题
#3)【二级缓存】解决AOP(提前执行AOP操作)---出现循环依赖才会提前【earlySingletonObjects二级缓存】
0、#creatingSet<beanName> #存放正在执行的bean名字
1、A实例化时--->A普通对象
2、A属性注入时,依赖B--->即实例化B对象
2.1 #B实例化--->依赖注入--->先去一级缓存中找---->
creatingSet判断是否循环依赖--->二级缓存找-->AOP--->A代理对象【需要A普通对象】--->放入二级缓存
2.2 。。。。。
2.3 B放入单例池
3、。。。。
4、AOP
4.5#earlySingletonObjects.get<A> #从二级缓存中取
5、A放入单例池
6、#creatingSet.remove<beanName>
#问题
没有打破循环依赖
#4)【三级缓存】--singletonFactories
0、creatingSet<beanName> #存放正在执行的bean名字
1、A实例化时--->A普通对象-->#singletonFactories.put(A, ()->getEarlyBeanReference(beanName,bd,A普通对象))
2、A属性注入时,依赖B--->即实例化B对象
2.1 #B实例化--->依赖注入--->先去一级缓存中找---->
creatingSet判断是否循环依赖--->二级缓存找--->三级缓存中找-->执行lambda
-->AOP--->A代理对象【需要A普通对象】--->放入二级缓存
2.2 。。。。。
2.3 B放入单例池
3、。。。。
4、AOP
4.5earlySingletonObjects.get<A> #从二级缓存中取
5、A放入单例池
6、creatingSet.remove<beanName>
#三级缓存只有出现循环依赖才会用到
#5)earlyProxyReferences 这个map控制aop执行时机
在循环依赖时,会提前进行aop操作,放在map中
在后面进行aop时,需要判断remove删除的bean是否和当前相等
一样:代表已经执行aop
为空不想等:执行aop
#6)@Async 和 @Lazy
问题:
A属性填充B出现循环依赖,但是没有aop操作,即得到二级缓存放的是普通对象
@Async自己定义代理类生成(和aop生成动态代理不同),在源码中,出现循环依赖,后面的对象不容许发生改变【报错】
在注入的属性加@Lazy注解,就不会出现循环依赖问题【创建对象的代理对象】
1、Aware方法回调
接口 | 回调方法 | 作用 |
---|---|---|
BeanFactoryAware | setBeanFactory(Beanfactory beanfactoy) | 回调注入beanfactory |
BeanNameAware | setBeanName(String beanName) | 回调注入beanName |
ApplicationContextAware | setApplicationContext(ApplicationContext context) | 回调注入applicationContext |
1、BeanFactoryPostProcess和BeanPostProcess
#1)BeanFactoryPostProcess【子接口BeanFactoryRegistryPostProcess--用于注册】---自定义注解实现
在BeanDefinition填充之后,Bean实例化之前
#2)BeanPostProcess ---日志实现
在Bean实例化之后,填充到singletonObjects之前
#执行顺序
1、对象实例化
2、执行实现BeanPostProcess接口的before方法
3、执行实现InitializingBean初始化接口的afterPropertiesSet方法(重写的初始化方法)
4、执行自定义初始化init方法
5、执行实现BeanPostProcess接口的after方法
#应用场景:
#BeanFactoryPostProcess:
【占位符】:配置文件properties中占位符的替换操作
【springboot自动配置】:解析@Import注解,加载对应的bean
#BeanPostProcess:
AOP
1、BeanFactory&FactoryBean
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hVXhCpcZ-1689324564370)(http://43.143.239.200:9000/monkey/bookImages/image-20220507234253462.png)]
- 实现FactoryBean接口【mybatis、feign使用】
//自定义创建Bean
public class MyFactoryBean implements FactoryBean<Person> {
@Override
public Person getObject() throws Exception {
//自定义创建Bean,想咋实现就咋实现,不需要遵循标准流程
return new Person();
}
@Override
public Class<?> getObjectType() {
return Person.class;
}
}
1、AOP-Cglib
class UserProxy extend User{ //继承用于类型强转
User target; //注入普通对象(因为代理对象不会属性注入)
//重写父类方法
public void test(){
//执行切面逻辑
。。。。。。
//事务
@Transactional开启事务
1、新建一个数据库连接conn
2、conn.autocommit = false
//执行普通方法
target.test();
//提交事务
conn.commit
}
}
1、事务-实现BeanPostFactory接口进行增强
#1)原理
1、将所有jdbc操作交由spring统一事务管理器管理
2、执行事务方法时,会生成代理类对象
3、代理类判断是否存在@Transactional事务注解
4、有---新建一个数据库连接,并设置自动是否提交为false
5、执行事务对应的方法
6、提交事务
#2)事务方法内部调用其他事务的方法-----其他事务失效原理
1、当前方法会产生一个代理类
2、但是代理类调用的是普通方法,即对应内部方法也是普通方法
3、即不会判断是否有事务注解
#3)解决方法
1、拆分类---注入对应方法
2、自己注入自己,调用其注入的内部方法【常用】
1、@Configuration原理
#1)不加注解--事务问题
1、代理类创建的conn连接放在 ThreadLocal<Map<DateSource, conn>>中
由于一个方法,可能有多个dataSource,执行sql1、sql2使用不同的数据源
2、不加注解
jdbcTemplate和transactionManager都会去创建dataSource数据源--事务失效
#2)原理
class UserProxy extend User{
#jdbc
public void jdbcTemplate(){
super.jdbcTemplate() #执行代理类的方法
}
#dataSource
public void dataSource(){
super.dataSource() #执行代理类的方法
}
}
#3)说明
1、@Configuration注解动态代理
2、执行代理对象的方法
3、注入dataSource时,先判断是否存在,没有再创建
1、refresh方法流程(12个)
#1)prepareRefresh()---准备一些环境信息
【java信息】java版本、默认编码
【操作系统信息】JAVAHOME、JAVAPATH
【自定义property信息】自定义yaml、property配置文件对应的key-value(用@value注解读取)
#2)obtainFreshBeanFactory()
创建beanFactory容器
加载bean定义信息到BeanDefinition对象,主要通过xml、配置类、组件扫描将bean的定义信息,存入对象中
#3)prepareBeanFactory(beanFactory)
创建bean的一些准备工作,对beanfactory属性设置默认值
#4)postProcessBeanFactory()
留给子类实现(例如web环境下,request、session实现)
#5)invokeBeanFatoryPostProcessor
执行后置处理器(可用来修改或补充BeanDefinition、占位符替换)
ConfigurationClassPostProcesssor-解析@Configuration、@Bean、@Import、@PropertySource
#6)registerBeanPostProcessor
会注册一些BeanPostProcessor,用于功能增强
--AutowiredAnnotationBeanPostProcessor用于处理@AutoWired注解
--AnnotationAwareAspectJAutoProxyCreater用于@Aspect注解,切面使用
#7)initMessageSource【国际化】
#8)initApplicationEventMulticaster【广播器--发消息】
#9)onfresh【子类实现--springboot内嵌web容器】
#10)registerListeners【时间监听-收消息】
#11)finishBeanFactoryInitialization
创建所有非延时、单例Bean
#12)finishRefresh
来控制容器中需要生命周期管理的Bean--lifecycleProcesssor
1、事务失效场景(8种)
#1)抛出检查异常【例如FileNotFoundException】
原因:Spring默认回滚非检查异常(Error、RunTimeException及其子类)
解决:添加rollbackFor属性,指定异常回滚Exception
#2)try-catch块
原因:对该方法代理,外部环绕通知无法得知内部是否异常,扑捉到异常才能回滚
解决:1、catch中抛出异常
2、使用TranslationalInterceptor.currentTransactionStatus().setRollBackOnly()
#3)自定义AOP切面顺序
原因:事务优先级最低,自定义aop切换在其内测,如果没有抛出异常,也会导致最外层事务没有捕捉到异常,回滚失败
解决:1、使用2中方式【推荐】
#4)加在非public方法
原因:spring默认的
解决:方法必须是public修饰
#5)父子容器导致
原因:子容器扫描范围过大,把一些没有事务配置的Bean扫描进来
解决:1、各自扫各自的
2、只设置一个容器
#6)调用本类的方法导致事务传播行为失效
原因:本类方法的调用不经过代理,因为无法增强
解决:1、自己注入自己,来调用方法
2、AopContext拿到当前类的代理对象
#7)@Transactional不能保证原子性
原因:
解决;
#8)
原因:
解决;
1、常用注解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2g0EbYE-1689324564371)(http://43.143.239.200:9000/monkey/bookImages/image-20230531222534015.png)]
#1)
#2)
#3)
#4)
#5)
#6)
1、
#1)
#2)
#3)
#4)
#5)
#6)
七、Spring MVC
1、拦截器
#1)区别(过滤器 & 拦截器)
归属不同:Filter属于Servlet技术,而Interceptor属于SpringMVC技术
拦截内容不同:Filter对所有访问进行增强,Interceptor仅对SpringMVC的访问进行增强
1、执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NLdq8Jq3-1689324564372)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201113165007864.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjExODk4MQ%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70%23pic_center&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1672728104&t=ecc0e1bed168deeafa2e536d588fe68f)]
八、Mybatis
1、FactoryBean
@Component
public class MonkeyFactoryBean implements FactoryBean {
private SqlSession sqlSession;
@Autowired
public MonkeyFactoryBean(SqlSessionFactory sqlSessionFactory){
sqlSessionFactory.getConfiguration().addMapper(UserMapper.class);
this.sqlSession = sqlSessionFactory.openSession();
}
@Override
public Object getObject() throws Exception {
//生成mapper代理对象
return sqlSession.getMapper(UserMapper.class);
// Object o = Proxy.newProxyInstance(MonkeyFactoryBean.class.getClassLoader(),
// new Class[]{UserMapper.class},
// new InvocationHandler() {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// return null;
// }
// });
// return o;
}
@Override
public Class<?> getObjectType() {
return UserMapper.class;
}
}
1、入门
public static void main(String[] args) throws IOException {
//1. 加载mybatis的核心配置文件,获取 SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,用它来执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 执行sql,这步不用记,一般这步都是使用mapper代理开发 sqlSession.getMapper(UserMapper.class);
//参数是一个字符串,该字符串必须是映射配置文件的namespace.id
//List<User> users = sqlSession.selectList("test.selectAll");
//3.1获取UserMapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(users);
//4. 释放资源
sqlSession.close();
}
1、执行流程
1、延迟加载
1、mybatis缓存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eabRJ0ug-1689324564373)(http://43.143.239.200:9000/monkey/bookImages/image-20230531223414098.png)]
1、涉及的涉及模式
1)缓存-装饰器模式
#1)perpetualCache处理简单,需要对其做增强
1、数据淘汰机制
2、缓存的存放机制
2)日志-适配器模式、代理模式
#1)Log接口
不同日志框架实现,对应不同的处理
#2)jdbc操作日志处理
实现InvocationHandler接口,重写invoke方法,实现代理增强
3)工厂模式
#1)数据源工厂、SqlSessionFactory
4)构建者模式
#1)SqlSessionFactoryBuilder
1、SqlSessionFactory理解
#1)定义
是Mybatis的一个非常核心的API,是一个SqlSessionFactory工厂,用来创建SqlSession对象
#2)特点
只有一份,即单例对象
其创建是通过SqlSessionFactoryBuilder来构建的,在完成SqlSessionFactory的创建同时,会把全局的配置文件和相关的映射文件加载解析,保存在Configuration配置类中
1、SqlSession理解
#1)定义
是Mybatis的一个非常核心的API,通过相关的API来实现对应数据库数据的操作
#2)特点
获取通过SqlSessionFactory来实现的,是一个会话级别,
当一个新的会话来的时候,就会创建一个SqlSession对象
当一个会话结束,需要关闭对应的会话资源
#3)请求处理方式
1、通过对应增删改查API直接处理
2、通过getMapper(xxx.class)来获取相关的mapper接口代理对象来处理
1、Mybatis理解
#1)工作中使用频率最高的一个ORM框架、持久层框架
#2)提供很多的方便的API来实现增删改查操作
#3)支持灵活的缓存方案,一级、二级缓存
#4)提供非常多的xml标签去实现复杂的业务,if foreach where trim set
#5)相对Hibernate会更加灵活
1、Mybatis分页原理
#1)逻辑分页:RowBonds
#2)物理分页:拦截器
分页插件的原理就是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内,拦截待执行的SQL,然后根据设置的dialect(方言),和设置的分页参数,重写SQL ,生成带有分页语句的SQL,执行重写后的SQL,从而实现分页
1、SqlSession安全问题
#1)原因
SqlSession实现DefaultSqlSession是线程不安全的
#2)spring整合实现线程安全
spring中提供一个SqlSessionTemplate实现SqlSession,通过SqlSessionProxy代理对象对其进行相关的操作,实现方法级别的线程安全
1、Mybatis插件
#1)MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
2、ParameterHandler (getParameterObject, setParameters)
3、ResultSetHandler (handleResultSets, handleOutputParameters)
4、StatementHandler (prepare, parameterize, batch, update, query)
#2)些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
1、传统JDBC不足
#1)频繁的创建和释放数据库连接的资源,造成系统资源的浪费,从而影响系统的性能
解决:mybatis全局配置数据库连接池,spring也可以配置相关的数据库连接池
#2)SQL直接添加到代码中,造成维护成本比较高
解决:SQL与代码分离,mybatis提供映射文件,通过对应标签来写我们的SQl
#3)向SQL中传递参数也很复杂
解决:mybatis的Java对象和sql参数的映射
#4)对于结果集的映射也很麻烦,SQl本身的变化导致解析难度
解决:通过ResutSetHandler来自动将结果集映射到对应的Java对象中
#5)传统的不支持事务、缓存、延迟加载等功能
1、多个参数传值
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
九、Redis
1、面试题
1、数据类型
1、redis缓存问题
1)缓存穿透
2)缓存击穿
3)缓存雪崩
1、双写一致性
1、RDB
#1)定义
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。
简单来说就是把内存中的所有数据都记录到磁盘中。
当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
快照文件称为RDB文件,默认是保存在当前运行目录。
#2)触发方式 -- 手动停机自动执行,故障丢失
- 执行save命令 #主线程执行
- 执行bgsave命令 #开启子线程执行
- Redis停机时 #停机自动执行一次save命令
- 触发RDB条件时
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
#3)小结
RDB方式bgsave的基本流程?
- fork主进程得到一个子进程,共享内存空间
- 子进程读取内存数据并写入新的RDB文件
- 用新RDB文件替换旧的RDB文件
RDB会在什么时候执行?save 60 1000代表什么含义?
- 默认是服务停止时
- 代表60秒内至少执行1000次修改则触发RDB
RDB的缺点?
- RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时
1、AOF
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Vsufaur-1689324564380)(http://43.143.239.200:9000/monkey/bookImages/image-20230525145315438.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTNusNno-1689324564380)(http://43.143.239.200:9000/monkey/bookImages/image-20230525150720207.png)]
#1)定义
AOF 全称为 Append Only File(追加文件)。
Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件
#2)修改配置
appendonly yes ----开启
# appendfsync always----总是执行
appendfsync everysec----每次异步执行【默认】
# appendfsync no ----交由操作系统执行
#3)重写功能
# 重写触发配置
auto-aof-rewrite-percentage 100 --超过一倍重写
auto-aof-rewrite-min-size 64mb --超过64m重写
1、缓存过期策略
1、数据淘汰策略
1、分布式锁
实现原理
1、Redis集群
1)主从集群
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TvCFLZ0W-1689324564383)(http://43.143.239.200:9000/monkey/bookImages/image-20230526105639747.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I6uTXIJg-1689324564384)(http://43.143.239.200:9000/monkey/bookImages/image-20230526105727275.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYxH2xx3-1689324564384)(http://43.143.239.200:9000/monkey/bookImages/image-20230526105807954.png)]
2)哨兵机制
#1)监控原理
主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的 sentinel 都认为该实例主观下线,则该实例 客观下线。quorum 值最好超过 Sentinel 实例数量的一半。
3)分片集群
1、优点(IO多路复用)
#渐进式Rehash
将一次大量拷贝的开销,分摊到多次请求中
两次hash表,每次请求只对一个节点操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WOjLupY-1689324564387)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111358868.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCKH7Ftp-1689324564388)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111447591.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Axya6ab0-1689324564388)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111535502.png)]
1、Redis网络模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ZOY2jzp-1689324564388)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111731924.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-npkNB3s6-1689324564388)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111839227.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulIXIsLO-1689324564389)(http://43.143.239.200:9000/monkey/bookImages/image-20230526111924660.png)]
1、Redis应用场景
1)作为缓存使用
#1)验证码
存:
生成验证码code值,放入redis中(用指定前缀+uuid作为key,设置2分钟过期时间)
验证:
查询redis中的code(根据前端传的uuid拼接指定前缀)
删除缓存数据
校验是否一致
#2)token
登录:
校验完用户信息,随机生成tokenId,然后生成token
根据指定前缀+tokenId组成key值,存入缓存中
将生成token返给前端
过滤器:
从request获取token,解析为用户信息
取处之前随机生成tokenId,到redis中查询
有就放行
#3)
#4)
2)分布式锁使用
#1)定时任务
在多实例集群部署时,定时任务会在每个实例都执行一遍
使用redis中setnx命令,添加互斥锁
#2)接口幂等性
#3)抢单
1)Redis限流实现
#1)定义限流注解(设置限流key值、时间、次数)
#2)aop切面实现前置通知,对使用注解的接口处理
#3)执行限流lua脚本,实现限流
1)消息队列
#1)基于List的LPUSH + BRPOP实现
#缺点:
消息丢失、不支持重复消费、不支持分组消费
#2)基于Zset实现(延时队列)
#缺点
不容许重复消费
#3)PUB/SUB发布订阅模式
一个消息发布到多个消费者
#缺点
消息丢失、消息积压问题
#4)基于Stream实现
1)
#1)
#2)
#3)
#4)
1、为什么单线程
#1)为什么不使用多线程
1、CPU不是瓶颈,受制于网络,内存
2、支持批处理,每秒100万个请求
3、单线程维护成本比较低,多线程需要处理线程安全问题,死锁,线程切换问题
4、多线程无法使用渐进式rehash
#2)6.0为什么用多线程
1、需要更大的QPS,使用多线程
2、是IO多线程,内部执行命令还是单线程
1、big key问题
#1)内存空间不均匀
在Cluster中,会造成 节点的内存空间不均匀
#2)超时阻塞
由于单线程的特性,操作bigKey比较耗时,会发生阻塞
#3)网络阻塞
产生的网络流量比较大
1、提高缓存的命中率
#1)提前加载
#2)增加存储空间,提高缓存数据
#3)调整缓存的存储类型
#4)提高缓存的更新频次(binlog,mq同步)
1、造成阻塞
#1)key * 命令
#2)bigkey操作
#3)清空库命令flushdb
#4)AOF
#5)从库加载RDB
1、使用跳表,不使用B+树?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-egrESBuc-1689324564389)(http://43.143.239.200:9000/monkey/bookImages/image-20230707213156927.png)]
#1)简单实现
跳跃表的实现相对简单,相比B+ Tree而言,代码实现难度要小很多。在实际应用中,简单的实现通常能够提高代码的可读性和可维护性,因此跳跃表比B+ Tree更适合Redis这种注重性能和易用性的应用。
#2)范围查询更好的内存效率
十、springboot
1、@Conditional条件注入
//按条件注入bean
@Configuration
public class UserConfiguration {
@Bean
@Conditional(ConditionTest.class)
public User getUser(){
return new User();
}
}
//实现Condition,重写方法----返回true注入,返回false不注入
public class ConditionTest implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//判断是否有jedis,有注入user
boolean flag = true;
try {
Class<?> jedis = Class.forName("redis.clients.jedis.Jedis");
} catch (ClassNotFoundException e) {
flag = false;
}
return flag;
}
}
1、 @Enable* 注解
#1)概念
SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import注 解导入一些配置类,实现Bean的动态加载
#2)SpringBoot 工程是否可以直接获取jar包中定义的Bean?
不能,所以自定义注解注入
1、@Import注解
@Enable*底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供4中用法:
#1)导入Bean 【@Import(User.classs)】
#2)导入配置类 【@Import(UserConfig.class)】
#3)导入 ImportSelector 实现类。一般用于加载配置文件中的类
@Import(MyImportSelector.class)
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.itheima.springbooyembal.domain.User","com.Role"};
}
}
#4)导入 ImportBeanDefinitionRegistrar 实现类。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
registry.registerBeanDefinition("user",beanDefinition);
}
}
#说明
#1)@EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class)来加载配置类。
#2)配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会自动加载 这些配置类,初始化Bean
#3)并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean
1、执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1u1MJlxm-1689324564389)(http://43.143.239.200:9000/monkey/bookImages/image-20220518213728758.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bxz7VFk-1689324564389)(http://43.143.239.200:9000/monkey/bookImages/image-20220518213807162.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VEuwFNrd-1689324564390)(http://43.143.239.200:9000/monkey/bookImages/image-20220518213833047.png)]
1、当启动springboot应用程序的时候,会先创建SpringApplication的对象,在对象的构造方法中会进行某些参数的初始化工作,最主要的是判断当前应用程序的类型以及初始化器和监听器,在这个过程中会加载整个应用程序中的spring.factories文件,将文件的内容放到缓存对象中,方便后续获取。
2、SpringApplication对象创建完成之后,开始执行run方法,来完成整个启动,启动过程中最主要的有两个方法,第一个叫做prepareContext,第二个叫做refreshContext,在这两个关键步骤中完整了自动装配的核心功能,前面的处理逻辑包含了上下文对象的创建,banner的打印,异常报告期的准备等各个准备工作,方便后续来进行调用。
3、在prepareContext方法中主要完成的是对上下文对象的初始化操作,包括了属性值的设置,比如环境对象,在整个过程中有一个非常重要的方法,叫做load,load主要完成一件事,将当前启动类做为一个beanDefinition注册到registry中,方便后续在进行BeanFactoryPostProcessor调用执行的时候,找到对应的主类,来完成@SpringBootApplicaiton,@EnableAutoConfiguration等注解的解析工作
4、在refreshContext方法中会进行整个容器刷新过程,会调用中spring中的refresh方法,refresh中有13个非常关键的方法,来完成整个spring应用程序的启动,在自动装配过程中,会调用invokeBeanFactoryPostProcessor方法,在此方法中主要是对ConfigurationClassPostProcessor类的处理,这次是BFPP的子类也是BDRPP的子类,在调用的时候会先调用BDRPP中的postProcessBeanDefinitionRegistry方法,然后调用postProcessBeanFactory方法,在执行postProcesseanDefinitionRegistry的时候回解析处理各种注解,包含@PropertySource,@ComponentScan,@ComponentScans,@Bean,@lmport等注解,最主要的是@Import注解的解析
5、在解析@Import注解的时候,会有一个getimports的方法,从主类开始递归解析注解,把所有包含@Import的注解都解析到,然后在processlmport方法中对Import的类进行分类,此处主要识别的时候AutoConfigurationlmportSelect归属于ImportSelect的子类,在后续过程中会调用deferredImportSelectorHandler中的process方法,来完整EnableAutoConfiguration的加载。
十一、SpringCloud
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iLMoR34w-1689324564390)(http://43.143.239.200:9000/monkey/bookImages/image-20230601111949054.png)]
1、五大组件
1、eureka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKVaxIec-1689324564390)(http://43.143.239.200:9000/monkey/bookImages/image-20230601162143524.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytMgg2Mf-1689324564391)(http://43.143.239.200:9000/monkey/bookImages/image-20230601162227247.png)]
1、Nacos
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmP86fQK-1689324564391)(http://43.143.239.200:9000/monkey/bookImages/image-20230601162247327.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kpHvQ9Gi-1689324564391)(http://43.143.239.200:9000/monkey/bookImages/image-20230601162305929.png)]
1、Ribbon负载均衡
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlrVUOrK-1689324564391)(http://43.143.239.200:9000/monkey/bookImages/image-20230601163625138.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SzISHZyj-1689324564392)(http://43.143.239.200:9000/monkey/bookImages/image-20230601163646420.png)]
1、熔断降级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4AR7r2vC-1689324564392)(http://43.143.239.200:9000/monkey/bookImages/image-20230601163936311.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lGTGs1w-1689324564393)(http://43.143.239.200:9000/monkey/bookImages/image-20230601164008877.png)]
1、服务监控(skywalking)
1、接口限流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXtcsPqC-1689324564394)(http://43.143.239.200:9000/monkey/bookImages/image-20230601164330175.png)]
1)Nginx限流-漏桶算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6s1tLl45-1689324564395)(http://43.143.239.200:9000/monkey/bookImages/image-20230601164448722.png)]
2)网关限流-令牌桶算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hf7EeScz-1689324564395)(http://43.143.239.200:9000/monkey/bookImages/image-20230601164639096.png)]
3)区别
#1)【漏桶算法】的出水速度是恒定的--不能够有效地使用网络资源,因为漏桶的漏出速率是固定的
那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)
漏桶算法通常可以用于限制访问外部接口的流量,保护其他人系统,比如我们请求银行接口,通常要限制并发数。
#2)【令牌桶算法】生成令牌的速度是恒定的--限制数据的平均传输速率的同时还允许某种程度的突发传输
而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,可以处理瞬时流量,而且拿令牌的过程并不是消耗很大的事情。
令牌桶算法通常可以用于限制被访问的流量,保护自身系统。
1、CAP和BASE理论
1)C
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXuyYGuK-1689324564396)(http://43.143.239.200:9000/monkey/bookImages/image-20230601165007213.png)]
2)A
3)P
4)BASE理论
1、分布式事务-Seata
1)XA模式
2)AT模式
3)TCC模式
1、接口幂等性
#1)幂等:
多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
#2)场景
用户重复点击(网络波动)
MQ消息重复
应用使用失败或超时重试机制
1、分布式任务调度
1、
1、
1、
十二、排序算法
1、二分查找
public static int binarySearch(int[] arr, int target){
int left = 0, right = arr.length - 1;
while (left <= right){
//【解决int溢出问题】
int middle = (left + right) << 2;
if (arr[middle] == target){
return middle;
} else if (target < arr[middle]){
right = middle - 1;
} else {
left = middle + 1;
}
}
return -1;
}
1、冒泡排序
//方式1
public static void bubble(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//方式2(优化)---即本次所有元素未交换,停止
public static void bubble(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
//用于判断有序标志
boolean flag = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if (!flag){
break;
}
}
}
//方式3(最佳优化)--获取每次交换最后索引,作为下一次比较的末尾
public static void bubble(int[] arr){
int n = arr.length - 1;
while (true) {
//用于判断最后一次需要比较的索引
int last = 0;
for (int j = 0; j < n; j++) {
if (arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
last = j;
}
}
n = last;
if (last == 0){
break;
}
}
}
1、选择排序
//【定义】每次选最小的元素,以此放在前面
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i; j < arr.length - 1; j++) {
if (arr[i] > arr[j + 1]){
min = j + 1;
}
}
if (i != min){
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
//选择&&冒泡 对比
1、时间复杂发都是O(n^2);
2、选择一般快于冒泡,因为交换次数少
3、数组有序性高,冒泡快
4、冒泡稳定,选择不稳定
1、插入排序
//【定义】每次插入前面的有序部分
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (temp < arr[j]){
arr[j + 1] = arr[j];
}else {
break;
}
}
arr[j+1] = temp;
System.out.println(Arrays.toString(arr));
}
}
1、希尔排序
#定义
按照一定间隔(N/2、N/4 ...),进行插入排序排序
1、快速排序
//方法1【单边循环】每次选取一个基准点,比他大放在其右侧,比他小放在其左侧,直至元素小于(分治思想)
public static void partition(int[] arr, int start, int end) {
//递归结束判断条件
if (start >= end){
return;
}
int i = start, j = start;
while (j < end){
if (arr[j] < arr[end]){
swap(arr, i, j);
i++;
}
j++;
}
swap(arr, i, end);
//左右分别比较
partition(arr, start, i - 1);
partition(arr, i + 1, end);
}
//方法2【双边循环】
public static void partition(int[] arr, int start, int end) {
if (start >= end){
return;
}
int i = start, j = end;
while (i < j){
while (i < j && arr[j] > arr[start]){
j--;
}
while (i < j && arr[i] <= arr[start]){
i++;
}
swap(arr, i, j);
}
swap(arr, j, start);
partition(arr, start, j - 1);
partition(arr, j + 1, end);
}
1、
1、
十三、集合原理
0)概述
1)ArrayList
1、ArrayList扩容机制
#1)无参的构造方法
初始容量为0
#2)指定容量的构造方法
为指定容量的数组
#3)指定集合的构造方法
指定集合的大小(注意:对象转为数组,直接将地址赋值给elementData)
#4)add方法
首次扩容为10,每当数组容量满时,扩容为上次容量的1.5倍
#源码:
如果容量为空, 取初始容量10和需要容量 最大值
如果不为空,容量不足扩容为原来的1.5倍
还不够,直接扩容到需要的大小,
超过最大容量会报错
【复制到一个新数组,赋值给elementData】
1、fail-fast && fail-safe机制
#1)定义
fail-fast:遍历的同时,其他人来修改,则立即抛出异常
fail-safe:遍历的同时,其他人来修改,有其应对策略【例如:牺牲一致性来让整个集合遍历完成】
#2)ArrayList(fail—fast)-->遍历不能修改(Vector也是)
遍历创建一个迭代器,迭代器外面和内部分别维护一个变量【修改次数】
当遍历时修改,使得外部的修改次数和内部的不一致,会抛出异常
#3)CopyOnWriteArrayList(fail-safe)-->遍历可以修改,读写分离
调用add方法时,会copy一个新的数组进行修改,而迭代器遍历使用的原数组,即【读写分离,牺牲数据一致性】
1、ArrayList && LinkedList
#1)ArrayList
①基于数组,内存连续
②随机访问快(根据下标访问快,但是根据元素查找慢)
③尾部插入、删除快;其他部分相对慢(因为会移动元素)
④可以利用cpu缓存,局部性原理
cpu和内存之间读写时间比较长,因此加入CPU缓存,每次读写到缓存中,再以合适时机写入磁盘
CPU缓存每次读取【连续的内存块】,因为相邻数据再次被使用的频率比较【又叫局部性原理】
#2)LinkedList
①基于双向链表(非循环,jdk1.7之后),内存不连续
②随机访问慢(沿着链表去遍历)
③首尾插入和删除快;其他部分效率比ArrayList效率还低(因为遍历链表需要花很多时间)
④占用内存多
Node对象,包含前后指针
1、源码分析
2)HashMap
1、JDK1.7 & 1.8区别
#1)1.7(头插法)
数组+链表
#2)1.8(尾插法)
数组+链表 或 红黑树
1、常见问题
#1)为何要用红黑树,为啥不上来就用,树化的阈值为啥为8
①避免单个链表过长,导致查询效率变低(偶然情况,一般不会超过8,用来避免Dos攻击)
②链表更新的时间复杂度O(1),而红黑树的时间复杂度为O(log2 n),TreeNode占用空间也比普通Node大
③hash值足够随机,在hash表中按松柏分布,在负载因子0.75情况下,链表长度超过8的概率为亿分之六,就是为了树化的几率足够小
#2)树化条件
链表长度超过8,且数组容量>64
#3)退化条件
情况1:数组在扩容时,拆分树元素<=6会退化链表
情况2:remove节点时,若root节点、左孩子、右孩子、左孙子有一个为null【移除前检查】,也会退化为链表
#4)索引如何计算?hashcode都有了,为何要需要hash()方法?数组容量为啥为2的n次幂?
①先计算hashCode(),在调用HashMap的hash方法进行二次哈希,最后 &(capacity -1)得到索引
②综合高位分布,让哈希分布更为均匀【(h = key.hashCode()) ^ (h >>> 16)取高16位做异或】
③计算索引时,可以用位运算【n&(16-1)】替换取模运算【n%16】,效率更高
扩容时,链表移动也有优化
#缺点
例如偶数,取模16只会是偶数,质数的结果相对比较均匀
#5)加载因子为啥0.75
空间占用和查询时间取得较好的平衡
大于这个值,空间节省了,但链表会较长影响性能
小于这个值,冲突减少了,但扩容会更加频繁,空间占用多
#6)key是否可以为null,作为key对象有啥要求?
可以为null,但其他Map不一定
作为key对象,必须要实现hascode()和equals()方法,且key不可变
1、put方法
#1)懒惰创建数组,首次使用时创建数组(默认初始容量16)
#2)计算桶下标【key.hashCode() ^ (h >>> 16),在使用位运算取模】
#3)如果桶下标没有被占用,创建Node节点
如果桶下标已经被占用
①是TreeNode走红黑树逻辑
②是Node走链表逻辑,如果链表过长超过树化阈值,走树化逻辑
#4)返回前检查是否超过阈值,超过扩容
1、多线程问题
#1)数据错乱(1.7,1.8)
例如:两个线程同时put数据,一个线程得到桶下标,但没有放入,
另一个线程也去判断,就都导致判断都为空,后面会覆盖前面的值
#2)并发死链(1.7)--头插法引起的
两个线程同时进行头插法,会导致循环引用问题
1、红黑树
1、
#1)
#2)
#3)
#4)
十四、设计模式
1、单例模式
#1)定义
一个类只允许创建一个对象(或者实例)
#2)类型
1、饿汉式:
优点:在类加载的时候完成实例化,不会出现线程安全问题
缺点:没有懒加载lazy loading效果,没有使用但已经占用内存【内存浪费】
2、懒汉式【线程不安全】:
优点:实现懒加载lazy loading,但是只能单线程使用
缺点:线程安全问题
3、懒汉式【双重检查机制】
效率高,线程安全
4、静态内部类
外部类加载【不会】加载静态内部类
调用getInstance方法,加载静态内部类【只会加载一次,且线程安全】
5、枚举
优点:线程安全,防止反序列化
#3)jdk应用
RunTime类【饿汉式单例】
Collections工具类,获取空集合
//1.饿汉式(类加载完成实例化)
class Singleton{
//私有成员变量
private final static Singleton singleton = new Singleton();
//私有构造方法
private Singleton(){
}
//静态方法,返回实例对象
public static Singleton getSingleton(){
return singleton;
}
}
//2.懒汉式(线程不安全)
class Singleton{
private static Singleton singleton;
private Singleton(){
}
//静态方法,创建单例对象
public static Singleton getSingleton(){
if (singleton == null){
return new Singleton();
}
return singleton;
}
}
//3.懒汉式(双重检查机制)
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;
}
}
/************
volatile: 防止指令重排【创建对象3步:开辟空间、执行构造、引用赋值--其中后两步可能颠倒】
双重:提高性能
************/
//4.静态内部类
class Singleton{
private Singleton(){
}
//静态内部类,被动加载、线程安全
private static class StaticInner{
private final static Singleton singleton = new Singleton();
}
public static Singleton getSingleton(){
return StaticInner.singleton;
}
}
//5.枚举
enum Singleton{
SINGLETON;
public void method(){
System.out.println("枚举实现单例");
}
}
1、代理模式
#1)定义
通过代理实现对真实对象的访问。
#2)应用
AOP、vping
① 静态代理
public class StaticProxy {
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
proxyPoint.sell();
}
}
interface SellTicket{
void sell();
}
//火车站卖票
class TrainStation implements SellTicket{
@Override
public void sell() {
System.out.println("火车站买票!");
}
}
//代售点
class ProxyPoint {
private TrainStation trainStation = new TrainStation();
public void sell() {
System.out.println("收取费用!");
trainStation.sell();
}
}
② JDK动态代理
public class JdkProxy {
public static void main(String[] args) {
ProxyJdk proxyJdk = new ProxyJdk();
proxyJdk.getProxy().sell();
}
}
class ProxyJdk{
private SellTicket sellTicket = new TrainStation();
public SellTicket getProxy(){
SellTicket sellTicket1 = (SellTicket) Proxy.newProxyInstance(
sellTicket.getClass().getClassLoader(),
sellTicket.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("收取费用!");
Object invoke = method.invoke(sellTicket, args);
return invoke;
}
}
);
return sellTicket1;
}
}
//动态生成的代理类对象(阿里Arthas查看)
//$Proxy0实现SellTicket接口,代理类和真实类都实现接口
//$Proxy0提供匿名内部类提供给父类
final class $Proxy0 extends Proxy implements SellTicket{
private static Method m3;
protected $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("no14_proxy.SellTicket").getMethod("sell", new Class[0]);
}
@Override
public final void sell() {
this.h.invoke(this, m3, null);
}
}
class Proxy{
protected InvocationHandler h;
}
//执行过程
1、测试类中代理对象调用sell()方法;
2、根据多态的特性,执行$Proxy0中的sell()方法;
3、其方法又调用了InvocationHandler接口我们自己实现子类对象的invoke()方法;
4、invoke方法通过反射执行真实对象TrainStation中的sell()方法;
③ CGlib动态代理
class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置父类字节码文件
enhancer.setSuperclass(Server.class);
//设置拦截回调的函数
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(
Object o,
Method method,
Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("before......");
Object obj = methodProxy.invokeSuper(o, args);
System.out.println("after......");
return obj;
}
});
//创建代理类
Server server = (Server) enhancer.create();
//执行方法
server.show();
}
}
class Server{
public void show() {
System.out.println("真实服务器执行。。。");
}
}
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
1、
1、
1、
十五、RabbitMQ
1、如何保证消息不丢失
1)生产者确认机制
2)消息持久化
3)消费者确认
1、消息的重复消费问题如何解决
1、延迟队列=死信交换机+TTL(生存时间)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SuWOHrLy-1689324564405)(http://43.143.239.200:9000/monkey/bookImages/image-20230605134035645.png)]
1、消息堆积怎么解决
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kdp4YxBW-1689324564406)(http://43.143.239.200:9000/monkey/bookImages/image-20230605134138732.png)]
1、高可用机制
1)普通集群
2)镜像集群
3)仲裁集群
1、
1、
十六、Kafka
1、技术对比(四种mq)
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
#1)追求可用性:Kafka、 RocketMQ 、RabbitMQ
#2)追求可靠性:RabbitMQ、RocketMQ
#3)追求吞吐能力:RocketMQ、Kafka
#4)追求消息低延迟:RabbitMQ、Kafka
#1)优点:
1、解耦
2、异步
3、流量削峰填谷
#2)缺点:
1、可用性降低
2、复杂性提高
3、一致性问题
1、kafka优点
#1)支持消息的持久化(保证消息不丢失)
#2)高吞吐(百万计)
#3)支持动态扩展
#4)支持多客户端(Java、C、C++、GO)
#5)流处理stream
#6)消息压缩
1、如何保证消息丢失
1)出现丢失的环节
2)场景一:生产者发送消息到Brocker丢失
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pv8V7XQK-1689324564407)(http://43.143.239.200:9000/monkey/bookImages/image-20230605090544924.png)]
3)场景二:消息在Brocker中存储丢失
4)场景三:消费者从Brocker接收消息丢失
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PE6hXPQ-1689324564408)(http://43.143.239.200:9000/monkey/bookImages/image-20230605090935381.png)]
1、如何保证消息不重复
#1)生产者消息重复
1、原因:
producer发出一条消息,broker落盘以后,因为网络等原因,发送端得到一个发送失败的响应或者网络中断,然后producer收到 一个可恢复的Exception重试消息导致消息重复。
2、解决:
enable.idempotence=true //此时会默认开启acks=all
每个生产者producer都有一个唯一id,producer每发送一条数据都会带上一个sequence,当消息落盘,sequence就会递增1。只需判断当前消息的sequence是否大于当前最大sequence,大于就代表此条数据没有落盘过,可以正常消费;不大于就代表落盘过,这个时候重发的消息会被服务端拒掉从而避免消息重复。
#2)消费者消息重复
1、原因
offset提交失败,消费成功
2、解决:
设置为手动提交模式;手动判断幂等
(1)、使用唯一键:操作数据的时候先判断库里(数据库或者缓存)有没有这个唯一键,若没有则存入这个唯一键,若有则表示之前已经操作过了,本次不再操作。
(2)、将版本号(offset)存入到数据里面,然后再要操作数据的时候用这个版本号做乐观锁,当版本号大于原先的才能操作。
1、如何保证消费的顺序性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1amzSO3-1689324564409)(http://43.143.239.200:9000/monkey/bookImages/image-20230605091208114.png)]
1、高可用机制
1)集群模式
2)分区备份机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRiAaTh5-1689324564410)(http://43.143.239.200:9000/monkey/bookImages/image-20230605091739662.png)]
1、数据清理机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUeCbSSB-1689324564411)(http://43.143.239.200:9000/monkey/bookImages/image-20230605092143296.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y25VuQmi-1689324564411)(http://43.143.239.200:9000/monkey/bookImages/image-20230605092237623.png)]
1、高性能设计
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Usmz6v1l-1689324564412)(http://43.143.239.200:9000/monkey/bookImages/image-20230605092416539.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K512Tqan-1689324564412)(http://43.143.239.200:9000/monkey/bookImages/image-20230605092435287.png)]
1、消息堆积
#1)生产者发送速度过快,消费者处理速度跟不上。
这种情况下,可以考虑增加消费者的数量,或者优化消费者的代码以提高处理速度。
#2)消费者使用了较慢的消费者组。
Kafka 的消费者组根据一定的规则来分配分区,如果消费者组中某个消费者的处理速度较慢,就会导致该消费者组内的其他消费者也受到影响。可以考虑将消费者组细分为更小的组,或者将较慢的消费者移动到一个新的组中。
#3)分区数量太少,无法满足高并发场景。
可以在 Kafka 集群中增加更多的 Broker,从而增加分区数量,以支持更高的并发。
#4)消息大小超过了 Kafka 配置的最大大小限制。
可以通过增加 message.max.bytes 参数的值来解决此问题。
#5)网络问题。如果网络不稳定或带宽不足,会导致消息传递变慢,从而导致消息堆积。
可以考虑优化网络连接,或者增加更多的 Broker 来提高网络吞吐量。
1、
十七、技术场景
1、点单登录
单点登录的英文名叫做:Single Sign On(简称SSO),只需要登录一次,就可以访问所有信任的应用系统【JWT】
1、权限认证
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgW0casL-1689324564413)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162136662.png)]
1、上传数据的安全性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sDacZcRa-1689324564413)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162229038.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUfMfJ4h-1689324564413)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162247012.png)]
1、遇到的难题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SBXNEQoX-1689324564413)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162327272.png)]
1、日志采集
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XudquxMb-1689324564414)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162412185.png)]
1、查看日志命令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cW90PcbR-1689324564414)(http://43.143.239.200:9000/monkey/bookImages/image-20230608162555574.png)]
1、生产问题怎么排查
已经上线的bug排查的思路:
#1,先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
#2,远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环境,方便调试代码)
1、快速定位系统的瓶颈
#1)压测(性能测试),项目上线之前测评系统的压力
压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
指标:响应时间、 QPS、并发数、吞吐量、 CPU利用率、内存使用率、磁盘IO、错误率
压测工具:LoadRunner、Apache Jmeter …
后端工程师:根据压测的结果进行解决或调优(接口慢、代码报错、并发达不到要求…)
#2)监控工具、链路追踪工具,项目上线之后监控
监控工具:Prometheus+Grafana
链路追踪工具:skywalking、Zipkin
#3)线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查
sellTicket.getClass().getClassLoader(),
sellTicket.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("收取费用!");
Object invoke = method.invoke(sellTicket, args);
return invoke;
}
}
);
return sellTicket1;
}
}
```java
//动态生成的代理类对象(阿里Arthas查看)
//$Proxy0实现SellTicket接口,代理类和真实类都实现接口
//$Proxy0提供匿名内部类提供给父类
final class $Proxy0 extends Proxy implements SellTicket{
private static Method m3;
protected $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("no14_proxy.SellTicket").getMethod("sell", new Class[0]);
}
@Override
public final void sell() {
this.h.invoke(this, m3, null);
}
}
class Proxy{
protected InvocationHandler h;
}
//执行过程
1、测试类中代理对象调用sell()方法;
2、根据多态的特性,执行$Proxy0中的sell()方法;
3、其方法又调用了InvocationHandler接口我们自己实现子类对象的invoke()方法;
4、invoke方法通过反射执行真实对象TrainStation中的sell()方法;
③ CGlib动态代理
class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置父类字节码文件
enhancer.setSuperclass(Server.class);
//设置拦截回调的函数
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(
Object o,
Method method,
Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("before......");
Object obj = methodProxy.invokeSuper(o, args);
System.out.println("after......");
return obj;
}
});
//创建代理类
Server server = (Server) enhancer.create();
//执行方法
server.show();
}
}
class Server{
public void show() {
System.out.println("真实服务器执行。。。");
}
}
#1)
#2)
#3)
#4)
1、
#1)
#2)
#3)
#4)
1、
1、
1、
1、
十五、RabbitMQ
1、如何保证消息不丢失
[外链图片转存中…(img-6CEgeo7y-1689324564402)]
[外链图片转存中…(img-Lh1w4FeC-1689324564402)]
1)生产者确认机制
[外链图片转存中…(img-SusyZJqV-1689324564402)]
2)消息持久化
[外链图片转存中…(img-TVwVhjyM-1689324564404)]
3)消费者确认
[外链图片转存中…(img-RHxFaMT3-1689324564404)]
1、消息的重复消费问题如何解决
[外链图片转存中…(img-6cqgRlsQ-1689324564404)]
1、延迟队列=死信交换机+TTL(生存时间)
[外链图片转存中…(img-qJ2EIpJU-1689324564405)]
[外链图片转存中…(img-SlqTfSAB-1689324564405)]
[外链图片转存中…(img-SuWOHrLy-1689324564405)]
[外链图片转存中…(img-KhPvFnUE-1689324564405)]
1、消息堆积怎么解决
[外链图片转存中…(img-Kdp4YxBW-1689324564406)]
1、高可用机制
[外链图片转存中…(img-jmMCcwba-1689324564406)]
1)普通集群
[外链图片转存中…(img-5QvnUWYM-1689324564406)]
2)镜像集群
[外链图片转存中…(img-HPx36Hij-1689324564406)]
3)仲裁集群
[外链图片转存中…(img-Qyjt983N-1689324564407)]
1、
1、
十六、Kafka
1、技术对比(四种mq)
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
#1)追求可用性:Kafka、 RocketMQ 、RabbitMQ
#2)追求可靠性:RabbitMQ、RocketMQ
#3)追求吞吐能力:RocketMQ、Kafka
#4)追求消息低延迟:RabbitMQ、Kafka
#1)优点:
1、解耦
2、异步
3、流量削峰填谷
#2)缺点:
1、可用性降低
2、复杂性提高
3、一致性问题
1、kafka优点
#1)支持消息的持久化(保证消息不丢失)
#2)高吞吐(百万计)
#3)支持动态扩展
#4)支持多客户端(Java、C、C++、GO)
#5)流处理stream
#6)消息压缩
1、如何保证消息丢失
[外链图片转存中…(img-JjY4nsNe-1689324564407)]
1)出现丢失的环节
[外链图片转存中…(img-FUWWVRhH-1689324564407)]
2)场景一:生产者发送消息到Brocker丢失
[外链图片转存中…(img-Pv8V7XQK-1689324564407)]
3)场景二:消息在Brocker中存储丢失
[外链图片转存中…(img-R3yPkoJf-1689324564408)]
4)场景三:消费者从Brocker接收消息丢失
[外链图片转存中…(img-iv3uMphv-1689324564408)]
[外链图片转存中…(img-1PE6hXPQ-1689324564408)]
1、如何保证消息不重复
#1)生产者消息重复
1、原因:
producer发出一条消息,broker落盘以后,因为网络等原因,发送端得到一个发送失败的响应或者网络中断,然后producer收到 一个可恢复的Exception重试消息导致消息重复。
2、解决:
enable.idempotence=true //此时会默认开启acks=all
每个生产者producer都有一个唯一id,producer每发送一条数据都会带上一个sequence,当消息落盘,sequence就会递增1。只需判断当前消息的sequence是否大于当前最大sequence,大于就代表此条数据没有落盘过,可以正常消费;不大于就代表落盘过,这个时候重发的消息会被服务端拒掉从而避免消息重复。
#2)消费者消息重复
1、原因
offset提交失败,消费成功
2、解决:
设置为手动提交模式;手动判断幂等
(1)、使用唯一键:操作数据的时候先判断库里(数据库或者缓存)有没有这个唯一键,若没有则存入这个唯一键,若有则表示之前已经操作过了,本次不再操作。
(2)、将版本号(offset)存入到数据里面,然后再要操作数据的时候用这个版本号做乐观锁,当版本号大于原先的才能操作。
1、如何保证消费的顺序性
[外链图片转存中…(img-N1amzSO3-1689324564409)]
[外链图片转存中…(img-v34rJcA4-1689324564409)]
1、高可用机制
[外链图片转存中…(img-2GemFhyP-1689324564409)]
1)集群模式
[外链图片转存中…(img-DBvjPcYu-1689324564410)]
2)分区备份机制
[外链图片转存中…(img-Q2IgUJFw-1689324564410)]
[外链图片转存中…(img-YRiAaTh5-1689324564410)]
1、数据清理机制
[外链图片转存中…(img-3YoG665Y-1689324564411)]
[外链图片转存中…(img-nUeCbSSB-1689324564411)]
[外链图片转存中…(img-y25VuQmi-1689324564411)]
1、高性能设计
[外链图片转存中…(img-184YSCoN-1689324564412)]
[外链图片转存中…(img-Usmz6v1l-1689324564412)]
[外链图片转存中…(img-K512Tqan-1689324564412)]
1、消息堆积
#1)生产者发送速度过快,消费者处理速度跟不上。
这种情况下,可以考虑增加消费者的数量,或者优化消费者的代码以提高处理速度。
#2)消费者使用了较慢的消费者组。
Kafka 的消费者组根据一定的规则来分配分区,如果消费者组中某个消费者的处理速度较慢,就会导致该消费者组内的其他消费者也受到影响。可以考虑将消费者组细分为更小的组,或者将较慢的消费者移动到一个新的组中。
#3)分区数量太少,无法满足高并发场景。
可以在 Kafka 集群中增加更多的 Broker,从而增加分区数量,以支持更高的并发。
#4)消息大小超过了 Kafka 配置的最大大小限制。
可以通过增加 message.max.bytes 参数的值来解决此问题。
#5)网络问题。如果网络不稳定或带宽不足,会导致消息传递变慢,从而导致消息堆积。
可以考虑优化网络连接,或者增加更多的 Broker 来提高网络吞吐量。
1、
十七、技术场景
1、点单登录
单点登录的英文名叫做:Single Sign On(简称SSO),只需要登录一次,就可以访问所有信任的应用系统【JWT】
[外链图片转存中…(img-tIoHr8lM-1689324564412)]
1、权限认证
[外链图片转存中…(img-QgW0casL-1689324564413)]
1、上传数据的安全性
[外链图片转存中…(img-sDacZcRa-1689324564413)]
[外链图片转存中…(img-UUfMfJ4h-1689324564413)]
1、遇到的难题
[外链图片转存中…(img-SBXNEQoX-1689324564413)]
1、日志采集
[外链图片转存中…(img-XudquxMb-1689324564414)]
[外链图片转存中…(img-zs7S5qJK-1689324564414)]
1、查看日志命令
[外链图片转存中…(img-cW90PcbR-1689324564414)]
1、生产问题怎么排查
已经上线的bug排查的思路:
#1,先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
#2,远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环境,方便调试代码)
1、快速定位系统的瓶颈
#1)压测(性能测试),项目上线之前测评系统的压力
压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
指标:响应时间、 QPS、并发数、吞吐量、 CPU利用率、内存使用率、磁盘IO、错误率
压测工具:LoadRunner、Apache Jmeter …
后端工程师:根据压测的结果进行解决或调优(接口慢、代码报错、并发达不到要求…)
#2)监控工具、链路追踪工具,项目上线之后监控
监控工具:Prometheus+Grafana
链路追踪工具:skywalking、Zipkin
#3)线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查