第一天
1. 快速排序
java
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 分区操作,获取基准元素的最终位置
int pivotIndex = partition(arr, low, high);
// 递归排序基准元素左边的部分
quickSort(arr, low, pivotIndex - 1);
// 递归排序基准元素右边的部分
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
// 选择最后一个元素作为基准元素
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
// 交换 arr[i] 和 arr[j]
swap(arr, i, j);
}
}
// 将基准元素放到正确的位置
swap(arr, i + 1, high);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
int n = arr.length;
System.out.println("排序前的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
quickSort(arr, 0, n - 1);
System.out.println("\n排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
2. 插入排序
java
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;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
3. 解释java三大特征
- 封装:指的是把对象的属性和方法捆绑在一起,同时隐藏对象的内部实现细节,仅对外提供必要的访问方式。这样做可以增强数据的安全性,避免外部随意修改对象内部数据。
- 继承:指一个类(子类)可以继承另一个类(父类)的属性和方法,从而实现代码的复用和扩展。子类能够拥有父类的所有非私有成员,还能添加自己的独特属性和方法。
- 多态:意味着同一方法可以根据调用对象的不同类型表现出不同的行为。多态主要通过方法重写和接口实现来达成(编译时多态是在编译阶段就确定要调用的方法,它主要通过方法重载来实现。方法重载指的是在一个类中可以定义多个同名的方法,但这些方法的参数列表(参数的类型、个数或顺序)不同;运行时多态是在运行阶段才确定要调用的方法,它主要通过方法重写和向上转型来实现。方法重写是指子类重写父类的方法,向上转型是指将子类对象赋值给父类引用。)
4. 反射机制
在 Java 中,反射机制允许程序在运行时动态地获取类的信息,并且可以调用类的方法、访问和修改类的属性等。下面将详细介绍 Java 反射机制的原理、关键类以及使用场景。
Java 反射机制的核心原理在于 Java 程序运行时,每个类都会在内存中生成一个 java.lang.Class 对象,该对象包含了这个类的完整结构信息,如类名、父类、接口、方法、字段等。通过这个 Class 对象,程序就能在运行时动态地获取和操作类的各种信息。
java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
// 定义一个示例类
class Person {
private String name;
public int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void sayHello() {
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}
}
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 获取 Person 类的 Class 对象
Class<?> personClass = Person.class;
// 创建 Person 类的实例
Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
Person person = (Person) constructor.newInstance("Alice", 20);
// 调用 Person 类的方法
Method sayHelloMethod = personClass.getMethod("sayHello");
sayHelloMethod.invoke(person);
// 访问和修改 Person 类的字段
Field ageField = personClass.getField("age");
ageField.set(person, 21);
// 调用修改后的方法
sayHelloMethod.invoke(person);
}
}
5.深克隆和浅克隆
- 浅克隆(Shallow Clone)
定义:在浅克隆中,创建一个新对象,新对象的基本数据类型(如int、double等)会复制其值,而对于引用类型的成员变量,仅仅复制引用,即新对象和原对象的引用类型成员变量指向同一个内存地址。这意味着如果修改其中一个对象的引用类型成员变量所指向的对象内容,另一个对象也会受到影响。 - 深克隆(Deep Clone)
定义:深克隆会创建一个新对象,并且递归地复制原对象的所有成员变量,包括引用类型的成员变量。这意味着新对象和原对象的所有成员变量都有各自独立的内存空间,修改其中一个对象的任何成员变量都不会影响另一个对象。
6. 数据库和缓存如何保持一致性?
先更新数据库,再删除缓存(推荐)
此策略是先对数据库中的数据进行更新,更新成功后,将缓存中的对应数据删除。
- 优点:
数据一致性较高:在大多数情况下能保证数据的最终一致性。因为读请求在缓存未命中时会从数据库获取最新数据并更新缓存。
性能较好:相比于更新缓存,删除缓存的操作更加轻量级。 - 缺点:
存在短暂不一致:在更新数据库后、删除缓存前,如果有读请求进来,会读取到旧的缓存数据。不过这种不一致的时间窗口通常较短。
保证一致性的额外措施
- 重试机制
在使用 "先更新数据库,再删除缓存" 策略时,如果删除缓存失败,可以引入重试机制。可以将删除缓存的操作记录到消息队列中,通过消息队列的重试功能来保证缓存最终被删除。 - 异步更新
对于一些对实时性要求不是特别高的场景,可以采用异步更新的方式。例如在更新数据库后,通过消息队列异步地更新或删除缓存,这样可以减少对业务逻辑的影响,提高系统的吞吐量。 - 分布式锁
在高并发场景下,可以使用分布式锁来保证同一时间只有一个线程进行数据库和缓存的更新操作,避免并发问题导致的数据不一致。 - 缓存过期时间
为缓存设置合理的过期时间,这样即使出现短暂的数据不一致,在缓存过期后也能从数据库获取到最新数据并更新缓存,保证数据的最终一致性。
7.HashMap 底层原理
- 数据结构
在 Java 中,HashMap 底层数据结构是数组 + 链表 + 红黑树(JDK 1.8 及以后)。 - 扩容机制
HashMap 有一个负载因子(默认为 0.75),当键值对数量超过数组长度乘以负载因子时,会触发扩容操作。扩容时,数组长度会变为原来的 2 倍,然后将原数组中的所有键值对重新计算哈希值并插入到新数组中。 - 线程安全
HashMap 不是线程安全的。在多线程环境下,对 HashMap 进行并发操作可能会导致以下问题:
- 数据不一致
多个线程同时对 HashMap 进行插入、删除或修改操作时,可能会出现数据覆盖或丢失的情况。例如,线程 A 和线程 B 同时插入一个键值对,且它们计算得到的数组下标相同,可能会导致其中一个线程的插入操作被覆盖。 - 死循环(JDK 1.7)
在 JDK 1.7 中,HashMap 在扩容时采用头插法,当多个线程同时进行扩容操作时,可能会导致链表形成环形结构,从而引发死循环。 - 解决方案
如果需要在多线程环境下使用哈希表,可以考虑以下替代方案:
ConcurrentHashMap:是线程安全的哈希表,它采用分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8)的方式来保证线程安全,并发性能较高。
Hashtable:也是线程安全的哈希表,但它使用 synchronized 关键字对整个方法进行同步,并发性能较低。
8. ConcurrentHashMap和hashtable和hashmap区别和作用
区别
- 线程安全性
HashMap:它并非线程安全的类。在多线程环境下,如果多个线程同时对 HashMap 进行读写操作,可能会引发数据不一致、死循环(JDK 1.7)等问题。因此,HashMap 适用于单线程环境。
Hashtable:是线程安全的类。它的方法都被 synchronized 关键字修饰,这意味着同一时间只能有一个线程访问 Hashtable 的方法。这种实现方式虽然保证了线程安全,但在多线程高并发场景下,由于所有操作都需要获取锁,会导致性能严重下降。
ConcurrentHashMap:同样是线程安全的类,但它的并发性能要比 Hashtable 高很多。在 JDK 1.7 中,ConcurrentHashMap 使用分段锁机制,将整个哈希表分成多个段(Segment),每个段都有自己的锁,不同段之间的操作可以并发进行。在 JDK 1.8 中,ConcurrentHashMap 摒弃了分段锁,采用 CAS(Compare-And-Swap)和 synchronized 来保证并发操作的线程安全,进一步提高了并发性能。 - 对 null 键和 null 值的支持
HashMap:允许键和值为 null。也就是说,你可以向 HashMap 中插入一个键为 null 的键值对,也可以插入值为 null 的键值对。
Hashtable:不允许键或值为 null。如果尝试插入 null 键或 null 值,会抛出 NullPointerException。
ConcurrentHashMap:和 Hashtable 一样,不允许键或值为 null。这是因为 ConcurrentHashMap 主要用于多线程环境,null 值可能会导致歧义,例如在 get 方法返回 null 时,无法确定是键不存在还是值本身为 null。 - 性能
HashMap:在单线程环境下,由于不需要考虑线程安全问题,HashMap 的性能是最高的。
Hashtable:由于采用了全量同步机制,在多线程环境下,所有线程都需要竞争同一把锁,性能较低,尤其是在高并发场景下,会成为系统的性能瓶颈。
ConcurrentHashMap:在多线程环境下,通过分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8)的方式,允许更多的线程同时进行读写操作,并发性能远远高于 Hashtable。
作用
- HashMap
HashMap 适用于单线程环境,当你的应用程序不需要考虑线程安全问题,并且对性能有较高要求时,可以使用 HashMap。例如,在一个单线程的工具类中,需要存储一些临时数据,使用 HashMap 是一个不错的选择。 - Hashtable
由于 Hashtable 的性能较低,现在已经很少被使用。但在一些对线程安全有要求,且并发程度不高的场景下,仍然可以考虑使用 Hashtable。不过,通常更推荐使用 ConcurrentHashMap 来替代 Hashtable。 - ConcurrentHashMap
ConcurrentHashMap 主要用于多线程环境,特别是在高并发场景下。例如,在一个多线程的缓存系统中,多个线程可能会同时对缓存进行读写操作,使用 ConcurrentHashMap 可以保证线程安全,同时提供较高的并发性能。
9.分段锁
- 原理
ConcurrentHashMap 在 JDK 1.7 里将整个哈希表划分成多个相互独立的段(Segment),每一个段本质上就是一个小的 HashTable,并且每个段都有属于自己的锁。不同的段能够并发地进行操作,这样在多线程环境下,不同线程可以同时访问不同的段,进而提升了并发性能。
10.ThreadLocal
ThreadLocal 是 Java 里的一个类,它为每个使用该变量的线程都单独创建一个独立的副本,各个线程能够独立地改变自己的副本,而不会对其他线程的副本产生影响。
- 原理
ThreadLocal 的核心原理是借助每个线程内部的 ThreadLocalMap 来存储数据。ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它以 ThreadLocal 对象作为键,以线程的变量副本作为值。当线程调用 ThreadLocal 的 set 方法时,实际上是把值存进了当前线程的 ThreadLocalMap 中;当调用 get 方法时,会从当前线程的 ThreadLocalMap 里获取对应的值。 - 使用场景
线程安全:当某些对象不是线程安全的,但又希望在多线程环境下使用时,可以借助 ThreadLocal 为每个线程创建一个独立的对象副本,以此保证线程安全。例如,SimpleDateFormat 不是线程安全的,使用 ThreadLocal 能为每个线程创建一个独立的 SimpleDateFormat 实例。
上下文管理:在一个线程的执行过程中,需要在不同的方法之间传递一些上下文信息,此时可以使用 ThreadLocal 来存储这些信息,避免在方法参数中频繁传递。例如,在一个 Web 应用中,可以使用 ThreadLocal 来存储当前用户的信息。
java
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalExample {
// 创建一个 ThreadLocal 对象,用于存储 SimpleDateFormat 实例
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static String formatDate(Date date) {
// 从 ThreadLocal 中获取当前线程的 SimpleDateFormat 实例
SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
return dateFormat.format(date);
}
public static void main(String[] args) {
// 创建一个日期对象
Date date = new Date();
// 创建两个线程,分别格式化日期
Thread thread1 = new Thread(() -> {
String formattedDate = formatDate(date);
System.out.println("线程 1 格式化的日期: " + formattedDate);
});
Thread thread2 = new Thread(() -> {
String formattedDate = formatDate(date);
System.out.println("线程 2 格式化的日期: " + formattedDate);
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待线程执行完毕
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 移除当前线程的 ThreadLocal 变量
dateFormatThreadLocal.remove();
}
}
11.索引
索引是一种数据结构,用于提高数据库表中数据的查询效率。它就像一本书的目录,能够帮助数据库快速定位到所需的数据行,而不必全表扫描。
- 常见类型
B-tree 索引:最常见的索引类型,适用于全值匹配、范围查询、前缀匹配等多种查询场景。
哈希索引:基于哈希表实现,只能用于精确匹配,查询速度非常快,但不支持范围查询。
全文索引:用于在文本类型的列中进行全文搜索,能够快速找到包含特定关键词的记录。 - 使用场景和优化
经常用于查询条件、连接条件的列上适合创建索引。
索引并非越多越好,过多的索引会增加数据插入、更新和删除的成本,因为每次数据变更都需要同时更新索引。
12.事务
事务是一组数据库操作,这些操作要么全部成功执行,要么全部不执行,是一个不可分割的工作单元,以保证数据库的一致性。
- 特性(ACID)
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不执行,不会出现部分执行的情况。
- 一致性(Consistency):事务执行前后,数据库的完整性约束没有被破坏,数据从一个一致状态转换到另一个一致状态。
- 隔离性(Isolation):多个事务并发执行时,相互之间不会干扰,就像每个事务都是独立执行一样。
- 持久性(Durability):事务一旦提交,其对数据库的修改就会永久保存,即使数据库发生故障也不会丢失。
13.并发问题及隔离级别
- 脏读:一个事务读取了另一个未提交事务修改的数据。
- 不可重复读:在同一个事务中,多次读取同一数据时,由于其他事务对该数据进行了修改并提交,导致每次读取的结果不一致。
- 幻读:在一个事务中,按照某个条件查询数据时,由于其他事务插入或删除了符合该条件的数据,导致前后两次查询的结果集不一致。
- 隔离级别:包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。MySQL 的默认隔离级别是可重复读,通过 MVCC(多版本并发控制)来解决并发问题,避免脏读、不可重复读和部分幻读情况的发生。
14.MVCC(多版本并发控制)
MVCC 是 MySQL 在可重复读隔离级别下实现高并发的关键技术。它通过在每行数据后面保存两个隐藏的列来实现,一个是创建版本号,一个是删除版本号。
当事务读取数据时,会根据事务的版本号和数据的版本号来判断数据是否可见,从而实现了不同事务之间的并发访问控制,避免了数据的冲突和不一致。
15.三个日志
- redo log(重做日志)
用于记录数据库的物理修改,即数据页的修改记录。当数据库发生故障时,通过 redo log 可以将数据库恢复到故障前的状态,保证事务的持久性。
它是顺序写入的,写入性能很高,并且在事务提交时,会将 redo log buffer 中的数据刷新到磁盘上的 redo log 文件中。 - undo log(回滚日志)
主要用于事务回滚和 MVCC。在事务执行过程中,如果需要回滚,就可以根据 undo log 中的记录来撤销对数据的修改。
同时,undo log 也为 MVCC 提供了数据的历史版本信息,使得在不同事务中可以看到数据的不同版本。 - bin log(二进制日志)
记录了数据库的所有更新操作,包括数据的插入、更新、删除等,不包含查询语句。它主要用于数据库的备份、恢复以及主从复制。
前的状态,保证事务的持久性。
它是顺序写入的,写入性能很高,并且在事务提交时,会将 redo log buffer 中的数据刷新到磁盘上的 redo log 文件中。 - undo log(回滚日志)
主要用于事务回滚和 MVCC。在事务执行过程中,如果需要回滚,就可以根据 undo log 中的记录来撤销对数据的修改。
同时,undo log 也为 MVCC 提供了数据的历史版本信息,使得在不同事务中可以看到数据的不同版本。 - bin log(二进制日志)
记录了数据库的所有更新操作,包括数据的插入、更新、删除等,不包含查询语句。它主要用于数据库的备份、恢复以及主从复制。
在主从复制中,主库将 bin log 中的日志发送给从库,从库根据这些日志来重新执行相应的操作,从而实现主从库的数据一致性。