12种基学习器,评价指标为RMSE、MAE、MAPE和R2,最终选定Catboost和LightGBM
论文:Xception: Deep Learning with Depthwise Separable Convolutions (CVPR 2017)
源码:Keras开源代码
Very Deep Convolutional Networks for Large-Scale Image Recognition
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EqopFZG5-1686536899651)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20230419190536360.png)]
JPA和Mybatis区别
前端服务器Nginx,后端服务器Tomcat,开发前端内容时,可以把前端的请求通过前端服务器转发给后端(称为反向代理)
用户信息明文存储在数据库中,不安全
三大概念
四大功能
1)所有SQL语句全部自己写
2)手动解析实体关系映射转换为MyBatis内部对象注入容器
3)不支持Lambda形式调用
1)强大的条件构造器,满足各类使用需求
2)内置的Mapper,通用的Service,少量配置即可实现单表大部分CRUD操作
3)支持Lambda形式调用
4)提供了基本的CRUD功能,连SQL语句都不需要编写
5)自动解析实体关系映射转换为MyBatis内部对象注入容器
Mybatis Plus 加载流程:
1、加载配置文件(数据源,以及映射文件),解析配置文件,生成Configuration,MapperedStatement
2、通过使用Configuration对象,创建sqlSessionFactory,用来生成SqlSeesion
3、sqlSession通过调用api或者mapper接口传入statementId找到对应的MapperedStatement,来调用执行sql
4、通过Executor核心器,负责sql动态语句的生成和查询缓存的维护,来进行sql的参数转换,动态sql的拼接,生成Statement对象
5、借助于MapperedStatement来访问数据库,它里面封装了sql语句的相关信息,以及返回结果信息
Spring Security和Shiro的区别?
项目中redis用什么客户端部署?
Java 访问 Redis 主要是通过 Jedis 和 Lettuce 两种由不同团队开发的客户端(提供访问、操作所需的 API),Jedis 比较原生,Lettuce 提供的能力更加全面。
本项目用Spring Data Redis,Spring Data Redis是在 Lettuce 的基础上做了一些封装,与 Spring 生态更加贴合,使用起来也更简便。
java怎么连接数据库?
配置maven依赖->配置数据库(application.properties)
项目还有哪些不足之处?
后端提升响应速度
仅就软件来说,努力的方向有三个,一是 代码,二是 “技术应用”,三是 “优化设计”。
系统安全问题
sql注入的原因及解决
用户注册登录流程
用户管理
用户信息: 显示用户的基本信息(昵称、联系方式、角色、部门等)
组织架构: 显示、配置(增删改)组织架构,一般为树结构
用户操作: 为用户分配角色(多对多)、组织架构(多对多),删除用户
用户黑白名单: 对特殊用户进行特别控制
角色管理
角色信息: 显示角色的基本信息(名称、权限等)
角色操作: 根据需要增删角色、为角色分配权限(多对多,按不同粒度分配,并实现权限的互斥性检验)
权限管理:菜单、功能、数据
开发要点:
菜单权限:
功能权限:
数据权限:
SELECT * FROM book WHERE uid = #{uid}
的语句来实现。用户加密加盐
认证方案
延时双删:先清除缓存,在更新数据库后,等一段时间,再去第二次执行删除操作。
项目中学到了什么?
技术方面
沟通交流
项目难点?如何解决?
Java 程序从源代码到运行的过程:
反射就是把java类中的各种成分映射成一个个的Java对象,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
Spring/Spring Boot、MyBatis 这些框架中大量使用了动态代理,而动态代理的实现也依赖反射。
抽象类:abstract
a、抽象类不能被实例化只能被继承;
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。
接口:interface
Throwable
类Checked Exception 和 Unchecked Exception 有什么区别?
catch
或者throws
关键字处理的话,就没办法通过编译。try-catch-finally-return执行顺序?
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
//结果是30!
针对每个目标类都单独创建一个代理类
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Spring AOP、RPC框架的实现依赖动态代理
InvocationHandler
接口和 Proxy
类是核心MethodInterceptor
接口和 Enhancer
类是核心。编译
加载
类加载器会在指定的classpath中找到.class这些文件,然后读取字节流中的数据,将其存储在JVM方法区
根据.class的信息建立一个Class对象,作为运行时访问这个类的各种数据的接口(一般也在方法区)。
验证格式、语义等
为类的静态变量分配内存并设为JVM默认的初值,对于非静态的变量,则不会为它们分配内存。
静态变量的初值为JVM默认的初值,而不是我们在程序中设定的初值。
字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用,无需等到运行时解析。
此时,执行引擎会调用()方法对静态字段进行代码中编写的初始化操作。
执行
Java集合两大接口: Collection
接口,主要用于存放单一元素;Map
接口,主要用于存放键值对
List和ArrayList的区别
ArrayList和LinkedList区别
Map常见实现类 底层+线程安全:
使用ArrayList、HashMap,需要线程安全怎么办呢?
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
底层使用synchronized代码块锁虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解
构造方法(四种)
put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
Node<K,V> e; K k;
//快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断插入的是否是红黑树节点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是红黑树节点则说明为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
get()方法
Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。
主内存 & 本地内存
JMM示意图:每个线程有个本地内存放副本,共享变量放主内存中
从示意图看,线程1和2咋通信?
多线程下,可能出现的线程安全问题?
So,JMM定义了8种同步操作&一些同步规则,规定一个变量如何从工作内存同步到主内存
程序员追求:易于理解和编程的强内存模型;编译器和处理器追求:较少约束的弱内存模型
happens-before 设计思想
和 JMM 的关系
原子性
含义:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
Java实现:synchronized
、各种 Lock
以及各种原子类
synchronized
和各种 Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
可见性
含义:一个线程对共享变量进行修改,另外的线程立即可以看到修改后的最新值。
Java实现:synchronized
、volatile
以及各种 Lock
有序性
含义:代码的执行顺序未必就是编写代码时候的顺序。
Java实现:volatile
关键字可以禁止指令进行重排序优化
什么是线程安全?
在Java世界里边,所谓线程安全就是多个线程去执行某类,这个类始终能表现出正确的行为,那么这个类就是线程安全的。
怎么解决线程安全问题?
其实大部分时间我们在代码里边都没有显式去处理线程安全问题,因为这大部分都由框架所做了,Tomcat、Druid、SpringMVC等等
解决线程安全问题的思路有以下:
总的来说,就是先判断有没有线程安全问题,如果存在则根据具体的情况去判断使用什么方式去处理线程安全的问题。
死锁
1、继承Thread类
class MyThread extends Thread{
@Override
public void run(){
System.out.println("这是重写的run方法,也叫执行体");
System.out.println("线程号:" + currentThread().getName());
}
}
public class Test{
public static void main(String[] args) throws Exception{
Thread t1 = new MyThread();
t1.start();
}
}
2、实现Runable接口
class MyThread implements Runable{
@Override
public void run(){
System.out.println("这是重写的run方法,也叫执行体");
System.out.println("线程号:" + Thread.currentThread().getName());
}
}
public class Test{
public static void main(String[] args) throws Exception{
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
t1.start();
}
}
3、Callable接口:
class MyThread implements Callable{
@Override
public Object call() throws Exception{
System.out.println("线程号:" + Thread.currentThread().getName());
return 10;
}
}
public class Test{
public static void main(String[] args) throws Exception{
Callable callable = new MyThread();
FutureTask task = new FutureTask(callable);
new Thread(task).start();
System.out.println(task.get());
Thread.sleep(10);//等待线程执行结束
//task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值
//get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待
System.out.println(task.get(100L, TimeUnit.MILLSECONDS));
}
}
Runnable和Callable的区别:
start()与run()的区别:
sleep() 与 wait() 与 join() 与 yield():
六个状态:
NEW: 初始状态,线程被创建出来但没有被调用 start()
。
RUNNABLE: 运行状态,线程被调用了 start()
等待运行的状态。
BLOCKED :阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。
Object
,而不是当前线程Thread
sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。volatile
**关键字volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。用 **volatile
**关键字
如果我们将变量声明为 volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
volatile使用场景:双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;//volatile修饰!
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {//没有实例化过才进入加锁代码
synchronized (Singleton.class) {//类对象加锁
if (uniqueInstance == null) {
/*这句话分三步进行:
1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址
JVM指令重排,可能变成1->3->2,因此需要volatile*/
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
synchronized
、Lock
或者AtomicInteger
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现java.util.concurrent.atomic
包下面的原子变量类(比如AtomicInteger
、LongAdder
)version
字段,表示数据被修改的次数version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行 CAS 操作synchronized
主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
1. 修饰实例方法(锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {
//业务代码
}
2. 修饰静态方法(锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
3. 修饰代码块(锁指定对象/类)
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。
synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
注意:构造方法不能用synchronized修饰!(构造方法本身就线程安全)
底层依赖于JVM,分为修饰方法和修饰代码块两个部分
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
在执行**monitorenter
**时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
对象锁的的拥有者线程才可以执行 monitorexit
指令来释放锁。在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
ACC_SYNCHRONIZED
标识)synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
同步语句块用monitorenter
指令和 monitorexit
指令,修饰方法用**ACC_SYNCHRONIZED
** 标识,不过两者的本质都是对对象监视器 monitor 的获取
Synchronized
是通过对象内部的一个叫做监视器锁(monitor)来实现的,monitor锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。synchronized
和 volatile
是互补的存在,区别有四点:
volatile
关键字是线程同步的轻量级实现,性能比synchronized好volatile
只能用于变量,synchronized
可以修饰方法以及代码块volatile
能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证volatile
主要用于解决变量在多个线程之间的可见性,而 synchronized
关键字解决的是多个线程之间访问资源的同步性ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁AbstractQueuedSynchronizer
)
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS,添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类
ReentrantLock
默认)都是可重入锁(相同点)
可重入锁也叫递归锁,指的是线程可以再次获取自己的内部锁
JDK所有现成的Lock
实现类,包括synchronized
关键字锁都是可重入的
依赖对象不同
synchronized 依赖于 JVM
ReentrantLock 依赖于 API(需要lock()和unlock()方法配合try/finally语句块完成)
ReentrantLock功能更多
ReentrantLock
增加了三点高级功能:
ReentrantLock
可以指定是公平锁还是非公平锁(默认非公平)synchronized
只能是非公平锁synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制ReentrantLock
类需要借助于Condition
接口与newCondition()
方法实现AQS就是一个抽象类,翻译过来就是抽象队列同步器
使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
核心思想:
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,
这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 使用 int 成员变量 state
表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作
// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;
作用:Semaphore
(信号量)可以用来控制同时访问特定资源的线程数量(想OS中咋用的)
// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
当初始的资源个数为 1 的时候,Semaphore
退化为排他锁。
Semaphore 两种模式:
公平模式: 调用 acquire()
方法的顺序就是获取许可证的顺序,遵循 FIFO;
非公平模式: 抢占式的。
应用场景:Semaphore
通常用于那些资源有明确访问数量限制的场景比如限流
Semaphore
是共享锁的一种实现,它默认构造 AQS 的 state
值为 permits
调用semaphore.acquire()
:相当于OS中P(S),state>=0表示可以获取成功,使用CAS操作state减1;state<0表示数量不足,创建Node结点加入阻塞队列,挂起当前线程
调用semaphore.release()
:相当于OS中V(S),使用CAS操作state+1,释放许可证成功后,唤醒同步队列中的一个线程。被唤醒的线程重新尝试state-1,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程
作用:允许count个线程阻塞在一个地方,直到所有线程任务执行完毕;一次性的,计数器的值只能在构造方法中初始化一次,使用完毕后不能再次使用
原理:共享锁的一种实现,默认构造AQS的state值为count
CountDownLatch
会自旋 CAS 判断 state == 0
,是的话释放所有线程,await()之后的语句执行场景:多线程读取多个文件处理,具体如下:
CountDownLatch
对象CountDownLatch
对象的 await()
方法,所有文件读取完后截止执行后面的逻辑基于AQS管程的CountDownLatch工具类配合线程池模拟了一个高并发的场景,对缓存进行了一个测试,我先让主线程创建一个CountDownLatch,然后把它的初始值设为一,然后创建一个线程池,在线程池中创建一百个线程让他们启动起来之后都调用CountDownLatch.await()方法进入条件变量等待队列等待,然后主线程等待一秒等线程池中的线程都起动起来进入等待状态,然后主线程调用CountDownLatch.countDown()将CountDownLatch减为一,然后线程池中的大量线程就会唤醒,这样就模拟出来了一个高并发的场景对缓存进行测试,是这样的一个测试
作用:本地线程变量,让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
使用场景:解决多线程中因为数据并发产生不一致的问题。
1、存储用户Session
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session
会话管理。
5、数据跨层传递(controller,service,dao):
package com.kong.threadlocal;
public class ThreadLocalDemo05 {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1 {
public void service1(User user){
//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用户:"+user.name);
new Service3().service3();
}
}
class Service3 {
public void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用户:"+user.name);
//在整个流程执行完毕后,一定要执行remove
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
//创建ThreadLocal保存User对象
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name){
this.name = name;
}
}
执行的结果:
service2拿到的用户:jack
service3拿到的用户:jack
ThreadLocal 在项目中的使用 - 知乎 (zhihu.com):
ThreadLocal
作为一级缓存使用, redis 作为二级缓存使用,同一个线程内, 用户信息所有方法共享。任何地方只要需要使用用户信息, 直接从threadlocal 取。ThreadLocal
类原理:
每个线程都有一个 ThreadLocalMap
(ThreadLocal内部类)
Map 中元素的键为 ThreadLocal
,而值对应线程的变量副本。
Map 是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。
Thread
类源代码:
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据不为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
常用方法还有**remove()
**。
ThreadLocal
数据结构:
Thread
中都具备一个ThreadLocalMap
(ThreadLocal
的内部静态类),而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。内存泄露
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,产生内存泄露。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//代表ThreadLocal对象是一个弱引用
value = v;
}
}
如何避免?
ThreadLocal
都调用它的**remove()
方法清除数据**为什么key不是强引用?
ThreadLocal
,引用被回收,但是由于ThreadLocalMap
的Entry强引用了ThreadLocal
, 造成ThreadLocal
无法被回收,也无法避免内存泄露。如何正确使用ThreadLocal
?
ThreadLocal
变量定义成**private static
**的,这样的话ThreadLocal
的生命周期就更长,由于一直存在ThreadLocal
的强引用,所以ThreadLocal
也就不会被回收,也就能保证任何时候都能根据ThreadLocal
的弱引用访问到Entry的value值,然后remove它,防止内存泄露线程池就是管理一系列线程的资源池,有任务处理->线程池拿线程->处理完不销毁->线程等待下一个任务
提供限制和管理资源的方式 && 维护一些基本统计信息
ThreadPoolExecutor
构造函数
源码:在执行的时候,重点就在于它维护了一个ctl参数,这个ctl参数的用高3位表示线程池的状态,低29位来表示线程的数量
Executor
框架的工具类 Executors
线程达到最大数量,队列也被放满了任务,ThreadPoolExecutor.
定义的策略:
.AbortPolicy
(默认):抛出异常拒绝新任务的处理。.CallerRunsPolicy
:调用自己的线程运行任务。.DiscardPolicy
:不处理新任务,直接丢弃。.DiscardOldestPolicy
:丢弃最老未处理的任务请求。新任务到来->当前运行线程数==核心线程数?->√->新任务放队列
不同线程池用不同的阻塞队列
LinkedBlockingQueue
(无界队列):队列永远不会被放满SynchronousQueue
(同步队列):没有容量,不存储元素。DelayedWorkQueue
(延迟阻塞队列):内部采用“堆”太大->增加上下文切换成本
太小->大量请求/任务堆积
CPU密集型任务(N+1):线程数设置为 N(CPU核心数)+1
I/O密集型任务(2N):线程处理I/O不会占用CPU,可以将CPU交出给其他线程,所以可以多配点线程
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
:核心线程池容量 ,任务队列未达到队列容量时,最大可以同时运行的线程数量。maximumPoolSize
:最大线程池容量 ,任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
:工作队列容量, 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。ThreadPoolExecutor
其他常见参数 :
keepAliveTime
:线程池中的线程数量大于 corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime
才会被回收销毁;unit
: keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:饱和策略。(简单理解就是:我有一个任务,提交给了 Future
来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future
那里直接取出任务执行结果。)
Callable和Future的关系
FutureTask
提供了 Future
接口的基本实现,常用来封装 Callable
和 Runnable
//FutureTask 有两个构造函数,可传入Callable或者Runnable对象
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
// 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
JDK1.7运行时数据区域:线程共享 堆、方法区、直接内存;线程私有程序计数器、虚拟机栈、本地方法栈
JDK1.8运行时数据区域:
OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。StackOverFlowError
:栈内存大小不允许动态扩展,线程请求栈深度>当前 JVM 栈最大深度,抛出OutOfMemoryError
:栈内存大小允许动态扩展,JVM 动态扩展栈时无法申请到足够的内存空间,抛出StackOverFlowError
和 OutOfMemoryError
两种错误。JVM管理内存中最大的一块;所有线程共享的一块内存区域
垃圾收集器管理的主要区域,因此也被称作 GC 堆
堆内存结构:新生代(Eden,s0,s1)、老年代(tenured)、永久代/元空间(元空间使用本地内存)
-XX:MaxTenuringThreshold
设置),就会被晋升到老年代中堆容易出现的 OutOfMemoryError
错误:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
:JVM花太多时间垃圾回收,只能回收很少的堆空间java.lang.OutOfMemoryError: Java heap space
:堆内存空间不足以存放新创建的对象作用:避免字符串重复创建,是JVM为提升性能+减少内存消耗 为String类专门开辟的一块区域
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true!!!
存放位置:JDK1.7之前-永久代;1.7之后-字符串常量池和静态变量移动到堆中
1.8之后拆成了 加载的类信息(元数据区) 和 运行时常量池(堆)
存放的是程序中永远唯一的元素(感觉就是类、方法的定义结果,以及静态变量)
和永久代以及元空间的关系:
元空间溢出会得到OOM错误:java.lang.OutOfMemoryError: MetaSpace
-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimitedMaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
方法还没有执行,所有的字段都还为零。一般来说,执行 new 指令之后会接着执行
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。Object对象占用16字节,对象在内存中的布局可以分为 3 块区域:对象头(8个字节)、实例数据和对齐填充。
对象头具体包含哪些内容?
Mark Word:存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
Klass pointer:虚拟机通过这个指针来确定这个对象是哪个类的实例
数组长度(仅针对数组对象)
JVM是怎么升级锁的?
无锁->偏向锁->轻量级锁->重量级锁。(随着竞争的激烈而逐渐升级,只能升级,不能降级)
无锁:
偏向锁:
轻量级锁(自旋锁):
重量级锁:
Java 程序通过栈上的 reference 数据来操作堆上的具体对象。访问对象的两种主流方式:
使用句柄:Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
(好处:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。)
直接指针:reference 中存储的直接就是对象的地址。(速度快,节省一次指针定位开销)
Bootstrap
(启动类加载器):加载java核心库(如$JAVA_HOME/jre/lib/rt.jar)
Extension
(扩展类加载器):加载拓展库($JAVA_HOME/jre/lib/ext/*.jar)AppClass
(应用程序类加载器):根据java的类路径加载类,一般的java应用的类($CLASSPATH)垃圾:不再被使用的对象,死亡的对象
哪些垃圾需要回收?
引用计数法:每个对象添加一个引用计数器,当为0时,就表示死亡;
可达性分析算法:以根对象集合(GC Roots)为起点,分析GC Roots连接的对象是否可达,解决了循环引用问题。
GC Roots:就是对象!
问题:多线程下更新了访问过的对象的引用
误报:原被引用的对象不再被引用。影响较小,顶多减少GC次数。
漏报:将已被访问过的对象设置为未被访问过。影响较大,可能会使引用对象被GC,导致jvm崩溃。
什么时候回收?
单线程,所有线程stw,
新生代标记-复制,老年代标记-整理
缺点:需要停止所有工作线程,效率低
场景:对应用的实时性要求不高的client级别(桌面应用)的默认方式,单核服务器
Serial的多线程版本,stw, 复制算法
新生代标记-复制,老年代标记-整理
实际线程默认和cpu数量相同
优点:有效利用cpu
缺点:和Serial一样
场景:Sever模式下的新生代,和CMS配合
新生代收集器、复制算法,多线程
与ParNew不同点:追求和精确控制高吞吐量,而ParNew尽可能缩短用户线程的停顿时间;
场景:注重高效利用CPU
Serial的老年代版本,标记整理算法
场景:client、单核。与PS收集器搭配
Parallel Scavenge的老年代版本,多线程,标记整理算法
JDK7,8默认老年代收集器
Concurrent Mark Sweep
多线程、标记清除算法
特点:获取最短回收停顿时间
流程:
三色标记算法(用于并发标记阶段)
黑色对象不再扫描:第一次扫描的时候已经标记完成,并且孩子也标记,自己已经标记,fields都标记完成
灰色:自己标记完成,还没来得及标记fields,则继续扫描其孩子就可以了
白色:没有遍历到的结点
问题:
错标(标记过不是垃圾的,变成了垃圾,也叫浮动垃圾)
objD.fieldE = null
,DE断开,EFG变成不可达,应该回收,但因为E灰,仍然会被当做存活对象遍历下去,EFG不会被回收漏标(如下图,GC 线程已经遍历到E,E变灰,应用进程执行1. G = objE.fieldG; 2. objE.fieldG = null; 3. objD.fieldG = G;
,EG断开,此时切回到GC,因为EG断,G不会灰;尽管D重新引用G,但D已黑,所以不会重新遍历,G就会一直白,被当做垃圾处理,影响程序正确性)
解决方法:CMS用写屏障+增量更新处理,G1用写屏障+SATB原始快照处理
objD.fieldG = G;
),我们可以利用写屏障,将 D 新的成员变量引用对象 G 记录下来。即:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历。objE.fieldG = null;
),我们可以利用写屏障,将 E 原来成员变量的引用对象 G 记录下来。即:当原来成员变量的引用发生变化之前,记录下原来的引用对象。优点:并发收集,低停顿
缺点:
标记整理(整体) + 复制(局部)
特点:
原理:
将java堆分为大小相同的独立区域Region,新生代和老年代区域混合;
步骤
* 复制算法、并行、stw
* 动态调整年轻区大小:根据历史数据,和用户指定的停顿时间目标
老年代收集:
混合式收集:
7种收集器关系
如果两个收集器之间存在连线,就说明它们可以搭配使用
大致思路:串行Serial不够用了->并行PS->想和用户线程并发->CMS,但需要和新生代配合,PS不行->设计ParNew和CMS配合
收集器 | 串/并行/并发 | 新/老 | 收集算法 | 目标 | 场景 |
---|---|---|---|---|---|
Serial | 串行 | 新 | 复制 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
jstat
jstat
查看了 GC 的情况。又通过查看GC log,分析了GC 的详细状况。jstat -gcutil ${pid} 1000
每隔一秒打印一次 GC 统计信息。什么是heap dump
Heap dump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用情况。Heap Dump文件是指定时刻的Java堆栈的快照,是一种镜像文件。Heap Dump一般都包含了一个堆中的Java Objects, Class等基本信息。同时,当你在执行一个转储操作时,往往会触发一次GC,所以你转储得到的文件里包含的信息通常是有效的内容(包含比较少,或没有垃圾对象了) 。我们可以这么理解:heap dump记录内存信息的,thread dump是记录CPU信息的。
怎么得到heap dump
一般来说,两种方式:
事先开启HeadDumpOnOutOfMemoryError,这样出现OOM的时候能自动留下Dump,留好第一现场。这是最推荐的方式。
JVM的启动参数中加入如下的一些参数:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/oom
//第一个参数意思是在OOM的时候自动dump内存快照出来,第二个参数是说把内存快照存放在哪里
手工得到heap dump,这个方式就很多了,命令或者一些第三方工具,都可以获得指定java进程的heap dump。比如jdk自带的jvisualvm.exe,这个是图形化的也不用额外下载。其他方式可以自己去,有很多。通过jvisualvm.exe获取heap jump.
JVM频繁full gc问题排查
常见原因:full gc 触发条件是 老年代空间不足, 所以追因的方向就是导致 老年代空间不足的原因:大量对象频繁进入老年代 + 老年代空间释放不掉
定位思路:如果有监控,那么通过图形能比较直观、快速的了解gc情况;
如果没有监控,那么只能看gc日志或jstat来分析 这是基本技能 一定要熟练
频繁young gc问题排查
分析方法
解决方法
堆内存相关
显式指定堆内存–Xms
和-Xmx
-Xms<heap size>[unit] # 最小堆内存,如-Xms2G
-Xmx<heap size>[unit] # 最大堆内存,如-Xmx5G
显式新生代内存(Young Generation)
方式一:
-XX:NewSize=<young size>[unit] # 最小新生代内存,如-XX:NewSize=256m
-XX:MaxNewSize=<young size>[unit] # 最大新生代内存,如-XX:NewSize=1024m
方式二:
-Xmn256m # 新生代分配256m内存(最小、最大相同)
Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法
方式三:通过 -XX:NewRatio=
来设置老年代与新生代内存的比值。
-XX:NewRatio=1 # 设置老年代和新生代所占比值为1:1
显式指定永久代/元空间的大小
jdk1.8之前:永久代还没有被移除
-XX:PermSize=N #方法区 (永久代) 初始大小
-XX:MaxPermSize=N #方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
jdk1.8:元空间取代永久代
-XX:MetaspaceSize=N # Metaspace使用过程中触发Full GC的阈值,只对触发起作用,元空间初始大小对64位JVM来说是固定的,所以这句话不是设置元空间初始大小的
-XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
垃圾回收相关
垃圾回收器:JVM的四种GC实现
-XX:+UseSerialGC #串行
-XX:+UseParallelGC #并行
-XX:+UseParNewGC #CMS
-XX:+UseG1GC #G1
GC日志记录:一定会配置打印GC日志的参数,便于分析GC相关问题
# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
处理OOM
# 指示JVM在遇到OOM错误时将heap转储到物理文件中
-XX:+HeapDumpOnOutOfMemoryError
# HeapDumpPath表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof格式
-XX:HeapDumpPath=./java_pid<pid>.hprof
# 发出紧急命令,以便在内存不足的情况下执行。
# 内存不足时重启服务器,可以设置参数: -XX:OnOutOfMemoryError="shutdown -r"
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
# 一种策略,限制在抛出OOM错误之前在 GC 中花费的 VM 时间的比例
-XX:+UseGCOverheadLimit
其他
-server
: 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM-XX:+UseStringDeduplication
: Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char []
数组来优化堆内存。-XX:+UseLWPSynchronization
: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。-XX:MaxHeapFreeRatio
: 设置 GC 后, 堆空闲的最大百分比,以避免收缩。-XX:SurvivorRatio
: eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6
设置每个 survivor 和 eden 之间的比例为 1:6。-XX:+UseLargePages
: 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。-XX:+UseStringCache
: 启用 String 池中可用的常用分配字符串的缓存。-XX:+UseCompressedStrings
: 对 String 对象使用 byte []
类型,该类型可以用纯 ASCII 格式表示。-XX:+OptimizeStringConcat
: 它尽可能优化字符串串联操作为什么有gc还会出现内存泄露?
java内存泄露的根本原因?
java.lang.OutOfMemoryError: Java heap space
1、代码中可能存在大对象分配
2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
出现永久代或元空间的溢出的原因可能有如下几种:
1、在Java7之前,频繁的错误使用String.intern方法
2、生成了大量的代理类,导致方法区被撑爆,无法卸载
3、应用长时间运行,没有重启
1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM
java.lang.OutOfMemoryError : unable to create new native Thread
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
1、通过 -Xss
降低的每个线程栈大小的容量
2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
java.lang.OutOfMemoryError:GC overhead limit exceeded
这个是JDK6新加的错误类型,一般都是堆太小导致的。
Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2、添加参数-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
3、dump内存,检查是否存在内存泄露,如果没有,加大内存。
懒汉式 - 线程不安全
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
懒汉式 - 线程安全:在方法声明时加锁
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
双重检验锁(DCL)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
第一次判断是否为 null:
第二次判断 uniqueInstance 是否为 null:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (uniqueInstance == null){
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
饿汉式 - static final field
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
静态内部类 - static nested class
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
public class Singleton{
private static class SingletonHolder(){
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.instance;
}
}
项目中:一般我们项目里用静态内部类的方式实现单例会比较多(如果没有Spring的环境下),代码简洁易读
如果有Spring环境,那还是直接交由Spring容器管理会比较方便(Spring默认就是单例的)
枚举 - Enum
public class Singleton{
private Singleton(){}
/**
*枚举类型是线程安全的,并且只会装载一次
*/
public enum SingletonEnum{
INSTANCE;
private final Singleton instance;
SingletonEnum(){
instance = new Singleton();
}
private Singleton getInstance(){
return instance;
}
}
public static Singleton getInstance(){
return SingletonEnum.INSTANCE.getInstance();
}
}
工厂模式最主要解决的问题就是创建者和调用者的耦合,那么代码层面其实就是取消对new的使用。
Spring IOC容器可以理解为应用了「工厂模式」(通过ApplicationContext或者BeanFactory去获取对象)
简单工厂模式
也叫静态工厂模式
你要去买一台手机,你不用关心手机是怎么生产出来的,里面的零件具体又是怎么制造的,这些通通都交给工厂去处理,你尽管去买手机就好了。
问题:
随着手机品牌增多,工厂生产也需要对应的增加,工厂内部就需要不断的调整。
从代码层面——对内部代码需要增加(也就是需要修改内部代码:那么就会违反OOP原则—开闭原则:一个软件实体应当对扩展开放,对修改关闭
工厂方法模式
当新的手机品牌出现,不是放在同一个工厂生产,而是自己拥有独立工厂生产。那么就解决了上面静态工厂模式违反关闭原则的问题。
工厂方法模式解决简单工厂模式是需要付出代价的!
看到上图工厂方法模式图里新增用虚线画的Huawei品牌,每新增一个品牌就需要增加,对应新的工厂,会发现需要花费很大的成本,现在才三个新的品牌,那么等到十个、一百个的时候就会变得更加的复杂和难以维护。
抽象方法模式
在工厂方法模式中,一个具体的工厂负责生产一类具体的产品,即一对一的关系,但是,如果需要一个具体的工厂生产多种产品对象,那么就需要用到抽象工厂模式了。
比如说,我这边在处理请求的时候,会用到责任链模式进行处理(减免if else 并且让项目结构更加清晰)
JDK中涉及的设计模式总结-CSDN博客
MySQL 支持哪些存储引擎
存储引擎架构
MyISAM 和 InnoDB 区别(7点)
常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。(相当于数据的目录)
优点 :
缺点 :
为啥不用哈希表做MySQL索引数据结构?
SELECT * FROM tb1 WHERE id < 500;
,树直接遍历比 500 小的叶子节点就够了;哈希还要把1-499数据hash计算一遍来定位B树和B+树区别?(3点)
B+树比B树的优势
1.单一节点存储更多的元素,使得查询的IO次数更少;
2.所有查询都要查找到叶子节点,查询性能稳定;
3.所有叶子节点形成有序链表,便于范围查询。
主键索引:一张数据表只能有一个主键,且不能为null,不能重复
二级索引
聚簇索引&非聚簇索引
覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引
InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
如下图,查询的name字段就是索引,查到name直接返回就行,不用再找name对应的key:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bZUXu28J-1686536899655)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20230601222840313.png)]
联合索引
索引下推
原子性(Atomicity
) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;【undo log】
一致性(Consistency
): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;【最终目的】
隔离性(Isolation
): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;【锁+MVCC】
持久性(Durability
): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。【redo log】
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
脏读(Dirty read)
某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的
丢失修改(Lost to modify)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
不可重复读(Unrepeatable read)
在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom read)
在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
悲观控制模式
乐观控制模式
多版本并发控制(MVCC,Multiversion concurrency control) 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log。
undo log : undo log 用于记录某行数据的多个版本的数据。
read view 和 隐藏字段 : 用来判断当前版本数据的可见性。
在事务隔离级别 RC
和 RR
(InnoDB 存储引擎的默认事务隔离级别)下,InnoDB
存储引擎使用 MVCC
(非锁定一致性读),但它们生成 Read View
的时机却不同
在 RC 隔离级别下的 每次select
查询前都生成一个Read View
(m_ids 列表)
在 RR 隔离级别下只在事务开始后 第一次select
数据前生成一个Read View
(m_ids 列表)
读取未提交(READ-UNCOMMITTED) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
读取已提交(READ-COMMITTED) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
可重复读(REPEATABLE-READ) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。(InnoDB 存储引擎默认隔离级别)
可串行化(SERIALIZABLE) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别是怎么实现的?
UPDATE
、DELETE
语句时,如果 WHERE
条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
S锁 | X锁 | |
---|---|---|
S锁 | 不冲突 | 冲突 |
X锁 | 冲突 | 冲突 |
作用:如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。用意向锁可以快速判断是否可以对某个表使用表锁
分类
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁之间是兼容的:
IS 锁 | IX 锁 | |
---|---|---|
IS 锁 | 兼容 | 兼容 |
IX 锁 | 兼容 | 兼容 |
意向锁和(表级)共享锁、(表级)排它锁互斥:
IS 锁 | IX 锁 | |
---|---|---|
S 锁 | 兼容 | 互斥 |
X 锁 | 互斥 | 互斥 |
快照读(一致性非锁定锁):就是单纯的 SELECT
语句,但不包括下面这两类 SELECT
语句:
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
当前读 (一致性锁定读):就是给行记录加 X 锁或 S 锁
常见sql语句类型:
# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...
redo log
(重做日志)是InnoDB
存储引擎独有的,它让MySQL
拥有了崩溃恢复能力。【持久性+完整性】redo log 和 undo log 区别在哪?
InnoDB
存储引擎为 redo log
的刷盘策略提供了 innodb_flush_log_at_trx_commit
参数,它支持三种策略:
redo log 和 binlog 有什么区别?
1、高性能
2、 高并发
5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
跳表
压缩列表
IO多路复用是啥?
IO多路复用的三种机制?(都是系统调用)
适用:读请求比较多的场景
原则:应用程序只和缓存交互,不再和数据库交互
读写穿透策略:
先更db,再更缓存
如下图,更新顺序,最终db存2,缓存为1,不一致!
先更缓存,再更db
如下图,更新顺序,最终db存1,缓存为2,不一致!
无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。
阿旺定位出问题后,思考了一番后,决定在更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
先删缓存,再更db
可以看到,先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题。
先更db,再删缓存
「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
redis内存数据库,但会把缓存数据存到硬盘
->自带两种持久化技术:AOF日志&RDB快照
->redis默认开启RDB快照,重启redis,之前的缓存数据会被重新加载
三种AOF持久化方式:
这 3 种持久化方式的主要区别在于 fsync
同步 AOF 文件的时机(刷盘)。
appendfsync always
:主线程调用 write
执行写操作后,后台线程( aof_fsync
线程)立即会调用 fsync
函数同步 AOF 文件(刷盘),fsync
完成后线程返回,这样会严重降低 Redis 的性能(write
+ fsync
)。appendfsync everysec
:主线程调用 write
执行写操作后立即返回,由后台线程( aof_fsync
线程)每秒钟调用 fsync
函数(系统调用)同步一次 AOF 文件(write
+fsync
,fsync
间隔为 1 秒)appendfsync no
:主线程调用 write
执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write
但不fsync
,fsync
的时机由操作系统决定)。Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决
缓存击穿中,请求的 key 对应的是 热点数据(频繁被访问的数据) ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决
缓存穿透和缓存击穿有什么区别?
缓存在同一时间大面积的失效 or Redis故障宕机,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
解决
缓存雪崩和缓存击穿有什么区别?
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
如果不在,则正常读取键值;
如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
redis使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
在设置了过期时间的数据中进行淘汰:
server.db[i].expires
)中挑选最近最少使用的数据淘汰。(Redis3.0 之前,默认的内存淘汰策略)server.db[i].expires
)中挑选将要过期的数据淘汰。server.db[i].expires
)中任意选择数据淘汰。所有数据范围内淘汰:
server.db[i].dict
)中任意选择数据淘汰。不进行数据淘汰:
基础版本:
是什么?超文本传输协议
超文本
传输
协议
所以,HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
状态码
常见字段
区别
语义
GET :从服务器获取指定资源
POST :根据报文body处理指定资源
安全性(不破坏服务器上资源)
GET√(因为GET只读
POST×(因为POST会修改服务器资源
幂等(执行多次操作结果相同)
可被缓存?
短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接
长连接:建立SOCKET连接后不管是否使用都保持连接,直到一方关闭连接
HTTPS加密过程
HTTPS加密过程 - 简书 (jianshu.com)
客户端发起HTTPS请求
服务端的配置
采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。证书其实就是一对公钥和私钥。
如果对公钥不太理解,可以想象成一把钥匙和一个锁头,只是世界上只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
给客户端传送证书
这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
【避免篡改】
用哈希算法对报文提取定长摘要;用私钥对摘要进行加密,作为数字签名;将数字签名附加到报文末尾发送给客户端;
客户端解析证书
【避免篡改】
用公钥对服务器的数字签名进行解密;用同样的算法重新计算出报文的数字签名;比较解密后的签名与自己计算的签名是否一致,如果不一致,说明数据被篡改过;如果发现异常,则会弹出一个警示框,提示证书存在的问题。
如果证书没有问题,那么就生成一个随机值。然后用证书(也就是【公钥】)对这个【随机值进行加密】。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
给服务端传送加密信息
服务端解密信息
传输给客户端加密后的信息
客户端解密信息
「安全」的HTTP协议(客户端与服务端的传输链路中进行加密)
保密问题:
HTTPS的优缺点
UDP 一般用于即时通信,比如: 语音、 视频 、直播等等。【DNS】
TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。【FTP文件传输、HTTP/HTTPS】
Linux怎么查看TCP状态?
netstat -napt
命令三报文而不是两报文?四报文
阻止重复历史连接的初始化:为了防止已失效的连接请求报文段(历史连接)突然又传送到了TCP服务器,因而导致错误
不使用两次握手的原因:在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费
同步双方初始序列号
避免资源浪费:【不使用四次握手的原因】
为什么序列号是随机的?
为啥需要四次?
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。FIN
报文时,先回一个 ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN
报文给客户端来表示同意现在关闭连接。ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。第一次挥手丢失会发生什么?
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
第二次挥手丢失会发生什么?
第三次挥手丢失会发生什么?
服务端收到第一次挥手后,内核会自动回复ACK,同时处于CLOSE_WAIT
状态,即等待应用进程调用close函数关闭连接
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会第三次挥手,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
这时第三次挥手丢了!迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries
参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
第四次挥手丢失会发生什么?
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
举个例子,假设 tcp_orphan_retries 为 2,当第四次挥手一直丢失时,发生的过程如下:
为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是 Maximum Segment Lifetime,报文最大生存时间,如果超过这个时间,这个TCP报文就会被丢弃。2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。为什么需要 TIME_WAIT 状态?
谁有这个状态?主动发起关闭连接的一方,所以客户端和服务端都有可能!
为什么需要?(两点原因)
保证让迟来的TCP报文有足够的时间被识别并丢弃
SEQ = 301
报文,被网络延迟了,接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301
这时抵达了客户端,会发生数据错乱等问题保证「被动关闭连接」的一方,能被正确的关闭
如下图,四次挥手的最后⼀个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了
按照 TCP 可靠性原则,服务端会重发 FIN 报文,发现对方关了,客户端收到FIN返回RST报文,解释为一个错误。
TIME_WAIT过多有什么危害?
32768~61000
,也可以通过 net.ipv4.ip_local_port_range
参数指定范围。服务器出现大量TIME_WAIT状态的原因?
应用层协议,基于UDP协议之上
两种查询解析模式
迭代
主机->本地DNS->根服务器->TLD DNS->权威DNS->本地DNS->主机
递归
DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议
什么是内核态和用户态?
用户态(User Mode) :
内核态(Kernel Mode):
**为什么要设置这两个核态?**只有内核态不行吗?
内核态和用户态如何切换?
如何降低两个核态切换开销?
为什么切换开销大?用户态转化为内核态时,需要执行系统调用,保存现场,也就是保存用户态的寄存器等, 然后去系统调用,并在内核态执行,最后恢复现场。并且由于内核对于用户的不信任, 因此内核需要对用户进行一些额外的检查,这就需要耗费更多的工作了。(保存现场+安全检查开销大)
降低进程通信的开销,可以用共享内存方式进行进程通信
mmap
对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
减少系统调用(System Calls): 系统调用是用户态和内核态之间的切换触发点。减少系统调用的次数可以减少内核态和用户态之间的切换开销。可以通过使用更高效的系统调用、批量操作和缓存数据等方式来减少系统调用次数。
使用轻量级的同步机制: 同步机制,如锁、信号量等,会导致内核态和用户态之间的切换。使用轻量级的同步机制可以减少内核态和用户态之间的切换次数。例如,使用非阻塞的自旋锁或无锁数据结构,避免频繁的阻塞和唤醒操作。
使用事件驱动模型: 事件驱动模型可以避免线程的阻塞和切换,从而减少内核态和用户态之间的切换开销。通过使用异步IO、回调函数或事件循环机制,可以使程序在一个线程内处理多个事件,减少线程的创建和销毁,从而降低切换开销。
使用线程池和协程: 使用线程池和协程可以减少线程的创建和销毁,从而减少内核态和用户态之间的切换次数。线程池可以复用线程,减少线程创建的开销,而协程则可以在一个线程内实现多个协程的切换,避免线程的切换开销。
常用的ipc划横线了
管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。【开销最小】
套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
synchronized
关键词和各种 Lock
都是这种机制。CyclicBarrier
是这种机制。页式
段式
段页式
死锁:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁的四个必要条件:
解决死锁的方法:
Spring MVC是Spring中很重要的模块,核心思想是将业务逻辑、数据、显示分离来组织代码
Spring Boot 简化了Spring配置,如果需要构建 MVC 架构的 Web 程序,还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。
为什么叫控制反转?
控制 :指的是对象创建(实例化、管理)的权力
反转 :控制权交给外部环境(Spring 框架、IoC 容器)
SpringIOC有两个核心思想就是IOC控制反转和DI依赖注入
IOC 控制反转的基本思想是,将原来的对象控制从使用者,有了spring之后可以把整个对象交给spring来帮我们进行管理。
DI 依赖注入,就是把对应的属性的值注入到具体的对象中。spring提供标签和@Autowired和@Resource注解等方式注入,注入方式本质上是AbstractAutowireCapableBeanFactory的populateBean() 方法先从beanDefinition 中取得设置的property值,*例如autowireByName方法会根据bean的名字注入;autowireByType方法根据bean的类型注入,完成属性值的注入(涉及bean初始化过程)。*对象会存储在map结构中,在spring使用Map结构的singletonObjects存放完整的bean对象(涉及三级缓存和循环依赖)。整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理(涉及bean的生命周期)。
IOC 容器底层就是对象工厂(BeanFactory 接口)。IOC的原理是基于xml解析、工厂设计模式、反射实现的。使用IOC可以降低代码的耦合度。
@Component
注解作用于类,而@Bean
注解作用于方法。@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean
注解比 @Component
注解的自定义性更强,而且很多地方我们只能通过 @Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring
容器时,则只能通过 @Bean
来实现。@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为 byName
(根据名称进行匹配)。@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过 @Qualifier
注解来显式指定名称,@Resource
可以通过 name
属性来显式指定名称。set()
方法设置一些属性值。BeanNameAware
接口,调用 setBeanName()
方法,传入 Bean 的名字。BeanClassLoaderAware
接口,调用 setBeanClassLoader()
方法,传入 ClassLoader
对象的实例。BeanFactoryAware
接口,调用 setBeanFactory()
方法,传入 BeanFactory
对象的实例。*.Aware
接口,就调用相应的方法。BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法InitializingBean
接口,执行afterPropertiesSet()
方法。BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法DisposableBean
接口,执行 destroy()
方法。AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
1、main方法中SpringApplication.run(HelloBoot.class,args)的执行流程中有refreshContext(context)。
2、而这个refreshContext(context)内部会解析,配置类上自动装配功能的注解@EnableAutoConfiguration中的,@EnableAutoConfiguration中的,引入类AutoConfigurationImportSelector。
3、AutoConfigurationImportSelector这个类中的方法SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()会读取jar包中的/项目中的META-INF/spring.factories文件。
4、spring.factories配置了自动装配的类,最后根据配置类的条件,自动装配Bean。
jdbcTemplate
、hibernateTemplate
等以Template结尾的对数据库操作的类,它们就使用到模板模式。Controller
。Spring Boot 常用注解汇总 - 云天 - 博客园 (cnblogs.com)
@Transactional
我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 savePerson()
方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。
public void savePerson() {
personDao.save(person);
personDetailDao.save(personDetail);
}
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:
- 将小明的余额减少 1000 元。
- 将小红的余额增加 1000 元。
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
编程式事务管理(少用)
TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。声明式事务管理(推荐)
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional
的全注解方式使用最多)。
使用 @Transactional
注解进行事务管理的示例代码如下:
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
Spring 框架中,事务管理相关最重要的 3 个接口如下:
PlatformTransactionManager
:(平台)事务管理器,Spring 事务策略的核心。事务上层的管理者
TransactionDefinition
:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
TransactionStatus
:事务运行状态。
PlatformTransactionManager:事务管理接口
Spring 并不直接管理事务,而是提供了多种事务管理器。
接口定义的三个方法:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
TransactionDefinition:事务属性
事务管理器接口 PlatformTransactionManager
通过 getTransaction(TransactionDefinition definition)
方法来得到一个事务,这个方法里面的参数是 TransactionDefinition
类 ,这个类就定义了一些基本的事务属性。
事务属性:事务的一些基本配置,描述了事务策略如何应用到方法上。
事务属性包含5个方面:
在TransactionDefinition
接口中的实现:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
@Nullable
String getName();
}
TransactionStatus:事务状态
TransactionStatus
接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。
PlatformTransactionManager.getTransaction(…)
方法返回一个 TransactionStatus
对象。
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事务
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务传播行为种类:
PROPAGATION_REQUIRED
:默认的,使用最多。
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
举个例子:如果我们上面的aMethod()
和bMethod()
使用的都是PROPAGATION_REQUIRED
传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.REQUIRED)
public void bMethod {
//do something
}
}
PROPAGATION_REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
举个例子:如果我们上面的bMethod()
使用PROPAGATION_REQUIRES_NEW
事务传播行为修饰,aMethod
还是用PROPAGATION_REQUIRED
修饰的话。如果aMethod()
发生异常回滚,bMethod()
不会跟着回滚,因为 bMethod()
开启了独立的事务。但是,如果 bMethod()
抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()
同样也会回滚,因为这个异常被 aMethod()
的事务管理机制检测到了。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
PROPAGATION_NESTED
:如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED
类似的操作。
简单举个例子:如果 bMethod()
回滚的话,aMethod()
不会回滚。如果 aMethod()
回滚的话,bMethod()
会回滚。
@Service
Class A {
@Autowired
B b;
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
b.bMethod();
}
}
@Service
Class B {
@Transactional(propagation = Propagation.NESTED)
public void bMethod {
//do something
}
}
.PROPAGATION_MANDATORY
:很少用。
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
其他三种传播行为很少用。
项目中怎么用事务传播的?
@Transactional
;添加积分:@Transactional(propagation = Propagation.NESTED)
隔离级别
同mysql
超时属性
一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
只读属性
只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException
的子类)时才会回滚,Error
也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
@Transactional(rollbackFor= MyException.class)
作用范围
方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
接口:不推荐在接口上使用。
常用配置参数
原理
面试中在问 AOP 的时候可能会被问到的一个问题。
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。@Transactional
什么时候会失效?
非public方法:注解标注方法修饰符为非public时,@Transactional注解将会不起作用
在类内部调用调用类内部@Transactional标注的方法:若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。【AOP自调用问题】
这是由于Spring AOP
代理的原因造成的,因为只有当 @Transactional
注解的方法在类以外被调用的时候,Spring 事务管理才生效。
//MyService类中的method1()调用method2()就会导致method2()的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
3. **异常处理不当**:事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。
4. **数据库不支持**:如 MySql 的 MyISAM 引擎
ps 和 top 命令常用来查看Linux系统进程相关信息。
**ps命令:**可以查看进程的瞬间信息。
ps命令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
使用场景:
ps -ef #显示所有当前进程
ps aux #显示所有当前进程
ps -ax #显示所有当前进程
ps -u pungki #根据用户过滤进程
ps -aux --sort -pcpu | less #根据 CPU 使用来升序排序
ps -aux --sort -pmem | less #根据用户过滤进程
ps -aux --sort -pcpu,+pmem | head -n 10 #查询全10个使用cpu和内存最高的应用
ps -C getty #通过进程名和PID过滤
ps -f -C getty #带格式显示的,通过进程名和PID过滤
ps -L 1213 #根据线程来过滤进程
ps -axjf(或pstree) #树形显示进程
ps -eo pid,user,args # 显示安全信息
ps -U root -u root u #格式化输出 root 用户(真实的或有效的UID)创建的进程
**top命令:**可以持续的监视进程的信息。
top命令用来显示执行中的程序进程,使用权限是所有用户。
第一行表示的项目依次为当前时间、系统启动时间、当前系统登录用户数目、平均负载。
第二行显示的是所有启动的进程、目前运行的、挂起(Sleeping)的和无用(Zombie)的进程。
第三行显示的是目前CPU的使用情况,包括系统占用的比例、用户使用比例、闲置(Idle)比例。
第四行显示物理内存的使用情况,包括总的可以使用的内存、已用内存、空闲内存、缓冲区占用的内存。
第五行显示交换分区使用情况,包括总的交换分区、使用的、空闲的和用于高速缓存的大小。
第六行显示的项目最多,下面列出了详细解释
PID 进程id
USER 进程所有者
PR 进程优先级
NI nice值。负值表示高优先级,正值表示低优先级
VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
SHR 共享内存大小,单位kb
S 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程,N表示该进程优先值是负数。
%CPU 上次更新到现在的CPU时间占用百分比
%MEM 进程使用的物理内存百分比
TIME+ 进程使用的CPU时间总计,单位1/100秒
COMMAND 进程名称(命令名/命令行)
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
一个最基本的分布式锁需要满足:
不论是本地锁还是分布式锁,核心都在于“互斥”。
在 Redis 中, SETNX
命令是可以帮助我们实现互斥。SETNX
即 SET if Not eXists (对应 Java 中的 setIfAbsent
方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX
啥也不做。
SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0
释放锁的话,直接通过 DEL
命令删除对应的 key 即可。
DEL lockKey
(integer) 1
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
为什么选择来银行?
银行是一个有稳定发展前景的行业,而且在当今的经济环境下,银行的角色非常重要。选择银行是因为希望能够在一个稳定的行业中工作,同时为社会做出一些贡献。银行是一个注重团队合作和个人成长的行业。我希望能够在一个团结、相互协作的环境中工作,学习和发展自己的能力,不断提高自己的技能和经验,为银行和自己的未来发展做出贡献。
我对银行的核心业务和金融知识有浓厚的兴趣。希望通过在银行的工作中,深入了解银行的业务和金融知识,并将其应用于日常工作中,以提高自己的专业技能和知识水平。
*:若同一类中的其他没有 @Transactional
注解的方法内部调用有 @Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。【AOP自调用问题】
这是由于Spring AOP
代理的原因造成的,因为只有当 @Transactional
注解的方法在类以外被调用的时候,Spring 事务管理才生效。
//MyService类中的method1()调用method2()就会导致method2()的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
3. **异常处理不当**:事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。
4. **数据库不支持**:如 MySql 的 MyISAM 引擎
ps 和 top 命令常用来查看Linux系统进程相关信息。
**ps命令:**可以查看进程的瞬间信息。
ps命令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
使用场景:
ps -ef #显示所有当前进程
ps aux #显示所有当前进程
ps -ax #显示所有当前进程
ps -u pungki #根据用户过滤进程
ps -aux --sort -pcpu | less #根据 CPU 使用来升序排序
ps -aux --sort -pmem | less #根据用户过滤进程
ps -aux --sort -pcpu,+pmem | head -n 10 #查询全10个使用cpu和内存最高的应用
ps -C getty #通过进程名和PID过滤
ps -f -C getty #带格式显示的,通过进程名和PID过滤
ps -L 1213 #根据线程来过滤进程
ps -axjf(或pstree) #树形显示进程
ps -eo pid,user,args # 显示安全信息
ps -U root -u root u #格式化输出 root 用户(真实的或有效的UID)创建的进程
**top命令:**可以持续的监视进程的信息。
top命令用来显示执行中的程序进程,使用权限是所有用户。
第一行表示的项目依次为当前时间、系统启动时间、当前系统登录用户数目、平均负载。
第二行显示的是所有启动的进程、目前运行的、挂起(Sleeping)的和无用(Zombie)的进程。
第三行显示的是目前CPU的使用情况,包括系统占用的比例、用户使用比例、闲置(Idle)比例。
第四行显示物理内存的使用情况,包括总的可以使用的内存、已用内存、空闲内存、缓冲区占用的内存。
第五行显示交换分区使用情况,包括总的交换分区、使用的、空闲的和用于高速缓存的大小。
第六行显示的项目最多,下面列出了详细解释
PID 进程id
USER 进程所有者
PR 进程优先级
NI nice值。负值表示高优先级,正值表示低优先级
VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
SHR 共享内存大小,单位kb
S 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程,N表示该进程优先值是负数。
%CPU 上次更新到现在的CPU时间占用百分比
%MEM 进程使用的物理内存百分比
TIME+ 进程使用的CPU时间总计,单位1/100秒
COMMAND 进程名称(命令名/命令行)
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
一个最基本的分布式锁需要满足:
不论是本地锁还是分布式锁,核心都在于“互斥”。
在 Redis 中, SETNX
命令是可以帮助我们实现互斥。SETNX
即 SET if Not eXists (对应 Java 中的 setIfAbsent
方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX
啥也不做。
SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0
释放锁的话,直接通过 DEL
命令删除对应的 key 即可。
DEL lockKey
(integer) 1
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
为什么选择来银行?
银行是一个有稳定发展前景的行业,而且在当今的经济环境下,银行的角色非常重要。选择银行是因为希望能够在一个稳定的行业中工作,同时为社会做出一些贡献。银行是一个注重团队合作和个人成长的行业。我希望能够在一个团结、相互协作的环境中工作,学习和发展自己的能力,不断提高自己的技能和经验,为银行和自己的未来发展做出贡献。
我对银行的核心业务和金融知识有浓厚的兴趣。希望通过在银行的工作中,深入了解银行的业务和金融知识,并将其应用于日常工作中,以提高自己的专业技能和知识水平。