admin管理员组文章数量:1599449
这里写目录标题
- Java基础
- 常用语法结构:
- 1.Java异常
- 2.Java反射
- 关于构造方法
- 关于成员变量
- 3.Java注解
- 元注解
- @Retention
- @Documented
- @Target
- @Inherited
- @Repeatable
- 作用在代码的注解
- @SuppressWarnings
- @Deprecated
- @Override
- @FunctionalInterface
- @SafeVarargs
- 4.Java内部类
- 成员内部类
- 5.Java泛型
- 6.Java复制
- 基本概念和规则
- Cookie和Session的区别:
- 时间复杂度
- 关于存储结构
- 关键字
- final
- synchronized
- 标识符
- serialVersionUID
- 成员变量和局部变量:
- 数组和集合互相转换:
- 重载和重写:
- 抽象类和接口:
- object 中定义了哪些方法?
- hashCode()
- wait()和sleep()的区别
- java创建对象的方式:
- 面向对象的三大特征
- 封装
- 继承
- 多态
- 访问修饰符之间的区别
- 基本数据类型和引用数据类型
- Java常用包
- **`java.lang` 包:**
- **`java.util` 包:**
- 集合
- Java中有几种类型的流:字节流和字符流
- Java是值传递,还是引用传递?
- Integer 缓存实现
- equals和==的区别
- break和continue的区别
- this和super关键字作用
- 同步通信和异步通信:
- java8新特性
- Lambda表达式
- 特性
- 四个内置核心函数式接口
- 方法引用
- Stream
- Stream创建
- 中间操作
- 终端操作
- 查找与匹配
- **Optional类**:
- 数据结构
- RPC
- Redis
- 概述
- Redis为什么快?
- **Redis**
- **概念:**
- 流程
- Redis Sentinel(哨兵)
- 概念
- 流程(监控和自动故障转移)
- 节点选举:
- 概念
- 流程
- Term(任期)
- 心跳机制:
- 节点的三个状态:
- 三个子问题:
- 分布式
- 分布式理论
- cap
- BASE
- 分布式锁
- **分布式会话**
- 分布式搜索
- 分布式消息队列
- RabbitMQ
- 概念:
- 各组件功能
- 看不见的
- 看的见的
- 流程
- 限流
- 如何避免消息重复导致重复执行的问题?
- 分布式服务
- SpringCloud
- 分布式缓存
- `缓存穿透`:
- `缓存击穿`:
- `缓存雪崩`:
- `缓存预热`
- `缓存更新(数据一致性)`
- `缓存降级`
- 网络
- TCP/IP 原理
- 三次握手
- 连接终止协议(四次挥手)
- JDK
- **JVM**
- 概念
- 参数
- 解读
- 一阶段(class文件是怎么进入JVM的)
- 类加载器
- 文件结构
- 双亲委派
- 过程
- 二阶段(JVM的内存分配和程序执行:)
- 内存分配(角色)
- **堆** ->
- **栈**
- **程序计数器(Program Counter Register)**
- **本地方法栈(Native Method Stack)**
- 内存划分
- 年轻代
- 老年代
- 元空间(持久代)
- 关于内存划分的问题:
- 程序执行
- 三阶段(垃圾回收)
- 概念:(为什么被回收)
- websocket
- 优势:
- `低延迟`:
- `性能`:
- `数据传输`:
- `跨域支持`
- 概念
- http与webSocket的区别:
- socket与webSocket
- UDP和TCP
- `连接性`
- `可靠性`
- `流控制`
- `应用场景`
- spring
- IOC,DI,AOP(内核)
- `IOC控制反转`
- 概念:
- 生命周期
- `AOP面向切面编程`
- **Spring加载Bean的方式以及使用场景**:
- 常用注解
- springMVC
- 常用注解
- **执行流程**
- springBoot
- springBoot常用注解:
- springBoot的自动装配:
- Mybatis
- Jpa
- tomcat
- sercurity
- 泛型
- 多线程
- 并发编程遇到的问题
- 线程死锁
- 上下⽂切换
- 为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能 直接调⽤ run() ⽅法?
- 线程的生命周期:
- 创建线程的几种方式:
- 1.解决方法
- 2、锁的种类:
- Sql
- 数据库三大范式
- **SQL语言**:
- **修改和删除表**
- **增删改查**
- **MySQL架构**
- Server层
- **连接层**
- **分析器**
- **优化器**
- **执行器**
- **引擎**
- InnoDB引擎:
- **MySQL集群和分布式**
- **Mysql的事务实现**
- 事物(ACID)
- 14 怎么解决慢查询问题
- 2 MVCC(多版本并发控制)
- 并行事务带来的问题:
- 事务隔离(TRANSACTION_ISOLATION)
- sql优化(索引失效)
- 行锁:
- 表锁:
- 文件结构:
- 一样
- 不一样
- 解读:
- Memory:
- MERGE:
- 索引
- 索引类型:
- 普通(Normal):
- 唯一(Unique):
- 全文(Fulltext):
- 联合索引(Compound Index)
- 空间索引(SPATLAL)
- 索引方法
- **B+树索引**
- hash索引:
- 物理存储类型
- 聚簇索引
- 非聚簇索引
- 数的概念
- **NGINX**
- java算法
- 1.二分查找
- 2.冒泡排序
- 3.插入排序
- 4.快速排序
- 5.希尔排序
- 6.归并排序
- 7.桶排序
- 8.基数排序
- 9.剪纸算法
- 10.回溯算法
- 12.最大子数组算法
- 14.最小生成树算法
- 算法
- 动态规划(Dynamic Programming,简称DP)
- 最长公共子序列算法
- 最短路径算法
- Linux命令
- mysql:
- 加密算法
- Docker
Java基础
常用语法结构:
普通方法(公有(public)、保护(protected)、默认(default) 、私有(private))
静态方法(类方法)
final修饰的方法(最终方法)
构造函数方法(构造器、构造方法)
接口方法
- 默认方法->解决修改接口时候,需要修改全部实现该接口的类
抽象方法
泛型方法
1.Java异常
在程序执行期间发生的意外或错误事件
程序发生异常,在代码块中抛出异常对象
语法:throw new 异常对象
在方法中声明可能抛出的异常,并未实际抛出异常
语法:返回类型 方法名(参数列表) throws 异常对象
捕获和处理可能发生的异常。
如果
try
块中代码抛出异常,会转移到catch
块中处理异常finally:程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行。
语法:
try {
// 可能抛出异常的代码
} catch (CustomException e) {
// 处理异常
} finally {
// 无论是否发生异常,都会执行的代码块
}
常见的异常分为两类一个是受检异常(Checked Exception)和非受检异常( Unchecked Exception)
受检异常->在编译时检测到的异常,必须强制处理或声明异常,继承自Exception 类
- IOException(包括FileNotFoundException-)
- SQLException
非受检异常->在运行时检测到的异常,不强制处理或声明异常,继承自 RuntimeException 类
- NullPointerException(空指针异常)
- IndexOutOfBoundsException(下标越界异常)
- ClassNotFoundException (指定的类不存在)
- NoSuchMethodError (方法不存在)
- NumberFormatException (数字格式异常)
- ArrithmeticException(算数异常)
- java.lang.IllegalArgumentException(方法传递参数错误)
2.Java反射
概念:
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个属性和方法;这种动态获取的信息以及动态调用对象的方法的功能称为 java反射。
应用场景:Spring/Spring Boot等框架中都大量使用了反射机制,这些框架的功能实现就是利用反射。
反射的原理?
Java程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进元空间,反射就是去 获取这些信息,然后进行各种操作
场景:idea里面自动提示功能就是用反射来实现的
获取Class对象的三种方式:
1.Class.forName(“全类名”)
2.类名.class
3.对象.getClass();
编写java文件->编译成class字节码文件,在此阶段是没有把代码给加载到内存当中的,只是在硬盘里执行了操作,这个阶段我们也称之为源代码阶段,这里我们用第一种方法。
将字节码文件加载到内存中,此时是加载阶段,用第二种方式
在内存中创建该字节码对象,是运行阶段 ,用第三种方式
关于构造方法
Class类中用于获取构造方法的方法:
Constructor<?>[] getConstructors(): 返回所有公共构造方法对象的数组
Constructor<?>[]getDeclaredConstructors(): 返回所有构造方法对象的数组
Constructor getConstructor(Class<?>… parameterTypes): 返回公共单个公共构造方法对象
Constructor getDeclaredConstructor(Class<?>… parameterTypes): 返回单个构造方法对象
Constructor类中用于创建对象的方法:
T newInstance(Object…initargs): 根据指定的构造方法创建对象setAccessible(boolean flag): 设置为true,表示取消访问检查
获取权限修饰符
在获取到构造函数后使用变量名.getModifiers()
获取权限修饰符值
常量字段 | 值 |
---|---|
PUBLIC | 1 |
PRIVATE | 2 |
PROTECTED | 4 |
STATIC | 8 |
FINAL | 16 |
SYNCHRONIZED | 32 |
VOLATILE | 64 |
TRANSIENT | 128 |
NATIVE | 256 |
INTERFACE | 512 |
ABSTRACT | 1024 |
STRICT | 2048 |
关于成员变量
Class类中用于获取成员变量的方法
Field[]getFields():返回所有公共成员变量对象的数组
Fieldl[] getDeclaredFields():返回所有成员变量对象的数组
Field getField(String name): 返回单个公共成员变量对象
Field getDeclaredField(String name): 返回单个成员变量对象
Field类中用于创建对象的方法
void set(Object obj,Object value): 赋值
Object get(Object obj) 获取值。
3.Java注解
是在 Java SE 5.0 版本中开始引入的概念。
创建方式和接口类似,在Interface里添加@
元注解
:给其他普通标签进行解释说明
@Retention
、@Documented
、@Target
、@Inherited
、@Repeatable
5种
Java注解本质上是一个标签,注解可以标记在类上、方法上、属性上等,标签自身也可以设置一些值,有了标签之后,就可以在编译或者运行阶段去识别这些标签,例如我们常见的AOP,使用注解作为切点就是运行期注解的应用以及lombok注解在编译期中的运行
元注解
@Retention
注解保留策略
- RetentionPolicy.SOURCE(源代码阶段):注解只在源代码中存在,编译时会被丢弃,不会包含在编译后的字节码中。这意味着在运行时无法获取到这些注解信息。
- RetentionPolicy.CLASS(类文件级别):注解会被保留到编译后的字节码文件中,但在运行时不会被虚拟机保留,因此通过反射等机制无法获取这些注解信息。
- RetentionPolicy.RUNTIME (运行时级别):注解会被保留到编译后的字节码文件中,并且在运行时可以通过反射机制获取到注解信息。
@Documented
生成javadoc文档时,是否显示注解
@Target
指定注解运用的地方。
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
如果一个注解被
@Inherited
修饰,并且这个注解被用于一个类,那么它的子类将继承该注解。
@Repeatable
重复使用的注解
作用在代码的注解
@SuppressWarnings
指示编译器去忽略注解中声明的警告。
@Deprecated
标识过时的元素
@Override
子类重写父类中被@Override方法
@FunctionalInterface
函数式接口注解
@SafeVarargs
抑制使用可变参数的方法或构造函数时的编译器警告
4.Java内部类
在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类
成员内部类
位于一个类的内部
Draw类像是Circle类的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
class Circle { //外部类
private double radius = 0;
public static int count =1;
public Circle(double radius) {
this.radius = radius;
}
class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
System.out.println(count); //外部类的静态成员
}
}
}
5.Java泛型
6.Java复制
对象复制
-
浅拷贝
概念:
浅复制会复制包括基本数据类型的值和引用类型变量的地址值。
对于
基本数据类型
,修改其中一个对象值不会影响另一个对象对于
引用数据类型
,修改其中一个对象值会影响到另一个对象。方式:
使用
clone()
方法public class Person implements Cloneable { private String name; private int age; // 构造函数 public Person(String name, int age) { this.name = name; this.age = age; } // Getter 和 Setter 方法 // 重写clone()方法 @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) { try { // 创建原始对象 Person originalPerson = new Person("John", 30); // 使用clone()方法创建新对象 Person clonedPerson = (Person) originalPerson.clone(); // 打印原始对象和复制对象的信息 System.out.println("Original Person: " + originalPerson.getName() + ", " + originalPerson.getAge()); System.out.println("Cloned Person: " + clonedPerson.getName() + ", " + clonedPerson.getAge()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
拷贝构造函数进行复制
public class Person { private String name; private int age; // 构造函数 public Person(String name, int age) { this.name = name; this.age = age; } // 拷贝构造函数 public Person(Person other) { this.name = other.name; this.age = other.age; } // Getter 和 Setter 方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static void main(String[] args) { // 创建原始对象 Person originalPerson = new Person("John", 30); // 使用拷贝构造函数创建新对象 Person copiedPerson = new Person(originalPerson); // 打印原始对象和复制对象的信息 System.out.println("Original Person: " + originalPerson.getName() + ", " + originalPerson.getAge()); System.out.println("Copied Person: " + copiedPerson.getName() + ", " + copiedPerson.getAge()); } }
-
深拷贝
概念:
在复制对象时,不仅复制对象本身,还递归复制其所有对象引用,使得复制后的对象和原始对象完全独立,不共享任何内部对象
实现:
重写克隆方法:递归地复制所有引用类型的成员变量。
序列化:先原对象序列化再反序列化拷贝对象
基本概念和规则
生命周期结束后其占用的内存空间也会被释放
intern方法
如果当前字符串内容存在于字符串常量池,直接返回字符串常量池中的字符串。否则,将此String对象添加到池中,并返回String对象引用
Cookie和Session的区别:
Cookie是web服务器发送给浏览器的一块信息,浏览器会在本地文件中给 服务器存储cookie。以后浏览器再给服务器发送请求时,同时会发送所有为该服务器存储的cookie
Session是存储在web服务器的一块信息,session对象存储的是用户会话所需的属性及配置信息。当用户在应用程序的web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去
无论客户端做怎样的设置,session 都能够正常工作。客户端可以禁用 cookie不使用
在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型对象
时间复杂度
- O(1) - 常数时间复杂度:
- 表示算法的执行时间是固定的,与输入规模无关。例如,对数组进行索引操作、插入和删除链表的头结点等。
- O(log n) - 对数时间复杂度:
- 表示算法的执行时间与输入规模的对数成正比。例如,二分查找。
- O(n) - 线性时间复杂度:
- 表示算法的执行时间与输入规模成线性关系。例如,数组遍历。
- O(n log n) - 线性对数时间复杂度:
- 表示算法的执行时间与输入规模的对数乘以线性成正比。例如,快速排序、归并排序。
- O(n^2) - 平方时间复杂度:
- 表示算法的执行时间与输入规模的平方成正比。例如,简单的嵌套循环排序算法。
- O(2^n) - 指数时间复杂度:
- 表示算法的执行时间与输入规模的指数成正比。例如,解决某些组合问题的朴素递归算法。
- O(n!) - 阶乘时间复杂度:
- 表示算法的执行时间与输入规模的阶乘成正比。例如,解决某些排列问题的朴素递归算法。
关于存储结构
顺序存储、链接存储、索引存储、散列存储
关键字
final
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变,如果修饰引用,那么表示引用不可变,引用指向的内容可变
synchronized
解决多个线程之间访问资源的同步性,保证被synchronized修饰的方法或者代码块在任意时刻只能有一个线程执行,在Java早期版本jdk1.6之前,synchronized属于重量级锁,效率低下,在Java6之后从JVM层面对synchronized做出较大优化,所以现在的synchronized锁效率也优化得很不错了。synchronized底层就是监视器锁,他是对象头的一个标志位,使用enter和exit控制对象是否获取到锁
因为监视器锁(monitor)是依赖于底层操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原生线程之上。如果要挂起或者唤醒一个线程,都需要操作系统来帮忙完成,而操作系统实现线程之间的切换时需要从用户态
转换到内核态
,这状态之间的转换需要相对较长的时间,如自旋锁
,适应性自旋锁
,锁消除
,锁粗化
,偏向锁
,轻量级锁
等技术来减少锁操作的开销
用户态:
内核态:
监视器锁是对象头里的一个标志
标识符
serialVersionUID
serialVersionUID
是 Java 中用于版本控制的一个标识符,是一个类型long值静态常量,用于在对象序列化和反序列化过程中识别类的版本。
如果不显式指定 serialVersionUID
,Java 编译器会根据类的结构(字段、方法等)自动生成一个。
在序列化过程中,serialVersionUID
会被写入字节流,而在反序列化时,会比较传入对象的 serialVersionUID
和当前类的 serialVersionUID
是否一致。如果不一致,会抛出 InvalidClassException
,从而防止反序列化过程中的版本不一致问题。
显式指定 serialVersionUID
的好处是可以在类的版本发生变化时,手动控制版本号,避免自动生成的版本号导致的问题。
成员变量和局部变量:
-
成员变量:
在方法、构造方法、代码块之外的变量,没有被赋予值的会自动以类型默认值赋值,可以被访问修饰符修饰以及被类中的方法访问
实例变量(非静态成员变量)属于对象,只有通过实例化才能访问到,并且每个实例对象有自己的实例变量,相互独立
静态变量属于类,存储在方法区中,不需要实例化,可以直接根据变量名输出访问并且在所有类的实例对象中共享相同的静态变量,通常和final关键字搭配声明为常量。
-
方法区是java虚拟机的一部分,用于存储类的结构信息、静态变量、常量、方法代码等
-
在静态环境中访问非静态变量?
静态方法属于类本身,而不是某个对象的实例。在类加载时,静态方法会被分配和初始化,因此,静态方法中只能访问静态成员(静态变量和静态方法),而不能直接访问实例变量和实例方法。
-
-
局部变量:
在方法、构造方法、代码块中声明的变量,只能在其声明的范围内使用,不会自动赋值,不能被访问修饰符修饰,不可以被声明为静态
基本数据类型 直接存储在栈内存中
引用数据类型 存储的是该对象在栈中引用,真实的数据存放在堆内存里
共性:
- 成员变量和局部变量都能被final所修饰,被 final关键字修饰的变量必须显式地赋值
生命周期:
-
成员变量:
实例变量:
随着对象的创建而存在(四种方式)
,随着对象的销毁而消失(当对象不再被引用)
静态变量:类加载时初始化,在类卸载时销毁。
-
局部变量:随着方法或代码块的执行而存在,执行结束后消失
数组和集合互相转换:
数组转集合:Arrays.asList(数组名)
集合转数组:集合名.toArray()
重载和重写:
重载:发生在本类中,方法名字相同,参数列表必须不同,存在相同的参数列表要打乱顺序,与返回值、访问修饰符无关
重写:发生在父类与子类之间,子类重新定义父类中已经有的方法,具有相同的方法签名(方法名、参数列表、返回类型),只有代码块里面的内容不一样,重写的访问权限不能比父类中被重写的方法的访问权限更低,构造方法不能被重写
抽象类和接口:
接口在Java中实现了多继承的特性,允许子接口继承多个父接口。当多个父接口中存在相同的默认方法时,子接口需要进行默认方法的覆盖重写,以解决冲突。实现类可以选择实现父接口,此时只需实现父接口中的抽象方法。如果实现类同时实现了子接口,那么必须实现父接口和子接口中的所有抽象方法。
相同点:都不能够实例化,抽象方法不能被static、final修饰,访问修饰符只能是public
不同点:一个类可以实现多个接口,但是一个类只能继承一个父类
抽象类有构造器,接口类没有构造器
接口类的实例对象只能是静态常量,抽象类实例对象没限制
在 Java 8 以后,允许在接口中增加静态方法和默认方法
抽象类除了不能实例化,可以声明抽象类外和普通类没有区别
如果子类继承了抽象类但未实现所有抽象方法,子类必须声明为抽象类
如果其他类实现接口类但未实现所有接口方法,该类必须声明为抽象类
object 中定义了哪些方法?
getClass(); 获取类结构信息
hashCode()
常见的散列算法·:
- MD5 (Message Digest Algorithm 5): 产生128位的哈希值。由于其安全性较低,已经不再推荐用于加密目的,但仍在一些非安全性要求严格的场景中使用。
- SHA-1 (Secure Hash Algorithm 1): 产生160位的哈希值。类似于MD5,由于其安全性问题,已经不再推荐用于安全性要求高的场景。
- SHA-256 (Secure Hash Algorithm 256-bit): SHA-2家族中的一个,产生256位的哈希值。广泛用于加密、数字签名等安全性要求较高的应用。
- SHA-3 (Secure Hash Algorithm 3): 最新的SHA标准,基于Keccak算法,提供了不同的哈希长度选项,如SHA3-256、SHA3-512等。
- CRC32 (Cyclic Redundancy Check): 主要用于数据校验,生成32位哈希值。不适用于安全性要求高的场景,但在文件校验和类的应用中比较常见。
- CityHash: 由Google开发,用于快速哈希大量数据,适用于分布式系统中的数据分片。
- MurmurHash: 一系列哈希函数,MurmurHash2和MurmurHash3是比较常见的版本,用于散列数据以用于哈希表等数据结构。
- bcrypt: 用于密码哈希的算法,主要用于存储密码的安全散列。它的计算较慢,增加了破解密码的难度。
- SHA-256d: 一种双重SHA-256哈希,常用于比特币和其他加密货币的工作证明(Proof of Work)。
这些哈希函数都使用散列算法,其主要目的是将输入数据映射为固定大小的输出,通常为哈希值或散列值
散列算法的主要特征是:
确定性: 相同的输入将始终产生相同的哈希值。
固定输出长度: 无论输入的大小如何,哈希函数的输出长度是固定的
常见的
散列存储结构
包括哈希表(Hash Table)、哈希集合(HashSet)、哈希映射(HashMap)等Hash Table:一种数据结构,存储的是键值对,根据键值映射到对应的值,通过
哈希函数
将键映射到表中位置HashMap:
扩容
概念
在Java中,默认情况下,Object
类中的 hashCode
方法返回的是基于对象的内存地址生成的哈希码值
。哈希码通常用于在哈希表等数据结构确定对象的存储位置,避免遍历整个集合来查找对象,提高查找效率以及确保equals方法的一致性。然而,Object
类的默认实现并未考虑对象的内容,而是依赖于对象的引用地址,可能导致相等的对象在哈希表中具有不同的哈希码。为了确保在哈希表等数据结构正确工作,尤其是在使用自定义类作为键时,建议重写 equals
和 hashCode
方法,以确保相等的对象具有相等的哈希码,从而正确存储和检索对象。需要注意的是,生成的哈希码值最后通过int类型返回但是int类型的范围是有限的,而对象的可能性是无限的,因此存在不同的对象具有相同哈希码的可能性,也因此它们会被映射到相同的位置上,我们称为哈希冲突,常见的处理办法就是链地址法
和开放寻址法
链地址法:
hashcode()和equals()方法之间
两个对象相等,其HashCode一定相同;
两个对象不相等,其HashCode有可能相同;
HashCode相同的两个对象,不一定相等;
HashCode不相同的两个对象,一定不相等;
equals(Object) 默认比较对象地址值否相等,子类可以重写比较规则
toString() 对象转变成字符串
notify() 多线程中唤醒功能
notifyAll() 多线程中唤醒所有等待线程功能
wait()和sleep()的区别
sleep属于Thread类中方法 ,释放cpu给其它线程,不释放锁资源
wait属于Object类中方法,释放cpu给其他线程,同时释放锁资源让持有对象锁线程进入等待,且必须配合同步锁一起使用,不然在运行时就会抛出IllegalMonitorStateException异常
wait(long timeout) 让持有对象锁线程进入等待,设置超时毫秒数时间
wait(long timeout, int nanos) 让持有对象锁线程进入等待,设置超时纳秒数时间
finalize() 垃圾回收前执行方法
clone() 用于对象克隆
java创建对象的方式:
反射:获取类的class对象->获取该类的构造方法->使用构造方法创建实例对象
Clone()方法,
new关键字
反序列化(java.io.ObjectInputStream下的readObject()方法)
面向对象的三大特征
封装
把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口
继承
从已有类得到继承信息创建新类的过程。
提供继承信息的类被称为父类,得到继承信息的类被称为子类
多态
允许不同子类型的对象对同一消息作出不同的响应
多态存在的三种条件:继承或者接口实现,方法重写,子类对象指向父类引用。
面向过程注重方法和方法之间的调用
访问修饰符之间的区别
public(公共):无论同包还是其他包,所有类可见
protected(受保护):在同包内,被继承的子类或其他类可见
default(默认不写,包私有):同包内可见
private(私有):在同包内不可见,只在其所在类内可见。
基本数据类型和引用数据类型
基本数据类型:byte,short,int,long,boolean,char,double,float
引用数据类型:
-
类(Class):
- 类是一种引用数据类型,用于创建对象。对象是类的实例。
-
接口(Interface):
- 接口定义了一组抽象方法,类可以实现接口。接口在Java中提供了一种多继承的机制。
-
数组(Array):
- 数组是一种引用数据类型,可以存储相同类型的多个元素。在Java中,数组有固定的长度,可以通过索引访问元素。
-
枚举(Enum):
- 枚举类型是一种特殊的引用数据类型,用于定义一组命名的常量。枚举常常用于表示一组相关的值。
-
泛型(Generic):
- 泛型允许在编写类、接口或方法时使用类型参数。通过泛型,可以实现代码的重用和类型安全。
-
用户自定义的引用数据类型:
- 在Java中,程序员可以创建自己的类和接口,从而定义自己的引用数据类型。
-
字符串(String):
-
字符串是一种引用数据类型,但它具有一些特殊的性质:
不可变性
不可变性:为了确保String类的不可变性,通过final关键字来限制
String类的常用方法:
indexOf():返回指定字符索引。
charAt():返回指定索引字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后字符串数组。
getBytes():返回字符串 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
String适用于少量字符串操作 ,执行速度最差。
StringBuffer适用于多线程环境下的大量操作,执行速度其次StringBuilder适用于单线程环境下的大量操作,执行速度最快
String和StringBuffer线程安全,StringBuilder线程不安全
StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的对象。String类的对象一旦创建后其本身不可修改,引用可以修改,任何对
String
的操作实际上都会创建一个新的String
对象。String有两种实例化方式一种是通过字面量方法,直接给所定义的字符串赋值,另一种是通过new+构造器的方法StringBuffer、StringBuilder只能通过构造方法来实例化
三者底层使用char型数组存储
三者实现Serializable接口,支持
序列化
StringBuilder 或 StringBuffer reverse 的方法实现字符串反转
new String(“jay”)创建了几个字符串对象?
首先在这个代码中有一个
new
关键字,这个关键字在程序运行时根据已经加载的系统类String在堆内存里面去实例化一个字符串对象,然后在这个String的构造方法里面传递了一个"jay"的一个字符串,因为String里面的字符串成员变量是一个final修饰的,所以它是一个字符串常量,所以接下来JVM会去拿字面量"jay"去字符串常量池里面去试图找到对应它的一个String的对象引用,如果拿不到就会在堆内存里面去创建一个"jay"的String对象并且把引用保存到字符串常量池里面,后续如果再有字面量"jay"的定义,就只需要从常量池里面获取对应的引用就可以了,不需要再次创建,对于这个问题我有两个答案:- 如果"jay"这个字符串常量不存在,则创建两个对象,分别是"jay"这个字符串常量,以及new String这个实例对象
- 如果"jay"这个字符串常量存在,则只会创建一个对象
字符串拼接是如何实现的?
最常用的是使用
+
运算符和StringBuilder
类- 当使用
+
运算符进行字符串拼接时,实际上是通过StringBuilder
类的append
方法实现 StringBuilder
是一个可变的字符串缓冲区,在进行字符串拼接时,它允许在同一个对象上进行多次修改,而不必创建新的对象,使得在循环或大量字符串拼接的情况下,使用StringBuilder
效率更高
序列化和反序列化:
用于对象持久化的过程
序列化:
- 将对象转换为字节流,这一过程将对象的属性和数据转化为字节序列,通过写入文件或传输给其他系统进行实现。在Java中,要实现序列化,类需要实现
Serializable
接口。这个接口是一个标记接口,用来标识一个类可以被序列化,一般建议创建的JavaBean类都实现 Serializable
反序列化:
- 将字节流转换回对象,恢复序列化时保存的对象状态。在Java中,要实现反序列化,可以通过读取字节流并使用
ObjectInputStream
,将字节流还原为相应的对象。
-
Java常用包
java.lang
包:
java.lang
包包含Java语言的核心类,如基本数据类型的包装类(Integer、Double等)、异常类(Exception、RuntimeException等)、字符串处理类(String、StringBuilder、StringBuffer等)等。这个包中的类是默认导入的,因此在Java程序中无需显式导入。
ThreadLocal:
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的,Threadlocal类就是让每个线程绑定自己的值,实现每个线程都有自己的专属变量
。创建的实例对象会在线程中创建一个独立副本,且创建的副本是线程私有的,不会被其他线程访问到,在多线程环境中,每个线程可以独立操作自己的ThreadLocal实例,而不互相影响,可以通过get()方法获取副本值,set()方法设置副本新值,
原理:
java.util
包:
java.util
包包含工具类,包括集合框架(如List、Set、Map等)、日期和时间类、随机数生成器、扫描器(Scanner)等。是Java中处理常见任务的核心工具包。
集合
是 Java 中一组对象的容器,用于存储、检索和操作数据,主要的集合接口包括
Collection
、Set
、List
、Queue
和Map
Collection
接口是所有集合框架的根接口,它继承了 Iterable
接口,提供了一些基本的方法用于操作集合中的元素。主要的子接口有 Set
、List
和 Queue
。
-
List
接口允许重复元素,其接口维护元素的插入顺序,能够保证元素插入的顺序性。其下实现类包括ArrayList
、LinkedList
和Vector
。ArrayList: 底层是
数组
实现,线程不安全,查询和修改速度快-
扩容机制:
扩容容量一般为原数组的1.5倍
ArrayList
对象在创建时其初始容量,默认为0,当第一次添加数据时初始容量为10, 每次添加元素到ArrayList
中时,都会检查当前元素数量是否达到数组容量。如果达到了,就调用grow方法扩容。 扩容时,通常会创建一个新的数组,随后将旧数组的元素复制到新数组中,最后ArrayList内部的数组引用指向新数组。在性能关键的场景中,为了减少扩容次数,可以提前估计并设置适当的初始容量。 -
实现线程安全?
1.自己写包装类,根据业务一般是对写操作进行加锁
2.Collections.synchronizedList(new ArrayList<>());使用synchronized加锁
3.使用ReentranLock加锁
CopyOnWriteArrayList
设计思想:读写分离+最终一致
- 是ArrayList的线程安全版本,采用
读写分离的并发策略
,当我们往容器里添加元素时,不直接往当前容器里添加,而是先将当前容器Copy,复制出一个新容器,然后在新容器里添加元素,添加完后将原容器的引用指向新容器,这样做的好处是可以并发读而不需要加锁,适用于读操作
频繁,写操作
较少的场景,因为每次写操作时会复制整个数组,内存中会同时存在两个对象,如果对象很大容易发生YongGC
和FullGc
Collections.synchronizedList:
可以将非线程安全的
List
转换为线程安全的List
。适用于读写操作比较频繁的场景,但是其效率相对较低,所有的读写操作都需要同步锁
LinkedList: 底层是双向链表,线程不安全,增加和删除速度快
Vector: 底层是数组实现,线程安全,每个操作方法都加的有synchronized关键字,性能较差
-
-
Set 接口
不允许重复元素,其接口不维护元素顺序,通常不能够保证元素的顺序性。然而,Set 接口的实现类中,TreeSet 和 LinkedHashSet 是有序的,而 HashSet 是无序的。
HashSet
boollean contains(Object o)
参数:测试其存在于该集合中的元素
返回值:此集合包含指定元素,则方法调用返回true
package com.tutorialspoint;
import java.util.*;
public class HashSetDemo {
public static void main(String args[]) {
// create hash set
HashSet <String> newset = new HashSet <String>();
// populate hash set
newset.add("Learning");
newset.add("Easy");
newset.add("Simply");
// check the existence of element
boolean exist = newset.contains("Simply");
System.out.println("Is the element 'Simply' exists: "+ exist);
}
}
-
Map 接口
不维护元素的插入顺序。TreeMap
和LinkedHashMap
是有序的,能够按照键的顺序进行存储。常用方法:
int size();//获取Map集合大小(即元素数量) boolean isEmpty();//判断是否为空 boolean containsKey(Object key);//判断是否包含某个键 boolean containsValue(Object value);//判断是否包含某个值 V get(Object key);//获取某个键对应的值 V put(K key, V value);//添加键值对(K,V) V remove(Object key);//移除某个键对应的键值对 void putAll(Map<? extends K, ? extends V> m);//添加另一个Map集合 void clear();//清空所有键值对 Set<K> keySet();//获取键的集合 Collection<V> values();//获取值的集合 Set<Map.Entry<K, V>> entrySet();//获取键值对实体的集合 interface Entry<K,V>//Map中的内部接口
HashMap是基于哈希表的Map接口实现,由
数组
+链表
+红黑树
组成,链表的作用是解决hash冲突,红黑树主要是去提高查询的效率,数组内部存储的是一个存放键值对结点的单链表头节点。key键,value值都可以为null,线程不安全,
默认初始容量为(DEFAULT_INITIAL_CAPACITY):16
默认最大容量:
1073741824
2的30次方默认负载因子:0.75f
默认树化转换阈值(链表):8
默认取消树化:6
默认树化数组长度:64
扩容阀值:数组长度*负载因子,默认为12
Node<K,V>[]
是一个数组类型,可以存储多个Node<K,V>
对象,形成桶(bucket)数组,每个桶用于存储具有相同哈希码的键值对。Node<K,V>
表示一个节点对象,包含了键值对的信息以及链表或树的下一个节点的引用。在哈希表中,一个Node<K,V>
对象通常表示一个键值对,而多个这样的节点可能形成一个链表或树结构,用于解决哈希冲突。
在
HashMap
的实现中,桶数组的类型是Node<K,V>[]
,而每个桶中的元素类型是Node<K,V>
。这种结构允许HashMap
在每个桶中存储多个键值对,通过链表或树结构来处理哈希冲突。 实现添加操作的put方法实际是调用内部的putVal方法,并且先对key键进行hash操作,具体操作是先计算出其哈希值,以及哈希值的高16位,两者之间进行异或运算,这样做的好处是可以让hash值的分布更加均匀,之后我是把添加操作分为两种情况
一种是第一次往数组里添加数据
另一种是数组中已有数据进行添加
首先说第一种,此时的桶数组为空,或者长度为0,进入resize()方法进行初始化扩容,把默认初始容量的值16付给
newCap
,扩容阀值12付给newThr
,之后new一个Node数组,并分配初始容量newCap
作为数组容量,经过类型转换将Node数组转化为Node桶数组的形式,并以此形式进行接收并返回,至此初始化扩容完成。之后根据key键计算出的hash值和该桶数组容量-1进行按位与运算得到数组索引,该数组索引位置的桶为空,直接创建新节点(newNode)将键值对插入 接着是第二种,因为已经添加过数据,就直接跳过初始化扩容,当索引位置上的桶不为空时,说明通过key键的hash值找到了桶数组中的某个数组位置,且该位置是有值的。
-
判断当前节点的hash值和自己将要存入键值对的hash值进行比对相同
key键相同的话,直接覆盖value值,在新节点上插入键值对,
key不相同的话,还有两种情况
- 如果是红黑树,就调用红黑树
putTreeVal
方法,直接在树上插入键值对 - 否则的话就遍历链表,判断头节点的下一个节点是否为空,为空则说明是该链表的最后一个节点,并在此节点插入键值对,然后看当前的链表长度是否大于8,否的话不做改变,是的话就转换为红黑树插入。在遍历过程中发现插入键值对中的key在链表中不存在则继续遍历链表,存在则break,终止遍历并覆盖value
- 如果是红黑树,就调用红黑树
在每次插入成功后,判断实际存在的键值对数量size是否超过了扩容阈值,如果超过,进行扩容。
当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
java.io
包:
java.io
包提供了用于进行输入和输出操作的类和接口,如文件操作、流操作、对象序列化和反序列化等。它是处理输入输出的重要工具。
java
包:
java
包包含了用于网络编程的类和接口,如URL
、URLConnection
等。通过这个包,Java程序可以实现网络通信、HTTP请求、Socket编程等。
java.awt
包
java.awt
包提供了基本的窗口、图形和用户界面组件
java.sql
包:
java.sql
包提供了一组接口和类,使得 Java 程序能够与数据库进行交互,执行 SQL 查询、更新和管理数据库连接等操作
Java中有几种类型的流:字节流和字符流
Java是值传递,还是引用传递?
Java语言是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。JVM 的内存分为堆和栈,其中栈中存储 了基本数据类型和引用数据类型实例的地址,也就是对象地址。 而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递
Integer 缓存实现
底层是通过Integer.valueOf()方法实现。
int 基本数据类型,interger 是 int的 包装类
int 默认值为 0 ,interger 默认值为 null, Interger 使用需要判空处理
装箱:将基本类型转换成包装类对象
拆箱:将包装类对象转换成基本类型
为了节省内存和提高性能,在一个特定范围对Integer对象进行缓存,在这个范围内的整数值,会直接返回缓存中的对象,而不是每次都创建新的对象,默认值为-128到127,且这种机制仅在自动装箱
时候有用,在使用构造器
创建Integer 对象时无用。最后其范围值可以通过java虚拟机中的 - XX:AutoBoxCacheMax参数进行修改。
equals和==的区别
== 相等运算符
-
对于基本类型,判断值是否相等
-
对于对象引用,判断内存地址值是否相等
equals方法
- 默认情况下Object类的"equals"方法与"=="行为相同,即比较引用内存地址值是否相等
- 重写equals方法,判断两对象是否相等。
public boolean equals(Object obj) {
return (this == obj);
}
break和continue的区别
break:使流程跳出 switch 语句体,在循环结构终止本层循环体,提前结束本层循环。
continue:跳过本次循环,即进行下一次循环条件判定
this和super关键字作用
this:指向对象本身的一个指针,主要用于解决成员变量和局部变量同名问题,允许调用成员变量和成员方法,同时在方法中可以省略使用 this
,但在静态方法中不允许出现
super:引用父类的成员变量,调用父类构造方法和普通方法
同步通信和异步通信:
在计算机通信中,同步通信和异步通信是两种不同的通信方式。同步通信是一种顺序执行的模型,发送方必须等待接收方处理完毕后才能继续执行,具有明显的时序性和协同性,适用于简单的、顺序执行的任务。异步通信则是一种非顺序执行的模型,发送方发送消息后不需等待接收方,可以继续执行其他操作,具有相对独立的特性,适用于提高系统并发性和吞吐量的场景。选择同步通信还是异步通信取决于应用的需求和场景,对于简单任务和控制顺序的情况,可以选择同步通信;而对于需要提高系统并发性和吞吐量的情况,异步通信是更为合适的选择。在实际应用中,两者可以结合使用,根据任务的性质和要求灵活选择合适的通信方式,以达到更有效的信息传递和处理。
同步通信是一种顺序执行的模型,发送方必须等待接收方处理完毕后才能继续执行,适用于简单的、顺序执行的任务。异步通信则是一种非顺序执行模型,发送方发送消息后不需等待接收方,在此期间可以执行其他操作,适用于提高系统并发性和吞吐量的场景。
同步通信是一种顺序执行的模型,发送方必须等待接收方处理完毕后才能继续执行。
异步通信是一种非顺序执行模型,发送方发送消息后不需等待接收方,在此期间可以执行其他操作。
java8新特性
Lambda表达式
特性
Lambda 表达式是 JDK 8 中引入的一项重要特性,是 Java中 匿名内部类的语法糖
(原理不变的代码语法),简化传统匿名内部类的语法,用来实现函数式接口
,其中Lambda 表达式具有一些可选的语法元素,包括类型声明
、参数圆括号
、大括号以及返回关键字
。
- 首先,Lambda 表达式的参数类型不需要显式声明,编译器可以根据上下文推断参数的类型
- 然后,对于只有一个参数的 Lambda 表达式,可以省略参数圆括号
- 最后,主体只有一个表达式的 Lambda 表达式,可以省略大括号和返回关键字
- 在Lambda 表达式中可以访问外层的局部变量,但是不能同名和修改值
四个内置核心函数式接口
Supplier 供给型接口,无参有返,返回一个 T 类型的返回值
Cousumer 消费型接口,有参无返,接收一个 T 类型的参数
Predicate 断言型接口,有参有返,接收一个 T 类型的参数,返回一个 boolean 类型的返回值
Function 函数型接口,有参有返,接收一个 T 类型的参数,返回一个 R 类型的返回值
- Callable
- Comparator
- Runnable
方法引用
如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
构造方法引用->( 对象::new )
- 符合(方法参数)->new 对象(构造参数)使用
静态方法引用-> (类名::静态方法名)
- 接口的方法调用了另一个类中的静态方法。要求两个方法的参数要一致。
实例方法引用->(对象名::方法名)
- 接口的方法调用了对象的方法。要求两个方法的参数要一致。
对象方法引用->(类名::方法名)
Stream
java.util.Stream
Stream
是 JDK 8 引入的一种处理集合数据的抽象概念,用来对集合进行操作,并把将要处理的集合看作成一种流, 流在管道中传输, 经过中间操作(intermediate operation)的处理,最后由终端操作(terminal operation)得到前面处理的结果,可以看成是一种流式处理
Stream创建
当创建流的时候,数组类型是基本数据类型的时候,需要用到boxed(),把流中的数据变成引用数据类型(装箱)
-
可以通过Coolection系列集合提供的stream()或者parallelStream()来创建
List<String> list = new ArrayList<>(); Collections.addAll(list, "a", "b", "c"); Stream<String> stream1 = list.stream(); Stream<String> stream2 = list.parallelStream();
-
通过Arrays中的静态方法stream()获取数组流
Integer[] array = {1, 2, 3, 4, 5,}; Stream stream = Arrays.stream(array);
-
通过Stream中的静态方法of()
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
-
创建无限流,流中的元素有·无限多个,通常和limit使用iterate(),generate()
List<Integer> collect1 = Stream.generate(() -> new Random().nextInt(100)).limit(20).collect(Collectors.toList()); List<Integer> collect2 = Stream.iterate(1, s -> s * 2).limit(10).collect(Collectors.toList());
中间操作
- filter — 过滤流中的元素,保留满足条件的元素。
- limit — 截断流,使其元素不超过指定数量
- skip — 跳过元素
- distinct — 筛选,去除重复元素
- sort — 流中的数据排序
- map — 映射(将流中的每个元素映射到另一个元素)
- floatMap–将一个元素映射成一个 Stream,然后将这些 Stream 连接成一个单一的 Stream
- sorted — 对流中元素进行排序
终端操作
查找与匹配
-
anyMatch – 检查流中是否有任意一个元素匹配给定条件
-
allMatch – 检查流中所有元素是否都满足给定的条件
-
noneMatch – 检查流中是否没有元素匹配给定的条件
-
findAny – 返回流中任意一个元素
-
findFirst – 返回流中的第一个元素
-
count – 返回流中元素个数
-
min 和 max – 返回流中最大值和最小值
-
reduce – 将流中元素进行归约操作
-
collect – 将流中的元素收集到集合或者其他数据结构中
-
groupingBy – 流中元素根据条件进行排序
-
foreach – 遍历流中的元素并对每个元素执行指定的操作
-
toArray – 将流中元素转换成数组
-
toList/toset – 将流中元素收集到List/Set中
-
joining – 链接六种元素成为一个字符串
-
sorted – 对流中的元素进行排序
Optional类:
Java 中8 引入的一个用于处理可能为 null 的值的容器类,当返回的对象不知是否为空时使用
常用方法:
- isPresent() ,返回容器中是否有值,有值 true
- optional.get() 从容器中获取值,如果没有值,会抛出异常
- ifPresent(消费一个数据) 如果容器中有数据,我们该怎么处理这个数据
- orElse(值) 是前两个方法的组合,如果容器中有数据,就返回容器中的数据,没有的话,使用括号中的值
Date Time API : 加强对日期与时间处理
数据结构
数组(Array)
链表(Linked List)
栈(Stack)
堆(Heap)
哈希表(Hash Table)
队列(Queue)
树(Tree)
图(Graph)
集合(Set)和映射(Map)
栈堆(Heap Stack)
RPC
Redis
概述
在6.0版本中使用多线程是为了提升IO读写的效率,Redis的性能瓶颈在于网络IO而非cpu
常用于做缓存,点赞量,排行榜,消息队列以及分布式环境下实现分布式锁
Redis是—个单线程运作且开源的内存数据存储系统,基于键值对(key-value)的NoSQL数据库,读写能力强大
,支持 事务
分布式架构
数据持久化
和多种数据结构
-
多种数据结构:string (字符串)、hash (哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、GEO(地理信息定位)等
-
事务:
其中MULTI命令作为事物的开始会把后续填写命令放进一个队列里,不立刻执行,通过EXEC命令来执行队列里的命令,且这些命令按照 原子顺序执行,在执行完毕后一次性返回运行结果,要注意的是Redis不支持事务回滚,发生错误情况下,不能回滚到事务开始前的状态
-
数据恢复:
通过持久化机制把内存中的数据同步到
硬盘文件
。Redis重启后把硬盘文件
重新加载到内存,达到恢复数据的目的。-
RDB和AOF:
根据应用,权衡性能与数据安全性的需求,有时候会选择同时使用RDB和AOF的混合方式。
如果只需要数据在服务器运行时存在,可以不使用任何持久化方式。
-
1.区别:
RDB:定期将内存数据快照写入磁盘,适合对性能要求高、磁盘空间敏感、能接受数据丢失的场景,
AOF:以日志方式记录每次所写命令(解决数据持久化的实时性)->适合对数据完整性要求高的场景,但相对占用更多磁盘空间和可能引起轻微性能降低。
-
-
Redis为什么快?
竞态:多线程或多进程环境中,两个或多个并发执行的任务争夺同一资源
首先基于内存存储
,将数据保存在内存中,提高读写速度,其次采用单线程模型
,避免线程切换和竞态
产生的消耗,还支持多种高效的数据结构,适用于不同类型的数据处理,最后基于非阻塞IO模型允许并发执行多个操作,增强系统并发能力。
Redis
四种模式:单机模式、主从模式、sentinel模式、Redis Cluster。
概念:
主从复制就是指一个master节点和多个slave节点,Master节点负责数据的读写,Slave节点,负责数据的读取。Master节点收到数据变更会同步到Slave节点上实现数据同步。实现Redis的读写分离,提升数据的查询效率。但是 Redis主从复制不提供扩容
和自动恢复
功能。
恢复(Sentinel模式)
- 一旦Master节点挂掉,不会自动选取出新的Master,会导致后续客户端所有的写请求直接失败。所以 Redis提供了哨兵机制,专门用来监听Redis主从集群,能够及时发现主节点故障并从剩余的从节点中选举出一个作为主节点。
扩容(Redis Cluster)
流程
Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。部分复制是针对全量复制 过高开销做出的一种优化措施
如果主节点要求密码验证,从节点也必须写入正确的密码才能通过验证.
从节点(slave)保存主节点(master) 的ip地址和port端口号后会和主节点建立网络连接,网络连接成功后从节点发送ping请求进行首次通信,检测主从之间网络套接字是否可用以及主节点当前是否可以接受命令请求,连接正常通信后,主节点会把持有的数据全部发送给从节点,之后主节点会持续地把写命令发送给从节点,当从节点 (slave) 复制主节点 (master) ,出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区
内存在这部分数据则直接发送给从节点,
Redis Sentinel(哨兵)
概念
由两部分组成,哨兵节点和数据节点:
哨兵节点: 特殊的 Redis 节点,不存储数据,对数据节点进行监控。
数据节点: 主节点和从节点
配置提供者:客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址
通知:哨兵可以将故障转移的结果发送给客户端。
配置提供者和通知功能,需要在和客户端的交互中体现
流程(监控和自动故障转移)
监控
和自动故障转移
功能:使哨兵及时发现主节点故障并完成转移.
节点选举:
概念
使用Raft算法
实现,通过选举机制确保系统的节点中有一个领导者
,前提是
流程
在Raft算法中,选举由定时器触发。节点定时器超时后,它将状态由Follower(追随者)转为Candidate(候选者),并向其他节点发起RequestVote请求。有三种可能的情况发生:
-
如果请求获得过半数投票,节点成为领导者,并向其他节点发送心跳以保持领导者地位。
-
如果在候选者状态时收到较大Term的Append Entries请求,节点成为Follower;否则,保持候选者状态并拒绝请求。
-
如果选举定时器再次超时,节点递增Term,重新发起选举。
在一个Term期间,每个节点只能投票一次。多个Candidate同时存在时,它们将递增Term、重置定时器,并重新发起选举。
节点接收客户端的日志,将日志追加到本地Log,通过心跳同步给其他节点。Leader收到大多数Follower的ACK后,将日志设置为已提交,并追加到本地磁盘,通知客户端。在下一个heartbeat中,Leader通知所有Follower将日志存储在各自本地磁盘中,确保数据一致性。
Term(任期)
在Raft一致性算法中,“term”(任期)是关键的概念,用于标识不同的选举周期。每个任期内可能产生一个新的领导者,负责在该任期内维护系统状态。节点在发起选举时递增当前任期值,并在心跳消息中广播该任期,以防止过时领导者发送过期命令。新的领导者通过获得大多数选票成为领导者,其中包括了当前任期的信息。"term"在确保选举和日志同步一致性方面起到了关键的作用。如果其他节点发现心跳消息中的更大任期,它们会更新自己的任期并转变为追随者状态,确保了系统的一致性和稳定性。
心跳机制:
概念:分布式系统中常用的一种机制,用于检测节点的存活状态或者服务的可用性。通过定期发送心跳消息,节点可以向其他节点或监控系统发出信号,表明它们仍然是活跃的
节点的三个状态:
在Raft一致性算法中,运行时主要存在两种状态,即Leader(领导者)和Follower(追随者)。
领导者
负责日志的同步管理,处理客户端的读写请求,并定期发送心跳以保持其领导者地位。追随者则负责复制领导者的日志,处理客户端的读请求,并响应领导者的请求。在没有领导者的情况下,追随者可以成为候选者,发起新一轮的选举。
候选者
负责向其他节点发送选举请求,如果得到大多数节点的支持,就成为新的领导者。如果在一定时间内没有获得足够的选票,选举失败,候选者可能再次成为追随者。
这种状态机的设计确保了系统在面对节点故障或变化时能够保持一致性,并有效地选出新的领导者以维护系统的正常运行。
三个子问题:
Raft将算法流程分为三个子问题:领导者选举、日志复制和安全性。
在领导者选举
阶段,系统中的节点通过相互通信来选举一个领导者。这确保了系统具有活跃的领导者,当现任领导者失效时,其他节点可以通过选举机制迅速产生新的领导者。
在日志复制
方面,领导者负责接收客户端请求,将这些请求添加到自身的日志中,并通过心跳机制将日志复制给其他节点。这样,系统保持一致的状态,因为其他节点要按照领导者日志执行相同的操作。
安全性
是确保系统数据一致性和正确性的关键。一致性要求如果任意节点提交了一个日志条目,那么这个日志条目必须存在于所有节点的日志中。这确保了操作在整个系统中得到正确地复制和执行。正确性方面,选举机制的限制确保了新领导者拥有最新的日志,防止旧节点成为领导者,从而维护系统的正确性。这一系列保证了数据的安全性,使得Raft算法在分布式系统中能够可靠地实现一致性。
分布式
分布式理论
cap
任何⼀个分布式系统都⽆法同时满⾜Consistency(⼀致性)、Availability(可⽤性)、 Partition tolerance(分区容错性) 这三个基本需求。最多只能满⾜其中两项。其中Partition tolerance(分区容错性) 是必须的,因此⼀般是CP,或者AP
BASE
BASE理论是Basically Available(基本可⽤)、Soft state(软状态)和Eventually consistent(最终⼀致性)三个短语的简写,是分布式系统设计的一组原则,其中基本可用性保证在出现故障时仍能提供核心功能,系统允许存在中间状态,即软状态,允许在特定时间窗口内数据不一致,最终一致性确保系统在一定时间内最终达到一致状态。这一理论相对于ACID原则,更注重提高系统的可用性和分区容忍性,允许在一段时间内保持数据的不一致性,从而更好地适应分布式系统的需求,广泛应用于互联网应用、大规模分布式系统和NoSQL数据库等领域。
分布式锁
在多线程并发的情况下,如果同时对一个共享资源进行读写,会造成数据错乱的问题,可以加入同步锁解决,其他线程需要等待持有锁线程处理完后,才能正常工作,但随着用户量的日益增多,服务器的压力会越来越大,可以通过Nginx分布式集群和负载均衡的技术来处理服务器压力。
同步锁是JVM级别,经过分布式集群之后每台服务器在并发的情况下,不能保证在同一时刻只有一个线程能够对同一资源进行加锁访问,这里就需要用分布式锁来解决,通常基于分布式存储系统(如ZooKeeper、Redis等)实现,我这里通过Redis的SetNX实现分布式锁。
实现方案:
基于数据库实现分布式锁
主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。
基于缓存实现分布式锁
理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的SETNX key value这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。
分布式会话
在传统的单体应用中,会 话信息通常存储在单一的服务器内存中,但在分布式系统中,由于应用的水平扩展和负载均衡的需求,会话信息需要跨多个服务器进行共享和管理。
分布式搜索
分布式消息队列
RabbitMQ
作用:流量消峰,应用解耦,异步处理
保证消息不丢失:
开启确认机制, 将exchange、queue和message都进行持久化
消息丢失的情况有哪些?
- 生产者到交换机 confirmCallback
- 交换机到队列 returnCallback
- 队列到消费者 手动开启ACK机制
怎么成为死信消息
- 消息 存活时间 过期
- 队列达到最大长度
- 消息被拒绝
死信队列的方式间接实现了延时队列,死信队列中的消息就是延时消息
概念:
消息中间件,是存放消息的中转站,生产者接收消息,然后通过交换机将消息发送给消费者。
各组件功能
看不见的
Server:接收客户端的连接,实现AMQP
实体服务
Connection:应用程序与Server的网络连接(TCP连接)。
Message:消息,应用程序和服务器之间传送的数据,由Properties和Body组成。-
- Properties为外包装,可以对消息进行修饰,比如消息优先级、消息延迟等高级特性;
- Body:消息体内容。
RoutingKey:路由键,生产者将消息发送给交换器时,会发送一个路由键来指定路由规则,这样交换器就知道把消息发送到哪个队列。
看的见的
Channel(信道):消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。Channel 作为轻量级的连接极大减少了操作系统建立TCP 连接的开销
Virtual Host(虚拟主机):逻辑隔离。虚拟主机里面可以有若干个Exchange、Queue,且两者在同一个虚拟主机中名称为唯一标识
Exchange(交换器):接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,返回给生产者或者直接丢弃。
Binding(绑定):交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey
,绑定
信息被保存到 交换机
的查询表中,作为 消息
的分发依据
流程
准备
-
生产者连接Server,建立Connection(连接)
开启信道后声明交换机、队列并设置相关属性,
再通过RoutingKey(路由键)将交换机和队列进行Binding(绑定)。
-
消费者进行建立连接,开启信道等操作,便于接收消息。
开工
生产者将消息发布到虚拟主机
的交换机
中,交换机根据路由键
选择的路由规则将消息发送到一个或多个队列,然后消费者从队列中获取消息进行处理。
限流
RabbitMQ的限流一般在消费端实现,当我们在客户端中监听消息时,可以通过channel调用basicQos()方法;
void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
prefetchSize:0,单条消息大小限制,0代表不限制
prefetchCount:一次性接受的消息数量。
global:true、false。
- 设置为 true时,表示整个信道上的所有消费者都受到这个 Qos 设置的影响
- 设置为 false 时,只有当前的消费者受到影响
如何避免消息重复导致重复执行的问题?
幂等性是解决这类问题的关键概念,其核心思想是,无论操作执行多少次,其效果都是相同的。
-
可以使用
唯一标识符
、版本号
、分布式事务
等机制。确保每个操作都有一个唯一标识符在处理消息或请求之前,检查系统的状态,确保已经处理过相同标识符的操作。如果已经处理,可以选择忽略或返回相同的结果。-
唯一标识符:
-
数据库以
分布式ID
充当唯一主键 ->适用于插入、删除操作-
UUID
-
雪花算法
好处:
(1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。
(2)容量大:每秒中能生成数百万的自增ID。
(3)ID自增:存入数据库中,索引效率高。
-
数据库自增主键
-
Zookeeper分布式协调服务
-
-
Token令牌->适用于插入、更新、删除操作
- 调用方在调用接口时先向后端请求一个全局
ID(Token)
,并携带这个全局ID
一起请求,后端需要对这个Token
作为Key键
,用户信息作为Value值
到Redis
中进行键值内容校验,如果Key
存在且Value
匹配就执行后面的业务逻辑。如果不存在就返回错误信息
- 调用方在调用接口时先向后端请求一个全局
-
-
版本号:乐观锁->适用于更新操作
在对应数据表中多添加一条字段,充当当前数据的版本标识
-
消息堆积,保证消息的顺序
分布式服务
SpringCloud
五大组件:
Nacos:注册中心,配置中心
Seata:分布式事务
事务是针对数据库的,在执行多个DML语句时,要么同时成功,要么同时失败,mysql默认的事务隔离级别是可重复读,
不在同一个服务器上,AT模式,执行前和执行后的数据库状态,RM分支事务
Seata角色
-
Transaction Coordinator (TC): 事务协调器,维护全局事务的运⾏状态,负责协调并驱动全局事务的提交或回滚
-
Transaction Manager™: 控制全局事务的边界,负责开启⼀个全局事务,并最终发起全局提交或全局回滚的决议
-
Resource Manager (RM): 控制分⽀事务,负责分⽀注册、状态汇报,并接收事务协 器的指令,驱动分⽀(本地)事务的提交和回滚
Getwary:所有服务的出入口(路由、断言、过滤)
sentinal:
zipkin:链路追踪
分布式缓存
缓存穿透
:
问题:客户端请求的数据,在缓存和数据源中查不到,每次请求都会查询数据源
-
布隆过滤器
缺点:不支持删除元素,有一定的误判性
布隆过滤器包含一个位数组,由大量的二进制位组成,且每个位只能取0或1,是一种概率性数据结构,用于检查元素是否存在,使用哈希函数,将元素映射为
位数组
的多个位置,并计算出元素对应多个位数组位置的值,如果所有值都为1,那么元素“可能”存在,否则不存在。 -
缓存空数据
:请求的数据在数据源中不存在时,将空值缓存,并设置TTL
避免保留空值缓存 -
黑名单
:当请求的数据键匹配黑名单的键时,拒绝请求
缓存击穿
:
问题:一个热门key失效,大量请求访问数据源。
锁
: 在访问缓存时,可以使用互斥锁或分布式锁来保证只有一个线程能够去查询底层存储系统,并在查询得到结果后更新缓存。逻辑过期
:设置适当的TTL
缓存雪崩
:
问题:有多个热门key同时失效,大量请求访问数据源。
缓存失效时间
:设置适当的TTLRedis集群
缓存预热
作用:减少对数据库或其他数据的访问,提高应用程序性能和响应时间
数据加载方法:全量、部分、增量加载等
数据源:数据库、外部服务、文件系统
解释:系统上线后,将相关的缓存数据直接加载到缓存系统, 以避免用户请求时, 先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据,只有当缓存中没找到所需数据才访问数据库。
做法:确认程序中的热点数据
后并选择合适的数据加载方法
将数据源
的数据导入到缓存中。
实现:
- 编写缓存刷新页面或者接口,上线时手动操作
- 数据量不大,可以在项目启动的时自动进行加载。
- 定期刷新缓存。
缓存更新(数据一致性)
- 手动清理缓存:
- 定时任务: 设置定时任务,定期清理或刷新缓存。
- 事件驱动更新: 相关数据发生变化时,触发一个事件,然后在事件处理程序中更新缓存。
- 版本控制: 在缓存中存储数据的版本信息,当数据发生变化时,更新版本号。在访问缓存时,比较版本号,如果版本不匹配,则从源中获取新数据。
- 自动过期: 设置缓存的过期时间。当缓存过期时,系统会自动重新获取最新的数据并更新缓存。
- 刷新策略: 为不同类型的数据实现不同的刷新策略。一些数据可能需要实时更新,而另一些可能需要延迟更新。
- 事务性更新:将数据库更新和缓存更新包装在一个事务中
- 延时双删策略:
- 第一次删除(First Delete): 在用户或系统触发删除操作时,不立即执行。系统会将其标记为待删除状态,不释放相关的资源。
- 延时处理(Delayed Processing): 标记为待删除状态的资源被保留一段时间,以确保没有误删除的可能性。这段时间可以根据系统的需求来设定。
- 第二次删除(Second Delete): 在经过一定的延时后,系统再次进行删除操作,将资源从系统中永久删除,并释放相关的资源。这个操作是在第一次删除后的延时期满后执行的。
缓存降级
为了确保系统的稳定性和性能,有意地减少或关闭缓存的使用
- 高并发时: 在系统面临高并发请求的情况下,缓存可能增加对数据存储和计算资源的压力。为了减轻负载,可以考虑降级缓存,以便直接从数据源获取数据,而不是依赖于缓存。
- 缓存失效时: 如果缓存系统出现问题或缓存失效的频率很高,为了避免提供过时的数据,可能会选择在缓存失效时暂时降级缓存,直到问题得到解决。
- 资源有限时: 当系统资源有限,无法支持大规模的缓存时,可以选择降级缓存。
- 故障处理: 在系统故障或性能问题的情况下,可能会选择降级缓存以确保基本的服务可用性。这可以防止缓存的问题影响整个系统。
- 特定场景下: 对于某些特定的业务场景,可能并不需要使用缓存,或者可以采用轻量级的缓存策略,以降低系统的复杂性和开销。
网络
七层架构
应用层:网络协议中最高层次,包含了用户应用程序与网络通信的协议和接口。常见的应用层协议包括HTTP(Hypertext Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)等。这些协议定义了在应用程序之间传输数据的规则。
表示层:
会话层:
传输层:负责端到端的通信,为应用层提供可靠的数据传输。两个主要协议:TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)
网络层:网络层处理不同网络之间的数据包传输。它负责寻址、路由选择和数据包的分片和重组。IP(Internet Protocol)是网络层最为著名的协议,它定义了数据包在网络中的寻址和路由规则。
数据链路层:建立在物理层之上,主要负责通过物理连接的直接通信。它将比特流组织成帧(Frame),处理物理层的错误,并提供基本的错误检测和纠正功能。此层还涉及到数据帧的寻址和流控制,通常包括子层的两个部分:逻辑链路控制(Logical Link Control,LLC)和介质访问控制(Media Access Control,MAC)。
物理层:网络协议中最底层的层次,负责传输比特流(bitstream)通过物理介质,如电缆、光纤、无线信号等。物理层关注的是数据的传输、电压、光信号等硬件细节,而不考虑数据的具体含义。
TCP/IP 原理
因特网整个TCP/IP协议族
网络接口层、网络层、传输层、应用层
三次握手
建立一个TCP连接时,需要客户端和服务器总共发送3个包,确认双方的接收能力和发送能力是正常的,指定自己的初始化序列号为后面的可靠性传送做准备
连接服务器指定端口,建立TCP连接,并同步连接双方序列号和确认号交换TCP窗口大小信息。
第一次握手:客户端向服务端发送一个SYN报文,建立连接
得出客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端收到后向客户端发送,SYN,ACK报文,为这次连接分配资源
得出服务端的发送能力正常
第三次握手:客户端收到ACK报文后,向服务端发送ACK报文,开始分配资源
得出客户端的接受能力正常。
至此服务端和客户端的发送和接受能力均为正常,
连接终止协议(四次挥手)
SYN:表示建立连接
FIN:表示释放连接
ACK:确认序号有效。表示响应
CLOSE_WAIT:被动关闭
LAST_ACK:等待中断请求的确认状态
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭,当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN仍能发送数据。首先进行关闭的一个方法执行主动关闭,而另一方执行被动关闭
第一次挥手:客户端向服务端发送FIN报文,关闭客户端到服务端的数据传送。
第二次挥手:服务器收到FIN报文,会给客户端发送ACK报文,Server进入被动关闭(CLOSE_WAIT)状态
第三次挥手:服务器发送FIN报文给客户端,关闭服务端到客户端的数据传送,Server等待中断请求(LAST_ACK)
第四次挥手:客户端收到FIN后,给服务端发送ACK报文确认,并将确认序号设置为收到序号+1, Server进入关闭(CLOSED)状态,完成四次挥手
JDK
JDK
Java开发的工具包
JRE
运行Java应用程序的运行时环境
JVM
概念
负责把我们所编写的Java代码编译成Java字节码,并将其翻译成我们的机器指令。确保Java程序在不同平台上实现“一次编写,到处运行”的目标。
JVM里面有一个解释器
参数
-Xms:Java堆内存的大小
-Xmx:Java堆内存的最大大小
-Xmn:Java堆内存中新生代大小,扣除新生代剩下的就是老年代的内存大小了
-Xss:每个线程的栈内存大小
-XX:MetaspaceSize:元空间大小
-XX:MaxMetaspaceSize:元空间最大大小
示例:
java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar
解读
首先对代码进行打包,生成常见的打包文件jar包或者war包,以便于在线上环境中进行部署。在这个过程中,Java源代码通过Java编译器(javac)被编译成字节码文件,形成可以被Java虚拟机(JVM)执行的中间代码。
在线上部署阶段,我们可以使用不同的方式,例如通过Tomcat容器或使用Java命令,将打包好的应用程序部署到目标服务器上。这涉及将编译后的class文件加载到JVM中,并在运行时执行。整个软件部署、编译和运行的流程协同工作,确保了代码的有效部署和顺利运行。
一阶段(class文件是怎么进入JVM的)
字节码文件是通过类加载器(Class Loader)加载到JVM中的。类加载器负责在运行时动态加载Java类,使得这些类可以被JVM执行。
类加载器
文件结构
- 启动类加载器 Bootstrap ClassLoader:加载Java安装文件jre\lib包下的文件 (老大)
- 扩展类加载器 Extension ClassLoader:加载Java安装文件jre\lib\ext包下的文件 (老二)
- 应用类加载器 Application ClassLoader: 加载classpath下的自己写的文件 (老三)
- 自定义类加载器 User ClassLoader : 自己编写的加载器,指定加载文件的路径和获取数据的方式 (老幺)
双亲委派
tomcat打破了双亲委派的规则。
是Java类加载器按层次结构委派加载类的方式。当一个类加载器收到加载请求时,它首先查看自己是否已加载该类,如果没有,则委派给其父加载器。这个过程一直向上委派,直到达到最顶层的启动类加载器。该机制防止了类的重复加载,确保了类的一致性,也提高了其安全性。
过程
七小步:加载->验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
加载:将class文件加载到内存中
验证:验证字节码文件的准确性
准备:为类的静态变量分配内存空间并赋予初始值
解析:类加载器加载类所需的其他类
初始化:为类以及变量赋予真正的值
使用:类的调用
卸载:类的垃圾回收
三大步:
-
1.加载:类加载器查找类的字节码文件,将字节码加载到内存中。字节码文件读取到JVM后,创建一个代表该类的
Class
对象。 -
2.链接:
验证:确保字节码文件的正确性,以防止恶意代码或错误的代码进入JVM,确保类的结构符合Java语言规范。
准备:为类的静态变量分配内存,并设置默认初始值,这些静态变量将在类加载的初始化阶段赋予真正的初始值。
解析: 将符号引用替换为直接引用,将类、字段、方法的符号引用翻译为在内存中的直接指针。确保程序运行时能够准确地定位类、字段或方法的内存位置
-
3.初始化:
在这个阶段,类的静态变量被赋予初始值,静态代码块被执行。这是类加载的最后一个阶段,确保类在首次使用前被正确初始化
元空间
存储类的结构信息、常量池(静态变量、常量),编译后的代码(字节码)等数据
二阶段(JVM的内存分配和程序执行:)
内存分配(角色)
初始化的对象放在堆里面,引用放在栈里面
堆 ->
先进后出
暂时存储的地方,
线性表:零个或多个数据元素的有限序列。
Java虚拟机所管理内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组。所有通过 `new` 关键字创建的对象都会在堆上分配内存,是Java程序运行时动态分配内存的主要区域。
栈
java方法执行时所在的地方,描述java方法执行时候线程内存模型,单位是栈帧(每一个压入栈的方法)。帧上面存储局部变量、操作数栈,方法出口等信息,每个线程都有自己的栈,用于跟踪方法调用和本地变量。
程序计数器(Program Counter Register)
记录当前线程执行字节码指令的内存地址值,方便下次继续执行。每个线程都有一个独立的程序计数器。我们java代码中所有的分支,循环,跳转,异常等都依赖程序计数器来完成的。
本地方法栈(Native Method Stack)
用于存储本地方法的信息。
内存划分
年轻代
用于存放新创建的对象区域。大部分对象在新生代中被快速分配和回收,生命周期短暂。
老年代
存放生命周期较长的对象,经过一定次数的垃圾回收后仍然存活的对象会被晋升到老年代,通常占用堆的较大部分。
元空间(持久代)
存储类的元数据,包括类的结构信息、静态变量等,在Java 8及以后版本,元空间取代了持久代,不再具有固定大小,而是使用本地内存,因此不再需要进行划分
关于内存划分的问题:
为了优化垃圾回收的`性能和效率`,考虑年轻代和老年代`对象的生命周期`、`垃圾回收算法的特点`
-
性能和效率
将堆划分为年轻代和老年代有助于减少全局垃圾回收导致的停顿时间。在年轻代进行垃圾回收时,只需暂停年轻代的线程,而不需要停止整个应用程序。可以减小垃圾回收的影响,并提高应用程序的响应性。
-
对象生命周期
大多数对象在其生命周期内经历了不同的阶段。许多对象在创建后很快就变得不可达(即没有引用指向它们),因此将这些对象放在年轻代中,可以通过一些高效的垃圾回收算法来快速回收不再使用的对象。相反,那些经过多次垃圾回收仍然存活的对象,被晋升为老年代
-
垃圾回收算法
jvm的优化本质就是减少Full GC的次数,尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。
规则:
Minor GC 是一种垃圾回收操作,其过程包括以下步骤:首先,所有活动线程暂停;接着,对新生代进行垃圾回收,清理掉不再被引用的对象,同时将存活的对象复制到Survivor区或老年代;最后,清理完成后,系统恢复运行,使得应用程序继续执行。这个过程通常具有较短的停顿时间,对用户体验的影响较小。 Minor GC 的主要目标是在新生代中进行快速的垃圾清理,以保持新生代的空间相对干净,为应用程序提供更好的性能。
Full GC:在Full GC期间,所有的应用线程都会停止,以确保垃圾回收器可以安全地进行整个堆的清理。这就导致了较长的停顿时间,用户可能会明显感受到系统的卡顿。
1.`Minor GC`后,剩余的存活对象的大小<Survivor区的大小,那么此时存活对象进入Survivor区域即可 2.`Minor GC`后,剩余的存活对象的大小<Survivor区的大小<老年代可用内存空间,那么此时存活对象进入老年区即可 3.`Minor GC`后,剩余的存活对象的大小<Survivor区的大小>老年代可用内存空间,那么此时就会触发一次"`Full GC`"
-
Full Gc触发机制:
老年代空间不足
元空间空间不足
手动触发
年轻代和老年代通常使用不同的垃圾回收算法。
-
年轻代
经常使用一些效率高的算法,如复制算法,来快速地识别和回收不再使用的对象,
在新生代的垃圾回收过程中,刚刚创建的对象被分配到Eden空间。当Eden空间满时,触发一次垃圾回收,清理不再被引用的对象。存活的对象会被移动到Survivor空间(s0和s1)。确保了及时回收短生命周期的对象,提高了垃圾回收的效率。
-
可达性分析算法
确定哪些对象是"可达"(reachable)的算法
-
引用计数算法
概念+流程:给每个对象关联一个
引用计数
,每当有一个新的引用指向对象时,引用计数加一;当引用失效或销毁时,引用计数减一。当引用计数为零时,说明对象不再被引用,可以被回收。出现的问题: 无法解决循环引用的问题。如果两个或多个对象相互引用,它们的引用计数永远不会为零,即使它们已经不再被程序使用。
-
根搜索算法
概念:从一组称为"根"的起始对象出发,通过对象之间的引用关系,遍历所有可达的对象,将其标记为"活动"(reachable),未被标记的对象即为"非活动"(unreachable),能够处理
引用计数算法
的弊病,但会产生内存碎片,影响内存的利用率。流程:垃圾回收的根搜索算法包括四个主要步骤。首先是初始标记阶段,该阶段通过标记根对象来启动垃圾回收过程。接着是并发标记阶段,在这个阶段,算法并发地遍历整个对象图,标记所有与根对象直接或间接可达的对象。随后是重新标记阶段,其目标是修正在并发标记期间发生变化的对象,确保准确地标记所有的可达对象。最后是清除阶段,在这个阶段,未被标记为可达的对象将被回收,释放它们占用的内存空间。
-
-
-
老年代
可能使用更复杂的算法,如标记-整理算法,以更好地处理长时间存活的对象,解决碎片化问题。
通过`仍然存活的对象`,`大对象`,`GC后对象太多无法放入Survivor区`可能会晋升到老年代。对于老年代,当其内存占用达到一定阈值时,垃圾回收器标记老年代中的存活对象,识别哪些对象仍然被引用,清理不再被引用的对象,但是老年代的垃圾回收算法速度较慢还会导致较长的停顿时间,因为在标记和清理的过程中,应用程序的执行可能会被暂停,如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。为了减小停顿时间,一些垃圾回收器采用了并发标记和清理的策略,尽量在不中断应用线程的情况下执行部分或全部的垃圾回收工作。这有助于在高并发应用场景中保持较短的垃圾回收停顿时间。
-
仍然存活的对象: 躲过15次GC之后进入老年代
-
大对象:一批对象的总大小大于了这块Survivor区域的内存大小的50%或者通过-XX:PretenureSizeThreshold 参数来指定对象超过一定大小时直接分配到老年代而不经过新生代
-
GC后对象太多无法放入Survivor区
直接转移到老年区
-
-
程序执行
在Java程序的执行过程中,首先经历了`类的加载阶段`,其中类加载器负责将类的字节码加载到元空间。随后,在`堆`上进行内存分配,通过`new`关键字创建对象时,JVM在堆上分配内存,并返回对象引用。对象初始化阶段包括对成员变量的默认值初始化以及执行构造方法等步骤。在方法调用过程中,`栈帧`被入栈和出栈,包括局部变量表、操作数栈和方法返回地址等信息。在对象不再被引用时依靠`垃圾回收器`负责回收内存,保持堆的有效利用。最后,`元空间`管理类的加载和卸载,以及常量池管理等任务,确保Java程序在运行时的一致性和性能。这些阶段共同构成了Java程序的执行生命周期。
三阶段(垃圾回收)
概念:(为什么被回收)
当对象不再被引用或访问时,它们占据的内存应该被释放,以便其他对象或应用程序可以利用这些空间。否则,内存将会耗尽,导致程序崩溃或系统变得极其缓慢。
jdk8 默认Parallel收集器
jdk11/9版本默认 G1收集器
websocket
常用注解:
@ServerEndpoint 类似与servlet中的 RequestMapping
@OnOpen类似与servlet中的 init()初始化
@OnClose类似与servlet中的destroy() 销毁
@OnMessage类似于servlet中的service请求 (意思就是发送数据的方式 @doPost() / @doGet() 组合)
优势:
相对于传统的HTTP协议,WebSocket提供了更低的`延迟`、更高的`性能`和更高效的`数据传输`以及`跨域支持`
低延迟
:
- WebSocket建立在TCP协议之上,相对于HTTP协议,减少了握手和头部信息的开销,从而降低通信延迟。对于需要快速响应的应用程序非常重要
性能
:
- webSocket是一种持久连接,它消除了HTTP协议中频繁建立和关闭连接的开销。可以更有效地利用网络和服务器资源,提高性能。
数据传输
:
- WebSocket支持双向通信,客户端和服务器可以同时发送和接收数据。对于需要双向交互的应用程序来说非常重要
- 基于事件驱动,只有在有数据更新时才会进行通信,降低了服务器和客户端之间的通信开销–>减少不必要的网络流量
跨域支持
- WebSocket协议天生支持跨域通信,可以在不同域的服务器和客户端之间进行实时通信,
允许客户端和服务器之间建立持久性的连接,实现实时数据的双向传输
概念
全双工通讯协议:
在能力上相当于两个单工通信方式的结合,可以同时进行信号的双向传输。
WebSocket是一种在单个TCP连接上进行`全双工通讯协议`
- 全双工:数据是双向传输,通信的两个端点可以同时发送和接收数据
- 半双工:数据是双向传输,但不能同时进行,通信的两个端点之间只能有一个方向的数据传输,一方发送数据,另一方必须等待。
- 单工:数据是单向传输,数据只能在一个方向上传输
http与webSocket的区别:
连接方式:
- HTTP: 是一种无状态协议,每个请求和响应之间是独立的,服务器不保留与客户端之前的通信状态。
- WebSocket: 是一种持久性协议,它建立一次连接后,允许双方在任何时候都能够双向传输数据,而不需要重新建立连接。
通信开销:
- HTTP: 每次请求都需要发送完整的HTTP头,可能引入较大的通信开销。
- WebSocket: 一旦建立连接,数据传输的开销相对较小,因为在握手时已经经过了一次完整的通信。
推送支持:
- HTTP: 主要是请求-响应协议,服务器对客户端的主动推送支持有限。
- WebSocket: 支持服务器主动向客户端推送数据,适用于实时通信和推送的场景。
协议握手:
- HTTP: 建立连接时需要进行较为复杂的握手过程,每次请求都需要重新握手。
- WebSocket: 初始握手后,连接保持打开状态,之后的通信可以直接传输数据,无需重新握手。
应用场景:
- HTTP: 适用于一次性请求和响应的场景,如传统的网页浏览、API调用等。
- WebSocket: 适用于需要双向实时通信、推送和低延迟的应用,如在线游戏、聊天应用、实时协作等。
socket与webSocket
-
传输层协议:
- Socket: 通常指的是底层的套接字编程,可以使用不同的传输层协议,例如TCP或UDP。
- WebSocket: 是一种基于HTTP协议的高层协议,通常在HTTP的基础上建立,并且使用WebSocket协议进行通信。
-
连接性和通信方式:
- Socket: 可以支持面向连接的TCP通信(可靠但开销较大)或面向无连接的UDP通信(开销较小但不可靠)。
- WebSocket: 提供全双工通信,一旦建立连接,允许双方在任何时候都能够双向传输数据。
-
协议握手:
- Socket: 直接建立连接,没有类似于HTTP的握手过程。
- WebSocket: 建立连接时需要进行WebSocket握手,通过HTTP协议进行升级。
-
应用场景:
-
Socket: 通常用于底层网络编程,适用于一些需要直接控制网络通信的场景。
-
WebSocket: 适用于需要实时双向通信和推送的应用,例如在线游戏、聊天应用、实时协作等。
总的来说,Socket更底层,可以通过不同的传输层协议实现,而WebSocket是一种在HTTP基础上建立的高层协议,提供了更方便的全双工通信和推送支持。选择使用哪种协议取决于应用的需求和开发的复杂性。WebSocket通常用于Web开发中,而Socket可以用于更底层的网络编程。
-
UDP和TCP
是两种常见的传输层协议,两者之间区别主要涉及连接性
、可靠性
、流控制
、应用场景
连接性
- TCP是一种面向连接的协议,
通信前
需要建立连接,数据传输完毕后
需要释放连接。 - UDP是一种无连接的协议,通信不需要建立或释放连接
可靠性
-
TCP提供
有序
、可靠
、面向字节流
的连接。有序:接收方按照发送方的顺序将数据交付给应用层。
**可靠:**它用
确认
和重传
机制来确保数据的可靠传输。- 发送方在发送数据后等待接收方的确认,如果一定时间内未收到确认,就触发重传机制,确保数据得以正确接收。
面向字节流:应用层交付的数据视为一连串的字节流,而不是固定大小的消息,可以更灵活地处理数据
-
UDP不提供可靠,有序,面向字节流的连接
流控制
- TCP 拥有流控制和拥塞控制的机制
- UDP不具备流控制和拥塞控制的机制。
应用场景
- TCP的开销相对较高,适用于对数据传输可靠性要求高的场景
- UDP的开销相对较低,适用于快速传输和实时性要求高的场景
spring
IOC,DI,AOP(内核)
IOC控制反转
概念:
负责
对象的生命周期
以及对象和对象之间的关系
,对象的创建由Spring IOC容器来操作,被创建或被管理的对象在IOC容器中统称为Bean对象
生命周期
首先Bean会经历三个大的阶段,生产,使用,销毁。
Bean的作用域(scope)为singleton
时,该Bean的实例只会被创建一次。发生在Spring容器启动时,存在于容器的运行期间,应用关闭时销毁。如果Bean的作用域设置为prototype,那么每次通过getBean()
方法获取该Bean时,都会创建一个新的实例。这种Bean的生命周期是更短暂的,对象的创建和销毁由开发者手动管理,而不是由Spring容器负责,适用于一些需要频繁创建新实例或者生命周期较短的情境。
对象和对象之间的关系->DI依赖注入
概念:
- 在spring IOC容器中建立bean与bean之间的依赖关系的过程
- 业务层要用数据层的类对象,以前是自己new出来的,现在靠IOC容器注入进来
作用:
- service层运行需要依赖dao层对象,IOC容器中虽然有service层对象和dao层对象,但是两者之间没有任何关系,需要把dao层对象交给service层,也就是说要绑定service层和dao层对象之间的关系
AOP面向切面编程
AOP 的底层是通过 Spring 提供的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态生成代理对象,代理对象方法执行时进行增强功能的介入,再去调用目标对象的方法,从而完成功能的增强。
概述:
- 无侵入式,面向切面编程,在程序运行期间,在不修改源码的情况下对方法进行功能增强
两种动态代理方式:
- JDK 代理 : 基于接口的动态代理技术
- cglib 代理: 基于父类的动态代理技术
其本质是一个容器,java对象在容器中叫做bean对象
Spring加载Bean的方式以及使用场景:
1.通过XML配置文件中的元素定义Bean,然后Spring容器读取并解析配置文件,完成Bean的装载
2.使用@Component以及衍生注解(@Service,@Controller,)将Java类标记为Bean,Spring会自动扫描并加载这些标记的类
3.通过配置类的方式,在Java类中增加@Configuration注解,在指定方法上增加@Bean注解实现
4.Spring还提供了两个扩展接口ImportSelector和BeanDefinitionRegistrar可以灵活地控制Bean的加载和注册过程
常用注解
@Value 注入普通属性
@Resource 相当于@Autowired+@Qualifier,按照名称进行注入
- @Autowired 在字段上根据类型依赖注入
- @Qualifier 结合@Autowired一起使用,根据名称进行依赖注入
@Component
@Component注解其下的,三个衍生注解都带有@Component父注解,这四个注解,作用都一致,将标注的类放到容器里,有普通组件,持久层组件、业务层组件,控制层组件,主要作用就是区分组件
- @Controller
- @Service
- @Repository
@ComponentScan(“路径名”) 等价于在xml里配置注解包扫描
在xml配置文件里,通过启用注解包扫描,加载被标记的类并注册到Spring容器中
<context:component-scan base-package=“要扫描的包名” />
@Configuration 定义配置类,相当于Spring
配置文件
- @Bean:需要在@Configuration注解下进行创建,
springMVC
常用注解
@RequestBody将外部传递的json数组数据映射到形参的集合对象中作为数据
执行流程
前端控制器,处理器映射器(前端地址和后端地址),处理器适配器, 视图解析器
springBoot
springBoot常用注解:
@SpringBootApplication
- Spring Boot应用的启动类,包含了
@Configuration
、@EnableAutoConfiguration
、@ComponentScan
三个注解的功能。
@SpringBootTest
- Spring Boot应用的测试类
@RestController
- 相当于
@Controller
和@ResponseBody
的结合,用于标识控制器类,表明其所有方法返回的数据都是直接写入HTTP响应体中的。
**@RequestMapping
**
@GetMapping
、@PostMapping
等缩写注解,这些注解都是@RequestMapping
的变体,更加简洁
- 处理类或者单个方法的请求映射路径
@PathVariable
和@RequestParam
:
@PathVariable:URI模板中的变量映射到方法的参数上,处理RESTful风格的URL。
@RequestParam:将请求参数绑定到方法的参数上。
参数:
name
或value
: 用于指定参数的名称,可以互换使用。defaultValue
: 指定参数的默认值,如果请求中没有相应的参数,将使用默认值。
@RequestBody
springBoot的自动装配:
SpringBoot自动装配就是自动去把第三方组件的bean装载到ioc容器里面,不需要开发人员再去写bean的相关配置。
首先springBoot的启动是依靠main方法里的SpringApplication.run()方法,并在启动类上添加@SpringBootApplication注解实现自动装配。
@SpringBootApplication注解是一个复合注解
- @EnableAutoConfiguration AutoConfigurationImportSelector
- @SpringBootConfiguration等同于@Configuration
- @ComponentScan自动扫描并加载符合条件的Bean
自动装配的实现主要依靠三个核心的关键技术:
1.引入starter,启动依赖组件的时候,这个组件要包含一个@Configuration配置类,在这个配置类中,我们需要通过@Bean这个注解去声明需要装配到ioc容器里面的bean对象
2.这个配置类是在放第三方的jar包里面,然后通过springBoot中,约定优于配置的这样一个理念,把这个配置类的全路径放在classpath:/META-INF/spring.factories文件里面,然后再用@EnableAutoConfiguration里@import注解引入的AutoConfigurationImportSelector这个类,扫描所有jar包下META-INF的spring.factories文件,SpringBoot就可以知道第三方jar包里面这个配置类的位置,这个步骤主要使用到了Spring里面的SpringFactoriesLoader来完成的
3.SpringBoot拿到所有第三方jar包里面声明的配置类以后再通过spring提供的importSelector这样一个接口来实现对这些配置类的动态加载,从而去完成自动装配这样一个动作
Mybatis
ORM(Object Relational Mapping 对象关系映射):指的是持久化数据和实体对象的映射模式。
和Jpa的关系
工作原理:先封装SQL,接着调用JDBC操作数据库,最后把数据库返回的表结果封装成Java类
核心组件: SqlSessionFactoryBuilder(构造器)
SqlSessionFactory(工厂接口)
SqlSession(会话)
SqlMapper(映射器)由Java接口和XML文件构成,需要给出对应的SQL和映射规则,映射器
负责发送SQL去执行,并返回结果
增删改查标签、、、
结果映射器
数据库字段与Java对象属性的映射关系
- id:唯一标识符
- resultType:指定查询结果的封装类型(模型)
- parameterType:指定输入参数的类型
- keyProperty:自动生成主键的属性名
动态sql
mapper缓存
是针对查询操作的
一级缓存默认开启,SqlSession级别的缓存
二级缓存,默认不开启,mapper 映射级别的缓存
Jpa
jpa(Java Persistence API -> Java持久化API)是SUN公司推出的一套基于持久层ORM的一种规范,定义了接口和抽象方法。hibernate[ˈhaɪbəneɪt’]是jpa的具体体现。
tomcat
sercurity
sercurity的执行第一步要被拦截器给拦截下来Usern amePasswordAuthenticationFilter
,随后执行attemptAuthentication
方法请求认证,判断是否为post请求、以及对获取到的用户名和密码信息进行封装UsernamePasswordAuthenticationToken
,后面由认证管理器AuthenticationManager
接口的authenticate
方法在ProviderManager
类的具体实现动态获取封装信息,Class<? extends Authentication> toTest = authentication.getClass();
,提供匿名的认证管理以int类型接收,和已经得到的封装信息进行比对,不匹配那么就把封装信息给先传给parentResult,再传给result对象
第一次交由匿名管理AnonymousAuthenticationProvider
进行认证,验证不成功,则第二次交由DaoAuthenticationProvidero
进行认证,验证成功则通过determineUsername
方法得到用户的主体信息,boolean cacheWasUsed = true;
默认在缓存先中进行查找,不存在则将缓存关闭
request.getParameter()
从请求里获取验证参数
泛型
多线程
问题发现:多个线程访问同一资源时,并对资源有写操作,容易出现线程安全问题,java提供了同步机制来解决
多线程其实就是为了解决CPU、内存和硬盘速率差的问题,提高CPU的使用效率,服务器在处理一个任务,尤其是IO(磁盘读写)的时候,CPU大部分情况下都是处于空闲状态。例如记录用户浏览行为,这个流程一般情况是很快速的(尤其是将热点数据放入到了redis中),但是最后我们需要将用户行为数据保存到数据库,这个写入数据库的操作就是典型的IO操作,是一个很耗时的操作,我们可以再开启一个线程单独处理数据库写入的操作,不用进入阻塞状态,直接将结果返回给用户,这样用户的体验感会很好。
当然,使用多线程的时候我们还使用了线程池,省去频繁创建和销毁线程消耗的时间和资源。
在实际操作中,我们会定义线程池的配置类,用springboot的@Bean注解将线程池ThreadPoolTaskExecutor放入到容器中,我们会指定核心线程池大小,最大可创建的线程数,队列最大长度,线程池维护线程所允许的空闲时间。当我们使用的时候就可以从spring容器中拿ThreadPoolTaskExecutor对象,然后执行execute()方法。要是需要延迟任务,可以在配置类中导入ScheduledExecutorService对象,执行任务的时候,可以添加时间,表示延迟执行
并发编程遇到的问题
线程死锁
发生在两个或多个线程互相等待对方释放资源的情况下,导致程序无法继续执行
-
产生死锁的条件:
互斥:一个资源每次只能被一个线程占用(资源独立)->将互斥访问的资源改为共享访问,但为了系统安全很多时候不破坏互斥条件
请求与保持: 一个线程持有一个资源,并等待获取另一个资源,但同时又不释放已经持有的资源(不释放锁)->⼀次性申请所有的资源,
**不剥夺:**线程已经获得的资源不能被其他线程抢占,只有持有资源的线程才能够释放(抢夺资源)->占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到则主动释放它占有的资源。
**循环等待:**若干进程之间形成一种头尾相接,循环等待的资源关闭(死循环)->按顺序申请资源,释放资源则反序释放
上下⽂切换
为了让线程能够有效执行,cpu为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让其他线程使用,这个过程就属于一次上下文切换;
为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能 直接调⽤ run() ⽅法?
首先实例化一个Thread对象,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线 程进⼊了就绪状态,分配好时间⽚后自动执行run()方法。
如果直接执⾏ run() ⽅ 法,会把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,并不是多线程⼯作。
线程的生命周期:
新建、就绪、运行、阻塞、死亡
线程对象被创建后,进入新建状态,其他线程调用该对象的start()方法,启动该线程,并获取cpu权限后进行执行,线程执行完或者异常就退出run()方法,该线程结束。
其中的阻塞状态是因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
- 等待阻塞:使用wait()方法,让线程进入等待状态
- 同步阻塞:线程在获取synchronized同步锁时失败,锁被其他线程占用,进入同步阻塞状态
- 其他阻塞:调用线程的sleep()、join()、或发出I/O请求时,线程会进入到阻塞状态,sleep()状态超时,join()等待线程终止,I/O处理完毕时,线程重新转入就绪状态
创建线程的几种方式:
继承Thread
类创建线程
实现Runnable
、Callable
和Future
接口创建线程
使用线程池创建线程
#### 代码演示
import java.util.concurrent.*;
public class threadTest{
public static void main(String[] args) throws ExecutionException, InterruptedException {
//继承thread
ThreadClass thread = new ThreadClass();
thread.start();
Thread.sleep(100);
System.out.println("#####################");
//实现runnable
RunnableClass runnable = new RunnableClass();
new Thread(runnable).start();
Thread.sleep(100);
System.out.println("#####################");
//实现callable
FutureTask futureTask = new FutureTask(new CallableClass());
futureTask.run();
System.out.println("callable返回值:" + futureTask.get());
Thread.sleep(100);
System.out.println("#####################");
//线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
threadPoolExecutor.execute(thread);
threadPoolExecutor.shutdown();
Thread.sleep(100);
System.out.println("#####################");
//使用并发包Executors
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(thread);
executorService.shutdown();
}
}
class ThreadClass extends Thread{
@Override
public void run() {
System.out.println("我是继承thread形式:" + Thread.currentThread().getName());
}
}
class RunnableClass implements Runnable{
@Override
public void run(){
System.out.println("我是实现runnable接口:" + Thread.currentThread().getName());
}
}
class CallableClass implements Callable<String> {
@Override
public String call(){
System.out.println("我是实现callable接口:");
return "我是返回值,可以通过get方法获取";
}
}
1.解决方法
- 同步代码块、同步方法
synchronized(同步锁){
需要同步操作的代码
}
public class Ticket implements Runnable {
private int ticket = 100;
Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
synchronized (lock){
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
public synchronized void method(){
可能会产生线程安全问题的代码
}
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
//每个窗口卖票的操作 ,窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
- 锁机制
Lock lock = new ReentrantLock();
try{
lock.lock();//加锁操作
}finally{
lock.unlock();
}
public class Ticket implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作永远开启
while(true){
lock.lock();//加锁
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();//释放锁。
}
}
}
2、锁的种类:
Java中的锁用于保障多并发线程情况下的数据一致性。为了保障数据一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果锁正在被线程a使用,其他线程就会进入阻塞队列等待锁的释放.直到线程a执行完成并释放锁、其他线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象、从而保障数据的安全。 - 从乐观和悲观的角度可分为乐观锁和悲观锁, - 从获取资源的公平性角度可分为公平锁和非公平锁, - 从是否共享资源的角度可分为共享锁和独占锁, - 从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。
Sql
常用函数:
- count(*/column):返回行数
- sum(column): 返回指定列中唯一值的和
- max(column):返回指定列或表达式中的数值最大值
- min(column):返回指定列或表达式中的数值最小值
- avg(column):返回指定列或表达式中的数值平均值
MySql, Oracle,Sql Service的区别
- 环境:Sql Service只能在Windows上使用,MySql和Oracle可以在其他系统上使用,支持数据库不同系统之间的移植,MySql开源免费的,Sql Service和Oracle要钱。
- 功能(特性):Oracle支持大并发量,大访问量,而MySql的话压力没这么大,一般使用集群或者缓存来搭配使用。(mysql单击并发量500左右),Sql Service还行,
数据库三大范式
- 第一范式:每个列都不可以再拆分。
- 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分(表必须有主键)
- 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。(表中的字段不要有冗余)
SQL语言:
用于数据库查询的结构化语言
DQL–数据查询语言,select
DML–数据操纵语言,insert、update、delete
DDL–数据定义语言,create、alter(add/modify/drop)、drop
DCL–数据控制语言,commit、rollback
修改和删除表
˜添加列:alter table 表名 add 新增列名 数据类型
˜修改列:alter table 表名 modify 列名 新数据类型
˜删除列:alter table 表名drop column 列名;
˜修改列名: alter table 表名 rename column 列名 to 新列名;
˜修改表名:rename 表名 to 新表名
˜删除表:drop table 表名
truncate table 表名(用于删除表中的全部数据)
增删改查
˜添加信息:insert into 表名(列名)values(对应列)
˜查询信息:select *|列名 from 表名
˜修改信息:update 表名 set 列名=value [where 列名=value]
˜删除信息:delete from 表名 [where 列名=value]
MySQL架构
Server层和存储引擎层两个部分组成。
Server层
连接层
负责客户端和服务端的连接工作
分析器
词法分析、语法分析
优化器
索引选择
执行器
操作引擎层,返回结果
引擎
InnoDB引擎:
InnoDB支持事务,行级锁和表级锁,B+tree索引,聚簇索引,MVCC,相对于 MyISAM,InnoDB 的存储空间占用较大,通常为其2.5倍。
MySQL集群和分布式
集群(主从复制):
一主一从 主主复制 一主多从 多主一从
MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
Mysql的事务实现
事物(ACID)
原子性(Atomic):事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败,由Undo Log日志来保证
持久性:当事务提交或回滚后,数据库会持久化的保存数据
隔离性:多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间相互隔离。
**一致性:**使数据库从一个一致性状态变换到另外一个一致性状态。
日志
- 事务日志
两个日志都是 Innodb 存储引擎生成的。
Undo Log 记录事务**「开始前」的数据状态,记录更新之「前」**的值;
Redo Log 记录事务**「完成后」的数据状态,记录更新之「后」**的值;
-
二进制日志
MySQL 在完成一条更新操作后,Server 层会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 数据操作 统一写 入 binlog 文件。
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作
-
慢查询日志
14 怎么解决慢查询问题
1 开启慢查询日志,并设置慢查询时间,我们当时设置的时间是1秒。
2 在服务器上打开慢查询日志文件(mysql-slow.log),sql语句执行时间超过1秒的都会在日志文件里记录
3 定位到sql语句后,分析sql语句问题,有没有加索引,没有加索引可以考虑加索引。加了索引查看sql是否走了索引
4 explain + sql语句,查看type指标,类型至少是range 以上级别,如果低于range ,也就是 index 、 ALL级别,需要考虑sql语句索引失效的问题。
2 MVCC(多版本并发控制)
undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制) 。
-
首先A表示Atomic
原子性
,**需要保证多个DML(增、删、改)操作的原子性,要么都成功,要么都失败,**失败就意味着对原本执行成功的数据进行回滚。在Innodb数据引擎中有一个Undo Log(回滚日志),记录了事务执行过程中对数据的修改,以便在事务回滚或者数据库崩溃恢复时使用。每个事务开始时,InnoDB 会为该事务分配一个 Undo Log日志文件,并在事务执行过程中将修改的数据写入 Undo Log 中。这样,如果事务回滚,可以根据 Undo Log 的信息将数据还原到事务开始之前的状态。 -
其次就是C,表示Consistency
一致性
。表示其数据的完整性约束没有被破坏,这个更多是依赖业务层面的一些保障。数据库本身也提供了一些,比如说像主键的唯一约束,字段长度和类型的一些保障等。
并行事务带来的问题:
**脏读:**一个事务在执行过程中,只能读取不到其他事务已提交的数据
不可重复读:一个事务读取数据,其他事物在此期间对数据修改完成后,再一次读取数据时,两者值不一样
幻读: 一个事务在执行过程中读取不到其他事务提交的数据
- 在第一个事务开始时读取到一批数据,但此后另一个事务又插入了新数据并提交,此时第一个事务又读取这批数据但发现多了一条,即好像发生幻觉一样。
事务隔离(TRANSACTION_ISOLATION)
Mysql的事务隔离是是依靠锁来实现的,加锁会带来性能的损失。
- 首先读未提交(READ-UNCOMMITTED)是不加锁的,所以它的性能最好,没有加锁、解锁带来的性能开销,也因此衍生出
脏读、不可重复读、幻读
的并行事务带来的问题- 读已提交(READ-COMMITTED)
- 可重复读(REPEATABLE-READ),解决
脏读、不可重复读
,在读已提交的基础上,一个事务在执行过程中读取不到其他事务提交的数据,解决不可重复读
,是MySQL默认的隔离级别,对于同一条数据,某个事务在执行DML
语句未提交之前,其他事务不能再次进行DML
操作,会进入阻塞状态,在串行化(SERIALIZABLE)中又得到规范让事务按照先手顺序执行,每次只能有一个事务在执行,需要考虑死锁
的问题使用MVCC机制去解决了读未提交和读已提交的一个问题
让事务按照先后顺序执行,
使用了行锁或者表锁的方式可以解决可重复读的问题。
查看隔离级别:
show variables like 'transaction_isolation'; SELECT @@transaction_isolation
修改隔离级别:
set session transaction isolation level read committed;
查看版本:
select @@version;
查看InnoDB状态
SHOW ENGINE INNODB STATUS; ===================================== 2020-09-15 14:46:28 0x7f732fcff700 INNODB MONITOR OUTPUT->死锁日志的时间 ===================================== ------------------------ LATEST DETECTED DEADLOCK->死锁的详细信息 ------------------------
数据文件查看
SHOW VARIABLES LIKE 'datadir';
当前有多少事务正在运行
select * from information_schema.innodb_trx;
当前有哪些线程正在执行
show processlist;
数据引擎的切换:
2.设置存储引擎和默认字符集
create table user ( id int not null, name varchar(30) default null, pwd varchar(30) default null, primary key(id) ) engine=MyISAM default charset=utf8;
事务执行流程:
start transaction
->一系列执行操作->commit/roback
事务开始:
start transaction
开始,事务开始于start transaction
命令之后的第一条语句执行的时候**事务提交:**commit
**事务回滚:**rollback
-
接下来呢, I表示Isolation
隔离性
,表示多个并行事物对同一个数据进行操作的时候,如何去避免多个事物的干扰,导致数据混乱的一个问题。
而Innodb里面呢,实现了SQL-92(1992 年发布的 SQL 语言标准)的一个标准提供了4种隔离级别,**分别是RU(未提交读),RC(已提交读),RR(可重复读)以及Serializable(串行化),**InnoDB 默认采用的隔离级别是RR(可重复读) -
最后一个是D,表示Persistence
持久性
,也就是说,只要事务提交成功,那么对于这个数据结果的影响一定是永久的,不能因为数据库宕机或者其他原因导致数据变更的一个失效。理论上来说,事物提交之后直接把数据直接发到磁盘里面就可以了。
但实际没有这么做,因为磁盘io的效率很低,所以InnoDB 里面设计了Buffer Pool
缓冲区来进行优化,也就是说当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页
(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。那么在这样一个机制里面呢,也有可能出现如果就会导致数据丢失,也就是说无法去满足持久化,所以在InnoDB 里面引入了Redo Log,这个文件存储了数据库变更之后的一个值。当我们通过事务进行数据更改的时候,除了修改内存缓冲区里面的数据以外呢,还会把本质修改的一个值追加到Redo Log里。当事物提交的时候,直接把Redo Log里面的日志刷新到磁盘里面进行持续化,一旦数据库出现宕机,在MySql重启以后,可以直接用Redo Log里面保存的重写日志读取出来再去执行一遍,从而去保证数据的一个持久性。
redo log记录的是事务完成以后数据的操作,可以解决 Buffer Pool因为缓存数据丢失的问题。
Buffer Pool 是提高了读写效率没错,但是Buffer Pool 是基于内存存储的,而内存总是不可靠,万一出现断电、重启、数据库宕机,还没来得及落盘的脏页数据就会丢失,也就是说无法去满足持久化。所以在InnoDB 里面引入了Redo Log当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 Redo Log 里面,并更新内存, 当事物提交的时候,可以直接把Redo Log里面的日志刷新到磁盘里面进行持续化。在MySQL 重启后,可以根据 Redo Log的内容,将所有数据恢复到最新的状态。
以上就是我对这个问题的理解
sql优化(索引失效)
1.避免在开头模糊查询(右模糊查询)
2.避免使用in和not in (连续数值时,用between代替)
3.避免使用or(用union代替)
4.避免对null值的判断(字段添加默认值0)
5.避免在where条件等号左侧进行表达式、函数操作
6.避免使用where 1=1,(有where条件就加and,没有就去掉where)
7.查询条件不用<>或者!=
8.有联合索引时,遵循最左匹配原则
9.隐式类型转换用不了索引
行锁:
操作数据库的时候,将表中的一行数据锁住,不能操作被锁定的这行数据
表锁:
在对某个表进行操作(如插入、更新、删除)时,锁定该表,阻止其他会话对同一表进行写操作
文件结构:
一样
.frm 是 MySQL 里面表结构定义的文件,不管你建表的时候选用任何一个存储引擎都会生成
不一样
.MYD 文件 ,是 MyISAM 的数据文件,存放数据
.MYI 文件,是 MyISAM 的索引文件,存放索引
常用引擎包括:MYISAM、Innodb、Memory、MERGE
表级锁,不支持事务,默认使用B-tree 索引,非聚簇索引
每个MyISAM表最大索引数是64,每个索引最大的列数是16。
如果有100W的数据要插入到数据库该怎么办?
我们可以将表的储存引擎先改成MyISAM,数据插入完毕后,再将储存引擎修改回来。这样做的原因是,MyISAM在插入大量数据时可能比InnoDB更快,因为它不支持事务,而且在插入时不会涉及到事务日志和索引的维护
解读:
基于ISAM存储引擎的一种变体,支持传统的` B-tree 索引`以及全文索引,对于一些读密集型的应用场景(如日志记录、报表生成等),能够提供较高的查询和插入速度。
MyISAM 适用于一些对并发性能要求不高、读操作较为频繁的场景,对于需要高并发性能、事务支持和数据完整性的应用,可能需要考虑使用其他支持这些特性的存储引擎,比如 InnoDB
Memory:
表级锁 ,不支持事务,默认使用HASH索引,非聚簇索引
与其他存储引擎不同,Memory 存储引擎将表的数据存储在内存中,提供快速的读写操作,但会占用和数据量成正比的内存空间。在发送断电或服务重启等异常情况会导致数据丢失,因此通常用于临时性的数据存储,对持久性要求较低的场景。而不适用于需要长期保存的数据。
MERGE:
是一组MYISAM表的组合
索引
概念:是数据库管理系统(DBMS)中一个排序的数据结构,以协助快速查询、更新数据库表中数据。
索引的优缺点 :
优:可以加快数据的检索速度,创建索引的最主要原因
缺:
创建索引
和维护索引
要耗费时间
维护索引:当对表中的数据进行操作时,索引要动态维护,会降低执行效率;
创建索引:索引需要占物理空间。
索引类型:
普通(Normal):
也叫非唯一索引,是最普通的索引,没有任何的限制。
唯一(Unique):
要求键值不能重复。
- 主键索引是一种特殊的唯一索引,要求键值不能为空、不能重复
全文(Fulltext):
全文索引为在字符串数据中对复杂的词搜索提供有效支持
是一种用于在数据库中进行高效文本检索的技术,它通过在文本字段上建立特殊索引,支持自然语言查询
、模糊匹配
和分词器
等功能。与传统的关键词匹配不同,全文搜索能够更灵活地处理文本数据,使得用户可以以更自然的语言进行查询,而停用词过滤和模糊匹配等特性进一步提高了搜索的准确性和全面性。全文搜索常用于需要对大量文本进行复杂查询的应用场景,如博客、新闻、论坛等,为用户提供了更强大的检索功能。
只有文本类型的字段才可以创建全文索引,比如 char、varchar、text。
可以更好地支持对文本数据的全文本搜索:
特点:
- 自然语言查询:用户可以使用更自然的语言进行查询。
- 分词器:将文本数据分解成单词或词组,提高搜索的准确性。
- 停用词过滤:过滤掉一些常用的停用词,提高索引的效率和减小索引的大小。
- 模糊匹配:在搜索时进行部分匹配
联合索引(Compound Index)
- 使用多个字段同时建立一个索引。在联合索引,要想命中索引,需要按照最左匹配原则来执行,否则无法命中索引。(最左匹配原则:按照建立索引时的字段顺序挨个使用)
空间索引(SPATLAL)
主要的Spatial索引类型:R-Tree,Quadtree
用于处理空间数据(例如几何形状、地理位置、地图数据等)的索引类型,在MySQL中,支持Spatial索引的存储引擎包括MyISAM和InnoDB。
索引方法
B+树索引
- 叶子节点存数据,非叶子节点存主键+地址
- 叶子节点之间通过双向链表连接,方便范围查询(id in between 1 and 3 )
- 不管有多少数据,只需要查3次,范围查询数据很快
hash索引:
通过哈希函数将索引列的值映射到固定大小的哈希表,在绝大多数需求为单条记录查询的时候,可以选择哈希索引,实现快速的等值查找,但不支持范围查询和排序,且可能存在碰撞问题。
物理存储类型
聚簇索引
-
主键索引的叶子节点存储着数据,主键索引非常高效。
-
非主键索引的叶子节点存储着主键和其他带索引的数据,查询时做覆盖索引时会非常高效。
非聚簇索引
- 索引的叶子节点存储的是数据的地址,需要再寻址一次才能得到数据。
数的概念
NGINX
负载均衡,动静分离
java算法
1.二分查找
概念:又叫折半查找,用于在有序数组中查找指定元素位置
实现:不断将搜索范围缩小一半,直到找到目标元素或者确定目标元素不在数组中
public class Demo1 {
//二分查找
public static void main(String[] args) {
int[] sortedArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 1;
int result = biSearch(sortedArray, target);
if (result != -1) {
System.out.println("元素" + target + "在数组中");
} else {
System.out.println("元素 " + target + "在数组中找不到");
}
}
public static int biSearch(int []array,int a){
int lo=0;
//索引
int hi=array.length-1;
int mid;
while(lo<=hi){
mid=(lo+hi)/2;//中间位置
if(array[mid]==a){
return mid+1;
}else if(array[mid]<a){ //向右查找
lo=mid+1;
}else{ //向左查找
hi=mid-1;
}
}
return -1;
}
}
2.冒泡排序
比较相邻元素,并通过比较大小的方式交换它们之间的位置,直到整个序列有序
public class Demo2 {
//冒泡查询
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
System.out.println("原始数组: " + Arrays.toString(arr));
bubbleSort(arr);
System.out.println("排序数组: " + Arrays.toString(arr));
}
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
/**
* 从小到大 arr[j] > arr[j + 1]
* 从大到小 arr[j] < arr[j + 1]
*/
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素位置
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
运行结果:
原始数组: [64, 34, 25, 12, 22, 11, 90]
排序数组: [11, 12, 22, 25, 34, 64, 90]
3.插入排序
概念:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应的位置并插入
实现:
- 1.将数组分为两部分:已排序部分和未排序部分。
- 2.从未排序部分取出一个元素,依次与已排序部分的元素比较,找到合适的位置插入。
- 3.重复步骤2,直到未排序部分的元素全部插入到已排序部分,排序完成。
public class Demo3 {
//插入排序算法
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
System.out.println("原始数组: " + Arrays.toString(arr));
insertionSort(arr);
System.out.println("排序数组: " + Arrays.toString(arr));
}
//将未排序的部分逐步插入已排序的部分
public static void insertionSort(int[] arr) {
int n = arr.length;
// 从第二个元素开始,依次将未排序部分的元素插入到已排序部分
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
// 将比 key 小的元素依次向后移动,为 key 找到插入位置
/**
* 从小到大 < arr[j] < key
* 从大到小 > arr[j] > key
*/
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 插入 key 到合适的位置
arr[j + 1] = key;
}
}
}
4.快速排序
5.希尔排序
6.归并排序
7.桶排序
8.基数排序
9.剪纸算法
10.回溯算法
12.最大子数组算法
14.最小生成树算法
算法
动态规划(Dynamic Programming,简称DP)
是一种解决问题的数学方法,通常用于优化问题,通过将问题分解成子问题并存储子问题的解,避免重复计算,从而提高效率。动态规划常用于求解最优化问题,例如最长公共子序列、最短路径、背包问题等。
最长公共子序列算法
一种用于找到两个序列(字符串或数组)中的最长共同子序列的算法。
流程:
- 创建一个二维数组(通常称为DP表),其行数为第一个序列的长度加1,列数为第二个序列的长度加1。
- 初始化DP表的第一行和第一列为零,因为空序列与任何序列的LCS长度都为零。
- 从左上角开始,逐行逐列地填充DP表,根据以下规则:
- 如果当前两个元素相等,DP表中当前单元格的值等于左上方单元格的值加1。
- 如果当前两个元素不相等,DP表中当前单元格的值等于左方和上方两个单元格中的较大值。
- 最终,右下角的单元格的值即为两个序列的最长公共子序列的长度。
- 可以通过回溯DP表的路径,找到具体的最长公共子序列。
public class Demo1 {
//最长公共自序列算法
public static void main(String[] args) {
String X = "DBCA";
String Y = "BDCA";
int length = longestCommonSubsequence(X, Y);
System.out.println("最长公共子序列长度: " + length);
String lcs = findLCS(X, Y);
System.out.println("最长公共子序列: " + lcs);
}
public static int longestCommonSubsequence(String X, String Y) {
int m = X.length();
int n = Y.length();
// 创建一个(m+1) x (n+1)的二维数组并初始化为零
int[][] dp = new int[m + 1][n + 1];
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 最长公共子序列的长度存储在右下角的单元格
return dp[m][n];
}
public static String findLCS(String X, String Y) {
int m = X.length();
int n = Y.length();
// 创建一个(m+1) x (n+1)的二维数组并初始化为零
int[][] dp = new int[m + 1][n + 1];
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 回溯找到最长公共子序列
int lcsLength = dp[m][n];
char[] lcs = new char[lcsLength];
int i = m, j = n, index = lcsLength - 1;
while (i > 0 && j > 0) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
lcs[index] = X.charAt(i - 1);
i--;
j--;
index--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return new String(lcs);
}
}
最短路径算法
Linux命令
cpu占用率,top,ps
mysql:
show databases; 显示此用户下的数据库
use 数据库名; 切换数据库
show tables; 显示该数据库下所有表
加密算法
Docker
本文标签: 更新中
版权声明:本文标题:面试(持续更新中) 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1728313299a1153296.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论