面试笔记新
1、ConcurrentHashMap底层实现,CAS,原子引用,ABA问题,volatile?
ConcurrentHashMap是Java中一种线程安全的HashMap实现,它采用了分段锁的技术来提高并发读写的性能。
CAS(Compare and Swap)是一种乐观锁的实现,它通过原子操作来保证线程安全。CAS操作包括三个操作数:内存位置,预期原值和更新值,如果内存位置的值与预期原值相等,则更新为更新值,否则不更新。
原子引用是Java中的一种引用,它通过CAS操作保证了线程安全,使用原子引用可以避免加锁和同步的开销。
ABA问题是指在CAS操作中,由于线程的并发执行,导致内存位置的值被更新多次,导致预期原值与当前值不一致。为了避免ABA问题,ConcurrentHashMap采用了版本号(类似于时间戳)来判断内存位置的值是否发生变化。
volatile是Java中的一种关键字,它保证了变量的可见性,即当一个线程修改了一个volatile变量的值时,其他线程立即可以看到该变量的值。ConcurrentHashMap中使用了volatile关键字来保证分段锁的可见性。
////////////////////////////////////////////////////////////////////////////////////////////////////////////
以下为你详细介绍 ConcurrentHashMap 底层实现、CAS、原子引用、ABA 问题以及 volatile 关键字的相关知识:
1. ConcurrentHashMap 底层实现
JDK 7 版本
- 数据结构:采用分段锁机制,整体结构是一个
Segment数组,每个Segment又继承自ReentrantLock,且包含一个HashEntry数组。HashEntry是一个链表节点,用于存储键值对。 - 并发原理:不同的
Segment可以被不同的线程同时访问,实现并发操作。每个Segment相当于一个小的HashMap,只有在访问同一个Segment时才需要竞争锁,从而提高了并发性能。
JDK 8 版本
- 数据结构:摒弃了分段锁机制,采用数组 + 链表 + 红黑树的结构。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。
- 并发原理:使用
synchronized和CAS来保证并发安全。在插入、删除和查找操作时,首先通过CAS尝试更新节点,如果失败则使用synchronized对节点加锁,保证同一时刻只有一个线程能对该节点进行操作。
2. CAS(Compare-And-Swap)
- 概念:CAS 是一种乐观锁机制,它包含三个操作数:内存值(V)、预期值(A)和新值(B)。当且仅当预期值 A 和内存值 V 相同时,才将内存值 V 更新为新值 B,否则不做任何操作。
- Java 中的实现:在 Java 中,
java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong等)都使用了 CAS 操作。例如,AtomicInteger的compareAndSet方法就是基于 CAS 实现的:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
boolean result = atomicInteger.compareAndSet(10, 20);
System.out.println(result); // 输出 true
System.out.println(atomicInteger.get()); // 输出 20
}
}
- 优点:CAS 操作是无锁操作,避免了锁的开销,在多线程环境下能提高并发性能。
- 缺点:存在 ABA 问题。
3. 原子引用
- 概念:原子引用是 Java 中用于实现原子操作的引用类型,它可以保证对引用对象的操作是原子性的。在
java.util.concurrent.atomic包中,AtomicReference类就是用于实现原子引用的。 - 示例代码:
import java.util.concurrent.atomic.AtomicReference;
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
User user1 = new User("Alice", 20);
User user2 = new User("Bob", 30);
AtomicReference<User> atomicReference = new AtomicReference<>(user1);
atomicReference.compareAndSet(user1, user2);
System.out.println(atomicReference.get().getName()); // 输出 Bob
}
}
4. ABA 问题
- 问题描述:在 CAS 操作中,当一个值从 A 变为 B 再变回 A 时,CAS 操作会认为这个值没有发生变化,从而继续进行更新操作,但实际上这个值已经经历了变化,可能会导致一些意外的结果。
- 示例场景:线程 1 读取到变量值为 A,准备使用 CAS 将其更新为 C;此时线程 2 将变量值从 A 改为 B,然后又将其改回 A;接着线程 1 执行 CAS 操作,发现值仍然是 A,就会认为值没有变化,从而成功更新为 C,但实际上值已经经历了 A -> B -> A 的变化。
- 解决方案:可以使用带有版本号的原子引用
AtomicStampedReference或AtomicMarkableReference。AtomicStampedReference会在每次更新值时同时更新一个版本号,在进行 CAS 操作时,不仅会比较值是否相同,还会比较版本号是否相同,从而避免 ABA 问题。
5. volatile 关键字
- 内存可见性:
volatile关键字用于保证变量的内存可见性。当一个变量被声明为volatile时,它会保证对该变量的写操作会立即刷新到主内存中,而读操作会直接从主内存中读取,从而保证不同线程之间对该变量的可见性。 - 禁止指令重排序:
volatile关键字还可以禁止指令重排序,保证代码的执行顺序与编写顺序一致。在单例模式的双重检查锁定(Double-Checked Locking)中,使用volatile关键字可以避免由于指令重排序导致的线程安全问题。 - 示例代码:
public class VolatileTest {
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
while (!flag) {
// 等待 flag 变为 true
}
System.out.println("Flag is now true");
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag has been set to true");
}).start();
}
}
在上述代码中,flag 变量被声明为 volatile,保证了一个线程对 flag 的修改能立即被另一个线程看到。
///////////////////////////////////////////////////////////////////////////////////////////////////////////
2、并发修改异常解决办法?jdk8以后会使用红黑树优化
对于并发修改异常,常见的解决办法有以下几种:
加锁:在修改数据结构时加锁,从而避免多个线程同时修改数据结构。
同步:使用Java中的同步机制(如synchronized关键字)来保证数据结构的安全。
乐观锁:采用乐观锁的策略,如果修改失败则重试。
在jdk8中,ConcurrentHashMap在遇到并发修改异常时会使用红黑树来优化数据结构,从而提高效率。红黑树是一种平衡树,它保证了插入,删除和查询操作的复杂度为O(log n)。这样可以大大提高数据结构的性能,并避免并发修改异常的发生。
3、红黑树和二叉平衡树的区别,红黑树和B树,B+树的区别,Mysql二大引擎索引底层实现,HashMap在多线程环境中为何出错
红黑树和二叉平衡树的区别:
二叉平衡树是一种普通的平衡树,红黑树是二叉平衡树的一种变体。
红黑树通过设置红黑规则来保证平衡,而二叉平衡树则通过重新构造树来保证平衡。
红黑树的查询,插入和删除的时间复杂度为O(log n),而二叉平衡树的时间复杂度与具体的实现方法有关。
红黑树和B树/B+树的区别:
红黑树是一种内存数据结构,而B树/B+树是一种磁盘数据结构。
红黑树是一种键值对存储结构,而B树/B+树是一种索引存储结构。
红黑树的查询,插入和删除的时间复杂度为O(log n),而B树/B+树的时间复杂度为O(log m),其中m为B树/B+树的阶。
Mysql二大引擎索引底层实现:
MyISAM引擎:使用B+树存储索引信息。
InnoDB引擎:使用B树存储索引信息。
HashMap在多线程环境中为何出错:
HashMap在多线程环境中不保证线程安全,如果多个线程同时修改HashMap中的数据,会出现不一致的
##################################################################################
红黑树和二叉平衡树(AVL树)的区别
平衡程度
- 二叉平衡树(AVL树):是严格的平衡二叉树,它要求每个节点的左右子树的高度差不超过 1。这种严格的平衡条件使得树在任何时候都保持高度平衡,从而保证了查找、插入和删除操作的时间复杂度始终为 $O(log n)$。
- 红黑树:是一种弱平衡的二叉搜索树,它通过对节点进行红黑着色,并遵循特定的规则来保证树的大致平衡。红黑树并不像 AVL 树那样严格要求左右子树高度差不超过 1,而是保证从任意节点到其每个叶子节点的所有路径上的黑色节点数量相同,因此其平衡性相对较弱。
插入和删除操作的性能
- 二叉平衡树(AVL树):由于要严格保持平衡,在插入和删除节点时,可能需要频繁地进行旋转操作来调整树的结构,以满足高度差不超过 1 的条件。因此,插入和删除操作的开销相对较大。
- 红黑树:在插入和删除节点时,虽然也需要进行调整(变色和旋转),但由于其平衡要求相对宽松,调整的频率和复杂度通常比 AVL 树低,因此插入和删除操作的性能相对较好。
应用场景
- 二叉平衡树(AVL树):适用于对查找操作要求较高,而插入和删除操作相对较少的场景。例如,在数据库的索引系统中,如果数据的插入和删除操作不频繁,而查找操作非常频繁,使用 AVL 树可以保证高效的查找性能。
- 红黑树:更适合于插入、删除和查找操作都比较频繁的场景。例如,Java 中的
TreeMap和TreeSet就是基于红黑树实现的,它们需要在保证一定查找性能的同时,能够高效地处理插入和删除操作。
红黑树和 B 树、B+ 树的区别
数据结构
- 红黑树:是一种二叉搜索树,每个节点最多有两个子节点。它主要用于内存中的数据存储和查找,适用于处理较小规模的数据。
- B 树:是一种多路平衡搜索树,每个节点可以有多个子节点(通常大于 2)。B 树的节点可以存储多个键值对,并且所有叶子节点都在同一层上。B 树主要用于文件系统和数据库系统中,用于处理大规模的数据存储和查找。
- B+ 树:是 B 树的一种变体,它的所有数据都存储在叶子节点中,非叶子节点只存储索引信息。B+ 树的叶子节点之间通过指针相连,形成一个有序链表,便于进行范围查询。B+ 树同样广泛应用于数据库系统和文件系统中。
应用场景
- 红黑树:常用于内存中的数据结构,如 Java 的
TreeMap和TreeSet,以及操作系统的内存管理等。 - B 树和 B+ 树:主要用于磁盘存储和数据库系统中。由于磁盘的读写速度相对较慢,B 树和 B+ 树的多路特性可以减少磁盘 I/O 次数,提高数据的读写效率。例如,MySQL 的 InnoDB 和 MyISAM 存储引擎的索引就是基于 B+ 树实现的。
MySQL 二大引擎索引底层实现
InnoDB 引擎
- 索引结构:InnoDB 引擎使用 B+ 树作为索引结构。主键索引(聚簇索引)的叶子节点存储了完整的行数据,而辅助索引的叶子节点存储的是主键值。通过辅助索引查找数据时,需要先在辅助索引的 B+ 树中找到对应的主键值,然后再通过主键索引查找完整的行数据。
- 特点:InnoDB 引擎的聚簇索引将数据和索引存储在一起,因此在根据主键进行查找时,效率非常高。同时,InnoDB 支持事务和外键约束,适合处理高并发、事务性要求较高的应用场景。
MyISAM 引擎
- 索引结构:MyISAM 引擎同样使用 B+ 树作为索引结构,但它的索引和数据是分开存储的。主键索引和辅助索引的叶子节点都存储的是数据的物理地址。
- 特点:MyISAM 引擎不支持事务和外键约束,但其插入和查询性能相对较高,适合处理对事务要求不高、以读为主的应用场景。
HashMap 在多线程环境中为何出错
数据不一致问题
- 在多线程环境下,如果多个线程同时对
HashMap进行插入、删除或修改操作,可能会导致数据不一致的问题。例如,一个线程正在遍历HashMap,而另一个线程同时对HashMap进行了结构上的修改(如插入或删除元素),就可能会导致遍历结果不准确,甚至抛出ConcurrentModificationException异常。
死循环问题
- 在 JDK 7 及以前版本中,
HashMap的扩容机制在多线程环境下可能会导致死循环问题。当多个线程同时触发HashMap的扩容操作时,可能会导致链表形成环形结构,从而使后续的查找操作陷入死循环。
数据覆盖问题
- 由于
HashMap的插入操作不是原子操作,多个线程同时插入元素时,可能会出现数据覆盖的问题。例如,两个线程同时计算出相同的哈希桶位置,并且同时插入元素,就可能会导致其中一个元素被另一个元素覆盖。
为了在多线程环境下安全地使用哈希表,可以使用 ConcurrentHashMap 或 Collections.synchronizedMap 方法来包装 HashMap。
############################################################################
4、什么是IOC,AOP?
IOC(控制反转)和AOP(面向切面编程)是软件工程中的两种设计思想。
IOC是指在软件编程中,由框架或容器在运行时决定被调用组件的具体实现,而不是在代码编写时硬编码。这样做可以使代码更加灵活,易于维护和测试。
AOP是一种编程范式,用于在程序运行期间动态地将关注点(例如日志记录,安全检查等)分离出来,从而使代码更加清晰和可维护。
总的来说,IOC和AOP的目的是提高代码的可读性,可维护性和可测试性。
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
IOC(控制反转)
概念
IOC(Inversion of Control),即控制反转,是一种设计原则,它将对象的创建和依赖关系的管理从代码内部转移到外部容器中。在传统的编程方式中,对象的创建和依赖关系的维护是由程序本身负责的,而在 IOC 模式下,对象的创建、生命周期管理以及对象之间的依赖关系都由 IOC 容器来完成。控制反转的核心思想是“将对象的控制权交给外部容器,而不是由对象本身来创建和管理依赖对象”,这样可以降低代码的耦合度,提高代码的可维护性和可测试性。
示例
假设我们有一个 UserService 类,它依赖于 UserDao 类来进行用户数据的操作。
传统方式
// UserDao 接口
interface UserDao {
void saveUser();
}
// UserDao 实现类
class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存用户数据");
}
}
// UserService 类
class UserService {
private UserDao userDao;
public UserService() {
// 在 UserService 内部创建 UserDao 对象
this.userDao = new UserDaoImpl();
}
public void addUser() {
userDao.saveUser();
}
}
// 测试类
public class TraditionalTest {
public static void main(String[] args) {
UserService userService = new UserService();
userService.addUser();
}
}
在上述代码中,UserService 类负责创建 UserDao 对象,这使得 UserService 类与 UserDao 类紧密耦合。如果需要更换 UserDao 的实现类,就需要修改 UserService 类的代码。
IOC 方式
// UserDao 接口
interface UserDao {
void saveUser();
}
// UserDao 实现类
class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存用户数据");
}
}
// UserService 类
class UserService {
private UserDao userDao;
// 通过构造函数注入 UserDao 对象
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void addUser() {
userDao.saveUser();
}
}
// 测试类
public class IOCTest {
public static void main(String[] args) {
// 创建 UserDao 对象
UserDao userDao = new UserDaoImpl();
// 创建 UserService 对象,并将 UserDao 对象注入
UserService userService = new UserService(userDao);
userService.addUser();
}
}
在 IOC 方式中,UserService 类不再负责创建 UserDao 对象,而是通过构造函数接收外部传入的 UserDao 对象。这样,UserService 类与 UserDao 类的耦合度降低,当需要更换 UserDao 的实现类时,只需要在外部创建新的 UserDao 实现类对象并注入到 UserService 中即可,无需修改 UserService 类的代码。在实际的 Java 开发中,Spring 框架的 IOC 容器就可以帮助我们自动完成对象的创建和依赖注入。
AOP(面向切面编程)
概念
AOP(Aspect - Oriented Programming),即面向切面编程,是一种编程范式,它允许开发者在不修改原有业务逻辑的基础上,对程序进行增强。AOP 的核心思想是将横切关注点(如日志记录、事务管理、权限验证等)从业务逻辑中分离出来,形成独立的切面(Aspect),然后在合适的时机将这些切面织入到业务逻辑中。这样可以提高代码的复用性和可维护性,使业务逻辑更加清晰。
示例
假设我们有一个 UserService 类,其中包含一个 addUser 方法用于添加用户,我们希望在 addUser 方法执行前后添加日志记录。
不使用 AOP 的方式
// UserService 类
class UserService {
public void addUser() {
System.out.println("开始记录日志:准备添加用户");
System.out.println("执行添加用户操作");
System.out.println("结束记录日志:用户添加完成");
}
}
// 测试类
public class NonAOPTest {
public static void main(String[] args) {
UserService userService = new UserService();
userService.addUser();
}
}
在上述代码中,日志记录代码与业务逻辑代码混杂在一起,导致代码的可维护性和复用性较差。如果需要在其他方法中添加日志记录,就需要重复编写日志记录代码。
使用 AOP 的方式(以 Spring AOP 为例)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
// 定义切面类
@Aspect
@Component
class LoggingAspect {
// 定义切入点,匹配 UserService 类的 addUser 方法
@Pointcut("execution(* com.example.UserService.addUser(..))")
public void addUserPointcut() {}
// 在 addUser 方法执行前执行
@Before("addUserPointcut()")
public void beforeAddUser(JoinPoint joinPoint) {
System.out.println("开始记录日志:准备添加用户");
}
// 在 addUser 方法执行后执行
@After("addUserPointcut()")
public void afterAddUser(JoinPoint joinPoint) {
System.out.println("结束记录日志:用户添加完成");
}
}
// UserService 类
class UserService {
public void addUser() {
System.out.println("执行添加用户操作");
}
}
// 测试类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan(basePackages = "com.example")
@EnableAspectJAutoProxy
class AppConfig {
}
public class AOPTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
userService.addUser();
context.close();
}
}
在使用 AOP 的方式中,我们定义了一个 LoggingAspect 切面类,其中包含了日志记录的逻辑。通过 @Pointcut 注解定义了切入点,指定了要增强的方法(UserService 类的 addUser 方法)。通过 @Before 和 @After 注解分别定义了在方法执行前和执行后要执行的日志记录方法。这样,日志记录逻辑就从业务逻辑中分离出来,提高了代码的可维护性和复用性。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
5、数据库事务隔离级别,数据库的四大属性底层实现原理、Spring如何实现事务、传播行为
数据库事务隔离级别:
数据库事务隔离级别指的是在多个事务并发访问数据库时,保证数据一致性和完整性的策略。常见的隔离级别有:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
数据库的四大属性(ACID)底层实现原理:
ACID是数据库事务的四个基本属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
原子性:数据库事务是不可分割的,要么全部完成,要么全部回滚;
一致性:在事务开始前和事务结束后,数据库的完整性约束没有被破坏;
隔离性:多个事务并发执行时,不同事务之间的执行不能互相影响;
持久性:事务一旦提交,对数据库的更改是永久性的。
底层实现原理:ACID的实现通常是通过使用日志文件、数据缓存、检查点等技术来保证数据的一致性和完整性。
Spring如何实现事务:
Spring通过使用事务管理器(Transaction Manager)实现事务
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
数据库事务隔离级别
读未提交(Read Uncommitted)
- 举例:你在银行办理转账业务,事务A 正在将 1000 元从账户 A 转到账户 B,但还未提交。此时事务 B 读取账户 B 的余额,发现多了 1000 元。然而,事务 A 随后回滚了,账户 B 实际上并没有增加这 1000 元,事务 B 读到的就是脏数据。
- 理解:就像你偷看别人还没写完的信,内容可能会改变,读到的信息不一定准确。
读已提交(Read Committed)
- 举例:你在图书馆借书,管理员在更新书籍的借阅状态。事务 A 正在更新某本书的状态为已借出,但未提交。此时事务 B 来查询这本书的状态,查不到更新。等事务 A 提交后,事务 B 再次查询,就能看到正确的已借出状态。
- 理解:只有当别人确定把信写完并封好交给你,你才能看到准确内容。
可重复读(Repeatable Read)
- 举例:你在网上购物,在一个订单事务中,你先查询了某商品的库存为 10 件。这时另一个事务往库存里增加了 5 件并提交。但你在当前订单事务中再次查询该商品库存,还是显示 10 件。
- 理解:你在一个时间段内看同一本书,不管外面发生什么,书的内容在这个时间段内对你来说是不会变的。
串行化(Serializable)
- 举例:你和朋友去银行办理业务,你们要操作同一个账户。在串行化隔离级别下,必须等你办完业务提交后,你朋友才能开始操作这个账户。
- 理解:就像排队过独木桥,一个人过去另一个人才能上桥,不会出现同时过桥的混乱情况。
数据库的四大属性(ACID)底层实现原理
原子性(Atomicity)
- 举例:你在自动售货机买饮料,投入硬币并选择饮料后,要么成功拿到饮料,硬币扣除;要么拿不到饮料,硬币退回。不会出现拿到饮料但硬币没扣,或者硬币扣了但没拿到饮料的情况。
- 实现原理:数据库通过日志记录事务的所有操作。如果事务执行过程中出错,就根据日志回滚到事务开始前的状态。就像自动售货机记录每一步操作,如果出问题就恢复到最初状态。
一致性(Consistency)
- 举例:你在银行转账,从账户 A 转 500 元到账户 B。转账成功后,账户 A 减少 500 元,账户 B 增加 500 元,两个账户的总金额不变。
- 实现原理:数据库通过约束条件(如唯一约束、外键约束)和触发器来保证数据符合业务规则。当事务执行时,数据库会检查这些规则是否满足,不满足就回滚事务。
隔离性(Isolation)
- 举例:你和朋友同时在不同窗口办理银行存款业务,你们的操作相互不影响,不会出现数据混乱。
- 实现原理:通过锁机制(共享锁、排他锁)和多版本并发控制(MVCC)。锁机制限制其他事务对数据的访问,MVCC 让不同事务可以访问数据的不同版本。
持久性(Durability)
- 举例:你在网上提交了一份重要文件,提交成功后,即使服务器突然断电,文件也不会丢失。
- 实现原理:数据库在事务提交时,将事务的修改结果先写入日志文件,再写入磁盘的数据文件。即使系统崩溃,也可以根据日志文件恢复数据。
Spring 如何实现事务
编程式事务管理
- 举例:你要组装一台电脑,每个步骤都要自己手动操作,什么时候开机箱、装硬件、连接线路等都要自己控制。在编程式事务管理中,你要手动编写代码来开启、提交和回滚事务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class ComputerAssemblyService {
@Autowired
private PlatformTransactionManager transactionManager;
public void assembleComputer() {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 执行组装电脑的操作,如安装硬件等
// ...
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
}
声明式事务管理
- 举例:你找装修公司装修房子,只需要告诉他们你的要求,他们会按照流程帮你完成装修,你不需要亲自参与每个步骤。在声明式事务管理中,你只需要使用
@Transactional注解声明方法需要事务管理,Spring 会自动处理事务的开启、提交和回滚。
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class HouseDecorationService {
@Transactional
public void decorateHouse() {
// 执行装修房子的操作,如刷墙、铺地板等
// ...
}
}
Spring 事务传播行为
PROPAGATION_REQUIRED(默认)
- 举例:你是一个项目负责人,你带领团队完成一个大项目(事务 A)。项目中有一个子任务(事务 B),如果这个子任务没有单独的事务要求,它就会加入到你带领的大项目事务中。
- 理解:子任务依赖于大项目的事务环境,一起成功或失败。
PROPAGATION_REQUIRES_NEW
- 举例:你在做一个主项目(事务 A),其中有一个紧急的子任务(事务 B)需要立即处理,并且不能受主项目事务的影响。那么这个子任务会创建一个新的事务,即使主项目事务失败回滚,子任务的事务也不受影响。
- 理解:子任务独立于主项目,有自己的事务环境。
PROPAGATION_NESTED
- 举例:你在写一篇长篇文章(事务 A),文章中有一个重要的段落(事务 B)。如果这个段落出了问题,你可以只回滚这个段落的修改,而不影响文章其他部分。但如果文章整体出问题回滚,段落也会跟着回滚。
- 理解:子事务是主事务的一部分,可以独立回滚,但主事务回滚时子事务也会回滚。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
6、CAP,BASE理论,分布式事务的四种解决方案
CAP定理指出,分布式系统不可能同时提供以下三种保证:一致性、可用性和分区容差。
BASE(基本可用、软状态、最终一致)是一组用于构建可扩展、非关系数据库的设计原则。
有四种常见的解决方案可以解决分布式事务的挑战:
两阶段提交(2PC)-一种用于确保分布式系统中事务原子性的协议。
三阶段提交(3PC)-一种为确保分布式系统中事务的原子性提供额外阶段的协议。
乐观并发控制-一种允许多个事务乐观地进行的技术,假设冲突很少。
悲观并发控制-一种假设可能发生冲突并锁定资源以防止并发访问的技术。
7、线程池实现原理,七大核心参数,如何合理的配置核心线程数?
线程池实现原理:
线程池是执行任务的工作线程的集合。当任务提交到池时,将分配一个工作线程来执行该任务。任务完成后,工作线程将返回到池以供重用。这减少了为每个任务创建和销毁线程的开销。
七大核心参数:
corePoolSize:池中要保留的线程数,即使它们空闲。
maximumPoolSize:池中允许的最大线程数。
keepAliveTime:多余的空闲线程在终止前等待新任务的最长时间。
unit:keepAliveTime参数的时间单位。
workQueue:用于在执行任务之前保存任务的队列。
threadFactory:执行器创建新线程时使用的工厂。
handler:由于达到线程边界和队列容量而导致执行受阻时要使用的处理程序。
如何合理的配置核心线程数:
应根据预期的并发任务数设置核心线程数。如果任务是CPU绑定的,那么内核线程的数量应该等于可用CPU内核的数量。如果任务是IO绑定的,那么在等待IO操作完成时,可能需要更多的线程来保持CPU忙碌。此外,工作队列的大小和最大池大小应根据预期任务到达率和完成任务的预期时间进行设置。
8、线程池的拒绝策略:
线程池的拒绝策略是当线程池中的任务队列已满,并且线程数已经达到了最大数量时,线程池如何处理新任务的策略。常见的拒绝策略有:
AbortPolicy: 直接抛出 RejectedExecutionException 异常。
DiscardPolicy: 丢弃队列中最旧的任务,并执行新任务。
DiscardOldestPolicy: 丢弃当前任务,执行队列中的下一个任务。
CallerRunsPolicy: 直接在调用者线程中运行当前任务。
应用可以选择适合自己业务场景的拒绝策略,或者实现自己的拒绝策略。
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
线程池的拒绝策略是指当线程池已经达到最大承载能力(线程数达到最大线程数且任务队列已满)时,对新提交的任务所采取的处理方式。Java中的ThreadPoolExecutor提供了四种内置的拒绝策略,以下为你详细介绍并举例说明:
AbortPolicy(默认策略)
- 策略描述:当线程池无法继续处理新任务时,直接抛出
RejectedExecutionException异常,阻止系统正常运行。 - 生活举例:想象一家餐厅,餐厅里的座位(线程)已经坐满了客人,而且等待区(任务队列)也站满了排队的人。这时又有新的客人来就餐,服务员直接告诉客人餐厅已满,无法接待,并把客人拒之门外,客人可能会很生气(抛出异常)。
- 代码示例:
import java.util.concurrent.*;
public class AbortPolicyExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
2, // 最大线程数
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), // 任务队列容量为 1
new ThreadPoolExecutor.AbortPolicy() // 使用 AbortPolicy 拒绝策略
);
try {
for (int i = 0; i < 4; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (RejectedExecutionException e) {
System.out.println("Task rejected: " + e.getMessage());
} finally {
executor.shutdown();
}
}
}
CallerRunsPolicy
- 策略描述:当线程池无法处理新任务时,将该任务返回给提交任务的线程来执行,从而降低新任务的提交速度。
- 生活举例:还是那家餐厅,当餐厅座位和等待区都满了,新来了客人。服务员告诉客人餐厅满了,让客人自己先把点的菜做了(提交任务的线程自己执行任务),等餐厅有空位了再过来吃。
- 代码示例:
import java.util.concurrent.*;
public class CallerRunsPolicyExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy() // 使用 CallerRunsPolicy 拒绝策略
);
for (int i = 0; i < 4; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
DiscardPolicy
- 策略描述:当线程池无法处理新任务时,直接丢弃该任务,且不抛出任何异常。
- 生活举例:餐厅满员后,新客人来了,服务员直接不做任何说明,把客人打发走,客人的就餐请求就被忽略了(任务被丢弃)。
- 代码示例:
import java.util.concurrent.*;
public class DiscardPolicyExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy() // 使用 DiscardPolicy 拒绝策略
);
for (int i = 0; i < 4; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
DiscardOldestPolicy
- 策略描述:当线程池无法处理新任务时,丢弃任务队列中最老的任务,然后尝试将新任务加入队列。
- 生活举例:餐厅满员后,新客人来了,服务员把等待区里等待时间最长的客人请走,腾出位置给新客人。
- 代码示例:
import java.util.concurrent.*;
public class DiscardOldestPolicyExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardOldestPolicy() // 使用 DiscardOldestPolicy 拒绝策略
);
for (int i = 0; i < 4; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
9、JUC并发包:信号灯,循环栅栏,倒计时器
UC (Java Util Concurrent) 并发包是 Java 的一个并发工具包,提供了很多并发编程的工具类。
信号灯:Semaphore 是一种基于计数的信号量,用于控制同时访问特定资源的线程数量。
循环栅栏:CyclicBarrier 是一种同步工具,用于等待一组线程中的所有线程到达公共屏障点。
倒计时器:CountDownLatch 是一种同步工具,允许一个或多个线程等待其他线程完成操作。
这三种工具类都是 JUC 并发包中常用的工具,用于解决多线程间的同步问题。
10、Redis缓存数据类型的应用场景以及底层数据结构,数据同步问题(双删策略),缓存雪崩,缓存穿透,缓存失效,高并发场景下的分布式锁,热点缓存,哨兵机制,持久化(主从)
Redis 是一个开源的内存数据存储系统,支持多种数据结构,具有高性能和高可用性。
Redis 支持多种数据类型,如字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set)等,适用于不同的应用场景。
Redis 的数据结构是基于内存存储的,并且支持持久化,以防止内存数据丢失。
在 Redis 分布式环境中,数据同步问题是一个重要的考虑因素。Redis 采用了双删策略来保证数据的同步:当有节点向其他节点发送“删除”指令时,其他节点会进行两次删除,以确保数据已经同步删除。
缓存雪崩是指在高并发情况下,由于缓存同时失效,导致系统压力过大,服务不可用的情况。缓存穿透则是指查询的数据不存在,但请求却不断发送,给数据库造成了过多的压力。
缓存失效是指缓存数据过期,需要从数据源重新获取。高并发场景下的分布式锁可以解决多个线程同时访问
11、JVM算法,垃圾收集器,垃圾回收机制,JMM和JVM内存模型,JVM调优,双亲委派机制,堆溢出,栈溢出,方法区溢出,你都有哪些手段用来排查内存溢出?
VM(Java虚拟机)负责运行Java应用程序。与JVM相关的一些关键概念包括:
垃圾收集(GC)算法:JVM实现不同的GC算法,如串行、并行、CMS、G1等,以自动回收不再使用的对象占用的内存。
JVM内存模型:JVM内存模型定义JVM中的内存管理规则,包括堆内存和堆栈内存。JMM(Java内存模型)指定线程之间的内存可见性和排序规则。
JVM优化:为了提高JVM的性能,我们可以通过调整堆大小、GC算法等各种JVM参数来执行JVM调优。
类加载:JVM使用称为“父委派模型”的类加载机制将类加载到JVM中。
内存不足错误:内存不足错误可能发生在JVM的不同部分,包括堆、堆栈和方法区域。
为了解决内存泄漏问题,我们可以使用JVisualVM、JConsole或堆转储分析工具来确定问题的原因。当发生内存不足错误时,我们还可以使用-XX:+HeapDumpOnOutOfMemoryError等JVM标志来生成堆转储。
JVM性能故障排除:为了提高JVM的性能,我们可以使用各种评测工具,如YourKit、JProfiler或Java Flight Recorder来识别性能瓶颈并解决它们。
12、MySQL优化,索引限制条件
MySQL优化涉及通过更改各种配置参数、数据库设计、查询执行和索引来提高数据库系统的性能和效率。优化MySQL的一些常见技术是:
索引:索引是数据库优化的关键组成部分,因为它可以更快地检索数据。MySQL中的索引被组织为B树,允许基于索引关键字值快速搜索和检索数据。
查询优化:编写高效和优化的SQL查询可以提高数据库的性能。这包括选择适当的索引、使用正确的联接类型、避免子查询以及明智地使用聚合函数。
模式设计:一个设计良好的数据库模式可以通过减少查询执行过程中需要扫描和处理的数据量来大大提高数据库的性能。
配置优化:通过调整缓冲池大小、查询缓存、排序缓冲区大小等各种配置参数,可以提高MySQL数据库的性能。
索引限制条件:索引仅用于在查询条件与索引限制条件匹配时执行查询。这些条件包括索引中的列数、列的数据类型以及列的顺序。要充分利用索引,必须了解这些限制并相应地设计索引。
////////////////////////////////////////////////////////////////////////////////////////////////
配置参数优化
调整 innodb_buffer_pool_size
- 原理:
innodb_buffer_pool_size是 InnoDB 存储引擎用于缓存数据和索引的内存区域大小。增大这个值可以减少磁盘 I/O,提高查询性能。 - 举例:一家小超市原本仓库(
innodb_buffer_pool_size)很小,每次顾客要货(查询数据)都得频繁去外面进货(从磁盘读取),效率很低。后来扩大了仓库,能多存一些货物,顾客要货时很多都能直接从仓库拿,速度就快了。 - 操作:在
my.cnf或my.ini配置文件中修改该参数,如innodb_buffer_pool_size = 2G。
调整 max_connections
- 原理:
max_connections决定了 MySQL 允许的最大连接数。如果连接数设置过小,可能会导致应用程序无法建立新连接;设置过大则会占用过多系统资源。 - 举例:一家餐厅有固定数量的餐桌(
max_connections),太少的话顾客来了没位置就走了(连接失败),太多的话服务员忙不过来(系统资源不足)。 - 操作:在配置文件中修改,如
max_connections = 200。
数据库设计优化
合理设计表结构
- 原理:避免数据冗余,采用合适的数据类型可以减少存储空间,提高查询效率。
- 举例:一个学生信息表,原本把学生的班级信息重复存了很多遍(数据冗余),后来把班级信息单独建一个表,通过外键关联,节省了空间。另外,原本用
VARCHAR(255)存学生年龄,现在改成TINYINT,更节省空间。 - 操作:设计表时遵循数据库范式,选择合适的数据类型。
垂直拆分和水平拆分
- 原理:垂直拆分是将一个表按列拆分成多个表,减少单个表的列数;水平拆分是将一个表按行拆分成多个表,减少单个表的行数。
- 举例:垂直拆分就像一本大百科全书,按主题拆分成多本小百科全书;水平拆分就像一本电话簿,按姓氏首字母拆分成多本电话簿。
- 操作:可以通过创建新表,将部分列或行迁移到新表中实现拆分。
查询执行优化
优化查询语句
- 原理:避免使用
SELECT *,尽量指定需要的列;避免在WHERE子句中使用函数,因为会导致索引失效。 - 举例:原本查询语句
SELECT * FROM users WHERE YEAR(register_date) = 2024;会使register_date索引失效,优化后SELECT id, name FROM users WHERE register_date BETWEEN '2024-01-01' AND '2024-12-31';可以使用索引。 - 操作:仔细分析查询需求,优化查询语句的写法。
使用 EXPLAIN 分析查询
- 原理:
EXPLAIN可以查看查询的执行计划,包括是否使用了索引、扫描的行数等信息,帮助我们找出查询的瓶颈。 - 举例:就像我们要去一个地方,
EXPLAIN能告诉我们走哪条路(执行路径),路上要经过多少个路口(扫描行数)。 - 操作:在查询语句前加上
EXPLAIN,如EXPLAIN SELECT * FROM users WHERE id = 1;
索引优化
创建合适的索引
- 原理:索引可以加快数据的查找速度,就像字典的目录一样。
- 举例:一个员工信息表,经常根据员工姓名查询信息,为
name列创建索引后,查询速度会大大提高。 - 操作:使用
CREATE INDEX语句创建索引,如CREATE INDEX idx_name ON employees (name);
避免过多索引
- 原理:过多的索引会占用额外的存储空间,并且在插入、更新和删除数据时会增加维护索引的开销。
- 举例:一本字典有太多的目录,不仅会让字典变厚(占用空间),而且每次更新字典内容时,修改目录也很麻烦(维护开销大)。
- 操作:只对经常用于查询条件和排序的列创建索引。
//////////////////////////////////////////////////////////////////////////////////////////////
13、公平锁,非公平锁,可重入锁,递归锁,自旋锁,读写锁,悲观锁,乐观锁,行锁,排它锁,共享锁,表锁,死锁,分布式锁,AQS,Synchronized
计算机科学中的锁是同步访问共享资源的机制。并发编程中的一些常见锁类型包括:
公平锁:公平锁是一种确保等待线程按照请求的顺序被授予对共享资源的访问权的锁。
非公平锁:非公平锁不能保证等待线程访问共享资源的顺序。
重入锁:重入锁允许已获取锁的线程重新进入并再次获取该锁,而不会导致死锁。
递归锁:递归锁是一种特殊类型的可重入锁,允许线程重复锁定和解锁同一个锁。
自旋锁:自旋锁是一种使用忙等待来实现同步的锁。
读写锁:读写锁允许多个线程同时读取共享资源,同时确保一次只能有一个线程写入资源。
悲观锁:悲观锁是一种锁,它假定共享资源可能被修改,因此在对共享资源执行任何操作之前获取锁。
乐观锁:乐观锁是一种锁,它假定共享资源不太可能被修改,因此只在必要时获取锁。
行锁:行锁是一种锁,允许多个事务同时访问数据库表的不同行,同时确保访问同一行的事务是互斥的。
独占锁:独占锁是一种锁,它在锁被持有时阻止任何其他事务访问共享资源。
共享锁:共享锁是一种类型的锁,允许多个事务同时访问共享资源,但阻止任何事务在锁定时对资源进行修改。
表锁:表锁是一种锁,它锁定整个数据库表,防止任何事务在锁定时访问表。
死锁:死锁是指两个或多个线程在等待对方释放锁,从而导致永久阻塞的情况。
分布式锁:分布式锁是一种在分布式系统中使用的锁,用于确保一次只有一个节点可以访问共享资源。
AQS(AbstractQueuedSynchronizer):AQS是一个在Java中实现同步构造的框架。
Synchronized:Java中的Synchronized关键字用于确保一次只有一个线程可以访问共享资源。
14、幂等性实现,单点登录,金额篡改问题,秒杀场景设计,库存超卖问题
幂等性实现:幂等性指一个操作可以执行多次,但其结果不会随着执行次数的增多而改变。可以通过使用唯一请求 ID 来实现幂等性,避免同一个请求重复执行。
单点登录:单点登录是指在多个应用程序或系统中,用户只需要登录一次,就可以在多个系统中使用相同的身份认证信息。可以通过使用令牌或 Session 实现单点登录。
金额篡改问题:金额篡改问题指在一个多人合作的系统中,有人篡改金额。可以通过使用分布式事务或数字签名等技术解决。
秒杀场景设计:秒杀场景需要考虑高并发和数据一致性问题。可以使用队列、分布式锁、限流、缓存等技术实现。
库存超卖问题:库存超卖问题指在电商系统中,由于高并发请求和抢购导致库存不足,从而导致超额销售。可以通过使用队列、分布式锁、限流等技术解决。
15、什么是OOM以及产生OOM的原因
JVM 内存不足 (OOM) 是 Java 虚拟机 (JVM) 在无法分配内存来执行操作时引发的错误。这可能是由于多种原因造成的,包括:
堆大小不足:如果 JVM 堆大小太小,无法满足应用程序的内存需求,则可能会出现 OOM。
内存泄漏:如果应用程序未正确释放对象和内存,JVM 堆可能会填满并导致 OOM。
GC 性能不佳:如果垃圾回收器无法及时回收内存,则可能会出现 OOM。
要诊断 JVM OOM,您应该首先收集数据,包括 JVM 堆大小、GC 日志和内存转储。此数据可以帮助确定 OOM 错误的根本原因,并建议可能的解决方案,例如增加堆大小、修复内存泄漏或优化 GC 性能。
/////////////////////////////////////////////////////////////////////////////////////////////////////
什么是 OOM
OOM 即 OutOfMemoryError,是一种在程序运行过程中出现的错误,当程序在运行时向系统请求的内存超过了系统所能提供的最大内存限制时,就会抛出 OOM 错误,导致程序无法继续正常运行。简单来说,就是程序想要使用的内存太多了,系统给不起,就报错了。
产生 OOM 的原因及举例
1. 内存泄漏导致 OOM
- 原因:内存泄漏指程序中一些对象不再被使用,但由于某些原因(如对象的引用未被正确释放),这些对象一直占用着内存,无法被垃圾回收机制回收。随着程序的运行,这些无用对象占用的内存越来越多,最终导致系统内存不足,引发 OOM。
- 举例:假设你开了一家图书馆,每本书代表一个对象。正常情况下,读者看完书后会把书还回图书馆,图书馆可以把书再次借给其他读者(相当于垃圾回收)。但如果有些读者看完书后不还,把书一直放在自己家里(对象的引用未释放),随着时间推移,图书馆里可供借阅的书越来越少(可用内存减少),最后图书馆就没有书可以借给新的读者了(OOM)。
- 代码示例(Java):
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakExample {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
while (true) {
list.add(new Object());
}
}
}
在这个例子中,list 不断地添加新的 Object 对象,而且这些对象没有被移除,也没有其他地方释放对它们的引用,导致内存不断被占用,最终会引发 OOM。
2. 大对象创建导致 OOM
- 原因:程序中一次性创建了非常大的对象,如大数组、大集合等,这些对象占用的内存超过了系统的可用内存,从而引发 OOM。
- 举例:你有一个小房间(系统内存),现在要搬进去一张超级大的床(大对象),这个床太大了,房间根本放不下,就会导致空间不足(OOM)。
- 代码示例(Java):
public class BigObjectExample {
public static void main(String[] args) {
// 创建一个非常大的数组
int[] bigArray = new int[Integer.MAX_VALUE];
}
}
在这个例子中,尝试创建一个长度为 Integer.MAX_VALUE 的整数数组,这需要大量的内存,很可能会导致 OOM。
3. 无限递归调用导致 OOM
- 原因:递归调用是指一个方法在执行过程中调用自身。如果递归没有正确的终止条件,或者终止条件无法满足,方法会不断地调用自身,每次调用都会在栈内存中分配新的栈帧,随着递归的深入,栈内存会不断被占用,最终导致栈溢出,引发 OOM(StackOverflowError 是 OOM 的一种)。
- 举例:想象你站在两面相对的镜子中间,你会看到镜子里有无数个自己(递归调用),但实际上你的身体只有一个,不可能无限复制。如果不断地生成新的“你”,最终会把这个空间撑爆(栈溢出)。
- 代码示例(Java):
public class InfiniteRecursionExample {
public static void recursiveMethod() {
recursiveMethod(); // 无限递归调用
}
public static void main(String[] args) {
recursiveMethod();
}
}
在这个例子中,recursiveMethod 方法不断地调用自身,没有终止条件,最终会导致栈溢出,抛出 StackOverflowError。
4. 堆内存设置过小导致 OOM
- 原因:Java 程序在启动时可以通过参数设置堆内存的大小。如果堆内存设置得太小,而程序运行时需要的内存超过了这个设置值,就会引发 OOM。
- 举例:你给一辆小车(程序)装了一个很小的油箱(堆内存),小车在行驶过程中需要更多的油(内存),但油箱太小装不下,就会导致小车无法继续行驶(OOM)。
- 操作示例(Java):如果你使用
java -Xmx10m BigObjectExample命令启动上面的BigObjectExample程序,-Xmx10m表示将堆内存的最大值设置为 10MB,程序运行时很可能因为内存不足而抛出 OOM 错误。
////////////////////////////////////////////////////////////////////////////////////////////////////
16、怎么诊断OOM和解决
当JVM发生OOM (Out of Memory) 时,排查和解决方案包括以下步骤:
收集JVM内存使用情况,包括内存使用量、GC日志、线程栈信息等。
使用内存分析工具,如jmap、jhat、VisualVM等,来查看内存使用情况和对象占用情况。
检查代码是否存在内存泄漏,例如,是否存在无用的强引用对象或者循环引用。
调整JVM内存配置,例如,增大堆内存大小或减小元数据空间 (Metaspace) 大小。
优化代码,减少对象的生成速度或减少对象的生存时间。
使用内存池(如DirectByteBuffer)来限制内存使用量。
对于一些特殊场景,使用CMS或G1 GC来提高内存回收效率。
通过以上步骤,可以解决JVM OOM的问题,也可以预防未来的内存问题。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
诊断 OOM(OutOfMemoryError)
1. 查看日志信息
- 原理:当程序抛出 OOM 错误时,系统或应用程序通常会记录详细的错误日志,其中包含错误的类型、发生的位置以及可能的原因等信息。通过分析这些日志,可以初步确定 OOM 出现的大致场景和可能的原因。
- 操作:在 Java 程序中,OOM 错误通常会打印在控制台或者日志文件中。例如,在 Tomcat 服务器中,日志文件一般位于
logs目录下的catalina.out文件中。如果看到类似java.lang.OutOfMemoryError: Java heap space的错误信息,就表明是堆内存不足导致的 OOM。
2. 使用内存分析工具
- 原理:内存分析工具可以帮助我们深入了解程序运行时的内存使用情况,包括对象的创建、引用关系、内存占用等信息。通过分析这些信息,可以找出内存泄漏的对象或者占用大量内存的大对象。
- 常见工具及操作
- VisualVM:它是 JDK 自带的可视化监控工具。启动 VisualVM 后,连接到正在运行的 Java 程序,在“监视”选项卡中可以查看堆内存、非堆内存的使用情况;在“线程”选项卡中可以查看线程的状态;在“堆 Dump”选项卡中可以生成堆转储文件,用于后续的详细分析。
- Eclipse Memory Analyzer(MAT):这是一个专门用于分析堆转储文件的工具。将 VisualVM 生成的堆转储文件导入到 MAT 中,MAT 会生成详细的分析报告,包括内存泄漏嫌疑对象、对象的分布情况等信息。
3. 监控系统资源
- 原理:通过监控系统的 CPU、内存、磁盘 I/O 等资源的使用情况,可以了解程序运行时的资源消耗情况,判断是否是由于系统资源不足导致的 OOM。
- 操作:在 Linux 系统中,可以使用
top、htop等命令查看系统的 CPU 和内存使用情况;使用iostat命令查看磁盘 I/O 情况。在 Windows 系统中,可以使用任务管理器查看系统资源的使用情况。
解决 OOM
1. 增加堆内存
- 原理:如果是因为堆内存设置过小导致的 OOM,可以通过增加堆内存的大小来解决。
- 操作:在 Java 程序启动时,可以通过
-Xms和-Xmx参数来设置堆内存的初始大小和最大大小。例如,java -Xms512m -Xmx1024m YourMainClass表示将堆内存的初始大小设置为 512MB,最大大小设置为 1024MB。
2. 修复内存泄漏
- 原理:找出内存泄漏的原因,释放不再使用的对象的引用,让垃圾回收机制能够回收这些对象占用的内存。
- 操作
- 检查对象的生命周期:确保对象在不再使用时及时被销毁。例如,在使用完数据库连接、文件流等资源后,要及时关闭它们。
- 检查静态集合:静态集合中的对象会一直存在于内存中,直到程序结束。如果静态集合中存储了大量不再使用的对象,会导致内存泄漏。定期清理静态集合中的无用对象。
- 使用弱引用和软引用:对于一些缓存对象,可以使用弱引用(
WeakReference)或软引用(SoftReference)来引用它们,这样当内存不足时,垃圾回收机制可以自动回收这些对象。
3. 优化大对象的使用
- 原理:避免一次性创建过大的对象,或者对大对象进行拆分和优化。
- 操作
- 分块处理:如果需要处理大量数据,可以将数据分块处理,每次只处理一部分数据,减少内存的占用。例如,在读取大文件时,可以使用缓冲区,每次读取一部分数据进行处理。
- 优化数据结构:选择合适的数据结构来存储数据,避免使用过于庞大的数据结构。例如,如果只需要存储少量的数据,可以使用
ArrayList而不是LinkedList;如果需要存储大量的键值对,可以使用ConcurrentHashMap而不是HashMap。
4. 优化递归调用
- 原理:确保递归调用有正确的终止条件,避免无限递归导致栈溢出。
- 操作
- 添加终止条件:在递归方法中添加合适的终止条件,当满足终止条件时,停止递归调用。例如,计算阶乘的递归方法:
public class Factorial {
public static int factorial(int n) {
if (n == 0 || n == 1) {
return 1; // 终止条件
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5));
}
}
- 使用迭代代替递归:对于一些可以使用迭代实现的功能,尽量使用迭代代替递归,避免栈溢出的风险。例如,上面的阶乘计算也可以使用迭代实现:
public class FactorialIterative {
public static int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
public static void main(String[] args) {
System.out.println(factorial(5));
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
17、介绍一下Spring Cloud Netflix
Spring Cloud Netflix是Spring Cloud保护伞内的一个项目,它提供与Netflix的OSS组件的集成,用于构建微服务架构。Spring Cloud Netflix提供的一些核心组件包括:
Eureka:一种服务发现机制,允许微服务注册自己并发现其他服务。
Ribbon:为服务间通信提供客户端负载平衡的负载平衡库。
Hystrix:为微服务提供容错和弹性的断路器库。
Zuul:为微服务提供动态路由、安全和监控功能的 API 网关。
通过使用Spring Cloud Netflix,开发人员可以利用Netflix OSS组件来构建强大,可扩展且有弹性的微服务。Spring Cloud Netflix还提供了与Spring框架的抽象和集成,允许开发人员使用熟悉的编程模型和工具构建微服务。
///////////////////////////////////////////////////////////////////////////////////////////////////////////
Spring Cloud Netflix 概述
Spring Cloud Netflix 是 Spring Cloud 生态系统中基于 Netflix 开源组件构建的一套微服务解决方案。Netflix 在构建大规模分布式系统时开发了一系列优秀的组件,Spring Cloud 对这些组件进行了集成和封装,让开发者可以更方便地使用这些组件来构建微服务架构。它包含了多个核心组件,每个组件都有特定的功能。
核心组件及举例说明
1. Eureka(服务注册与发现)
- 功能:类似于一个服务的“通讯录”。在微服务架构中,有很多服务,每个服务需要知道其他服务在哪里(IP 地址和端口)才能相互调用。Eureka 可以让服务将自己的信息注册到它这里,同时也能让其他服务从它这里获取所需服务的信息。
- 举例:想象一个大型商场,里面有很多店铺(服务)。商场有一个总服务台(Eureka),每家店铺开业时会到总服务台登记自己的位置(注册服务),当顾客(其他服务)想要去某家店铺时,就可以到总服务台查询该店铺的位置(发现服务)。
- 代码示例:
- 服务提供者:
@SpringBootApplication
@EnableEurekaClient
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
- 服务消费者:
@SpringBootApplication
@EnableEurekaClient
@RestController
public class OrderServiceApplication {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getProduct")
public String getProduct() {
return restTemplate.getForObject("http://product-service/getProductInfo", String.class);
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2. Ribbon(客户端负载均衡)
- 功能:当有多个相同的服务实例时,Ribbon 可以帮助客户端在调用这些服务时进行负载均衡,将请求均匀地分配到各个服务实例上,提高系统的性能和可用性。
- 举例:还是以商场为例,有好几家卖同一种商品的店铺(服务实例)。顾客(客户端)每次去买东西时,Ribbon 就像一个导购员,会根据一定的规则(如轮询、随机等)引导顾客去不同的店铺,避免所有顾客都集中在一家店铺。
- 代码示例:
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
3. Hystrix(熔断器)
- 功能:在微服务架构中,一个服务可能会依赖多个其他服务。当某个依赖的服务出现故障或响应过慢时,Hystrix 可以像电路中的熔断器一样,及时切断对该服务的调用,避免故障扩散,同时可以提供一个默认的返回值(熔断降级),保证系统的稳定性。
- 举例:假如商场里的一家餐厅(服务)因为厨师罢工(故障)无法正常提供食物,Hystrix 就会告诉顾客(调用方)这家餐厅暂时关闭,并推荐附近的其他餐厅(熔断降级),避免顾客一直等待。
- 代码示例:
@Service
public class ProductService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "getProductFallback")
public String getProduct() {
return restTemplate.getForObject("http://product-service/getProductInfo", String.class);
}
public String getProductFallback() {
return "Product service is unavailable.";
}
}
4. Zuul(API 网关)
- 功能:API 网关是微服务架构的统一入口,所有外部请求都先经过 Zuul。它可以进行请求路由、请求过滤等操作,将请求转发到相应的服务。
- 举例:商场的大门(Zuul)是顾客进入商场的唯一通道,顾客进入大门后,大门处的工作人员会根据顾客要去的店铺(服务),引导顾客前往相应的区域(路由),同时还会检查顾客是否携带违禁物品(过滤)。
- 代码示例:
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
总结
Spring Cloud Netflix 通过这些组件,为微服务架构提供了服务注册与发现、负载均衡、容错保护和统一入口等功能,帮助开发者更轻松地构建和管理分布式系统。不同的组件相互协作,共同保障了微服务系统的高效运行和稳定性。
/////////////////////////////////////////////////////////////////////////////////////////////////////////
18、springcloud五大组件:
1、注册中心组件(服务治理):Netflix Eureka;
2、负载均衡组件:Netflix Ribbon,各个微服务进行分摊,提高性能;
3、熔断器组件(断路器):Netflix Hystrix,Resilience4j ;保护系统,控制故障范围;
4、网关服务组件:Zuul,Spring Cloud Gateway;api网关,路由,负载均衡等多种作用;
5、配置中心:Spring Cloud Config,将配置文件组合起来,放在远程仓库,便于管理;
19、介绍一下nginx
Nginx(发音为“engine x”)是一个Web服务器,也可以用作反向代理,负载均衡器和HTTP缓存。Nginx的一些主要功能包括:
高性能:Nginx以其高性能和有效处理大量并发连接的能力而闻名。
反向代理:Nginx可以充当反向代理,将请求从客户端转发到后端服务器。
负载均衡:Nginx可以在多个服务器之间分配传入流量,提供高可用性和更好的性能。
HTTP缓存:Nginx可以缓存频繁请求的内容,减少后端服务器的负载并缩短响应时间。
模块化架构:Nginx具有模块化架构,允许用户根据需要添加或删除功能,并根据自己的特定需求自定义配置。
Nginx被网站和Web应用程序广泛用于处理流量和提高性能。它是一个免费的开源软件,可以在各种平台上运行,包括Linux,macOS和Windows。
20、mybatis总结
MyBatis 是一个 Java 持久性框架,它提供了一种灵活高效的方式来与关系数据库进行交互。MyBatis 的一些主要功能包括:
SQL 映射:MyBatis 允许您将 SQL 语句映射到 Java 对象,无需编写复杂的 SQL 代码并提高可维护性。
动态 SQL:MyBatis 支持生成动态 SQL,使构建复杂的动态查询变得容易。
对象关系映射(ORM):MyBatis提供了一个轻量级的ORM解决方案,允许您将关系数据映射到Java对象,反之亦然。
灵活配置:MyBatis 提供灵活的配置系统,支持您以多种方式配置数据源、事务和映射。
插件架构:MyBatis 有一个插件架构,允许您扩展其功能并向应用程序添加自定义行为。
MyBatis 广泛用于基于 Java 的应用程序,并提供了一种简单、快速和高效的方式来与关系数据库进行交互。它是一个免费的开源软件,可以很容易地与其他基于Java的框架集成,如Spring。
21、springboot的Hystrix熔断是怎么实现的
Hystrix是一个在Spring Boot中实现断路器模式的库。它提供了一种通过防止一个服务中的故障级联到其他服务来向微服务体系结构添加容错的方法。
Hystrix背后的基本思想是使用断路器包装对远程服务的调用,断路器监视服务的故障,并在故障达到某个阈值时打开电路并停止将请求转发到远程服务。这有助于防止级联故障并提高系统的整体稳定性。
Hystrix通过监控每次调用远程服务的响应时间和成功率来工作。当服务的故障率或响应时间超过指定的阈值时,Hystrix会打开电路并停止向服务转发请求。相反,Hystrix返回默认响应或回退到缓存的响应。
在Spring Boot中,可以通过添加“spring-cloud-starter-netflix-hy”将Hystrix添加到您的项目中。@HystrixCommand对方法的注释,以指示要使用 Hystrix 断路器包装方法调用。
下面是Spring Boot中一个简单的Hystrix实现示例:
@Service
public class MyService {
@HystrixCommand(fallbackMethod = "fallbackMethod")
public String callRemoteService() {
// logic to call the remote service
}
public String fallbackMethod() {
return "Fallback response";
}
}
在此示例中,callRemoteService方法注释为@HystrixCommand,这表示方法调用应使用 Hystrix 断路器包装。这fallbackMethod被指定为断路器打开时要调用的回退方法。
当callRemoteService方法执行后,Hystrix监控调用的响应时间和成功率。如果呼叫失败或时间过长,断路器将打开,并且fallbackMethod将改为调用。这有助于防止远程服务中的故障级联到系统的其他部分。
22、springboot的Hystrix熔断是做什么的
Spring Boot Hystrix是一个用于在微服务架构中实现断路器模式的库。断路器模式用于向系统添加容错能力,并防止一个服务中的故障级联到其他服务。
在微服务体系结构中,不同的服务通过网络相互通信。当其中一个服务失败或变慢时,它可能会导致依赖于它的其他服务中的故障连锁反应。断路器模式通过监视服务的运行状况并在检测到故障时“触发”断路器以停止将请求转发到故障服务来帮助防止这种情况。
Spring Boot Hystrix提供了一种简单灵活的方法,可以在Spring Boot应用程序中实现断路器模式。使用 Hystrix,您可以使用断路器包装对远程服务的调用,断路器将监视服务的故障,并在故障达到特定阈值时打开电路并停止将请求转发到远程服务。相反,Hystrix将返回默认响应或回退到缓存的响应,有助于防止级联故障并提高系统的整体稳定性。
总之,Hystrix是一个工具,可以帮助您为微服务架构添加容错能力,并防止一个服务中的故障影响其他服务。通过在 Spring 引导应用程序中使用 Hystrix,您可以提高系统的弹性和稳定性,并减少故障对用户的影响。
23、Eureka的服务发现会延迟,而nacos不会出现这种情况
Eureka和Nacos的服务发现性能之间的比较是一个有争议的问题。Eureka 和 Nacos 都是微服务架构中使用的服务发现解决方案,可用于缓解服务发现延迟。
但是,服务发现延迟对特定系统的实际影响取决于许多因素,包括服务数量、网络大小、服务发现过程的复杂性以及底层基础结构的性能。
话虽如此,Nacos 在设计时考虑了高性能和可扩展性,并具有缓存、动态负载平衡和多数据中心部署等功能,旨在最大限度地减少服务发现延迟。
相比之下,Eureka 具有更简单的架构,这可能使其更容易在中小型系统中设置和使用。但是,随着系统的扩展,由于其简单体系结构的限制,它可能会遇到延迟增加的情况。
最终,Eureka 和 Nacos 之间的选择将取决于您系统的特定要求,并且应基于对易用性、性能和可扩展性之间的权衡的仔细考虑。

浙公网安备 33010602011771号