快速失败(fail-fast)
设计的目的是为了避免在遍历时对集合进行并发修改,从而引发潜在的不可预料的错误。
通过迭代器遍历集合时修改集合: 如果你使用Iterator
遍历集合,然后直接使用集合的修改方法(如add()
或remove()
),集合的结构会发生变化,而迭代器并没有同步感知到这些变化,就会抛出ConcurrentModificationException
。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
list.remove(element); // 修改了集合结构,抛出 ConcurrentModificationException
}
}
通过增强型 for 循环遍历集合时修改集合: 增强型 for
循环实际上是基于迭代器的语法糖,因此在遍历过程中如果修改了集合,同样会抛出 ConcurrentModificationException
。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
if (element.equals("B")) {
list.remove(element); // 抛出 ConcurrentModificationException
}
}
如何避免异常
使用迭代器的remove()
方法: 如果需要在迭代过程中删除元素,可以使用迭代器自带的remove()
方法,它会安全地修改集合,并更新迭代器的状态,不会抛出异常。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
iterator.remove(); // 使用迭代器的 remove() 方法,安全移除元素
}
}
避免在遍历时修改集合: 如果你必须在遍历时修改集合,可以考虑先将要删除的元素收集到一个临时列表中,遍历完成后再删除它们。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> toRemove = new ArrayList<>();
for (String element : list) {
if (element.equals("B")) {
toRemove.add(element); // 将要删除的元素暂存
}
}
list.removeAll(toRemove); // 之后统一删除
使用并发集合类: 在多线程环境中,如果多个线程同时对集合进行操作,推荐使用并发集合类,如CopyOnWriteArrayList
或ConcurrentHashMap
,它们支持线程安全的修改。
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
if (element.equals("B")) {
list.remove(element); // 不会抛出异常,因为 CopyOnWriteArrayList 是线程安全的
}
}
HashMap
和 Hashtable
的区别
HashMap
:非线程安全,允许null键和null值,效率较高。Hashtable
:线程安全,不允许null键和null值,效率相对较低。
StringBuilder和StringBuffer的区别
synchronized
关键字的用法和实现原理?
用法:synchronized
可以用于方法或代码块,确保在多线程环境下,某些代码块在同一时间只有一个线程可以执行。
synchronized (this) {
// 线程安全的代码
}
实现原理:synchronized
会通过获取对象的监视器锁(Monitor Lock)来实现同步,锁住代码块或方法,保证临界区内的线程安全。
什么是volatile关键字?它有什么作用?
volatile
关键字是Java中的一个修饰符,用于保证多线程环境下共享变量的可见性。当一个变量被声明为volatile
时,它告诉Java编译器和运行时环境,任何对该变量的读写操作都要直接从主内存中进行,而不是从线程的本地缓存中读取。
写入:如果不使用volatile关键字,线程会将修改的值保存到缓存中,如果使用了volatile关键字,线程会将修改的值刷新到主内存中。保证多线程环境下共享变量的可见性。
读取:如果不使用volatile关键字,线程会从缓存中读取该变量,因为一般线程会将修改的值保存到缓存中。如果使用了volatile关键字,线程就会从主内存中读取修改后的变量值。
volatile
保证了变量的可见性,即一个线程对该变量的修改能立即被其他线程看到。volatile
防止了指令重排序,确保变量的读写顺序在并发环境中是安全的。- 但
volatile
不保证操作的原子性,对于涉及多个步骤的操作仍然可能需要同步或者使用更高级的并发工具类。
JVM的内存模型是怎样的?堆内存和栈内存的区别?
- JVM内存模型包括:方法区、堆、虚拟机栈、本地方法栈和程序计数器。
- 堆内存:用于存储所有的对象,堆是线程共享的。
- 栈内存:用于存储局部变量、方法调用等,栈是线程私有的。
JVM中的垃圾回收机制是怎样的?
- JVM的垃圾回收器采用“分代收集算法”,主要分为年轻代(Eden区、S0/S1区)和老年代。
- 垃圾回收算法主要包括:
- 标记-清除算法:标记活跃对象,清除未标记对象。
- 标记-整理算法:对老年代使用,整理内存空间。
- 复制算法:对年轻代使用,将存活对象复制到另一区域。
反射
反射(Reflection)是Java语言中的一种机制,允许程序在运行时动态地检查和操作类、接口、方法、属性等信息。通过反射,程序可以在运行时获取类的结构信息,并能够动态地创建对象、调用方法、访问字段等。反射通常用于框架、库开发和动态代理等场景。
反射的作用
- 动态获取类的信息:可以在运行时获取类的全限定名、构造方法、字段、方法等信息。
- 动态调用方法:在运行时通过反射调用对象的方法,而不需要在编译时确定具体调用的内容。
- 动态访问字段:可以访问对象的私有、受保护或公有字段,并且可以对其进行读取或修改。
- 动态创建对象:不需要提前知道类的名称,可以在运行时根据名称动态地创建类的实例。
反射的使用
Java中的反射机制主要通过java.lang.reflect
包来实现。以下是几个常用的反射类和方法:
Class<?>
:表示一个类的类对象,可以通过它获取类的元数据。Field
:表示类中的字段(成员变量)。Method
:表示类中的方法。Constructor<?>
:表示类的构造函数。
反射的具体例子
1. 获取类的Class
对象
可以通过三种方式获取某个类的Class
对象:
- 通过类的类名:
Class.forName("类的全限定名")
- 通过对象:
对象.getClass()
- 通过类名.class:
类名.class
// 通过类名获取
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 通过对象获取
MyClass obj = new MyClass();
Class<?> clazz2 = obj.getClass();
// 通过类名.class获取
Class<?> clazz3 = MyClass.class;
反射的缺点
- 性能开销大:反射涉及到大量的动态类型检查和方法调用,性能相对于直接调用稍差。
- 安全性问题:反射允许访问私有字段和方法,可能会破坏封装性。
- 编译时检查失效:使用反射时,很多错误会推迟到运行时才会暴露。
常见的设计模式有哪些
设计模式是为了解决软件开发中常见问题而总结出的可重用解决方案,主要分为创建型模式、结构型模式和行为型模式三类。
http请求(put post区别)
-
POST:用于向服务器发送数据,用于创建资源,或者向已存在的资源发送数据。POST 请求通常会导致服务器状态的变化或触发某些动作。
- 语义上是“添加”(append)数据到资源上。
- 常用于提交表单数据、上传文件、处理非幂等操作(即操作可能有不同的结果,每次执行结果不同)。
-
PUT:用于在服务器上创建或替换资源。PUT 请求是幂等的,也就是说,重复的 PUT 请求应产生相同的结果。
- 语义上是“更新”或者“替换”资源。如果资源不存在,则创建资源。
- 常用于更新某个已知的资源,例如通过
PUT /users/123
更新 ID 为 123 的用户信息。
开启线程的方法 线程的状态
一、开启线程的方法
1. 继承 Thread
类
通过继承 Thread
类并重写其中的 run()
方法来定义线程。
步骤:
- 创建一个继承自
Thread
类的类。 - 重写
run()
方法,将线程执行的代码放入其中。 - 创建线程对象并调用
start()
方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务
System.out.println("Thread is running...");
}
}
public class TestThread {
public static void main(String[] args) {
MyThread t1 = new MyThread(); // 创建线程对象
t1.start(); // 启动线程
}
}
2. 实现 Runnable
接口
通过实现 Runnable
接口并将其传递给 Thread
类的构造函数。
步骤:
- 创建一个实现了
Runnable
接口的类。 - 实现
run()
方法,将线程执行的代码放入其中。 - 将该
Runnable
对象传递给Thread
构造器并调用start()
启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的任务
System.out.println("Runnable is running...");
}
}
public class TestRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable); // 创建线程对象
thread.start(); // 启动线程
}
}
3. 使用匿名内部类
你可以通过匿名内部类的方式直接创建线程。
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous Runnable is running...");
}
});
thread.start(); // 启动线程
}
}
4. 使用 Lambda 表达式
在 Java 8 及以上版本中,使用 Lambda 表达式简化 Runnable
接口的实现。
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda Runnable is running...");
});
thread.start(); // 启动线程
}
}
二、查看线程的状态
Java 提供了一些方法和枚举来检查线程的状态。可以通过调用 Thread
类的方法来获取线程的当前状态。
1. 线程状态枚举(Thread.State
)
Thread.State
是枚举类型,它定义了线程的六种状态:
- NEW:线程对象已经创建,但尚未调用
start()
方法。 - RUNNABLE:线程正在 Java 虚拟机中运行。
- BLOCKED:线程被阻塞,正在等待监视器锁(同步锁)。
- WAITING:线程无限期等待另一个线程执行特定操作。
- TIMED_WAITING:线程在等待,超时后会被唤醒。
- TERMINATED:线程已完成执行。
2. 获取线程状态的方法
使用 Thread
类中的 getState()
方法来查看线程的状态。
Runnable和Callable的区别?
1. Runnable
详细说明:
Runnable
接口定义了一个run()
方法,用于封装并发任务。它不返回任何结果,也不会抛出任何受检异常。public class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running"); } } public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程,执行run()方法 } }
2. Callable
详细说明:
Callable
接口定义了一个call()
方法,可以返回任务的执行结果,并且可以抛出异常。通常与ExecutorService
结合使用。- 返回的结果可以通过
Future
对象来获取。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result";
}
}
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // 获取 Callable 的返回值
executor.shutdown();
}
}
线程池的工作原理?
线程池通过管理一组线程来提高应用程序的性能和资源利用率。通过合理的任务队列和线程管理策略,线程池可以高效地处理并发任务,并在多线程环境中保持系统的稳定性。
1. 线程池的组件:
- 核心线程:线程池初始化时创建的线程,始终保持在池中。
- 最大线程:线程池可以创建的最大线程数量,超出这个数量的任务会被放入任务队列。
- 任务队列:存放待执行任务的队列,可以是不同类型的队列(如无界队列、有界队列等)。
- 空闲线程存活时间:核心线程以外的线程在没有任务时的存活时间,超出这个时间会被销毁。
2. 工作流程
a. 线程池的创建:
线程池通过 Executors
或 ThreadPoolExecutor
创建,设定核心线程数、最大线程数和任务队列类型。
b. 任务提交:
- 使用
submit()
或execute()
方法将任务提交到线程池。 - 提交任务时,线程池会首先检查当前正在运行的线程数量。
c. 线程的处理:
-
如果运行的线程少于核心线程:
- 创建新线程来处理任务,并立即执行。
-
如果运行的线程已达到核心线程数:
- 将任务放入任务队列。
- 如果队列已满,且当前线程数小于最大线程数,则创建新线程来处理任务。
-
如果运行的线程已达到最大线程数:
- 根据任务队列的类型进行处理(丢弃、抛出异常、执行某种策略等)。
d. 任务执行:
- 线程从任务队列中获取任务并执行。
- 执行完毕后,线程返回线程池,准备接收新的任务。
e. 线程的销毁:
- 如果空闲线程的数量超过核心线程数,并且超过了指定的空闲存活时间,则会被销毁。
- 当线程池关闭时,未完成的任务将根据关闭策略(如
shutdown()
或shutdownNow()
)进行处理。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 提交任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
threadPool.submit(() -> {
System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " is completed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
threadPool.shutdown();
}
}
List Set Map的区别?
- List:适合需要按顺序访问元素的场景,如保存用户列表、待办事项等。
- Set:适合需要确保元素唯一的场景,如存储用户的邮箱地址、商品的ID等。
- Map:适合存储键值对关系的场景,如存储用户信息(用户名和对应的用户对象)、产品ID与产品详情的关系等。
B+树和红黑树的区别,哪个效果更好,为什么?
索引:帮助MySQL高效获取数据的排好序的数据结构,如二叉树,红黑树,Hash表,B-Tree
红黑树是一种二叉搜索树,当左右子树高度不平衡是会进行调整
B+树是一种多路搜索树,
- 非叶子节点不存储数据,只存储索引,可以放更多的索引(前一层存放的是下一层索引的第一个值)
- 叶子节点包含所有索引字段
- 叶子节点用指针链接,提高区间访问的性能
B+树的效果更好,因为高度更低,查询效率更高。
常见的字符串操作
public class StringExample {
public static void main(String[] args) {
String str = " Hello, World! ";
// 1. 字符串长度
System.out.println("Length: " + str.length());
// 2. 去除空格
System.out.println("Trimmed: '" + str.trim() + "'");
// 3. 转换为大写
System.out.println("Uppercase: " + str.toUpperCase());
// 4. 子字符串
System.out.println("Substring: " + str.substring(7));
// 5. 替换字符
System.out.println("Replaced: " + str.replace("World", "Java"));
// 6. 分割字符串
String[] parts = str.split(", ");
for (String part : parts) {
System.out.println("Part: " + part);
}
// 7. 格式化字符串
String formatted = String.format("Name: %s, Age: %d", "John", 30);
System.out.println("Formatted: " + formatted);
}
}
char和varchar的区别
数据库的sql失效一般有哪些场景
SQL 失效通常指的是在数据库中,原本可以使用的 SQL 查询突然变得无法执行或效率降低。以下是一些常见的场景:
-
数据库结构变化:
- 表结构、索引或数据类型的更改,可能导致原有查询计划失效。
-
统计信息过时:
- 数据库优化器依赖统计信息来生成执行计划。如果统计信息未更新,可能导致选择不合适的查询计划。
-
参数变化:
- 使用参数化查询时,不同参数值可能导致生成不同的执行计划,某些计划可能在某些参数下失效。
-
索引失效:
- 索引被删除、修改或变得不再适用,可能导致查询效率显著下降。
-
数据分布变化:
- 数据量或数据分布发生变化,例如数据倾斜,可能导致原有的查询计划不再高效。
-
锁竞争或阻塞:
- 由于高并发导致锁竞争或阻塞,可能导致原本顺利执行的 SQL 变得缓慢或失效。
-
缓存失效:
- 查询结果缓存失效,导致相同的查询需要重新执行而不是从缓存中获取结果。
-
数据库配置变更:
- 数据库参数配置或硬件资源的变化可能影响查询的执行计划。
-
代码逻辑修改:
- 应用程序中 SQL 查询的逻辑改变,可能导致执行效率下降。
面向对象和面向过程的区别
类的加载机制?
-
加载 (Loading):
- 从
.class
文件中读取字节码,并将其加载到内存中。加载器会根据类的名称和路径查找对应的.class
文件。
- 从
-
链接 (Linking):
- 验证 (Verification):检查加载的字节码是否符合 Java 虚拟机规范,确保安全性。
- 准备 (Preparation):为类变量分配内存,并设置其默认值。
- 解析 (Resolution):将常量池中的符号引用转换为直接引用,即确定实际的内存地址。
-
初始化 (Initialization):
- 执行类的静态初始化块和静态变量的初始化,设置变量的实际值。这是类加载的最后一步。
Java 中的类加载器主要包括以下几种:
-
引导类加载器 (Bootstrap ClassLoader):
- 负责加载 Java 核心类库,如
java.lang.*
、java.util.*
等。
- 负责加载 Java 核心类库,如
-
扩展类加载器 (Extension ClassLoader):
- 负责加载 JRE 的扩展库(通常在
lib/ext
目录中)。
- 负责加载 JRE 的扩展库(通常在
-
应用类加载器 (Application ClassLoader):
- 负责加载用户类路径(classpath)下的类,包括用户自定义的类。
3. 类加载的双亲委派机制:
- 在类加载过程中,类加载器遵循双亲委派模型,即一个类加载器在尝试加载类时,会首先将请求委派给它的父类加载器。如果父加载器无法加载该类,才会由当前加载器进行加载。这种机制可以防止类的重复加载和冲突。
获取一个class对象的方式?
可以通过三种方式获取某个类的Class
对象:
- 通过类的类名:
Class.forName("类的全限定名")
- 通过对象:
对象.getClass()
- 通过类名.class:
类名.class
// 通过类名获取
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 通过对象获取
MyClass obj = new MyClass();
Class<?> clazz2 = obj.getClass();
// 通过类名.class获取
Class<?> clazz3 = MyClass.class;
如何获得类的属性?
1. 通过getter
方法获取属性
这是最常见的方式,通常会在类中为每个属性定义相应的getter
方法,供外部代码调用。
public class Person {
private String name;
private int age;
// Getter methods
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.getName(); // 获取属性 name
person.getAge(); // 获取属性 age
}
}
2. 通过反射机制获取属性
Java的反射机制允许我们在运行时获取对象的属性,而不需要直接调用getter方法。通过反射,可以动态地获取和修改类的属性,即使这些属性是私有的。
import java.lang.reflect.Field;
public class Person {
private String name = "John";
private int age = 30;
}
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Person person = new Person();
// 获取属性
Field nameField = person.getClass().getDeclaredField("name");
nameField.setAccessible(true); // 设置访问权限
String name = (String) nameField.get(person);
Field ageField = person.getClass().getDeclaredField("age");
ageField.setAccessible(true);
int age = (int) ageField.get(person);
System.out.println("Name: " + name + ", Age: " + age);
}
}
3. 通过java.beans.Introspector
获取属性
Java中还提供了Introspector
类,可以通过内省机制来获取对象的属性信息。它主要用于Java Beans,提供了对类的属性、方法的描述。
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class Person {
private String name = "John";
private int age = 30;
// Getters and Setters
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 class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
for (PropertyDescriptor pd : Introspector.getBeanInfo(Person.class).getPropertyDescriptors()) {
if (pd.getReadMethod() != null && !"class".equals(pd.getName())) {
System.out.println(pd.getName() + ": " + pd.getReadMethod().invoke(person));
}
}
}
}
// 使用 StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();
// 使用 StringBuffer
StringBuffer sbf = new StringBuffer();
sbf.append("Hello");
sbf.append(" ");
sbf.append("World");
String result2 = sbf.toString();
springboot的多环境配置文件是怎么区分的
在 Spring Boot 中,多环境配置文件通常通过不同的配置文件和 spring.profiles.active
属性来区分。
1. 创建配置文件
在 src/main/resources
目录下,你可以创建不同的配置文件,例如:
application.properties
(默认配置)application-dev.properties
(开发环境)application-test.properties
(测试环境)application-prod.properties
(生产环境)
2. 配置 application.properties
在 application.properties
中,可以指定活动的配置文件,例如:
spring.profiles.active=dev
3. 运行时指定活动配置
你可以在启动应用程序时通过命令行参数指定活动的配置文件。例如:
java -jar your-app.jar --spring.profiles.active=prod
4. 使用环境变量
你也可以通过设置环境变量来指定活动的配置文件:
export SPRING_PROFILES_ACTIVE=dev
5. 访问配置属性
在代码中,可以使用 @Value
注解或 @ConfigurationProperties
注解来访问不同环境下的配置属性:
@Value("${some.property}")
private String someProperty;
6. 配置优先级
application.properties
是基础配置。- 环境特定的配置文件(如
application-dev.properties
)会覆盖基础配置。 - 可以根据需要添加其他特定于环境的配置。