1,笔试(两道全排序)
冒泡排序
java
public class BubbleSort {
public static int[] bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
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;
}
}
}
return arr;
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 6, 7};
int[] sortedArray = bubbleSort(arr);
for (int num : sortedArray) {
System.out.print(num + " ");
}
}
}
快速排序
java
public class QuickSort {
public static int[] quickSort(int[] arr) {
if (arr.length <= 1) {
return arr;
}
int pivot = arr[0];
int[] left = new int[0];
int[] right = new int[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left = addElement(left, arr[i]);
} else {
right = addElement(right, arr[i]);
}
}
int[] sortedLeft = quickSort(left);
int[] sortedRight = quickSort(right);
return mergeArrays(sortedLeft, pivot, sortedRight);
}
private static int[] addElement(int[] arr, int element) {
int[] newArr = new int[arr.length + 1];
for (int i = 0; i < arr.length; i++) {
newArr[i] = arr[i];
}
newArr[arr.length] = element;
return newArr;
}
private static int[] mergeArrays(int[] left, int pivot, int[] right) {
int[] result = new int[left.length + right.length + 1];
int index = 0;
for (int num : left) {
result[index++] = num;
}
result[index++] = pivot;
for (int num : right) {
result[index++] = num;
}
return result;
}
public static void main(String[] args) {
int[] arr = {5, 3, 8, 6, 7};
int[] sortedArray = quickSort(arr);
for (int num : sortedArray) {
System.out.print(num + " ");
}
}
}
这些代码实现了基本的冒泡排序和快速排序算法,可以对给定的整数数组进行全排序操作
2.java8有哪些新特性?
-
Lambda 表达式
- 简介 :Lambda 表达式是 Java 8 中最重要的特性之一。它提供了一种简洁的方式来表示匿名函数,可以将函数作为方法参数或者存储在变量中。Lambda 表达式的语法形式为
(parameters) -> expression
或(parameters) -> { statements; }
。 - 示例 :例如,在使用
java.util.Arrays.sort()
方法对一个整数数组进行排序时,可以使用 Lambda 表达式来定义比较规则。以前在 Java 7 及以前可能需要定义一个实现了Comparator
接口的类,而在 Java 8 中可以这样写:
javaimport java.util.Arrays; import java.util.Comparator; public class LambdaExample { public static void main(String[] args) { Integer[] numbers = {5, 3, 8, 6, 7}; Arrays.sort(numbers, (a, b) -> a - b); for (Integer num : numbers) { System.out.print(num + " "); } } }
这里
(a, b) -> a - b
就是一个 Lambda 表达式,用于定义两个整数比较大小的规则。 - 简介 :Lambda 表达式是 Java 8 中最重要的特性之一。它提供了一种简洁的方式来表示匿名函数,可以将函数作为方法参数或者存储在变量中。Lambda 表达式的语法形式为
-
函数式接口(Functional Interface)
- 定义与特性 :函数式接口是只包含一个抽象方法的接口。Java 8 为函数式接口提供了
@FunctionalInterface
注解,用于标记该接口是一个函数式接口,不过这个注解不是必须的。函数式接口可以被 Lambda 表达式或者方法引用(后面会介绍)实现。 - 示例 :
java.util.function
包中定义了许多函数式接口,如Consumer
、Supplier
、Function
等。以Consumer
为例,它表示接受一个参数并且没有返回值的操作。可以这样使用:
javaimport java.util.ArrayList; import java.util.List; import java.util.function.Consumer; public class FunctionalInterfaceExample { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); Consumer<Integer> printer = (num) -> System.out.print(num + " "); numbers.forEach(printer); } }
这里
Consumer<Integer>
就是一个函数式接口,(num) -> System.out.print(num + " ");
是它的 Lambda 表达式实现,用于打印列表中的每个元素。 - 定义与特性 :函数式接口是只包含一个抽象方法的接口。Java 8 为函数式接口提供了
-
方法引用(Method References)
-
类型与示例
:方法引用是一种更简洁的 Lambda 表达式形式,用于直接引用一个已经存在的方法。主要有以下几种类型:
- 静态方法引用 :语法为
ClassName::staticMethodName
。例如,如果有一个静态方法Math.sqrt()
用于计算平方根,可以这样引用:Function<Double, Double> squareRootFunction = Math::sqrt;
。 - 实例方法引用(对象::实例方法) :当已经有一个对象实例并且要引用它的实例方法时使用。例如,假设有一个
String
对象str
,要引用它的length()
方法,可以写成Function<String, Integer> lengthFunction = str::length;
。 - 构造方法引用 :语法为
ClassName::new
。用于引用一个类的构造方法。例如,Supplier<List<Integer>> listSupplier = ArrayList::new;
用于创建一个ArrayList
的实例。
- 静态方法引用 :语法为
-
-
接口的默认方法(Default Methods)和静态方法
- 默认方法 :接口在 Java 8 之前是不能有方法体的,但 Java 8 允许在接口中定义默认方法,默认方法使用
default
关键字。默认方法的主要作用是为接口添加新的方法而不破坏已有的实现类。例如,在java.util.List
接口中有一个默认方法sort
,它的实现可能如下:
javadefault void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } }
- 静态方法 :接口也可以有静态方法,这些静态方法可以通过接口名直接调用,就像在类中一样。例如,
java.util.stream.Stream
接口中有很多静态方法,如of
用于创建一个流,Stream.of(1, 2, 3)
可以创建一个包含整数 1、2、3 的流。
- 默认方法 :接口在 Java 8 之前是不能有方法体的,但 Java 8 允许在接口中定义默认方法,默认方法使用
-
新的日期和时间 API(java.time 包)
- 旧日期时间 API 的问题 :在 Java 8 之前,日期和时间处理主要使用
java.util.Date
和java.util.Calendar
,这些 API 存在一些问题,如线程安全性差、日期和时间操作复杂等。 - 新 API 的优势与示例 :新的
java.time
包提供了一系列用于日期、时间、时间间隔等处理的类。例如,LocalDate
用于表示日期,LocalTime
用于表示时间,LocalDateTime
用于表示日期和时间。可以这样使用:
javaimport java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; public class DateTimeExample { public static void main(String[] args) { LocalDate date = LocalDate.now(); System.out.println("今天的日期:" + date); LocalTime time = LocalTime.now(); System.out.println("现在的时间:" + time); LocalDateTime dateTime = LocalDateTime.now(); System.out.println("现在的日期和时间:" + dateTime); } }
- 旧日期时间 API 的问题 :在 Java 8 之前,日期和时间处理主要使用
-
Stream API
- 概念与优势 :Stream API 用于对集合(如
List
、Set
等)或数组中的元素进行高效的操作,如过滤、映射、排序、归约等。它提供了一种函数式编程风格的方式来处理数据,使得代码更加简洁和易读。Stream 操作是延迟执行的,只有在需要结果时才会真正执行操作。 - 示例:
javaimport java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = numbers.stream() .filter(num -> num % 2 == 0) .collect(Collectors.toList()); System.out.println(evenNumbers); } }
这里首先将
List
转换为一个Stream
,然后使用filter
方法过滤出偶数,最后使用collect
方法将结果收集到一个新的List
中。 - 概念与优势 :Stream API 用于对集合(如
3.Object类中有什么方法,有什么作用?
-
equals()
方法- 作用 :用于比较两个对象是否在逻辑上 "相等"。默认情况下,
equals()
方法是比较两个对象的引用是否相同,即它们是否是同一个对象。但是在很多情况下,我们希望根据对象的内容来判断是否相等。例如,对于两个自定义的用户类对象,可能希望比较它们的用户 ID 或者用户名等属性是否相同来判断是否相等。 - 示例 :假设我们有一个简单的
Person
类,有name
和age
两个属性。如果要根据name
和age
来判断两个Person
对象是否相等,可以重写equals()
方法如下:
javaclass Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass()!= o.getClass()) return false; Person person = (Person) o; return age == person.age && name.equals(person.name); } }
这样,当比较两个
Person
对象时,就会根据name
和age
属性来判断是否相等,而不是仅仅比较引用。 - 作用 :用于比较两个对象是否在逻辑上 "相等"。默认情况下,
-
hashCode()
方法- 作用 :
hashCode()
方法返回一个对象的哈希码值。哈希码主要用于在哈希表(如HashMap
、HashSet
等)中快速定位对象。在这些集合中,对象首先会通过hashCode()
方法计算哈希码,然后根据哈希码将对象存储在对应的位置。如果两个对象根据equals()
方法判断为相等,那么它们的hashCode()
值应该相同。但是,两个hashCode()
值相同的对象不一定相等。 - 示例 :对于上述
Person
类,当重写equals()
方法时,也应该重写hashCode()
方法,以保证一致性。可以这样重写:
java@Override public int hashCode() { int result = name.hashCode(); result = 31 * result + age; return result; }
这里使用了一种常见的计算哈希码的方式,结合了对象的属性。这样,当
Person
对象存储在哈希表中时,就可以正确地进行操作。 - 作用 :
-
toString()
方法- 作用 :
toString()
方法返回一个对象的字符串表示形式。默认情况下,它返回对象的类名和哈希码的十六进制表示。但是在实际应用中,我们通常希望toString()
方法返回对象的一些有意义的信息,比如对象的属性值等。这样在打印对象或者将对象转换为字符串时,能够更直观地了解对象的内容。 - 示例 :对于
Person
类,可以重写toString()
方法如下:
java@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
这样,当我们打印
Person
对象时,就会输出对象的name
和age
属性的值,而不是默认的类名和哈希码。 - 作用 :
-
clone()
方法- 作用 :用于创建并返回一个对象的副本。这个副本和原始对象具有相同的类和属性值。不过,要正确使用
clone()
方法,需要注意一些细节。首先,对象的类需要实现Cloneable
接口,这是一个标记接口,用于表示该类支持克隆。其次,在实现clone()
方法时,需要正确地复制对象的属性,特别是对于引用类型的属性,可能需要进行深度克隆。 - 示例 :假设我们有一个简单的
Point
类,有x
和y
两个属性,要实现clone()
方法,可以如下操作:
javaclass Point implements Cloneable { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public Point clone() { try { return (Point) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }
这里,
super.clone()
会创建一个新的Point
对象,并复制x
和y
属性的值。 - 作用 :用于创建并返回一个对象的副本。这个副本和原始对象具有相同的类和属性值。不过,要正确使用
-
getClass()
方法- 作用 :返回一个对象的运行时类,这个类的类型是
java.lang.Class
。通过getClass()
方法可以获取对象的详细信息,如类名、类的属性、方法等。它主要用于反射机制,可以在运行时动态地获取和操作对象所属的类的信息。 - 示例:
javapublic class ClassExample { public static void main(String[] args) { Person person = new Person("John", 30); Class<?> clazz = person.getClass(); System.out.println(clazz.getName()); } }
这里,
clazz.getName()
会输出Person
类的全限定名。 - 作用 :返回一个对象的运行时类,这个类的类型是
-
finalize()
方法- 作用 :
finalize()
方法是在对象被垃圾回收之前调用的方法。它主要用于释放对象占用的非 Java 资源,如打开的文件、数据库连接等。不过,在实际应用中,应该尽量避免使用finalize()
方法,因为它的执行时间不确定,而且可能会导致性能问题。现在更推荐使用try - with - resources
等方式来管理资源。 - 示例 :假设我们有一个类,它打开了一个文件,在
finalize()
方法中可以关闭这个文件。不过,这只是一个示例,实际中不建议这样做。
javaclass FileOpener { private File file; public FileOpener(String fileName) { try { this.file = new File(fileName); } catch (Exception e) { e.printStackTrace(); } } @Override public void finalize() { try { if (file!= null) { file.close(); } } catch (IOException e) { e.printStackTrace(); } } }
- 作用 :
4.介绍下 Collection 接口
-
Collection 接口概述
- 地位与定义 :Collection 接口是 Java 集合框架中的根接口之一,它位于
java.util
包中。这个接口定义了一组用于操作对象集合的通用方法,这些方法包括添加、删除、查询集合中的元素,以及判断集合的一些基本属性(如是否为空、大小等)。所有的集合类(如 List、Set、Queue 等)都直接或间接实现了 Collection 接口,这使得可以以一种统一的方式来处理不同类型的集合。
- 地位与定义 :Collection 接口是 Java 集合框架中的根接口之一,它位于
-
主要方法及其功能
-
添加元素方法
add(E e)
:用于向集合中添加一个指定的元素。如果集合因为添加操作而发生改变(例如集合允许添加重复元素并且添加成功,或者集合不允许重复元素但要添加的元素不存在于集合中),则返回true
;如果集合不允许添加该元素(例如元素类型不匹配或者集合是不可变集合),则返回false
。例如,对于一个ArrayList
集合,可以这样添加元素:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionAddExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); boolean result = collection.add("Hello"); System.out.println("添加元素是否成功:" + result); } }
addAll(Collection<? extends E> c)
:用于将指定集合中的所有元素添加到当前集合中。如果当前集合因为这个操作而发生改变(即至少添加了一个元素),则返回true
;否则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Arrays; import java.util.Collection; public class CollectionAddAllExample { public static void main(String[] args) { Collection<String> collection1 = new ArrayList<>(); Collection<String> collection2 = Arrays.asList("Apple", "Banana"); boolean result = collection1.addAll(collection2); System.out.println("添加集合是否成功:" + result); } }
-
删除元素方法
remove(Object o)
:用于从集合中移除指定的元素。如果集合中存在该元素并且成功移除,则返回true
;如果集合中不存在该元素,则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionRemoveExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Hello"); collection.add("World"); boolean result = collection.remove("Hello"); System.out.println("移除元素是否成功:" + result); } }
removeAll(Collection<?> c)
:用于从当前集合中移除所有包含在指定集合中的元素。如果当前集合因为这个操作而发生改变(即至少移除了一个元素),则返回true
;否则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Arrays; import java.util.Collection; public class CollectionRemoveAllExample { public static void main(String[] args) { Collection<String> collection1 = new ArrayList<>(); collection1.add("Apple"); collection1.add("Banana"); collection1.add("Cherry"); Collection<String> collection2 = Arrays.asList("Apple", "Cherry"); boolean result = collection1.removeAll(collection2); System.out.println("移除集合中的元素是否成功:" + result); } }
-
查询元素方法
contains(Object o)
:用于检查集合中是否包含指定的元素。如果集合中包含该元素,则返回true
;否则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionContainsExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Hello"); boolean result = collection.contains("Hello"); System.out.println("集合是否包含指定元素:" + result); } }
containsAll(Collection<?> c)
:用于检查当前集合是否包含指定集合中的所有元素。如果当前集合包含指定集合中的所有元素,则返回true
;否则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Arrays; import java.util.Collection; public class CollectionContainsAllExample { public static void main(String[] args) { Collection<String> collection1 = new ArrayList<>(); collection1.add("Apple"); collection1.add("Banana"); collection1.add("Cherry"); Collection<String> collection2 = Arrays.asList("Apple", "Banana"); boolean result = collection1.containsAll(collection2); System.out.println("集合是否包含指定集合中的所有元素:" + result); } }
size()
:返回集合中元素的数量。例如:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionSizeExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Hello"); collection.add("World"); int size = collection.size(); System.out.println("集合的大小为:" + size); } }
isEmpty()
:用于检查集合是否为空。如果集合中没有元素,则返回true
;否则返回false
。例如:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionIsEmptyExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); boolean result = collection.isEmpty(); System.out.println("集合是否为空:" + result); } }
-
集合转换方法
toArray()
:用于将集合转换为数组。它有两个重载方法,一个是Object[] toArray()
,另一个是<T> T[] toArray(T[] a)
。第一种方法返回一个包含集合中所有元素的Object
数组;第二种方法可以将集合中的元素存储到指定类型的数组中,如果指定的数组大小足够,就将元素存储进去并返回该数组,如果数组大小不够,就会创建一个新的合适大小的数组并返回。例如:
javaimport java.util.ArrayList; import java.util.Collection; public class CollectionToArrayExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Hello"); collection.add("World"); Object[] array = collection.toArray(); for (Object element : array) { System.out.println(element); } } }
-
迭代方法(间接)
:虽然 Collection 接口本身没有定义迭代方法,但通过
iterator()
方法可以获取一个迭代器(
Iterator
接口的实现),用于遍历集合中的元素。例如:
javaimport java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CollectionIteratorExample { public static void main(String[] args) { Collection<String> collection = new ArrayList<>(); collection.add("Hello"); collection.add("World"); Iterator<String> iterator = collection.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } } }
-
5.HashMap、ArrayList线程不安全如何解决?
-
解决
HashMap
线程不安全的方法-
使用
ConcurrentHashMap
- 原理 :
ConcurrentHashMap
是 Java 提供的线程安全的哈希表实现。它采用了分段锁(在 Java 7 及以前)和 CAS(Compare - And - Swap)操作结合的方式来实现高效的并发访问。在 Java 7 中,ConcurrentHashMap
将数据分为多个段(Segment),每个段有自己独立的锁,不同段之间的操作可以并发进行。在 Java 8 及以后,它在内部结构上进行了优化,采用了数组 + 链表 / 红黑树的结构,并且使用 CAS 操作来对一些操作进行无锁优化,在保证线程安全的同时,提高了并发性能。 - 示例:
javaimport java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 并发地进行put操作 new Thread(() -> { for (int i = 0; i < 1000; i++) { map.put("key" + i, i); } }).start(); new Thread(() -> { for (int i = 1000; i < 2000; i++) { map.put("key" + i, i); } }).start(); } }
- 原理 :
-
使用
Collections.synchronizedMap()
方法- 原理 :这个方法返回一个由指定
Map
对象包装后的线程安全的Map
。它通过在每个方法(如put
、get
、remove
等)上添加synchronized
关键字来实现线程安全。这意味着同一时间只有一个线程能够访问这个Map
的方法,虽然保证了线程安全,但可能会导致性能下降,尤其是在高并发场景下。 - 示例:
javaimport java.util.Collections; import java.util.HashMap; import java.util.Map; public class SynchronizedMapExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Map<String, Integer> synchronizedMap = Collections.synchronizedMap(map); // 并发地进行put操作 new Thread(() -> { for (int i = 0; i < 1000; i++) { synchronizedMap.put("key" + i, i); } }).start(); new Thread(() -> { for (int i = 1000; i < 2000; i++) { synchronizedMap.put("key" + i, i); } }).start(); } }
- 原理 :这个方法返回一个由指定
-
-
解决
ArrayList
线程不安全的方法-
使用
Vector
- 原理 :
Vector
是 Java 早期提供的线程安全的动态数组。它在方法上(如add
、get
、remove
等)都使用了synchronized
关键字来保证同一时间只有一个线程能够访问这些方法。不过,这种方式在高并发场景下可能会导致性能问题,因为所有的操作都需要获取锁。 - 示例:
javaimport java.util.Vector; public class VectorExample { public static void main(String[] args) { Vector<Integer> vector = new Vector<>(); // 并发地进行add操作 new Thread(() -> { for (int i = 0; i < 1000; i++) { vector.add(i); } }).start(); new Thread(() -> { for (int i = 1000; i < 2000; i++) { vector.add(i); } }).start(); } }
- 原理 :
-
使用
CopyOnWriteArrayList
- 原理 :
CopyOnWriteArrayList
是 Java 并发包中的一个类,它采用写时复制(Copy - On - Write)的策略来实现线程安全。在添加、删除或修改元素时,它会复制一个新的数组,在新数组上进行操作,然后将原数组引用指向新数组。这样,在读取元素时可以不用加锁,因为读操作和写操作是在不同的数组上进行的,从而提高了读取的并发性能。不过,这种方式在写操作频繁的场景下会导致性能下降,因为每次写操作都需要复制数组。 - 示例:
javaimport java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteArrayListExample { public static void main(String[] args) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); // 并发地进行add操作 new Thread(() -> { for (int i = 0; i < 1000; i++) { list.add(i); } }).start(); new Thread(() -> { for (int i = 1000; i < 2000; i++) { list.add(i); } }).start(); } }
- 原理 :
-
6.Spring AOP
-
AOP(Aspect - Oriented Programming)概述
- 定义与概念:AOP 是一种编程范式,它允许将横切关注点(Cross - Cutting Concerns)从业务逻辑中分离出来。横切关注点是那些跨越多个模块或方法的功能,如日志记录、事务管理、安全检查等。在传统的面向对象编程(OOP)中,这些功能可能会分散在各个业务方法中,导致代码的复用性差、可维护性差。AOP 通过将这些横切关注点封装成独立的模块(称为切面,Aspect),并在合适的时机(称为切点,Pointcut)将其织入(Weave)到业务逻辑中,从而实现了更好的代码模块化和复用性。
-
Spring AOP 的实现方式
-
基于代理(Proxy)的方式
- JDK 动态代理 :当被代理的目标对象(Target)实现了接口时,Spring 可以使用 JDK 动态代理来创建代理对象。JDK 动态代理是通过
java.lang.reflect.Proxy
类来实现的。它会在运行时创建一个实现了目标对象接口的代理类,这个代理类会拦截对目标对象方法的调用,并在方法调用前后执行切面逻辑。例如,假设有一个接口UserService
和它的实现类UserServiceImpl
,可以这样使用 JDK 动态代理:
javaimport java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JDKDynamicProxyExample { interface UserService { void addUser(); } static class UserServiceImpl implements UserService { @Override public void addUser() { System.out.println("添加用户"); } } static class LoggingHandler implements InvocationHandler { private Object target; public LoggingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法执行前日志记录"); Object result = method.invoke(target, args); System.out.println("方法执行后日志记录"); return result; } } public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LoggingHandler(target) ); proxy.addUser(); } }
- CGLIB 代理(字节码生成代理) :当目标对象没有实现接口时,Spring 会使用 CGLIB(Code Generation Library)来创建代理对象。CGLIB 通过字节码生成技术,在运行时生成目标对象的子类,这个子类会覆盖目标对象的方法,并在方法调用前后插入切面逻辑。例如,假设有一个没有实现接口的类
UserService
,可以这样使用 CGLIB 代理:
javaimport net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CGLIBProxyExample { static class UserService { public void addUser() { System.out.println("添加用户"); } } static class LoggingInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("方法执行前日志记录"); Object result = proxy.invokeSuper(obj, args); System.out.println("方法执行后日志记录"); return result; } } public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LoggingInterceptor()); UserService proxy = (UserService) enhancer.create(); proxy.addUser(); } }
- JDK 动态代理 :当被代理的目标对象(Target)实现了接口时,Spring 可以使用 JDK 动态代理来创建代理对象。JDK 动态代理是通过
-
-
Spring AOP 的核心概念
-
切面(Aspect):切面是一个包含了横切关注点(如日志记录、事务管理等)的模块。它是一个抽象的概念,在 Spring AOP 中,可以通过一个类来实现切面,这个类中可以包含多个通知(Advice)。例如,一个日志切面可以包含记录方法调用前、调用后、异常抛出时的日志的通知。
-
切点(Pointcut) :切点用于定义在哪些连接点(Join Point)上应用切面。连接点是指在程序执行过程中能够应用切面的点,如方法调用、方法执行结束、异常抛出等。切点可以使用表达式(如 AspectJ 切点表达式)来精确地指定哪些方法或者类应该被切面所影响。例如,
execution(* com.example.service.UserService.*(..))
这个切点表达式表示对com.example.service.UserService
类中的所有方法应用切面。 -
通知(Advice)
:通知是切面中的具体操作,它定义了在切点处应该执行的代码。Spring AOP 中有五种类型的通知:
- 前置通知(Before Advice):在目标方法调用之前执行的通知。例如,在日志切面中,可以在前置通知中记录方法开始执行的时间和参数信息。
- 后置通知(After Advice):在目标方法正常执行结束后执行的通知。可以用于记录方法的返回结果等。
- 返回通知(After - Returning Advice):和后置通知类似,但它可以访问目标方法的返回值,并且只有在目标方法正常返回时才会执行。
- 异常通知(After - Throwing Advice):在目标方法抛出异常时执行的通知。可以用于记录异常信息和进行异常处理。
- 环绕通知(Around Advice) :环绕通知可以在目标方法调用前后执行自定义的逻辑,它可以控制目标方法是否执行、何时执行以及如何执行。它是最强大的一种通知类型,通过
ProceedingJoinPoint
接口来调用目标方法。
-
-
Spring AOP 的应用场景
- 日志记录:可以将日志记录逻辑从业务方法中分离出来,通过切面在方法调用前后记录日志。这样可以方便地统一管理日志格式和内容,并且在需要修改日志记录策略时,只需要修改切面代码,而不需要在每个业务方法中进行修改。
- 事务管理:在企业级应用中,事务管理是一个重要的横切关注点。通过 Spring AOP,可以在服务层方法(如数据库操作方法)上应用事务切面,自动管理事务的开启、提交和回滚。例如,当一个业务方法包含多个数据库操作时,通过事务切面可以确保这些操作要么全部成功(提交事务),要么全部失败(回滚事务)。
- 权限验证:可以创建一个权限验证切面,在需要进行权限验证的方法调用前检查用户是否具有相应的权限。这样可以将权限验证逻辑集中在切面中,提高代码的安全性和可维护性。
7.Spring AOP 如何实现动态代理?
-
JDK 动态代理方式实现 Spring AOP
-
原理
- JDK 动态代理是基于接口的代理方式。当 Spring 确定要对一个实现了接口的目标对象进行代理时,会使用
java.lang.reflect.Proxy
类来创建代理对象。Proxy
类通过newProxyInstance
方法来生成代理对象,这个方法需要三个参数:目标对象的类加载器(ClassLoader
)、目标对象实现的接口数组(Interfaces
)和一个实现了InvocationHandler
接口的调用处理器。 - 调用处理器(
InvocationHandler
)是 JDK 动态代理的核心。在InvocationHandler
接口中有一个invoke
方法,这个方法会在代理对象的方法被调用时执行。在invoke
方法中,可以在调用目标对象的方法之前和之后添加额外的逻辑,比如记录日志、进行权限验证等,从而实现 AOP 的功能。
- JDK 动态代理是基于接口的代理方式。当 Spring 确定要对一个实现了接口的目标对象进行代理时,会使用
-
示例步骤
-
定义接口和实现类
javainterface UserService { void addUser(String name); } class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("添加用户:" + name); } }
-
创建调用处理器(
InvocationHandler
)实现类javaimport java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; class LoggingInvocationHandler implements InvocationHandler { private Object target; public LoggingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法执行前记录日志"); Object result = method.invoke(target, args); System.out.println("方法执行后记录日志"); return result; } }
-
创建代理对象并使用
javaimport java.lang.reflect.Proxy; public class JDKDynamicProxyExample { public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LoggingInvocationHandler(target) ); proxy.addUser("张三"); } }
-
在这个示例中,当通过代理对象
proxy
调用addUser
方法时,实际上会执行LoggingInvocationHandler
中的invoke
方法。在invoke
方法中,先打印了方法执行前的日志,然后调用目标对象的addUser
方法,最后打印方法执行后的日志。
-
-
-
CGLIB 动态代理方式实现 Spring AOP(字节码生成代理)
-
原理
- CGLIB(Code Generation Library)是一个强大的字节码生成库。当目标对象没有实现接口时,Spring 会使用 CGLIB 来创建代理对象。CGLIB 通过继承目标对象的方式来生成代理对象。它会在运行时生成目标对象的子类,这个子类会覆盖目标对象的方法。
- CGLIB 的核心是
net.sf.cglib.proxy.MethodInterceptor
接口,它有一个intercept
方法。在这个方法中,可以拦截目标对象的方法调用,在调用目标方法之前和之后添加额外的逻辑,实现 AOP 的功能。CGLIB 使用Enhancer
类来创建代理对象,通过设置superclass
(目标对象的类)和callback
(MethodInterceptor
实现)来生成代理对象。
-
示例步骤
-
定义目标类(没有接口)
javaclass UserService { public void addUser(String name) { System.out.println("添加用户:" + name); } }
-
创建
MethodInterceptor
实现类javaimport net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; class LoggingMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("方法执行前记录日志"); Object result = proxy.invokeSuper(obj, args); System.out.println("方法执行后记录日志"); return result; } }
-
创建代理对象并使用
javaimport net.sf.cglib.proxy.Enhancer; public class CGLIBProxyExample { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LoggingMethodInterceptor()); UserService proxy = (UserService) enhancer.create(); proxy.addUser("李四"); } }
-
在这个示例中,
Enhancer
类用于创建代理对象。设置了目标类UserService
和LoggingMethodInterceptor
作为回调。当通过代理对象proxy
调用addUser
方法时,会执行LoggingMethodInterceptor
中的intercept
方法。在intercept
方法中,先打印方法执行前的日志,然后通过proxy.invokeSuper
调用目标对象的方法,最后打印方法执行后的日志。
-
-
8.在项目中用过哪些多线程类
-
Thread
类基本使用场景 *:直接继承
Thread
类并重写run
方法是实现多线程的一种简单方式。在项目中,例如需要独立地执行一些后台任务时可以使用。假设我们有一个简单的文件读取任务,想要在一个新线程中执行,就可以这样做:javaclass FileReaderThread extends Thread { private String fileName; public FileReaderThread(String fileName) { this.fileName = fileName; } @Override public void run() { try { // 模拟文件读取操作 System.out.println("开始读取文件:" + fileName); Thread.sleep(2000); System.out.println("文件读取完成:" + fileName); } catch (InterruptedException e) { e.printStackTrace(); } } } public class ThreadExample { public static void main(String[] args) { FileReaderThread thread = new FileReaderThread("example.txt"); thread.start(); } }
- 优点和局限性 :这种方式简单直接,适合简单的多线程场景。但它的局限性在于,如果需要继承其他类,就不能再继承
Thread
类了,因为 Java 是单继承的。而且对于多个线程的管理和资源共享等操作可能会比较复杂。
- 优点和局限性 :这种方式简单直接,适合简单的多线程场景。但它的局限性在于,如果需要继承其他类,就不能再继承
-
Runnable
接口- 使用场景与示例 :实现
Runnable
接口可以避免单继承的限制。在实际项目中,当需要在多个线程中执行相同的任务逻辑时非常有用。比如,有一个网络请求任务,多个线程可以并发地发送相同类型的请求。
javaclass NetworkRequestRunnable implements Runnable { @Override public void run() { try { System.out.println("发送网络请求"); Thread.sleep(1000); System.out.println("网络请求完成"); } catch (InterruptedException e) { e.printStackTrace(); } } } public class RunnableExample { public static void main(String[] args) { Thread thread1 = new Thread(new NetworkRequestRunnable()); Thread thread2 = new Thread(new NetworkRequestRunnable()); thread1.start(); thread2.start(); } }
- 结合线程池使用 :
Runnable
接口在与线程池(ExecutorService
)结合使用时更加方便。例如,在一个服务器应用中,对于大量的客户端请求,可以使用线程池来管理Runnable
任务,提高资源利用效率。
javaimport java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class RunnableWithThreadPoolExample { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executorService.execute(new NetworkRequestRunnable()); } executorService.shutdown(); } }
- 使用场景与示例 :实现
-
Callable
接口和Future
接口Callable
接口的特点和应用场景 :Callable
接口类似于Runnable
接口,但是它可以返回一个结果并且可以抛出检查异常。在项目中,当需要在一个线程中执行一个任务并获取返回结果时,就可以使用Callable
接口。例如,在一个数据处理任务中,可能需要在一个线程中计算某个复杂的数学公式并返回结果。
javaimport java.util.concurrent.Callable; class MathCalculationCallable implements Callable<Integer> { @Override public Integer call() { try { System.out.println("开始计算数学公式"); Thread.sleep(3000); int result = 10 * 20; System.out.println("数学公式计算完成"); return result; } catch (InterruptedException e) { e.printStackTrace(); return null; } } }
Future
接口与获取结果 :Future
接口用于获取Callable
任务的结果。它提供了一些方法,如get
方法用于获取结果,isDone
方法用于检查任务是否完成等。通过ExecutorService
的submit
方法可以提交一个Callable
任务并返回一个Future
对象。
javaimport java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class CallableAndFutureExample { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(1); Callable<Integer> callable = new MathCalculationCallable(); Future<Integer> future = executorService.submit(callable); try { if (future.isDone()) { System.out.println("任务已完成,结果为:" + future.get()); } else { System.out.println("任务还未完成"); } } catch (Exception e) { e.printStackTrace(); } executorService.shutdown(); } }
-
CountDownLatch
类- 使用场景与原理 :
CountDownLatch
用于让一个或多个线程等待其他线程完成操作后再继续执行。在项目中的批量任务处理场景很有用,例如,主线程需要等待多个子线程完成数据加载任务后再进行数据合并操作。它内部有一个计数器,当一个线程完成任务后,计数器减 1,当计数器变为 0 时,等待的线程就可以继续执行。
javaimport java.util.concurrent.CountDownLatch; class DataLoaderThread implements Runnable { private CountDownLatch latch; public DataLoaderThread(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { System.out.println("开始加载数据"); Thread.sleep(2000); System.out.println("数据加载完成"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } } public class CountDownLatchExample { public static void main(String[] args) { int numThreads = 3; CountDownLatch latch = new CountDownLatch(numThreads); for (int i = 0; i < numThreads; i++) { new Thread(new DataLoaderThread(latch)).start(); } try { latch.wait(); System.out.println("所有数据加载完成,开始合并数据"); } catch (InterruptedException e) { e.printStackTrace(); } } }
- 使用场景与原理 :
-
CyclicBarrier
类- 应用场景和工作方式 :
CyclicBarrier
用于让一组线程在到达某个屏障点时互相等待,直到所有线程都到达后再一起继续执行。在并行计算场景中很有用,例如,在一个分布式计算任务中,多个计算节点完成自己的部分计算后,在一个屏障点等待其他节点,然后一起进行结果汇总。
javaimport java.util.concurrent.CyclicBarrier; class ParallelCalculationThread implements Runnable { private CyclicBarrier barrier; public ParallelCalculationThread(CyclicBarrier barrier) { this.barrier = barrier; } @Override public void run() { try { System.out.println("开始并行计算"); Thread.sleep(2000); System.out.println("并行计算完成,等待其他线程"); barrier.await(); System.out.println("所有线程完成计算,开始汇总结果"); } catch (Exception e) { e.printStackTrace(); } } } public class CyclicBarrierExample { public static void main(String[] args) { int numThreads = 3; CyclicBarrier barrier = new CyclicBarrier(numThreads); for (int i = 0; i < numThreads; i++) { new Thread(new ParallelCalculationThread(barrier)).start(); } } }
- 应用场景和工作方式 :
9.RPC有了解吗?让你设计一个RPC框架,怎么设计?
-
RPC(Remote Procedure Call)概述
- 定义与原理:RPC 是一种进程间通信(IPC)技术,它允许一台计算机(客户端)上的程序调用另一台计算机(服务器)上的子程序,就好像调用本地函数一样。其基本原理是通过网络将调用请求从客户端发送到服务器,服务器执行相应的函数并将结果返回给客户端。在这个过程中,涉及到数据的序列化和反序列化、网络传输协议等多个环节。
-
设计一个 RPC 框架的主要步骤和要点
-
服务定义(接口定义)
- 目的:明确客户端和服务器之间的通信接口,使得双方对于调用的方法、参数和返回值等有共同的理解。
- 方式 :可以使用接口定义语言(IDL),如 Protocol Buffers、Thrift 等。以 Protocol Buffers 为例,首先定义一个
.proto
文件,例如定义一个简单的用户服务接口:
protobufsyntax = "proto3"; package user_service; service UserService { rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse); } message GetUserInfoRequest { int32 user_id = 1; } message GetUserInfoResponse { string name = 1; int32 age = 2; }
然后通过 Protocol Buffers 的编译器生成对应的 Java(或其他语言)代码,这些代码包含了接口定义和消息结构,用于在客户端和服务器之间进行通信。
-
通信协议选择与实现
- 选择合适的协议:可以选择基于 TCP 或 UDP 的协议。TCP 协议提供可靠的、面向连接的通信,适合大多数 RPC 场景;UDP 协议则更轻量级,适用于对实时性要求高但对可靠性要求稍低的场景。
- 自定义协议头部:为了更好地支持 RPC 通信,通常需要在传输层协议基础上定义自己的协议头部。协议头部可以包含一些元信息,如消息长度、消息类型(请求 / 响应)、序列号等。例如,一个简单的协议头部定义如下:
javaclass RpcProtocolHeader { private int length; private byte messageType; private int sequenceNumber; // 构造函数、getter和setter方法等 }
- 网络传输层实现 :使用 Java 的 Socket(基于 TCP)或者 DatagramSocket(基于 UDP)来实现网络通信。以 TCP 为例,在服务器端可以通过
ServerSocket
监听端口,当有客户端连接时,获取Socket
对象并进行数据读取和写入。在客户端则通过Socket
连接服务器,并发送和接收数据。
-
序列化和反序列化机制
- 选择序列化方式 :序列化是将对象转换为字节流以便在网络上传输的过程,反序列化则是相反的过程。可以选择成熟的序列化框架,如 Protocol Buffers、Thrift、JSON 序列化(如 Jackson、Gson)或者 Java 自带的序列化(
java.io.Serializable
)。不同的序列化方式有不同的性能、兼容性和数据格式特点。例如,Protocol Buffers 在性能和数据紧凑性方面表现较好,适用于对性能要求较高的场景。 - 与通信协议结合:在发送请求之前,需要将请求对象(包括方法名、参数等)进行序列化,并将序列化后的字节流发送到服务器。在服务器端,接收到字节流后进行反序列化,得到请求对象并执行相应的方法。同样,服务器的响应也需要经过序列化和反序列化的过程。
- 选择序列化方式 :序列化是将对象转换为字节流以便在网络上传输的过程,反序列化则是相反的过程。可以选择成熟的序列化框架,如 Protocol Buffers、Thrift、JSON 序列化(如 Jackson、Gson)或者 Java 自带的序列化(
-
服务注册与发现
- 服务注册中心(Registry)设计:建立一个服务注册中心,用于存储服务提供者(服务器)的信息,如服务名称、IP 地址、端口号等。可以使用 ZooKeeper、Consul 等分布式协调工具来实现服务注册中心。以 ZooKeeper 为例,服务提供者在启动时将自己的服务信息注册到 ZooKeeper 的一个节点下,例如可以按照服务名称创建一个 ZNode,在 ZNode 中存储服务提供者的 IP 地址和端口号等信息。
- 服务发现机制:客户端在调用服务之前,需要从服务注册中心查找服务提供者的信息。可以通过 ZooKeeper 的客户端 API 来监听服务节点的变化。当客户端需要调用某个服务时,从服务注册中心获取服务提供者的列表,根据一定的负载均衡策略(如轮询、随机等)选择一个服务提供者进行通信。
-
负载均衡策略
- 多种策略实现:在有多个服务提供者的情况下,需要设计负载均衡策略来合理分配请求。常见的负载均衡策略包括轮询(Round - Robin)、随机(Random)、加权轮询(Weighted Round - Robin)和最小连接数(Least - Connections)等。例如,轮询策略可以通过一个计数器来记录上次选择的服务提供者,每次请求时按照顺序选择下一个服务提供者。
- 与服务发现结合:在服务发现过程中应用负载均衡策略。当客户端从服务注册中心获取到服务提供者列表后,根据负载均衡策略选择一个服务提供者进行通信。这样可以提高系统的整体性能和可用性,避免某个服务提供者负载过重。
-
客户端和服务器端的实现框架
- 客户端框架:客户端框架主要负责封装 RPC 调用的过程,使得用户可以像调用本地函数一样调用远程服务。它包括服务发现、请求序列化、网络发送、响应接收和反序列化等功能。例如,客户端可以提供一个简单的代理(Proxy)类,这个代理类实现了服务定义的接口,在接口方法中完成 RPC 调用的一系列操作。
- 服务器端框架:服务器端框架负责接收客户端的请求、反序列化请求、执行相应的服务方法、序列化响应并返回给客户端。服务器端可以通过反射机制来根据请求中的方法名和参数类型调用对应的服务方法。同时,需要处理多客户端并发请求的情况,可以使用线程池来提高并发处理能力。例如,服务器端可以监听指定端口,当收到客户端请求时,从线程池中获取一个线程来处理该请求。
-
10.分布式架构用过吗?如何使用分布式
- 分布式架构概述
- 定义与优势:分布式架构是一种将一个应用系统拆分成多个可独立部署的子系统(服务),这些子系统通过网络进行通信和协作,共同完成业务功能的架构风格。其主要优势包括提高系统的可扩展性、容错性和性能。例如,在电商系统中,订单服务、用户服务、商品服务等可以拆分成独立的服务,分别部署在不同的服务器上,这样当业务量增长时,可以方便地对各个服务进行扩展,而不是对整个系统进行扩展。
- 分布式架构的使用方式
- 服务拆分与独立部署
- 服务拆分原则:根据业务功能和边界进行服务拆分。以一个内容管理系统为例,可以将内容创作、内容审核、内容发布等功能拆分成独立的服务。每个服务应该有明确的职责范围,例如,内容创作服务负责作者创作文章、插入图片等操作,内容审核服务专注于对创作好的内容进行合规性审核,内容发布服务则将审核通过的内容发布到网站或其他平台上。
- 独立部署和运维:拆分后的服务可以独立进行部署,这样可以使用不同的技术栈、数据库等来满足每个服务的特定需求。例如,内容创作服务可能使用一种适合文本编辑的框架和数据库,而内容发布服务可能需要与网站服务器和 CDN(内容分发网络)集成,采用不同的部署方式和运维策略。
- 服务通信与协作
- 选择合适的通信协议:在分布式服务之间,常用的通信协议包括 RESTful API(基于 HTTP 协议)、gRPC(高性能 RPC 框架)、消息队列(如 RabbitMQ、Kafka)等。如果服务之间的通信需要符合互联网标准和易于跨平台集成,RESTful API 是一个不错的选择。例如,在一个移动应用后端系统中,用户服务可以通过 RESTful API 向订单服务提供用户信息,方便不同的移动客户端(如 iOS 和 Android)访问。而如果对性能和效率要求较高,并且服务之间是内部调用关系,gRPC 可能更合适。
- 数据一致性和事务处理:在分布式环境下,数据分布在不同的服务和数据库中,保证数据一致性是一个挑战。可以采用分布式事务处理技术,如两阶段提交(2PC)、补偿事务等。但这些技术往往会带来性能和复杂性的问题。另一种方式是采用最终一致性的策略,例如在电商系统中,当用户下单后,订单服务记录订单信息,库存服务可以异步地更新库存,在一定时间内(如几秒或几分钟)通过消息队列等方式来保证库存数据最终与订单数据一致。
- 分布式数据存储与管理
- 数据库分区与分片:对于大规模数据存储,可以采用数据库分区(根据数据的某个属性,如时间范围、地理位置等将数据划分到不同的存储区域)和分片(将数据分散存储到多个数据库节点)的技术。例如,在一个全球范围的日志存储系统中,可以根据日志产生的地区将数据分片存储到不同的数据库服务器上,每个地区的服务器负责存储本地产生的日志数据,这样可以提高数据存储的可扩展性和查询性能。
- 分布式缓存的使用:引入分布式缓存(如 Redis)可以提高系统的性能。缓存经常访问的数据,如热门商品信息、用户权限信息等。例如,在电商系统中,商品详情页的访问频率很高,将商品的基本信息(名称、价格、图片等)存储在 Redis 缓存中,当用户请求商品详情页时,先从缓存中获取数据,如果缓存中没有再从数据库中查询,这样可以大大减少数据库的压力,提高系统的响应速度。
- 分布式配置管理
- 配置中心的建立:使用分布式配置中心(如 Spring Cloud Config、Apollo)来集中管理各个服务的配置信息。配置中心可以存储服务的数据库连接信息、缓存配置、日志级别等各种配置。例如,当需要修改数据库连接字符串时,只需要在配置中心修改相关配置,各个服务可以自动获取更新后的配置,而不需要逐个服务进行修改,提高了配置管理的效率和灵活性。
- 配置的动态更新与热加载:配置中心应该支持配置的动态更新,并且服务能够在运行时热加载新的配置。例如,通过配置中心的推送机制或者服务定期拉取配置的方式,当配置发生变化时,服务能够及时感知并应用新的配置,而不需要重启服务,减少了对业务的影响。
- 分布式系统的监控与治理
- 服务监控指标:建立全面的监控体系,包括服务的性能指标(如响应时间、吞吐量)、资源使用情况(如 CPU 使用率、内存占用)、错误率等。可以使用开源的监控工具(如 Prometheus、Grafana)来收集和展示这些指标。例如,通过在每个服务中嵌入监控客户端,收集服务的响应时间和错误率数据,将这些数据发送到 Prometheus 服务器,然后在 Grafana 中进行可视化展示,方便运维人员及时发现服务的性能问题和故障。
- 服务治理策略:实施服务治理措施,如服务限流(防止某个服务被过多请求压垮)、熔断机制(当某个服务出现故障时,快速切断对其的调用,避免故障扩散)、服务降级(在系统压力大时,暂时关闭一些非核心功能,保证核心功能的正常运行)等。例如,在电商系统的促销活动期间,可能会出现大量用户访问订单服务的情况,通过服务限流可以限制每秒访问订单服务的请求数量,保证服务的稳定性。同时,如果库存服务出现故障,可以通过熔断机制暂时停止对库存服务的调用,使用缓存中的库存数据或者默认库存值来进行订单处理,实现服务降级。
- 服务拆分与独立部署
11.分布式锁有了解过吗
- 分布式锁概述
- 定义与作用:分布式锁是一种用于在分布式系统环境下,控制多个节点(进程或线程)对共享资源访问的机制。在分布式系统中,由于资源(如数据库记录、文件、缓存数据等)可能被多个节点同时访问,为了避免数据不一致、冲突等问题,需要一种跨节点的锁来协调对这些资源的访问,分布式锁就起到了这个作用。例如,在一个电商系统中,多个节点可能会同时对商品库存进行操作,通过分布式锁可以保证同一时刻只有一个节点能够修改库存数量,从而保证库存数据的准确性。
- 常见的分布式锁实现方式
- 基于数据库实现分布式锁
- 悲观锁方式 :可以利用数据库的排它锁(例如在 MySQL 中使用
FOR UPDATE
语句)来实现。当一个节点需要访问共享资源时,它在数据库中执行一条带有FOR UPDATE
的查询语句来锁住相关的记录。其他节点在尝试获取锁时,会被阻塞,直到持有锁的节点释放锁。例如,在一个分布式任务调度系统中,对于任务执行记录的互斥访问,可以通过这种方式实现。不过,这种方式可能会导致数据库性能问题,因为长时间的锁等待会占用数据库连接等资源。 - 乐观锁方式 :通过在数据库表中添加一个版本号(
version
)字段来实现。节点在更新数据时,会检查版本号是否与自己读取时一致,如果一致则更新数据并递增版本号,否则说明数据已经被其他节点修改,需要重新获取最新的数据并尝试更新。这种方式适用于读多写少的场景,减少了锁等待的时间,但在高并发写操作场景下可能会导致更新失败次数较多。
- 悲观锁方式 :可以利用数据库的排它锁(例如在 MySQL 中使用
- 基于 Redis 实现分布式锁
- SETNX 命令实现简单锁 :Redis 的
SETNX
(SET if Not eXists)命令是实现分布式锁的基础操作。当一个节点想要获取锁时,使用SETNX
命令设置一个特定的键(这个键代表锁),如果键不存在,则设置成功,表示获取锁;如果键已经存在,则设置失败,表示锁已经被其他节点获取。例如,在一个分布式缓存系统中,当需要更新缓存数据时,可以使用SETNX
来获取锁,防止多个节点同时更新缓存导致数据不一致。 - 带有过期时间的锁及安全性增强 :为了避免节点在获取锁后出现故障导致锁无法释放,需要给锁设置一个过期时间。可以使用
SET
命令的扩展参数来同时实现锁的获取和过期时间设置,如SET key value NX PX timeout
,其中NX
表示只有当键不存在时才设置,PX
用于设置过期时间(以毫秒为单位),timeout
是过期时间的值。同时,为了保证锁释放的安全性,可以使用 Lua 脚本在释放锁时进行原子性的检查和删除操作。 - RedLock 算法:在分布式环境下,单个 Redis 节点可能会出现故障或网络分区等问题,RedLock 算法通过在多个独立的 Redis 节点(通常是 5 个)上获取锁来提高可靠性。节点需要在多数(超过一半)的 Redis 节点上成功获取锁才能认为获取分布式锁成功。这种方式提高了分布式锁的可用性和容错性,但也增加了实现的复杂性和对 Redis 节点时间同步的要求。
- SETNX 命令实现简单锁 :Redis 的
- 基于 ZooKeeper 实现分布式锁
- 临时顺序节点方式:ZooKeeper 是一个分布式协调服务,它提供了强大的节点管理功能。在 ZooKeeper 中,可以通过创建临时顺序节点来实现分布式锁。当一个节点需要获取锁时,它在 ZooKeeper 的一个指定节点下创建一个临时顺序节点。然后,节点会检查自己创建的节点是否是所有子节点中最小的,如果是,则表示获取锁成功;如果不是,则监听比自己小的节点的删除事件,当比自己小的节点被删除时,再次检查自己是否是最小的节点来获取锁。这种方式利用了 ZooKeeper 的节点顺序性和临时节点的特性,保证了锁的公平性和自动释放(当节点与 ZooKeeper 的会话断开时,临时节点会自动被删除)。例如,在一个分布式文件系统中,多个节点对同一个文件进行读写操作时,可以使用这种方式来协调访问顺序。
- 基于数据库实现分布式锁
12.双亲委派机制
-
双亲委派机制的定义
- 双亲委派机制是 Java 类加载器(ClassLoader)的一种工作机制。它规定了一个类加载器在收到类加载请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,才会由子类加载器自己去加载。
-
类加载器的层次结构与双亲委派过程
-
类加载器层次结构
:在 Java 中有三种类加载器,它们构成了一个层次关系。
- 启动类加载器(Bootstrap ClassLoader) :它是最顶层的类加载器,负责加载 Java 的核心类库,比如
java.lang.Object
、java.util.Date
等这些存放在<JAVA_HOME>/lib
目录下的类。启动类加载器是由 C++ 编写的,在 Java 代码中无法直接获取它的引用。 - 扩展类加载器(Extension ClassLoader) :它的父加载器是启动类加载器,负责加载
<JAVA_HOME>/lib/ext
目录下的类库或者由java.ext.dirs
系统属性指定路径中的类库。这些类库主要是 Java 的扩展类,比如一些标准的 Java 扩展功能相关的类。 - 应用程序类加载器(Application ClassLoader) :也称为系统类加载器,它的父加载器是扩展类加载器。它负责加载用户类路径(
classpath
)上的类,也就是我们自己编写的 Java 类。在大多数情况下,如果没有特殊的类加载器设置,我们编写的 Java 类都是由应用程序类加载器加载的。
- 启动类加载器(Bootstrap ClassLoader) :它是最顶层的类加载器,负责加载 Java 的核心类库,比如
-
双亲委派机制的工作流程示例
:假设现在要加载一个自定义的
com.example.MyClass
类。
- 首先,这个加载请求会被发送到应用程序类加载器。应用程序类加载器会先查看自己是否已经加载过这个类,如果没有,它不会直接去查找
com.example.MyClass
,而是把这个加载请求委派给它的父加载器,也就是扩展类加载器。 - 扩展类加载器收到请求后,同样先检查自己是否已经加载过这个类,若没有,则把请求继续委派给它的父加载器,即启动类加载器。
- 启动类加载器在自己负责的核心类库范围内查找
com.example.MyClass
,由于这个类不在核心类库中,启动类加载器无法加载,它会反馈给扩展类加载器,表示自己无法完成这个加载任务。 - 扩展类加载器在自己负责的扩展类库范围查找,也找不到
com.example.MyClass
,于是它也反馈给应用程序类加载器,表示自己无法加载。 - 最后,应用程序类加载器在用户类路径下查找
com.example.MyClass
,如果能找到就进行加载。
- 首先,这个加载请求会被发送到应用程序类加载器。应用程序类加载器会先查看自己是否已经加载过这个类,如果没有,它不会直接去查找
-
-
双亲委派机制的优势
- 安全性保障 :通过双亲委派机制,保证了 Java 核心类库的安全性。例如,用户自己定义了一个
java.lang.Object
类,如果没有双亲委派机制,应用程序类加载器可能会加载这个自定义的类,这就可能会导致系统的混乱。但有了双亲委派机制,由于加载java.lang.Object
类的请求会先委派给启动类加载器,而启动类加载器会从 Java 核心类库中加载正确的java.lang.Object
类,从而避免了用户自定义的类干扰核心类库。 - 避免类的重复加载 :因为类加载请求会按照层次结构向上委派,只有在父类加载器无法加载时才会由子类加载器加载。这样可以确保一个类在整个虚拟机环境中只会被加载一次。例如,
java.util.Date
类只会由启动类加载器加载一次,而不会被不同的类加载器多次加载,从而节省了内存资源,也避免了因多次加载可能导致的版本不一致等问题。
- 安全性保障 :通过双亲委派机制,保证了 Java 核心类库的安全性。例如,用户自己定义了一个
13.JVM的栈堆 都是干嘛的?
-
JVM 栈(Java Virtual Machine Stack)
-
基本功能与存储内容
- JVM 栈主要用于存储方法的局部变量表、操作数栈、动态连接、方法出口等信息。它是线程私有的,每个线程在创建时都会创建一个自己的 JVM 栈。局部变量表用于存放方法中的局部变量,包括基本数据类型(如
int
、double
、boolean
等)和对象引用。例如,在下面这个方法中:
javapublic void method() { int a = 10; String str = "Hello"; // 方法体 }
变量
a
(基本数据类型)和str
(对象引用)就存储在当前线程的 JVM 栈的局部变量表中。操作数栈主要用于在方法执行过程中进行算术运算、方法调用等操作时,临时存储操作数和运算结果。 - JVM 栈主要用于存储方法的局部变量表、操作数栈、动态连接、方法出口等信息。它是线程私有的,每个线程在创建时都会创建一个自己的 JVM 栈。局部变量表用于存放方法中的局部变量,包括基本数据类型(如
-
方法调用与栈帧变化
- 当一个方法被调用时,JVM 会为这个方法创建一个栈帧(Stack Frame),并将其压入 JVM 栈。栈帧是 JVM 栈的基本数据单元,它包含了上述提到的局部变量表、操作数栈等信息。例如,当
methodA
调用methodB
时,JVM 会先为methodA
创建一个栈帧并压入栈,当开始执行methodB
时,又会为methodB
创建一个栈帧并压入栈。当methodB
执行完毕后,它对应的栈帧会从栈顶弹出,然后methodA
继续执行。这种栈帧的压入和弹出操作保证了方法调用的顺序和返回机制。
- 当一个方法被调用时,JVM 会为这个方法创建一个栈帧(Stack Frame),并将其压入 JVM 栈。栈帧是 JVM 栈的基本数据单元,它包含了上述提到的局部变量表、操作数栈等信息。例如,当
-
栈的大小限制与异常情况
- JVM 栈的大小是可以设置的(例如通过
-Xss
参数),如果一个方法的栈帧大小超过了栈的剩余空间,就会抛出StackOverflowError
异常。例如,下面这个递归方法如果没有正确的终止条件,就可能会导致栈溢出:
javapublic void recursiveMethod() { recursiveMethod(); }
另外,如果 JVM 栈的内存空间耗尽(比如创建了太多的线程,每个线程都有自己的 JVM 栈),也可能会出现
OutOfMemoryError
异常。 - JVM 栈的大小是可以设置的(例如通过
-
-
JVM 堆(Heap)
-
存储对象与内存分配
- JVM 堆是 Java 虚拟机所管理的内存中最大的一块,它主要用于存储对象实例。在 Java 程序中,通过
new
关键字创建的对象都会被分配到堆内存中。例如,Object obj = new Object();
这个语句就会在堆中分配一块内存来存储Object
这个实例。堆内存的分配是由 JVM 的垃圾回收器(Garbage Collector)来管理的,它会根据对象的存活状态来回收不再使用的对象占用的内存空间,以实现内存的高效利用。
- JVM 堆是 Java 虚拟机所管理的内存中最大的一块,它主要用于存储对象实例。在 Java 程序中,通过
-
分代存储与垃圾回收策略相关内容
- 为了更好地进行垃圾回收,JVM 堆通常被划分为不同的代(Generation),主要包括新生代(Young Generation)和老年代(Old Generation)。新生代又可以细分为 Eden 区和两个 Survivor 区(如
Survivor0
和Survivor1
)。大部分新创建的对象首先会被分配到 Eden 区,当 Eden 区满时,会触发一次 Minor GC(新生代垃圾回收),存活的对象会被移动到 Survivor 区或者晋升到老年代。老年代主要存放经过多次 Minor GC 后仍然存活的对象或者大对象。这种分代存储和垃圾回收策略可以根据对象的生命周期特点来提高垃圾回收的效率。例如,由于大多数对象都是 "朝生暮死" 的,在新生代进行频繁的垃圾回收可以快速回收大量不再使用的内存空间。
- 为了更好地进行垃圾回收,JVM 堆通常被划分为不同的代(Generation),主要包括新生代(Young Generation)和老年代(Old Generation)。新生代又可以细分为 Eden 区和两个 Survivor 区(如
-
堆内存溢出情况
- 如果在堆中创建的对象太多,导致堆内存无法满足对象的分配需求,就会抛出
OutOfMemoryError
异常。这可能是因为程序中存在内存泄漏(如对象被创建后,由于某些原因一直无法被垃圾回收)或者对大量数据处理时没有合理地管理内存等原因造成的。例如,下面这个简单的示例,如果不断地向一个列表中添加对象,最终可能会导致堆内存溢出:
javaimport java.util.ArrayList; import java.util.List; public class HeapOverflowExample { public static void main(String[] args) { List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); } } }
- 如果在堆中创建的对象太多,导致堆内存无法满足对象的分配需求,就会抛出
-
14.gc中如何判断对象是垃圾?
-
引用计数法(Reference Counting)
- 基本原理:引用计数法是一种简单的垃圾判断方法。它的原理是为每个对象添加一个引用计数器,当有一个地方引用这个对象时,计数器的值就加 1;当引用失效(如引用变量超出作用域或者被重新赋值)时,计数器的值就减 1。当对象的引用计数器的值为 0 时,就表示这个对象是垃圾,可以被回收。例如,在下面的代码片段中:
javaclass ObjectWithCounter { private int referenceCount = 0; public void incrementReference() { referenceCount++; } public void decrementReference() { referenceCount--; } public boolean isGarbage() { return referenceCount == 0; } } public class ReferenceCountingExample { public static void main(String[] args) { ObjectWithCounter obj = new ObjectWithCounter(); obj.incrementReference(); // 其他操作 obj.decrementReference(); if (obj.isGarbage()) { // 这里可以进行垃圾回收相关操作 } } }
- 优点与缺点 :引用计数法的优点是实现简单,判断效率高,并且能够实时地回收垃圾对象。但是它有一个致命的缺点,就是无法解决循环引用的问题。例如,有两个对象
A
和B
,A
引用B
,B
也引用A
,这两个对象的引用计数都不会为 0,即使它们在程序中已经无法被访问到,也不会被回收。
-
可达性分析(Reachability Analysis)
- 基本原理:可达性分析是目前主流的 JVM 判断对象是否为垃圾的方法。它的基本思想是从一系列被称为 "GC Roots"(垃圾收集根节点)的对象开始,通过引用关系向下搜索,搜索所走过的路径称为 "引用链"。如果一个对象到任何一个 GC Roots 都没有引用链相连,那么这个对象就是不可达的,就可以被判定为垃圾。GC Roots 对象包括虚拟机栈(栈帧中的本地变量表)中的引用对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI(Java Native Interface)引用的对象。例如,在一个方法执行过程中,方法中的局部变量所引用的对象就是通过虚拟机栈中的引用与 GC Roots 相连的。
- 工作过程示例 :假设我们有一个简单的 Java 程序,包含一个
main
方法和几个类。在main
方法中有一个局部变量obj
引用了一个对象A
,那么A
就可以通过虚拟机栈中的这个引用链与 GC Roots 相连。如果obj
变量超出了main
方法的作用域,那么这个引用链就断了,对象A
就可能会被判定为不可达对象。在实际的 JVM 垃圾回收过程中,会在安全点(Safe Point)暂停所有的用户线程,然后进行可达性分析,标记出所有可达的对象,未被标记的对象就是垃圾对象,后续会被垃圾回收器回收。 - 优势:可达性分析能够很好地解决循环引用的问题。因为只要对象无法从 GC Roots 到达,即使存在循环引用,也会被判定为垃圾。这种方法能够更准确地判断对象是否真正可以被回收,是一种比较全面和有效的垃圾判断方法。
14.JDK8后,JVM有哪些变化?
- 元空间(Metaspace)取代永久代(PermGen)
- 背景与原因:在 JDK 8 之前,方法区(存储类信息、常量池、静态变量等)是通过永久代实现的,它是堆内存的一部分。但是永久代存在一些问题,比如容易出现内存溢出,并且其大小是固定的(虽然可以通过参数调整,但调整起来比较复杂)。JDK 8 之后,引入了元空间来代替永久代。元空间并不在堆内存中,而是使用本地内存(Native Memory)。
- 优势与工作方式 :元空间的大小仅受本地内存限制,这使得它能够处理更多的类信息,减少了因永久代大小限制而导致的
OutOfMemoryError
。它的工作方式是在类加载时,将类的元数据信息(如类的版本、方法、字段等信息)存储到元空间中。当一个类被卸载时,其对应的元数据也会从元空间中释放。例如,在一个动态加载大量类的应用场景中,如插件化系统或者某些应用服务器,元空间能够更好地适应这种变化,而不会像永久代那样容易出现内存不足的情况。
- 默认垃圾回收器的改变
- Parallel Scavenge(新生代) + Parallel Old(老年代)组合成为默认:在 JDK 8 中,Parallel Scavenge(用于新生代垃圾回收)和 Parallel Old(用于老年代垃圾回收)组合成为默认的垃圾回收器。Parallel Scavenge 是一种吞吐量优先的垃圾回收器,它的目标是在单位时间内,让用户代码获得尽可能高的执行时间占比。Parallel Old 则是与 Parallel Scavenge 配合的老年代垃圾回收器,它在回收老年代时也采用多线程并行的方式,这两个垃圾回收器组合使用能够提高垃圾回收的效率,适合在多核 CPU 环境下对吞吐量要求较高的应用场景。
- 其他垃圾回收器的增强与发展趋势:除了默认的垃圾回收器组合外,JDK 8 还对其他垃圾回收器进行了增强。例如,G1 垃圾回收器(Garbage - First)在 JDK 8 中也得到了更多的应用和优化。G1 是一种面向服务端应用的垃圾回收器,它主要的特点是将堆内存划分为多个大小相等的 Region,在进行垃圾回收时,能够优先回收垃圾最多的 Region,并且可以预测垃圾回收的停顿时间。这种特性使得 G1 更适合对响应时间要求较高的应用场景,在 JDK 8 之后,其性能和稳定性也在不断地提升。
- 接口默认方法和静态方法的支持
- 对 Java 编程的影响 :JDK 8 允许在接口中添加默认方法和静态方法。默认方法使用
default
关键字定义,它使得接口可以有默认的实现,这样在接口更新添加新方法时,不会破坏已有的实现类。静态方法则可以通过接口名直接调用,类似于类中的静态方法。这改变了 Java 接口的传统设计模式,使得接口在 Java 8 之后更具灵活性。例如,在 Java 8 之前,如果一个接口添加了新的方法,所有实现这个接口的类都需要实现这个新方法;而有了默认方法后,接口可以提供一个默认的实现,实现类可以选择是否覆盖这个默认实现。 - 在 JVM 层面的实现细节:在 JVM 层面,对于接口默认方法和静态方法的调用有了新的字节码指令来支持。在类加载和方法调用过程中,JVM 能够正确地识别和执行接口中的这些新类型的方法。例如,当调用一个接口的默认方法时,JVM 会根据具体的实现类和接口之间的关系,查找并执行对应的默认方法的字节码。
- 对 Java 编程的影响 :JDK 8 允许在接口中添加默认方法和静态方法。默认方法使用
- Lambda 表达式和函数式接口相关优化
- Lambda 表达式的存储与执行 :JDK 8 引入了 Lambda 表达式,它是一种匿名函数。在 JVM 中,Lambda 表达式被编译成字节码后,会生成一个匿名内部类(在某些情况下)或者采用一种特殊的字节码指令来存储和执行。对于函数式接口(只包含一个抽象方法的接口),Lambda 表达式可以作为其实现。例如,在使用
java.util.stream.Stream
接口进行操作时,常常会使用 Lambda 表达式来定义过滤、映射等操作,这些 Lambda 表达式在 JVM 中会被有效地处理,以实现函数式编程的功能。 - 性能优化措施:JVM 在处理 Lambda 表达式和函数式接口时,会进行一些性能优化。例如,在方法调用时,如果一个函数式接口的实现是通过 Lambda 表达式提供的,JVM 可能会采用内联(Inline)的方式来提高方法调用的效率。内联是将被调用的方法的代码直接嵌入到调用处,减少了方法调用的开销。同时,对于频繁使用的 Lambda 表达式,JVM 也会进行一些缓存等优化措施,以提高程序的整体性能。
- Lambda 表达式的存储与执行 :JDK 8 引入了 Lambda 表达式,它是一种匿名函数。在 JVM 中,Lambda 表达式被编译成字节码后,会生成一个匿名内部类(在某些情况下)或者采用一种特殊的字节码指令来存储和执行。对于函数式接口(只包含一个抽象方法的接口),Lambda 表达式可以作为其实现。例如,在使用