一.基础
算法
1.二分查找
排序,定义左右边界,定义中间索引,在使用中间索引与查找的数进行比较
public class BinarySearch { public static int binarySearch(int[] arr, int target) { int low = 0; int high = arr.length - 1; while (low <= high) { int mid = (low + high) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return -1; } public static void main(String[] args) { int[] arr = {2, 4, 6, 8, 10}; int target = 8; int result = binarySearch(arr, target); if (result != -1) { System.out.println("Element found at index " + result); } else { System.out.println("Element not found in the array"); } } }
2.冒泡排序
就是对于一个数组进行排序,前一个大于后一个进行交换
public class BubbleSort { public static void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { // 交换 arr[j] 和 arr[j+1] int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } public static void main(String[] args) { int[] arr = {64, 34, 25, 12, 22, 11, 90}; bubbleSort(arr); System.out.println("排序后的数组:"); for (int num : arr) { System.out.print(num + " "); } } }
3.选择排序
找出最小值进行排序
public class SelectionSort { public static void selectionSort(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { int minIndex = i; for (int j = i + 1; j < n; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } // 交换 arr[i] 和 arr[minIndex] int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } public static void main(String[] args) { int[] arr = {64, 25, 12, 22, 11}; selectionSort(arr); System.out.println("排序后的数组:"); for (int num : arr) { System.out.print(num + " "); } } }
4.插入排序
前后元素进行比较排序
public class InsertionSort { 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; } } public static void main(String[] args) { int[] arr = {64, 25, 12, 22, 11}; insertionSort(arr); System.out.println("排序后的数组:"); for (int num : arr) { System.out.print(num + " "); } } }
5.希尔排序
总数除以2间隔排序,在利用总数除以4
public class ShellSort { public static void shellSort(int[] arr) { int n = arr.length; // Start with a large gap, then reduce the gap for (int gap = n/2; gap > 0; gap /= 2) { // Perform insertion sort on elements at the gap intervals for (int i = gap; i < n; i++) { int temp = arr[i]; int j; for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = temp; } } } public static void main(String[] args) { int[] arr = {64, 25, 12, 22, 11}; shellSort(arr); System.out.println("Sorted array:"); for (int num : arr) { System.out.print(num + " "); } } }
6.快速排序
选择一个基准点分区排序,在循环排序,基准点以总数除以2
单边循环
public class UnilateralLoop { public static void main(String[] args) { int count = 0; do { System.out.println("Count: " + count); count++; } while (count < 1); System.out.println("Loop completed"); } }
双边循环
public class BilateralLoop { public static void main(String[] args) { for (int i = 0; i < 5; i++) { System.out.println("Count: " + i); } System.out.println("Loop completed"); } }
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); } } public 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++; swap(arr, i, j); } } swap(arr, i + 1, high); return i + 1; } public 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 = {64, 25, 12, 22, 11}; int n = arr.length; quickSort(arr, 0, n - 1); System.out.println("Sorted array:"); for (int num : arr) { System.out.print(num + " "); } } }
集合
1.ArrayList
-
扩容机制 第一次是0扩容首次10之后都是1.5倍
ArrayList是Java集合框架中的一个动态数组实现类。它的扩容机制如下:
-
初始容量:当你创建一个ArrayList对象时,它会自动分配一个初始容量。默认情况下,初始容量为10。
-
当添加元素时,ArrayList会检查当前元素个数是否已经达到容量上限。如果元素个数已经达到容量上限,就需要进行扩容。
-
扩容机制:当需要扩容时,ArrayList会创建一个新的内部数组,并将原有的元素拷贝到新数组中。新数组的大小通常是当前容量的1.5倍。
-
内存开销:在扩容过程中,ArrayList需要分配新的内存空间,并复制原有元素到新的内存空间中。这个过程可能会导致一定的内存开销。
-
扩容频率:ArrayList的扩容频率取决于你添加的元素个数和操作方式。当你添加大量元素时,可能会涉及多次扩容操作,从而影响性能。为了避免频繁的扩容,你可以在创建ArrayList时指定一个较合适的初始容量,以减少扩容次数。
总结起来,ArrayList的扩容机制是在需要添加元素时,检查当前容量是否足够,如果不够则自动扩容。扩容过程中,会创建一个新的更大容量的数组,并将原有元素拷贝到新数组中,从而实现数组的动态增长。
2.Iterator
FailFast源码
-
在发生异常时下次才会报错
在Java中,"fail-fast"是一种迭代器(Iterator)的行为模式。它的基本思想是在迭代过程中,如果集合的结构发生了变化,通过抛出ConcurrentModificationException异常来立即失败,而不是继续迭代可能产生不确定结果的数据。
当你使用迭代器遍历一个集合时,如果在遍历过程中对集合进行了结构性修改(例如添加、删除元素),那么就会导致迭代器的"fail-fast"机制发生作用。
这个机制是通过迭代器内部维护的一个计数器(称为"modCount")来实现的。在每次修改集合结构时,这个计数器的值都会递增。而在每次调用迭代器的next()、hasNext()、remove()等方法时,都会检查计数器的值是否与迭代器创建时的值相同。如果不同,就说明集合的结构发生了变化,迭代器会立即抛出ConcurrentModificationException异常。
通过"fail-fast"机制,Java提供了一种早期检测并快速失败的机制,以避免在多线程环境下,多个线程对集合进行并发修改而导致的数据不一致和异常情况。这样可以提高程序的可靠性,并帮助开发人员及时发现并修复错误。但需要注意的是,这种机制并不能保证在所有情况下都能检测到并发修改,因此在多线程环境下,仍需采取额外的措施来保证数据的一致性和线程安全性。
FailSafe源码
-
牺牲了一致性
在Java中,"fail-safe"是另一种迭代器(Iterator)的行为模式。与"fail-fast"不同,Fail-Safe迭代器能够在迭代的过程中安全地对集合进行修改,而不会抛出ConcurrentModificationException异常。
Fail-Safe迭代器通过在迭代时对原始集合的副本进行操作来实现。这意味着,当你创建一个迭代器之后,即使在迭代的过程中对集合结构进行了修改(例如添加、删除元素),迭代器仍然会遍历原始集合的快照。
具体来说,当你调用集合的iterator()方法返回一个迭代器时,实际上是创建了一个专门用于迭代的副本。这个副本保存了集合的状态,以及在迭代过程中进行操作的数据。因此,在使用Fail-Safe迭代器时,你可以放心地对集合进行修改,而不用担心其他迭代器的影响。
需要注意的是,由于Fail-Safe迭代器操作的是集合的副本,所以它可能无法反映最新的修改。如果你希望在迭代过程中看到其他线程对集合的最新更改,那么Fail-Safe迭代器可能不适合。此外,Fail-Safe机制可能会产生额外的内存开销,因为需要维护集合的副本。
总结起来,Fail-Safe迭代器是一种安全的迭代器实现,它通过操作集合的副本来避免并发修改异常。它适用于对集合进行迭代和修改的并发场景,但需要注意可能无法反映其他线程的最新修改,以及可能产生额外的内存开销。
3.Arraylist和Linkedlist
ArrayList和LinkedList是Java集合框架中两种常见的列表实现类,它们在底层数据结构、性能特征和适用场景等方面存在一些区别。
-
底层数据结构:ArrayList使用动态数组作为底层数据结构,而LinkedList使用双向链表。
-
随机访问性能:ArrayList支持快速的随机访问,通过索引可以在O(1)的时间复杂度内访问元素。LinkedList在随机访问时性能较差,需要通过遍历链表来访问指定位置的元素,时间复杂度为O(n)。
-
插入和删除性能:ArrayList在末尾插入和删除元素的性能较好,平均时间复杂度为O(1),但在中间位置插入和删除元素时需要进行元素的移动,时间复杂度为O(n)。LinkedList在任意位置插入和删除元素的性能较好,平均时间复杂度为O(1),因为只需要修改相邻节点的指针即可。
-
内存占用:ArrayList占用的内存空间主要用于存储连续的数组元素,而LinkedList除了存储元素本身外,还需要存储每个节点的前驱和后继指针。因此,对于相同数量的元素,LinkedList通常会占用更多的内存空间。
-
遍历性能:ArrayList在顺序遍历元素时具有较好的性能,因为元素在内存中是连续存储的,利于CPU缓存的命中。LinkedList在遍历时需要遍历链表的每个节点,性能较差。
-
迭代器:对于ArrayList,迭代器操作效率较高;对于LinkedList,虽然它的迭代器支持快速的插入和删除操作,但在访问元素时效率较低。
根据以上特点,一般来说:
-
如果需要频繁进行随机访问和遍历操作,且对于中间的插入和删除操作要求不高,可以选择ArrayList。
-
如果需要频繁进行插入和删除操作,尤其是在链表的首尾部进行操作,而对于随机访问的需求较少,可以选择LinkedList。
需要根据实际场景和需求来选择适合的列表实现类。
4.Hashmap的数据结构
HashMap是Java集合框架中的一个常用类,它实现了Map接口,提供了基于键值对的存储和检索功能。以下是关于HashMap的一些重要特点和用法:
-
存储结构:HashMap使用哈希表来存储键值对。它通过将键的哈希值映射到内部数组中的索引位置来快速定位和存取值。
-
唯一键:HashMap的键是唯一的。如果插入一个新的键值对,如果该键已经存在,那么原有键对应的值将被新的值替换。
-
线程不安全:HashMap是非线程安全的,如果在多线程环境下使用HashMap,需要进行额外的同步或使用线程安全的Map实现类,如ConcurrentHashMap。
-
基本操作:常用的操作包括put(key, value)添加键值对、get(key)获取指定键对应的值、remove(key)移除指定键值对、containsKey(key)判断是否包含指定键等。
-
遍历:可以使用迭代器或者forEach循环来遍历HashMap的键值对。示例代码:
HashMap<String, Integer> map = new HashMap<>(); map.put("apple", 1); map.put("banana", 2); map.put("orange", 3); // 使用迭代器遍历 Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); System.out.println(entry.getKey() + " = " + entry.getValue()); } // 使用forEach循环遍历 for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); }
-
初始容量和负载因子:HashMap可以通过构造函数指定初始容量和负载因子。初始容量是哈希表的大小,负载因子表示哈希表在进行rehash操作(扩容)的阈值,默认为0.75。
-
null键和null值:HashMap允许存储null键和null值,但需要注意并发情况下的线程安全性。
HashMap提供了高效的存取和检索功能,适合大部分键值对的操作场景,但需要注意线程安全性。在特定场景下,可能需要使用线程安全的Map实现类,如ConcurrentHashMap。
5.hashmap的索引计算
在HashMap中,键值对的存储和检索是通过哈希算法来实现的。当我们向HashMap中插入一个键值对时,HashMap会根据键的哈希值计算出一个在内部数组中的索引位置,然后将值存储在该位置处。当我们通过键来检索值时,也会使用相同的哈希算法计算出索引位置,并从该位置处获取值。
在Java中,HashMap使用的哈希算法可以简述如下:
-
首先,通过调用键对象的hashCode()方法,获取键对象的哈希码(hash code)。
-
然后,HashMap会对哈希码进行一系列处理,以得到最终的索引位置。这个处理步骤包括对哈希码进行扰动(hash code的高16位与低16位进行异或运算),以及对结果取模操作(根据哈希表的大小取模)。
具体来说,HashMap中的索引计算可以通过以下伪代码表示:
index = hashCode(key) ^ (hashCode(key) >>> 16); index = index & (capacity - 1);
其中,hashCode(key)
表示键对象的哈希码,capacity
表示HashMap内部数组的长度(即哈希表的大小)。
通过上述计算,得到的index
即为键值对在HashMap内部数组中的索引位置。如果这个位置上已经存在了其他键值对(即发生了哈希碰撞),HashMap会使用链表或红黑树等数据结构来处理碰撞情况,并通过键的equals()方法来进行查找匹配。
需要注意的是,哈希算法的质量对于HashMap的性能和效率有很大的影响。好的哈希算法可以尽量减少碰撞的发生,提高存取效率。因此,在自定义的类作为HashMap的键时,需要重写hashCode()和equals()方法,以保证不同的键对象产生不同的哈希码,并正确判断键对象的相等性。
设计模式
1.单例模式
对于单例模式,除了刚刚提到的几种常见的实现方式(饿汉式、懒汉式、双重检查锁定),还存在其他一些实现方式。以下是单例模式的五种实现方式:
-
饿汉式(Eager Initialization):
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() { // 私有构造方法 } public static Singleton getInstance() { return instance; } }
在类加载时就创建实例对象,保证了线程安全,但可能会造成内存浪费。
-
懒汉式(Lazy Initialization):
public class Singleton { private static Singleton instance; private Singleton() { // 私有构造方法 } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
在首次被使用时才创建实例对象,使用synchronized关键字保证了线程安全。但synchronized会带来性能开销。
-
双重检查锁定(Double-Checked Locking):
public class Singleton { private volatile static Singleton instance; private Singleton() { // 私有构造方法 } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
使用双重检查来避免每次获取实例时都进行同步。使用volatile关键字确保多线程环境下对instance的可见性和有序性。
-
静态内部类(Static Inner Class):
public class Singleton { private Singleton() { // 私有构造方法 } private static class SingletonHolder { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }
通过内部类的方式延迟加载实例对象。静态内部类在首次被使用时才会加载,保证了线程安全。
-
枚举(Enum):
public enum Singleton { INSTANCE; // 可以定义其他成员变量、方法等 public void doSomething() { // 实例方法 } }
枚举天然地保证了实例的唯一性,线程安全,且能防止反射和序列化破坏单例。
以上是五种常见的单例模式实现方式,选择适合的方式取决于具体的需求和场景。每种方式都有其优点和缺点,需要根据实际情况进行选择。
2.jdk那些地方使用了单例模式
在JDK中,有一些类库和框架使用了单例模式来确保只有一个实例存在。以下是几个在JDK中使用单例模式的示例:
-
java.lang.Runtime:Runtime类是Java程序运行时的环境类,它通过以下方式获取实例:
public class Runtime { private static Runtime currentRuntime = new Runtime(); private Runtime() { // 私有构造方法 } public static Runtime getRuntime() { return currentRuntime; } // ... }
通过调用
Runtime.getRuntime()
方法来获取实例。 -
java.awt.Desktop:Desktop类用于访问桌面资源,如打开文件、打开浏览器等操作。它通过以下方式获取实例:
public class Desktop { private static final Desktop desktop = new Desktop(); private Desktop() { // 私有构造方法 } public static Desktop getDesktop() { return desktop; } // ... }
通过调用
Desktop.getDesktop()
方法来获取实例。 -
java.lang.System:System类是Java语言的核心类,提供了访问系统资源的方法。它以单例模式的形式存在,通过以下方式获取实例:
public final class System { private static final System instance = new System(); private System() { // 私有构造方法 } public static System getSecurityManager() { return instance; } // ... }
通过调用
System.getSecurityManager()
方法来获取实例。
需要注意的是,以上只是一些在JDK中使用了单例模式的例子,还有其他类库和框架可能也使用了单例模式。单例模式的使用可以确保全局只有一个实例存在,方便统一的访问和管理。
二.并发篇
1.线程的状态
在Java中,线程具有以下六种状态:
-
新建(New)状态:当线程对象被创建时,它处于新建状态。此时,线程尚未开始执行。
-
运行(Runnable)状态:在新建线程调用了start()方法之后,线程进入运行状态。此时,线程正在执行或准备执行任务。
-
阻塞(Blocked)状态:线程在等待某个条件的发生,例如等待锁的释放或等待用户输入等情况下,会进入阻塞状态。一旦条件满足,线程会重新进入运行状态。
-
等待(Waiting)状态:线程进入等待状态是因为调用了Object类的wait()方法、Thread类的join()方法,或者是LockSupport类的park()方法。线程会处于等待状态直到其他线程通过notify()、notifyAll()或interrupt()方法来唤醒它。
-
计时等待(Timed Waiting)状态:与等待状态类似,但是线程会在一定时间后自动唤醒。调用sleep()方法、带有超时参数的Object类的wait()方法、Thread类的join()方法,或者是LockSupport类的parkNanos()、parkUntil()方法,会使线程进入计时等待状态。
-
终止(Terminated)状态:线程执行完任务或者因异常退出时,线程会进入终止状态。一旦线程进入终止状态,它将不再运行。
2.线程的状态转换
线程的状态可以根据不同的操作和条件发生转换。下面是线程状态之间可能的转换:
-
新建(New)状态 -> 运行(Runnable)状态:通过调用线程对象的start()方法,线程从新建状态转换到运行状态。
-
运行(Runnable)状态 -> 阻塞(Blocked)状态:线程可能由于等待获取锁资源,或者等待其他线程的通知等,而进入阻塞状态。
-
运行(Runnable)状态 -> 等待(Waiting)状态:线程调用了Object类的wait()方法、Thread类的join()方法,或者是LockSupport类的park()方法,导致线程进入等待状态。
-
运行(Runnable)状态 -> 计时等待(Timed Waiting)状态:线程调用了sleep()方法、带有超时参数的Object类的wait()方法、Thread类的join()方法,或者是LockSupport类的parkNanos()、parkUntil()方法,导致线程进入计时等待状态。
-
运行(Runnable)状态 -> 终止(Terminated)状态:线程执行完任务或者因异常退出时,线程进入终止状态。
-
阻塞(Blocked)状态 -> 运行(Runnable)状态:当线程获取到所需的锁资源,或者等待的条件满足时,线程从阻塞状态转换为运行状态。
-
等待(Waiting)状态 -> 运行(Runnable)状态:线程被其他线程通过notify()、notifyAll()或interrupt()方法唤醒时,线程从等待状态转换为运行状态。
-
计时等待(Timed Waiting)状态 -> 运行(Runnable)状态:当计时等待的时间结束,或者线程被其他线程通过notify()、notifyAll()或interrupt()方法唤醒时,线程从计时等待状态转换为运行状态。
注意,线程状态之间的转换取决于线程调度器的具体实现和操作系统的特性,可能因不同的环境而有所差异。
3.线程池的核心参数
-
核心线程数
-
最大线程数
-
生存时间
-
时间单位
-
阻塞队列
-
线程工厂
-
拒绝策略
4.sleep和wait的区别
sleep()和wait()是Java中用于线程暂停执行的方法,它们有以下区别:
-
来源:
-
sleep()是Thread类的静态方法,可以在任何地方使用。它是线程类的一部分,用于控制当前线程的执行时间。
-
wait()是Object类的实例方法,只能在同步代码块或同步方法中使用。它是Object类提供的方法,用于线程间的通信和协调。
-
-
使用条件:
-
sleep()方法在执行过程中不会释放持有的锁,让出CPU给其他线程执行,线程仍然占据着锁资源。
-
wait()方法会释放持有的锁,将当前线程置于等待状态,直到其他线程通过notify()或notifyAll()方法来唤醒它。
-
-
锁的依赖:
-
sleep()方法不需要依赖锁对象,可以在任何地方使用。
-
wait()方法必须在同步代码块或同步方法中使用,因为它需要依赖于锁对象进行线程间的通信。
-
-
被唤醒:
-
sleep()方法在指定的时间过去后会自动唤醒,或者被其他线程中断。
-
wait()方法只有在其他线程通过notify()或notifyAll()方法来唤醒时才能继续执行。
-
-
异常:
-
sleep()方法不会抛出InterruptedException异常,所以在处理中断时需要手动检查中断状态。
-
wait()方法会抛出InterruptedException异常,需要在异常处理中进行处理。
-
总之,sleep()方法是线程的一种暂停执行的方式,时间到了或被中断后会继续执行,不释放锁;而wait()方法是线程的一种等待通知的方式,在等待期间会释放锁,直到被其他线程的通知唤醒。因此,sleep()适用于线程间时间间隔的控制,wait()适用于线程间的协作和通信。
5.lock与synchronized区别
lock(锁)和synchronized(同步)是Java中用于实现线程同步的两种不同的机制,它们之间有以下区别:
-
使用方式:
-
synchronized是Java关键字,可以直接使用在方法或代码块上。它是隐式锁,不需要手动释放,当线程执行完synchronized代码块或方法后会自动释放锁。
-
lock是Java提供的API(java.util.concurrent.locks.Lock接口的实现类),需要通过lock()方法来获得锁,并且在finally块中使用unlock()方法手动释放锁。
-
-
可重入性:
-
synchronized是可重入的,即同一线程可以多次获取同一个锁。
-
lock通过lock()方法显式地获取锁,也允许同一线程多次获取锁,但需要相应地释放相同次数的锁。
-
-
锁的获取方式:
-
synchronized是非公平锁,当多个线程同时竞争锁时,不保证哪个线程会获取到锁。
-
lock可以实现公平锁或非公平锁,可以通过参数来指定锁的获取方式。
-
-
锁的灵活性:
-
synchronized在锁的范围内执行的代码块或方法,无法中断执行。
-
lock可以通过tryLock()方法尝试获取锁,并可以指定获取锁的超时时间,还支持中断处理。
-
-
锁的粒度:
-
synchronized锁的粒度比较粗,只能对整个方法或代码块进行加锁。
-
lock可以细粒度地控制锁的范围,可以对不同的代码块加锁,提高并发性能。
-
总的来说,synchronized是Java语言内置的关键字,使用简便,适用于大部分简单的并发场景。而lock是一个更灵活、可扩展和功能更强大的锁机制,适用于复杂的多线程环境,并且提供了更细粒度的锁控制和更高的并发性能。在Java 5之前,synchronized是唯一的选择;但从Java 5开始,引入了Lock接口和相关的实现类,提供了更多的功能和控制选项。
6.volatile
volatile是Java中的关键字,用于标记变量,具有以下特性:
-
可见性(Visibility):当一个线程修改了volatile变量的值,其他线程将立即看到最新的值。即对volatile变量的写操作对于其他线程是可见的。
-
有序性(Ordering):volatile关键字可以禁止指令重排序优化,保证volatile变量的读写操作按照程序中的顺序执行。
-
不保证原子性(Non-Atomic):volatile修饰的变量并不保证执行原子性操作。如果需要保证多个线程对变量的原子性操作,需要使用其他的同步机制,如synchronized关键字或使用原子类。
-
禁止内存重排序:volatile变量在写操作后,会强制将修改后的值立即写回主内存,并且在读操作前,会强制从主内存刷新该变量的值,防止对其进行重排序。
关于volatile变量的注意事项:
-
volatile不能替代锁(synchronized)的使用。它适用于对于变量的单个读写操作,并不能解决复合操作的原子性问题。
-
volatile不能保证线程安全,它只能确保可见性和有序性。
-
在多线程环境下使用volatile需要注意同步问题,如多个volatile变量间的原子性操作,并且需要考虑内存模型的一致性问题。
总的来说,volatile关键字提供了一种轻量级的线程同步机制,用于控制变量的可见性和有序性。它主要用于保证变量在多线程环境下的内存可见性,但不能保证原子性。
7.悲观锁与乐观锁
悲观锁和乐观锁是并发编程中两种不同的锁策略,它们有以下区别:
悲观锁:
-
代表lock与synchronized
-
假设并发环境中会发生冲突,每次获取锁时都认为其他线程会修改数据。
-
当一个线程获取到悲观锁后,其他线程需要等待该线程释放锁才能继续执行,因此它是一种悲观的思想。
-
常见的悲观锁包括使用互斥锁(如synchronized关键字)和数据库中的行级锁(如SELECT ... FOR UPDATE)。
乐观锁:
-
代表Atomicintrger使用cas保证原子性
-
假设并发环境中冲突发生的概率较低,每次操作都认为不会发生冲突。它相信多个线程或进程之间可以自由地访问数据而不会造成问题。
-
在乐观锁中,没有锁定整个资源,在读取数据时不加锁,只在更新数据时进行检查,如果数据被其他线程修改,则重新尝试或执行回滚等策略。
-
乐观锁通常使用版本号或时间戳等方式来检测数据是否被修改。如果版本号或时间戳不匹配,则说明数据已经被其他线程修改。
-
常见的乐观锁实现包括使用CAS(Compare and Swap)操作和数据库中的乐观锁机制(如乐观锁字段)。
区别总结:
-
悲观锁对于并发冲突持保守态度,每次都需要获取锁。
-
乐观锁对于并发冲突持积极态度,假设冲突的概率低,并在操作时进行冲突检测。
-
悲观锁会造成线程的阻塞和切换,而乐观锁不需要阻塞线程,通过冲突检测来解决并发问题。
-
悲观锁适用于写操作多的场景,乐观锁适用于读操作多的场景,并且能有效提高并发性能。
选择使用悲观锁还是乐观锁取决于具体的场景和需求。悲观锁在并发冲突较多时能保证数据的一致性,但会带来较多的开销;乐观锁在并发冲突较少时可以提高并发性能,但可能需要根据具体情况处理冲突和进行重试。
8.Hashtable和ConcurrentHashMap
Hashtable和ConcurrentHashMap是Java中两个用于实现线程安全的哈希表(Hash Table)的类,它们之间有以下区别:
-
线程安全性:
-
Hashtable是线程安全的,所有方法都是原子操作,可以安全地在多线程环境下使用。
-
ConcurrentHashMap也是线程安全的,但它引入了更细粒度的锁机制。它在并发场景下可以提供更好的性能,允许多个线程同时进行读取操作。不同的部分通过分段锁(Segment Lock)实现并发控制。
-
-
锁策略:
-
Hashtable中的每个方法都使用了synchronized关键字对整个HashTable对象加锁。这意味着同一时间只能有一个线程执行Hashtable的操作。
-
ConcurrentHashMap使用分段锁(Segment Lock)机制,将整个数据集分割成多个段(Segment),每个段维护一个独立的锁。这样多个线程可以同时读取不同段的数据,提高并发性能。
-
-
性能:
-
在读多写少的场景下,Hashtable的性能相对较低。因为它使用了全局锁,多个线程同时读取数据时需要等待。
-
ConcurrentHashMap在读多写少的场景下具有更好的性能。因为它使用分段锁,允许多个线程同时读取不同段的数据。
-
-
迭代器:
-
Hashtable的迭代器是强一致性的,不会抛出ConcurrentModificationException异常。
-
ConcurrentHashMap的迭代器是弱一致性的,可能会抛出ConcurrentModificationException异常。这是由于在迭代过程中其他线程修改了集合的结构。
-
-
空值和键:
-
Hashtable不允许空值(null)作为键或值。
-
ConcurrentHashMap允许空值作为键或值。
-
总的来说,Hashtable和ConcurrentHashMap都是线程安全的哈希表实现,但ConcurrentHashMap通过分段锁(Segment Lock)机制提供了更好的并发性能。因此,在大多数情况下,推荐使用ConcurrentHashMap来替代Hashtable,特别是在并发读写较多的场景中。
9.Treadlocal
-
线程间隔离
-
线程内资源共享
ThreadLocal是Java中的一个类,用于实现线程局部变量(Thread-local Variable)的存储。它允许每个线程都有自己的变量副本,每个线程对变量的修改不会被其他线程所影响,从而提供了线程隔离的能力。
ThreadLocal的主要特点和使用方法如下:
-
线程局部变量:ThreadLocal提供了一个线程级别的变量存储,每个线程拥有独立的变量副本,互不干扰。
-
变量副本的初始化:ThreadLocal可以通过initialValue()方法或使用lambda表达式进行初始化,确保每个线程第一次访问变量时都有初始值。
-
线程间隔离:每个线程对ThreadLocal变量的修改不会影响其他线程的副本,达到了线程间的隔离效果。
-
并发访问安全:ThreadLocal保证了在多线程环境下,每个线程访问自己的变量副本时不会出现数据竞争和并发访问冲突。
-
内存泄漏风险:ThreadLocal使用线程作为键,将变量副本存储在ThreadLocalMap中,如果没有明确清除ThreadLocal的引用,可能会导致内存泄漏问题。
-
应用场景:ThreadLocal常用于跨方法或跨类的线程上下文传递,例如在Web应用中,可以在拦截器中将用户信息存储在ThreadLocal中,使得后续处理请求的方法可以方便地获取用户信息。
需要注意的是,虽然ThreadLocal可以提供线程间的隔离,但它并不解决线程安全的问题。在多线程环境下,仍然需要采用其他并发控制手段来保证操作的原子性和一致性,如使用锁(synchronized或Lock)或使用并发容器(如ConcurrentHashMap)等。
总结来说,ThreadLocal提供了一种线程级别的变量存储机制,每个线程都有自己的变量副本。它能够实现线程间的数据隔离,但需要注意内存泄漏问题,并不能解决线程安全的问题。
三.虚拟机篇
1.jvm的内存结构
JVM(Java Virtual Machine)的内存结构可以分为以下几个部分:
-
方法区(Method Area): 方法区用于存储类的信息、常量、静态变量、编译器优化后的代码等数据。在Java 8之前,方法区被称为永久代(Permanent Generation),在Java 8及以后的版本中,方法区被移到了本地内存(Native Memory)中。
-
堆(Heap): 堆是JVM中最大的一块内存区域,用于存储对象实例和数组。所有通过new关键字创建的对象都在堆中分配内存。堆被所有线程共享,可以进行垃圾回收来回收不再使用的对象,并进行自动内存管理。
-
栈(Stack): 栈是线程私有的内存区域,每个线程都有自己的栈。栈用于存储局部变量、方法调用和方法执行时的临时数据。栈中的数据遵循先进后出(FILO)的原则,方法的调用和执行都是在栈中进行的。
-
本地方法栈(Native Method Stack): 本地方法栈与栈类似,但它用于存储本地方法(Native Method)的数据。本地方法是使用其他编程语言(如C或C++)编写的方法,在JVM中通过JNI(Java Native Interface)进行调用。
-
PC寄存器(Program Counter Register): PC寄存器是每个线程私有的,用于存储当前线程执行的字节码指令地址。它在任何时候都指向当前正在执行的指令。
-
垃圾回收器和运行时数据区域: JVM中还包括垃圾回收器(Garbage Collector)和其他一些运行时数据区域,例如编译器(Compiler)、即时编译器(Just-In-Time Compiler)等。
这些内存区域的具体用途和具体实现可能会有所差异,不同的JVM厂商可能会有不同的实现方式。以上是JVM内存结构的一般概念,了解这些内存区域的作用可以帮助我们更好地理解Java程序在内存中的存储和执行过程。
-
线程私有 程序计数器 , 虚拟机栈
-
线程共享 堆 , 方法区
2.内存溢出
内存溢出(Memory Overflow)指的是程序在申请内存时,没有足够的可用内存供其使用,导致程序无法继续执行的错误。
常见的内存溢出情况包括:
-
堆内存溢出(Heap Overflow): 当程序需要创建大量的对象并且这些对象长时间保持,但是堆内存空间不足时,就会产生堆内存溢出。这通常是由于内存泄漏或分配了过多的内存导致。
-
栈溢出(Stack Overflow): 栈是线程私有的内存区域,用于存储方法调用和方法执行时的临时数据。当方法的嵌套调用层级过深或递归调用没有有效的终止条件时,会导致栈溢出。
-
方法区溢出(Method Area Overflow): 方法区用于存储类的信息、常量、静态变量等,在某些情况下,如果加载的类过多或者生成过多动态代理类时,可能会导致方法区溢出。
-
本地内存溢出(Native Memory Overflow): 本地内存是JVM以外的内存,用于存储本地方法调用的数据。如果本地方法分配了大量的内存但没有及时释放,可能会导致本地内存溢出。
遇到内存溢出时,常见的解决方法包括:
-
分析和修复内存泄漏问题,确保不再持有不需要的对象引用。
-
调整JVM的堆内存大小(通过-Xmx和-Xms参数),增加可用的堆内存空间。
-
优化程序设计,减少对象的创建和持有,以降低内存的使用。
-
采用更高效的数据结构或算法,减少内存的占用。
-
优化递归算法,避免栈溢出。
-
对于方法区溢出,可以增加方法区的大小(通过-XX:PermSize和-XX:MaxPermSize参数),或者使用较新版本的JVM,将方法区移到本地内存。
注意,内存溢出是一个严重的错误,需要仔细分析和处理。在实际应用中,合理配置内存大小,编写高效的代码和及时清理无用的资源是预防内存溢出的重要手段。
3.方法区,永久代,元空间的关系
-
方法区是jvm的内存区域
-
永久代与元空间是虚拟机对jvm的实现
在Java虚拟机中,方法区(Method Area)、永久代(Permanent Generation)和元空间(Metaspace)是三个不同的概念,它们在不同的JVM版本中承担着类似的角色。
-
方法区(Method Area): 方法区是Java虚拟机的一部分,用于存储类的信息、常量、静态变量、编译器优化后的代码等。在Java 8之前,方法区被称为永久代(Permanent Generation),它是堆内存的一个逻辑部分。方法区的大小可以通过启动参数(如-XX:PermSize和-XX:MaxPermSize)来控制。
-
永久代(Permanent Generation): 永久代是方法区的一个实现,它是一块Java堆内存的特殊部分,用于存储类的元信息(Class Metadata)和常量池等。永久代的大小也可以通过启动参数来配置。在Java 8及以后的版本中,永久代已经被移除,取而代之的是元空间。
-
元空间(Metaspace): 元空间是Java 8及以后版本中取代永久代的一种实现。它也是用于存储类的元信息,但是与永久代相比,元空间的内存分配并不在虚拟机的堆中,而是在本地内存(Native Memory)中。元空间的大小不再是固定的,它可以根据需要动态地调整,受限于物理内存的大小。
总结来说,方法区、永久代和元空间都是用于存储类的元信息和常量池等数据的内存区域。永久代是方法区的一种实现,在Java 8及以后的版本中被元空间所取代。通过使用元空间,内存分配更加灵活,并且不再受到固定大小的限制。对于方法区的大小调整,取决于使用的JVM版本和相应的启动参数。
4.jvm的内存参数
JVM(Java Virtual Machine)提供了一些可以通过启动参数来配置的内存参数,用于控制堆内存、栈内存、方法区大小等。以下是一些常用的JVM内存参数:
-
堆内存参数:
-
-Xms<size>:设置初始堆内存大小,例如-Xms512m表示初始堆内存为512MB。
-
-Xmx<size>:设置最大堆内存大小,例如-Xmx1024m表示最大堆内存为1024MB。
-
-Xmn<size>:设置新生代的大小,例如-Xmn256m表示新生代大小为256MB。
-
-XX:NewRatio=<value>:设置堆内存中新生代和老年代的比例。
-
-
栈内存参数:
-
-Xss<size>:设置每个线程的栈大小,例如-Xss1m表示每个线程的栈大小为1MB。
-
-
永久代(Java 8之前)/元空间(Java 8及以后)参数:
-
以Java 8为例,永久代已经被移除,取而代之的是元空间,可以通过以下参数进行配置:
-
-XX:MetaspaceSize<size>:设置元空间初始大小。
-
-XX:MaxMetaspaceSize<size>:设置元空间最大大小。
-
-
直接内存参数(NIO):
-
-XX:MaxDirectMemorySize<size>:设置直接内存的最大大小。
-
除了以上常用的内存参数,还有一些其他参数用于调试和监控内存使用情况,例如:
-
-XX:+HeapDumpOnOutOfMemoryError:当出现内存溢出错误时,生成堆转储快照文件。
-
-XX:HeapDumpPath=<path>:设置堆转储快照文件的路径。
-
-XX:+PrintGCDetails:打印详细的垃圾回收日志。
这些内存参数可以通过在命令行中使用"java"命令时添加对应参数来进行配置。需要注意的是,在调整这些参数时,需要根据应用程序的需求和当前的硬件环境来合理设置,以避免出现内存溢出或性能问题。
5.jvm的垃圾回收算法
-
标记清除
-
标记整理
-
标记复制
JVM(Java Virtual Machine)提供了多种垃圾回收算法,用于自动管理动态分配的内存,并回收不再使用的对象。以下是一些常见的垃圾回收算法:
-
标记-清除算法(Mark and Sweep): 标记-清除算法是最基本的垃圾回收算法之一。它分为两个阶段:
-
标记阶段:从根对象(如线程栈、静态变量)开始,对可达的对象进行标记。
-
清除阶段:遍历整个堆,清除未被标记的对象,释放其占用的内存空间。
标记-清除算法的缺点是会产生大量的不连续内存碎片,降低了内存的利用效率。
-
-
复制算法(Copying): 复制算法将内存空间划分为相等大小的两个区域:From空间和To空间。分为两个阶段:
-
标记阶段:从根对象开始,对可达的对象进行标记。
-
复制阶段:将存活的对象从From空间复制到To空间,并按顺序排列,然后清空From空间。
复制算法消耗的时间主要在复制阶段,但内存利用率高,不会产生内存碎片。经典的复制算法是"分代-复制"算法,将堆内存分为年轻代和老年代。
-
-
标记-整理算法(Mark and Compact): 标记-整理算法也是分两个阶段进行:
-
标记阶段:从根对象开始,对可达的对象进行标记。
-
整理阶段:将存活的对象向一端移动,然后清理边界以外的所有区域。
标记-整理算法解决了标记-清除算法的碎片问题,但需要移动对象,可能会增加开销。
-
-
分代收集算法(Generational Collection): 分代收集算法基于对象的生命周期将堆内存划分为不同的代:年轻代、中年代和老年代。大多数垃圾回收器使用分代收集算法。
-
年轻代主要采用复制算法,因为大部分对象的生命周期很短。
-
老年代主要采用标记-整理算法,因为老年代的对象一般较大,并且存在较多长期存活的对象。
-
除了以上算法,还有增量式收集、并行收集、并发收集等垃圾回收算法和收集器的组合方式。JVM会根据不同的垃圾回收器选择适合的算法进行垃圾回收,以达到尽量少的停顿时间和高效的内存利用。不同的垃圾回收器和算法可以通过JVM参数进行配置,以满足应用程序的需求。
6.垃圾判定
JVM(Java Virtual Machine)使用垃圾判定算法来确定哪些对象是可以被回收的垃圾对象。常见的垃圾判定算法有以下几种:
-
引用计数算法(Reference Counting): 引用计数算法是一种简单的垃圾判定算法,它通过为每个对象维护一个计数器来记录对象的引用数量。当引用数量变为0时,则对象被判定为垃圾。然而,引用计数算法无法解决循环引用的问题,即使对象之间相互引用但无法被外部访问,也无法被垃圾回收。
-
可达性分析算法(Reachability Analysis): 可达性分析算法是目前主流的垃圾判定算法之一。它通过从一组称为"GC Roots"(如栈帧中的本地变量表、静态变量、常量池中的引用等)开始,递归遍历所有的引用链,标记所有可以被访问到的对象,未标记的对象则被判定为垃圾。
-
再生活分析算法(Liveness Analysis): 再生活分析算法是一种基于可达性的垃圾判定算法,它考虑对象的再生活性(Reaching Alive)。
-
初始阶段,所有对象都被标记为未知状态。
-
根据对象的可达性标记活对象,并将活对象的引用链追踪到下一个对象。
-
当所有活对象都被标记后,未被标记的对象即为垃圾。
-
-
虚拟机引用(VM Reference): JVM提供了四种虚拟机引用,即强引用、软引用、弱引用和虚引用。通过不同类型的引用,可以影响垃圾回收器对对象的回收行为。例如,强引用的对象不会被垃圾回收器回收,而软引用、弱引用和虚引用可以在满足一定条件下被垃圾回收器回收。
这些垃圾判定算法通常通过垃圾回收器来执行,根据对象的可达性或其他标记信息来判断对象是否是垃圾。JVM的垃圾回收器会根据配置或自动选择合适的算法来进行垃圾回收,以实现高效的内存管理。
7.垃圾回收
JVM(Java Virtual Machine)使用垃圾回收(Garbage Collection,GC)机制来自动管理和回收不再使用的内存对象。垃圾回收器负责执行垃圾回收的操作,以下是关于JVM垃圾回收的一些重要概念和原则:
-
垃圾回收的原理: 当对象不再被引用或无法被访问时,即被判定为垃圾。垃圾回收器通过标记未被引用的对象并回收其占用的内存空间来清理垃圾。
-
垃圾回收器的种类: JVM提供了多种垃圾回收器,可以根据应用程序的需求和性能要求选择合适的回收器。常见的垃圾回收器有:Serial、Parallel、CMS(Concurrent Mark and Sweep)、G1(Garbage-First)、ZGC(Z Garbage Collector)等。
-
存活对象的判定: 垃圾回收器使用可达性分析算法来判断对象是否存活。从一组称为GC Roots的根对象开始遍历,标记所有可达的对象,未被标记的对象即为垃圾。
-
垃圾回收的过程:
-
标记阶段:通过可达性分析算法标记所有存活的对象。
-
清理阶段:清理未被标记的对象,回收它们占用的内存空间。
-
整理阶段(可选):对内存空间进行整理,消除内存碎片并提升内存利用率。
-
-
垃圾回收的类型:
-
Minor GC:发生在新生代,回收新创建的对象和短期存活的对象。常使用复制算法。
-
Major GC/Full GC:发生在整个堆内存中,包括新生代和老年代。常使用标记-整理或标记-清除算法。
-
-
垃圾回收的性能影响: 垃圾回收会带来一定的系统负载和暂停时间,因此需要在性能和响应时间之间做权衡。一般来说,适当配置垃圾回收器和调整相关参数可以优化垃圾回收的性能。
-
垃圾回收的配置: JVM提供了一系列的命令行选项和参数用于配置垃圾回收器的行为和性能,如内存大小、回收器类型、回收策略等。
了解和理解垃圾回收的工作原理以及各种垃圾回收器的特点,可以帮助开发人员优化应用程序的内存使用和性能。
8.GC和分代回收算法
垃圾收集(Garbage Collection,GC)是JVM(Java Virtual Machine)自动管理内存的机制,分代回收算法则是垃圾收集的一种策略。下面是它们之间的关系:
分代回收算法: 分代回收算法是一种将堆内存分为不同代(Generation)的垃圾回收策略。一般将堆内存分为新生代(Young Generation)、老年代(Old Generation)和持久代/元空间(Permanent Generation/Metaspace)。
-
新生代:新生代主要用于存放新创建的对象,因为大多数对象的生命周期很短,所以采用复制算法进行垃圾回收。新生代一般被划分为Eden区和两个Survivor区(通常是两个大小相同的区域,一般称为From区和To区)。
-
新创建的对象首先分配在Eden区。
-
当Eden区满时,触发Minor GC(小型垃圾回收)。
-
Minor GC会将存活的对象复制到To区,然后清空Eden区和From区。
-
Survivor区用于存放在Eden区和To区间复制过来的存活对象,当一次Minor GC后存活的对象会被移到另一个Survivor区,即From区变为To区,To区变为From区。
-
经过多次Minor GC后,仍然存活的对象会被移到老年代。
-
-
老年代:老年代主要用于存放存活时间较长的对象,一般采用标记-整理算法进行垃圾回收。老年代中对象的回收频率较低,回收速度较慢。
-
当老年代空间满时,触发Major GC或Full GC(完全垃圾回收)。
-
Major GC会对整个堆进行垃圾回收,包括新生代和老年代。
-
-
持久代/元空间:用于存放类和类相关的元数据信息,如常量池、静态变量等。在Java 8以前,一般采用永久代(Permanent Generation)实现。而在Java 8及以后版本,永久代被移除,使用元空间(Metaspace)实现。
垃圾收集(GC): 垃圾收集是JVM自动回收不再使用的对象的过程。根据分代回收算法,不同代的垃圾回收器会根据其特定的策略和算法来执行垃圾收集。
-
Minor GC:主要针对新生代进行垃圾回收,通过复制算法将存活的对象复制到Survivor区,并清空Eden区和From区。
-
Major GC/Full GC:主要针对整个堆进行垃圾回收,包括新生代和老年代。一般会使用标记-整理或标记-清除算法。
垃圾收集的目标是回收不再使用的对象,并释放其占用的内存空间,以减少内存的占用和提高程序的性能。分代回收算法通过针对不同对象的生命周期采取不同的回收策略,提高了垃圾收集的效率。
9.三色标记与并发漏标
三色标记与并发漏标是在并发垃圾回收算法中的两个关键概念。
-
三色标记(Tri-color Marking): 三色标记是一种并发垃圾回收算法中的标记过程。该算法使用三种颜色(一般是白色、灰色和黑色)来标记对象的状态。
-
白色:表示对象尚未被访问过,可能是垃圾或可回收对象。
-
灰色:表示对象被访问过,但其引用仍未完全遍历。
-
黑色:表示对象被访问过,并且其引用已经完全遍历。
三色标记算法使用这三种颜色对堆中的对象进行标记,从根对象开始递归遍历并标记活跃对象。在算法结束时,所有标记为白色的对象可以被回收。
-
-
并发漏标(Concurrent Misses): 并发漏标是指在并发垃圾回收过程中,新的对象被分配并引用,但垃圾回收器没有及时识别并标记这些新对象的情况。
并发垃圾回收算法要求并发标记和用户程序同时运行,这可能导致某些新对象在垃圾回收过程中创建并被引用,但回收器还没有来得及标记这些对象。这样的漏标会导致新对象被错误地回收,从而产生错误的垃圾回收行为。
解决并发漏标的常见方法是在并发标记过程中添加屏障或其他机制,以确保任何新创建的对象都被垃圾回收器正确地标记并处理。
并发垃圾回收算法是一种在应用程序运行时进行垃圾回收的方法,具有较低的停顿时间和对用户线程影响较小的优点。然而,必须仔细处理三色标记和并发漏标的问题,以确保正确的回收行为和内存一致性。
10.垃圾回收器
垃圾回收器(Garbage Collector)是Java虚拟机(JVM)中负责自动回收不再使用的内存对象的组件。它负责管理堆内存中的对象,并在内存不足时进行垃圾回收,以释放已死亡对象所占用的内存空间。
以下是一些常见的垃圾回收器:
-
Serial收集器(Serial Collector): Serial收集器是最基础的垃圾回收器,采用单线程执行垃圾回收操作,会触发全局停顿。它适用于单线程的低延迟环境。
-
Parallel收集器(Parallel Collector): Parallel收集器是Serial收集器的多线程版本,充分利用多核CPU的优势,提供更高的吞吐量。它适用于后台运算任务,可以通过参数调整吞吐量和停顿时间。
-
CMS收集器(Concurrent Mark and Sweep Collector): CMS收集器是一种并发垃圾回收器,主要用于减少垃圾回收导致的停顿时间。它通过并发标记和并发清除的方式来进行回收,适用于对响应时间有要求的应用。
-
G1收集器(Garbage First Collector): G1收集器是一种面向服务器应用的垃圾回收器,主要特点是将内存划分为多个独立的区域(Region),以增加回收精度和降低停顿时间。它适用于大内存应用和对低延迟有要求的场景。
-
ZGC收集器(Z Garbage Collector): ZGC收集器是针对大堆内存和对低停顿时间有严格要求的应用而设计的垃圾回收器。它采用并发整理算法,并通过着色指针和读屏障等技术来实现低停顿时间。
选择合适的垃圾回收器取决于具体的应用场景、性能需求和系统配置。在实际使用中,可以通过JVM参数进行配置和调优,选择最佳的垃圾回收器来满足应用的性能和响应时间要求。
11.内存溢出
-
误用线程池导致内存溢出
-
查询数据量太大导致内存溢出
-
动态生成类导致内存溢出
内存溢出(Memory Overflow)是指在程序运行过程中申请的内存超过了系统可用的内存资源,导致无法继续正常运行的情况。当程序需要的内存超过了系统分配给它的内存限制时,就会发生内存溢出。
常见的内存溢出场景和原因包括:
-
堆溢出(Heap Overflow): 在Java中,堆是用于存放对象实例的内存区域。当程序中创建的对象数量太多或者对象占用的内存过大,超出了堆的大小限制时,就发生了堆溢出。常见的原因包括内存泄漏、循环引用、大对象等。
-
栈溢出(Stack Overflow): 栈是用于存放方法调用和局部变量的内存区域。当递归调用或者方法调用层次过深时,栈的空间会不够用,就发生了栈溢出。常见的原因包括无限递归、方法调用层次过深等。
-
非堆溢出(Non-Heap Overflow): 非堆内存包括方法区(Metaspace)和本地内存等。当类信息、静态变量、常量池等占用的内存超出了限制,就会发生非堆溢出。常见的原因包括加载过多的类、大量的字符串常量等。
如何解决内存溢出问题?
-
调整内存配置: 增加JVM的堆内存大小或调整其他内存区域的大小,根据具体的应用场景和需求进行调整。
-
优化代码和资源使用: 避免创建过多且不必要的对象,及时释放无用的资源,合理使用缓存等。
-
定位和处理内存泄漏: 使用内存分析工具来检测和分析内存溢出的原因,找出内存泄漏的位置,修复代码或资源管理问题。
-
使用合适的数据结构和算法: 选择合适的数据结构和算法,减少内存占用或优化内存使用效率。
-
升级或更换硬件: 如果调整内存配置和优化代码后仍无法解决内存溢出问题,可能需要考虑升级或更换硬件来提供更大的内存容量。
及时处理和预防内存溢出问题,可以提高程序的稳定性和性能。
12.类加载
类加载是Java虚拟机(JVM)将字节码文件加载到内存中,并通过这些字节码创建Java类的过程。类加载是Java程序运行的基础,它负责将类的信息加载到JVM中,为程序的执行提供必要的支持。
类加载过程包括以下几个步骤:
-
加载(Loading): 加载是类加载的第一步,即通过类的全限定名(包名+类名)来查找并读取相应的字节码文件。字节码文件可以来自本地文件系统、网络、压缩包等。
-
验证(Verification): 验证阶段将对加载的字节码进行验证,确保其正确、安全,并符合Java虚拟机的规范。验证的内容包括语法检查、语义检查、类依赖性验证等。
-
准备(Preparation): 准备阶段是为类的静态字段(类变量)分配内存,并设置默认初始值(零值)。但该阶段并不会执行任何Java代码。
-
解析(Resolution): 解析阶段是将符号引用解析为直接引用,即将类、字段和方法的符号引用转换成直接指向内存中的对象。这一步可以在加载阶段之后进行,也可以在运行时进行。
-
初始化(Initialization): 初始化阶段是对类进行实际的初始化操作,包括为静态变量赋初值和执行静态代码块。在这个阶段,类的初始化按照顺序进行,父类会先于子类进行初始化。
-
使用(Usage): 类加载完成后,类被纳入Java虚拟机的运行环境,可以被其他类引用和使用。
类加载器(ClassLoader)是执行类加载的实际组件。Java虚拟机提供了不同类型的类加载器,如启动类加载器、扩展类加载器、应用类加载器(系统类加载器)等,它们按照不同的规则和顺序进行类加载。
类加载过程是JVM执行程序的关键环节,通过动态类加载和运行时动态生成类的机制,Java可以实现反射、动态代理等高级特性。这使得Java具有更大的灵活性和可扩展性。
13.类加载器
类加载器(ClassLoader)是Java虚拟机(JVM)中负责将类的字节码文件加载到内存并生成对应的类对象的组件。它是Java虚拟机的核心功能之一,提供了动态加载和使用类的能力。
在Java中,类加载器主要有以下几种类型:
-
启动类加载器(Bootstrap Class Loader): 启动类加载器是最顶层的类加载器,负责加载Java核心类库(如rt.jar)等,它是用本地代码实现的,不继承自java.lang.ClassLoader。
-
扩展类加载器(Extension Class Loader): 扩展类加载器负责加载Java的扩展类库(如jre/lib/ext目录下的jar文件)。它是由Java类实现的,继承自java.lang.ClassLoader。
-
应用类加载器(Application Class Loader): 应用类加载器也称为系统类加载器,负责加载应用程序的类。它是由Java类实现的,继承自java.lang.ClassLoader。是默认的类加载器,可以通过ClassLoader.getSystemClassLoader()方法获取。
-
自定义类加载器(Custom Class Loader): 自定义类加载器是通过继承java.lang.ClassLoader类并重写其中的方法来实现的。它可以加载用户自定义的类,也可以实现特定的类加载策略,如从网络、数据库、特定文件格式等加载类。
自定义类加载器可以扩展类加载的能力,实现热部署、代码加密、插件化等功能。它通过重写findClass()方法来实现类加载的过程,可以根据特定的规则和需求,动态地加载和卸载类。
类加载器采用了双亲委派模型(Parent Delegation Model),即当一个类加载器需要加载类时,首先将加载请求委托给其父类加载器,如果父类加载器无法找到类,再由自身进行加载。这种模型可以避免类的重复加载和安全性问题。
了解类加载器的工作原理和机制有助于理解Java的类加载过程,并能够更好地进行类加载的调优和扩展。
14.对象的引用类型
-
强软弱虚
在Java中,对象的引用类型用于声明和操作对象的引用。Java中的引用类型有以下几种:
-
类型引用(Class Reference): 类型引用主要用于引用类和接口类型。通过使用类名、接口名称或者使用关键词
class
、interface
来声明类型引用。示例:
String str; // 类型引用,引用了String类的对象 List<Integer> list; // 类型引用,引用了List接口的对象
-
对象引用(Object Reference): 对象引用用于引用类的实例化对象。通过使用类名、关键词
new
来创建对象引用。示例:
String str = new String("Hello"); // 对象引用,引用了String类的一个对象
-
数组引用(Array Reference): 数组引用用于引用一维或多维数组。通过使用类型名和方括号
[]
来声明数组的引用类型。示例:
int[] numbers; // 数组引用,引用了一个int类型的一维数组 String[][] names; // 数组引用,引用了一个String类型的二维数组
-
接口引用(Interface Reference): 接口引用用于引用实现了接口的对象。通过使用接口名称来声明接口引用类型。
示例:
Runnable runnable; // 接口引用,引用了实现了Runnable接口的对象
-
泛型引用(Generic Reference): 泛型引用用于引用泛型类型的对象。通过使用泛型类型和类型参数来声明泛型引用类型。
示例:
List<String> list; // 泛型引用,引用了泛型类型为String的List对象
不同的引用类型决定了对象在内存中的分配和操作方式。了解和正确使用引用类型是Java开发中的基础知识,可以帮助我们更好地管理和操作对象。
15.finalize
finalize()
是Java中的一个方法,用于对象在被垃圾回收前的清理工作。它是java.lang.Object
类中定义的一个方法,因此所有的Java对象都可以重写这个方法。
当一个对象变成垃圾并且即将被回收时,Java虚拟机会调用该对象的finalize()
方法。对象的finalize()
方法可以被重写,开发者可以在该方法中进行一些清理和释放资源的操作,比如关闭文件、释放网络连接等。需要注意的是,finalize()
方法并不是实时的,也不能保证一定会被调用。
finalize()
方法的声明如下:
protected void finalize() throws Throwable { // 清理和资源释放的代码 }
以下是一些关于finalize()
方法的注意事项:
-
finalize()
方法是一个实例方法,因此无法被静态方法重写。 -
finalize()
方法的访问修饰符是protected
,这是为了防止该方法被直接调用。 -
finalize()
方法的返回值为void
,表示它不会返回任何结果。 -
finalize()
方法抛出了Throwable
异常,因此在重写该方法时,可以抛出任何异常。
尽管Java提供了finalize()
方法来进行对象的清理和资源释放,但是在实际开发中,推荐使用显式的资源释放方法(如close()
等)来实现资源的释放,而不是依赖于finalize()
方法。这是因为finalize()
方法的执行时机是不确定的,而且过度依赖它可能会影响性能和资源管理。在Java 9版本中,finalize()
方法已被标记为废弃方法,未来可能会被移除。因此,建议使用try-with-resources或手动释放资源的方式来代替finalize()
方法实现资源释放。
四.框架篇
1.spring_refresh
Spring框架中的spring_refresh
操作并不是一个具体的标准步骤,可能是你自己提出的一个概念。然而,我可以为你介绍Spring应用程序的常见刷新/重启过程,其中包括一些被广泛认可的步骤。以下是一个简单的Spring应用程序刷新的一般步骤:
-
关闭当前应用程序上下文(Context): 在进行刷新之前,首先需要关闭当前的Spring应用程序上下文,以释放资源和清理状态。
-
创建新的应用程序上下文: 根据具体需求,创建一个新的Spring应用程序上下文。可以根据需要选择使用不同的ApplicationContext实现,如AnnotationConfigApplicationContext、XmlWebApplicationContext等。
-
配置应用程序上下文: 对新创建的应用程序上下文进行配置,包括设置属性、注册bean定义、指定需要扫描的包等。
-
刷新应用程序上下文: 调用新的应用程序上下文的refresh()方法,触发应用程序上下文的刷新过程。在这一步,Spring会加载并初始化所有定义的Bean,并完成依赖注入。
-
启动应用程序: 如果需要,执行启动应用程序的操作。这可能包括启动嵌入式服务器、注册监听器等。
-
执行需要的操作: 根据具体需求,执行应用程序所需的任何操作,例如初始化数据库连接、加载配置文件、初始化缓存等。
-
处理请求和响应: 处理来自客户端的请求,并根据业务逻辑生成响应。
-
销毁旧的应用程序上下文: 如果有旧的应用程序上下文正在运行,进行必要的清理和销毁操作,以确保资源的正确释放。
-
切换到新的应用程序上下文: 将新的应用程序上下文设为当前的上下文,并使用它来处理接下来的请求。
-
完成刷新过程: 刷新过程完成后,将应用程序标记为已刷新状态。这可能包括记录日志、发送通知等。
-
执行应用程序逻辑: 进行应用程序的正常运行,处理业务逻辑,响应请求。
-
关闭应用程序: 当应用程序结束时,合理地进行资源清理和关闭操作,以确保数据的完整性和一致性。
需要注意的是,上述步骤仅提供了一种通用的Spring应用程序刷新流程,并不能适用于所有场景。具体的应用程序刷新过程可能会有所不同,具体取决于应用程序的结构、需求和所使用的Spring版本。所以,在实际应用中,根据具体的情况进行相应的调整和扩展。
2.springbean的声明周期
在Spring框架中,每个Bean都有其生命周期,即从被创建到被销毁的整个过程。Spring框架为Bean的生命周期提供了相应的扩展点,使开发者能够在特定的阶段插入自定义的逻辑。下面是典型的Spring Bean的生命周期:
-
实例化(Instantiation): 首先,Spring容器通过构造函数或工厂方法创建一个Bean的实例。
-
属性赋值(Population of Properties): Spring容器会通过依赖注入(Dependency Injection)或其他方式将配置的属性值注入到Bean实例中。
-
Aware接口回调(Aware Interface Callbacks): 如果Bean实现了Aware接口(如ApplicationContextAware、BeanFactoryAware等),Spring容器会调用相应的回调方法,以使Bean获得特定的意识和能力。
-
BeanPostProcessor的前置处理(BeanPostProcessor Pre-processing): 在Bean的初始化前,Spring容器会调用注册的BeanPostProcessor的postProcessBeforeInitialization()方法,允许开发者在Bean初始化之前进行一些自定义的处理。
-
初始化(Initialization): Spring容器执行Bean的初始化操作。包括调用Bean的自定义初始化方法(如果有)以及执行InitializingBean接口的afterPropertiesSet()方法。
-
BeanPostProcessor的后置处理(BeanPostProcessor Post-processing): 在Bean的初始化后,Spring容器会调用注册的BeanPostProcessor的postProcessAfterInitialization()方法,允许开发者在Bean初始化之后进行一些自定义的处理。
-
使用(Bean is in use): 在初始化完成后,Bean可以被应用程序使用。在这个阶段,Bean处理业务逻辑,响应请求。
-
销毁(Destruction): 当Bean不再被需要,或容器关闭时,Spring容器会销毁Bean。包括执行自定义的销毁方法(如果有)以及执行DisposableBean接口的destroy()方法。
需要注意的是,不同类型的Bean在生命周期中的回调方法有所不同。例如,当使用配置文件声明Bean时,可以通过init-method和destroy-method属性指定Bean的初始化和销毁方法。而对于通过注解方式配置的Bean,则可以使用@PostConstruct和@PreDestroy注解来标注对应的方法。
理解Spring Bean的生命周期对于解决各种与Bean相关的任务和问题至关重要,如资源管理、依赖注入、AOP等。因此,开发者应该熟悉Bean的生命周期,并根据需要合理利用Bean的生命周期扩展点进行自定义逻辑的处理。
3.springmvc的生命周期
Spring MVC是一个基于Spring框架的Web应用程序开发框架,它有自己的生命周期与请求-响应过程相关。下面是Spring MVC的典型生命周期:
-
DispatcherServlet的初始化: 当Web容器(如Tomcat)启动时,会加载并初始化配置的DispatcherServlet组件。它充当了前端控制器的角色,并负责管理请求的分发和处理。
-
初始化阶段: DispatcherServlet在初始化时会加载并配置一些必要的组件,如处理器映射器(Handler Mapping)、处理器适配器(Handler Adapter)、视图解析器(View Resolver)等。
-
请求到达: 当客户端发送请求到服务器时,DispatcherServlet将根据配置的Handler Mapping找到合适的处理器(Handler)来处理请求。通常,Handler是一个Controller类或处理请求的方法。
-
Handler适配: 通过适配器(Handler Adapter)将Handler适配为Spring MVC能够理解的处理器。
-
处理器拦截器的执行: 在Handler执行前,可以配置拦截器(Handler Interceptor)来拦截请求并执行特定的逻辑,如身份验证、日志记录等。
-
Handler方法执行: 当Handler被适配后,Spring MVC会调用相应的Handler方法来处理请求。方法执行后,会返回一个ModelAndView对象,包含视图名和模型对象。
-
视图解析及渲染: Spring MVC使用视图解析器(View Resolver)来解析视图名,并选择合适的视图实现来进行渲染。渲染过程将模型数据合并到视图中,生成响应结果。
-
结果处理: 处理生成的响应结果,可以进行进一步的处理,如可以应用模型数据绑定、数据格式转换等。
-
渲染视图: 将渲染好的视图发送给客户端,并完成请求-响应的往返过程。
-
处理器拦截器的最后执行: 在返回响应给客户端之前,执行任何配置的处理器拦截器的后置逻辑,如清理资源等。
-
请求结束: 请求处理结束,DispatcherServlet释放请求相关的资源。
需要注意的是,上述的生命周期只针对单个请求,对于Spring MVC应用程序而言,会存在多个请求的并发处理,每个请求都会经历上述的生命周期过程。了解Spring MVC的生命周期有助于理解请求处理的整个过程,以及在特定的阶段插入自定义的逻辑进行处理。
4.springmvc的参数传递
在Spring MVC中,参数传递是通过请求和处理方法之间的方式进行的。Spring MVC提供了多种方式来传递参数,包括请求参数、路径变量、请求体等。下面是常见的参数传递方式:
-
请求参数传递: 请求参数是通过URL中的查询字符串传递的,格式是
?key=value
。Spring MVC可以通过在处理方法的参数上添加@RequestParam
注解来接收请求参数,如:@GetMapping("/user") public String getUser(@RequestParam("id") int userId) { // 处理请求参数 }
-
路径变量传递: 路径变量是通过URL中的路径传递的,格式是
/user/{id}
。Spring MVC使用@PathVariable
注解将路径变量映射到处理方法的参数,如:@GetMapping("/user/{id}") public String getUser(@PathVariable("id") int userId) { // 处理路径变量 }
-
请求体传递: 请求体是POST请求中附带的数据,可以是JSON、表单数据等。Spring MVC使用
@RequestBody
注解将请求体映射到处理方法的参数上,如:@PostMapping("/user") public String createUser(@RequestBody User user) { // 处理请求体 }
-
参数绑定: Spring MVC还支持参数绑定,可以通过
@ModelAttribute
注解将请求参数绑定到Java对象上,或使用BindingResult
接收参数验证结果,如:@PostMapping("/user") public String createUser(@ModelAttribute("user") @Valid User user, BindingResult result) { // 参数绑定和验证 }
-
请求头传递: 请求头是通过HTTP头部字段传递的数据,Spring MVC可以使用
@RequestHeader
注解将特定的请求头字段映射到处理方法的参数上,如:@GetMapping("/user") public String getUser(@RequestHeader("User-Agent") String userAgent) { // 处理请求头字段 }
通过上述的参数传递方式,Spring MVC能够灵活地处理不同类型的请求,并将请求中的参数传递给处理方法进行处理。需要根据具体的需求和业务逻辑选择合适的参数传递方式。
5.spring事物失效的场景
Spring事务的失效场景是指在某些情况下,由于配置不正确或程序逻辑错误,事务无法正常工作,导致数据库的一致性和可靠性受到影响。以下是一些常见的导致Spring事务失效的场景:
-
不正确的事务配置: 错误地配置事务管理器或事务注解可能导致事务失效。例如,未正确配置事务管理器或未在目标方法上添加@Transactional注解。
-
方法内部自调用: 当一个被@Transactional注解标记的方法在同一个类中被另一个方法调用时,事务注解可能会失效。这是因为Spring使用动态代理来实现事务切面,而动态代理只能拦截从外部调用的方法。
-
异常处理: 当一个事务方法中出现异常时,如果没有适当的异常处理策略,事务可能会失效。例如,将异常捕获并处理,而不是将其传播给上层调用。
-
不受事务管理的操作: 如果在一个事务方法中执行不受事务管理的操作,例如使用JdbcTemplate执行手动提交的SQL语句或直接访问数据库连接,事务可能会失效。
-
跨越多个数据源: 如果在一个事务中使用了多个数据源,并且没有正确配置跨数据源的事务管理器,事务可能会失效。
-
异步方法: 当使用Spring的异步方法(@Async)执行时,事务注解可能无法应用在异步方法上,从而导致事务失效。
-
不受检查的异常: 默认情况下,Spring事务只对受检查的异常触发回滚,而对于不受检查的异常(如RuntimeException)不会触发回滚。需要在@Transactional注解中使用rollbackFor属性来指定异常类型,以确保在出现指定异常时回滚事务。
-
与外部事务传播机制不匹配: 当使用@Transactional注解标记的方法嵌套在不同的事务传播机制中时,可能会导致事务失效。例如,两个方法都标记为REQUIRED传播类型,但它们在同一事务中调用时,内部方法不会重新开启一个事务。
要避免这些失效场景,需要仔细配置和管理Spring事务。确保正确配置事务管理器、合理应用事务注解、正确处理异常,以及理解事务传播行为等是保障事务正确工作的关键。
6.springmvc的执行流程
Spring MVC的执行流程描述了当客户端发送请求到前端控制器DispatcherServlet后,如何进行请求的处理和响应的返回。下面是Spring MVC的典型执行流程:
-
客户端发起请求: 客户端发送一个HTTP请求到Web服务器,并由Web容器(如Tomcat)接收。
-
前端控制器接收请求: Web容器将请求发送给DispatcherServlet,它是Spring MVC框架的核心组件。
-
处理器映射器确定处理器: DispatcherServlet使用处理器映射器(Handler Mapping)根据请求URL找到合适的处理器(Handler),通常是一个Controller类或处理请求的方法。
-
处理器适配器执行处理器: 处理器适配器(Handler Adapter)作为DispatcherServlet的一部分,负责执行处理器,并将请求参数传递给处理器方法。处理器适配器会根据处理器的类型选择适当的方式来执行处理器。
-
处理器方法处理请求: 处理器方法执行业务逻辑,处理请求并生成相应的结果。处理器方法可以访问请求参数、调用业务逻辑、获取模型数据等。
-
处理器方法返回模型和视图信息: 处理器方法返回一个包含模型数据和视图名称的ModelAndView对象或其他适当的返回类型(如字符串、JSON数据等)。
-
视图解析器解析视图: 视图解析器(View Resolver)将视图名称解析为实际的视图对象,通常是一个JSP页面、Thymeleaf模板、HTML页面等。
-
视图渲染: 视图对象将模型数据和页面模板进行合并,生成最终的视图结果。
-
返回响应: 最终的视图结果被发送给客户端作为HTTP响应数据,包括渲染好的HTML页面、JSON数据等。
整个流程中还涉及到拦截器、数据绑定、数据验证、异常处理等其他环节,这些环节可以根据需要进行配置和扩展来实现更复杂的功能。
了解Spring MVC的执行流程可以帮助开发者理解请求的处理过程,并在需要时进行定制和扩展。
7.spring注解
-
springweb
-
同一异常处理
-
mapping
-
rest
-
参数
-
数据类型转换
-
validation
-
scope
-
ajax
-
-
springboot的注解
-
spring缓存的注解
-
spring组件扫描与配置类的注解
-
spring切面的注解
-
spring事件,调度,异步注解
-
spring的核心注解
-
spring监控注解
-
spring事务注解
-
springlang注解
-
一共注解60+32+48
Spring框架提供了丰富的注解,用于声明和配置应用程序的各个方面。以下是一些常用的Spring注解:
-
@Component: 用于标识一个普通的Spring组件类,表示此类将被Spring自动扫描并注册为一个Bean。
-
@Controller: 用于标识一个控制器类,通常在Spring MVC中使用。它表示此类将处理用户请求并返回相应的响应。
-
@Service: 用于标识一个服务类,表示此类实现了业务逻辑,并通常被其他组件调用。
-
@Repository: 用于标识一个数据访问类,表示此类用于访问数据存储,如数据库、文件等。
-
@Autowired: 用于自动注入依赖关系,可以用于构造方法、字段、setter方法和普通方法上。Spring会自动查找匹配的Bean,并将其注入。
-
@Qualifier: 与@Autowired一起使用,用于指定具体的Bean名称注入。
-
@Value: 用于注入简单类型的值或表达式,可以注入配置文件中的属性值。
-
@Scope: 用于指定Bean的作用域,例如单例(Singleton)、原型(Prototype)等。
-
@Configuration: 用于声明配置类,Spring会将其作为Java配置进行处理。
-
@Bean: 用于声明一个Bean,在@Configuration类中使用,表示将方法的返回对象注册为Bean。
-
@RequestMapping: 用于映射请求URL和请求方法到处理器方法。可以在Controller类或处理器方法上使用。
-
@PathVariable: 用于从URL中获取路径变量的值,并注入方法参数。
-
@RequestParam: 用于获取请求参数的值,并注入方法参数。
-
@ResponseBody: 用于将方法的返回值直接作为响应体返回给客户端,通常用于返回JSON、XML数据等。
-
@ExceptionHandler: 用于捕获和处理特定异常,可以在Controller类中定义。
-
@Transactional: 用于指定方法或类的事务属性,表示此方法或类将在事务中执行。
以上只是一些常见的Spring注解,实际上Spring框架还提供了更多的注解来满足不同应用场景的需要。使用这些注解可以简化配置工作,提高开发效率,使代码更加清晰和易于维护。
Spring Boot是基于Spring框架的快速开发框架,它提供了一系列注解来简化配置和开发过程。以下是一些常用的Spring Boot注解:
-
@SpringBootApplication: 主要用于启动类上,标识一个Spring Boot应用的入口点,包含了@ComponentScan、@EnableAutoConfiguration和@Configuration三个注解。
-
@ConfigurationProperties: 用于将配置文件中的属性值绑定到Java对象上,可以在应用程序中直接使用这些属性。
-
@EnableAutoConfiguration: 用于启用自动配置机制,Spring Boot将根据项目的依赖和配置自动配置应用程序。
-
@ComponentScan: 用于扫描指定包及其子包下的组件,并注册为Spring管理的Bean。
-
@RestController: 类似于@Controller,用于标识一个控制器类,但是@ResponseBody注解会自动应用在所有处理方法上,将方法的返回值直接作为响应体返回给客户端。
-
@RequestMapping: 用于映射请求URL和请求方法到处理器方法上,类似于Spring MVC中的@RequestMapping。
-
@PathVariable: 用于从URL中获取路径变量的值,并注入方法参数。
-
@RequestParam: 用于获取请求参数的值,并注入方法参数。
-
@RequestBody: 用于将请求体的内容绑定到方法参数上,通常用于接收JSON或XML数据。
-
@ResponseBody: 用于将方法的返回值直接作为响应体返回给客户端,通常用于返回JSON、XML数据等。
-
@Autowired: 用于自动注入依赖关系,类似于Spring框架中的@Autowired。
-
@Value: 用于注入简单类型的值或表达式,可以注入配置文件中的属性值。
-
@Conditional: 用于根据条件动态地选择是否创建Bean。
-
@Bean: 通常与@Configuration一起使用,在配置类中声明一个Bean,Spring Boot会自动将其注册为Spring管理的Bean。
-
@EnableScheduling: 用于开启定时任务的支持。
-
@EnableAsync: 用于开启异步方法的支持。
以上只是一些常用的Spring Boot注解,实际上Spring Boot提供了更多的注解来简化配置和开发。这些注解的使用可以显著简化Spring应用程序的开发工作,提高开发效率。
8.spring的设计模式
-
一共 23种
在Spring框架中,采用了多种设计模式来实现不同的功能和解决不同的问题。以下是一些在Spring中常见的设计模式:
-
单例模式(Singleton Pattern): Spring默认使用单例模式管理Bean,确保在整个应用程序中只存在一个实例。这样可以节省资源并提高性能。
-
工厂模式(Factory Pattern): Spring使用工厂模式创建和管理Bean。通过配置文件或注解来声明Bean定义,Spring根据配置信息创建和管理相应的Bean实例。
-
依赖注入模式(Dependency Injection Pattern): Spring的核心特性之一就是依赖注入(DI),它通过注入依赖关系来解耦组件之间的依赖关系。通过依赖注入,可以实现松耦合和可测试性。
-
观察者模式(Observer Pattern): Spring的事件机制采用观察者模式实现。通过定义事件和监听器,以及发布和监听事件,实现了模块之间的事件通知和处理机制。
-
模板模式(Template Pattern): Spring在JDBC、JPA等数据访问模块中使用了模板模式,定义了通用的数据访问操作的模板,开发者只需提供少量的定制代码。
-
代理模式(Proxy Pattern): Spring使用代理模式实现AOP(面向切面编程),通过动态代理在目标对象的前后添加额外的功能。Spring中的AOP能够实现事务管理、日志记录等功能。
-
适配器模式(Adapter Pattern): Spring的适配器模式主要用于整合第三方库或旧代码。适配器模式允许Spring将外部类或接口适配为符合Spring框架要求的形式。
-
委托模式(Delegation Pattern): Spring中的委托模式常用于分发处理请求或事件。通过将任务委托给特定的处理器或对象,实现了职责的分离和模块化。
除了上述设计模式,Spring框架还使用了其他设计模式,如策略模式、装饰器模式、访问者模式等,以实现不同的功能和解决各种问题。这些设计模式的使用让Spring框架具备了灵活、可扩展和易于维护的特性。
9.设计模式
在软件开发中,有许多不同的设计模式用于解决特定的问题或实现特定的功能。以下是23种经典的设计模式,它们被分为三类:创建型模式、结构型模式和行为型模式。
创建型模式(Creational Patterns):
-
简单工厂模式(Simple Factory Pattern)
-
工厂方法模式(Factory Method Pattern)
-
抽象工厂模式(Abstract Factory Pattern)
-
单例模式(Singleton Pattern)
-
原型模式(Prototype Pattern)
-
建造者模式(Builder Pattern)
结构型模式(Structural Patterns):
-
适配器模式(Adapter Pattern)
-
桥接模式(Bridge Pattern)
-
组合模式(Composite Pattern)
-
装饰器模式(Decorator Pattern)
-
外观模式(Facade Pattern)
-
享元模式(Flyweight Pattern)
-
代理模式(Proxy Pattern)
行为型模式(Behavioral Patterns):
-
职责链模式(Chain of Responsibility Pattern)
-
命令模式(Command Pattern)
-
解释器模式(Interpreter Pattern)
-
迭代器模式(Iterator Pattern)
-
中介者模式(Mediator Pattern)
-
备忘录模式(Memento Pattern)
-
观察者模式(Observer Pattern)
-
状态模式(State Pattern)
-
策略模式(Strategy Pattern)
-
模板方法模式(Template Method Pattern)
这些设计模式各自解决了不同的问题,并在软件设计中发挥了重要作用。熟悉这些设计模式可以帮助开发者更好地设计和构建可维护、可扩展的软件系统。需要注意的是,并不是每个问题都需要使用设计模式,开发者应根据具体的情况选择适合的模式。
10.循环依赖
循环依赖指的是多个对象相互之间存在依赖关系形成闭环的情况。在软件开发中,循环依赖通常是一个设计上的问题,会导致程序无法正常初始化或运行。
在Java中,循环依赖的典型场景是类之间的循环依赖。例如,类A依赖类B,而类B又依赖类A,形成了一个循环依赖。
循环依赖可能会导致以下问题:
-
无法实例化对象:当两个或多个对象相互之间循环依赖时,无法判断首先实例化哪个对象,从而导致无法成功实例化。
-
死锁:循环依赖还可能导致死锁的情况发生,当两个对象互相等待对方的初始化完成,如果没有适当的机制来打破这种循环等待关系,程序可能会陷入死锁状态。
为了解决循环依赖问题,可以考虑以下方法:
-
优化设计:尽量避免设计上的循环依赖,可以通过重新定义类之间的关系或引入新的抽象解决循环依赖。
-
使用延迟初始化:延迟初始化可以推迟对象之间的依赖关系,减少循环依赖的发生。
-
控制依赖关系的注入时机:显式地控制依赖关系的注入时机,通过Setter方法或其他手段在对象初始化完成后再进行依赖的注入。
-
引入中间层或解耦:通过引入中间层或解耦来减少循环依赖的影响,将原本循环依赖的对象分离为不同的模块或组件。
总之,循环依赖是需要尽量避免的设计和编程问题。在开发过程中,应该尽量解除循环依赖关系,以确保应用程序的正常运行和可维护性。
11.代理模式
代理模式是一种结构型设计模式,它允许通过代理对象控制对另一个对象的访问。代理模式在访问对象时引入了一个额外的中间代理层,以便可以添加额外的功能或控制访问。
代理模式的核心思想是将客户端和真实对象之间进行解耦,客户端不需要直接访问真实对象,而是通过代理对象进行间接访问。这样可以在不修改真实对象的情况下,对其进行功能增强、权限控制、远程访问等操作。
代理模式涉及三个主要角色:
-
抽象主题(Subject):定义了真实主题和代理主题的共同接口,客户端通过该接口访问对象。
-
真实主题(Real Subject):实现了抽象主题接口,是客户端真正关心的对象。
-
代理主题(Proxy Subject):也实现了抽象主题接口,是客户端访问真实主题的代理对象。代理对象拥有对真实主题的引用,并可以在调用真实主题的方法前后执行额外的操作。
代理模式可以应用于以下场景:
-
远程代理:通过代理对象处理远程方法调用,隐藏网络通信的复杂性。
-
虚拟代理:代理对象在需要时创建真实对象,延迟加载真实对象,从而提高系统性能。
-
安全代理:代理对象可以进行权限验证或加密解密等安全操作,保护真实对象。
-
智能代理:代理对象可以添加额外的逻辑,如缓存、日志记录、性能统计等。
总结来说,代理模式在访问对象时引入了一个代理对象,提供了对真实对象的间接访问和控制。通过代理模式,我们可以实现对真实对象的功能增强、权限控制、远程访问等操作,同时保持了客户端与真实对象的解耦。
12.spring的三级缓存
在Spring框架中,Bean的创建过程中涉及到三级缓存,用于存储不同阶段的Bean实例。这三个级别的缓存分别是:单例对象缓存、早期对象缓存和原始对象缓存。
-
单例对象缓存: Spring框架中的单例Bean是指全局唯一的Bean实例。在Bean的创建过程中,如果是单例Bean并且已经创建完成,则会在这个缓存中找到对应的实例对象。
-
早期对象缓存: 早期对象缓存用于存储正在创建中的Bean实例。在Bean的创建过程中,当实例化的Bean对象尚未完成时,会先放入早期对象缓存中。这样可以防止循环引用导致的无限递归创建。
-
原始对象缓存: 原始对象缓存存储了通过工厂方法或构造函数创建的原始Bean对象。在Bean实例化完成后,会将对象放入原始对象缓存中,以便在后续使用的时候,可以使用缓存的对象而不是每次都重新创建。
三级缓存在Spring框架的Bean生命周期中发挥重要作用,确保了单例Bean的正确创建和管理。它们协同工作,使得Spring能够处理循环依赖问题,避免了无限递归创建对象的情况发生,并提高了性能。但需要注意的是,三级缓存仅在单例Bean的创建过程中使用,对于原型(Prototype)作用域的Bean并不适用。
13.循环依赖与三级缓存
循环依赖和三级缓存是两个不同的概念,它们在Spring框架中的Bean生命周期中扮演不同的角色。
循环依赖指的是多个Bean之间相互依赖形成闭环的情况。当两个或多个Bean之间存在循环依赖时,如果没有适当的处理机制,会导致无法正常初始化和注入依赖。Spring框架通过使用三级缓存来解决循环依赖问题,确保Bean的正确创建和初始化。
三级缓存是Spring框架中用于缓存Bean实例的机制,在Bean的创建过程中起到关键作用。三级缓存分为单例对象缓存、早期对象缓存和原始对象缓存,并协同工作来处理Bean的实例化和依赖注入。它们的具体作用如下:
-
单例对象缓存:用于存储已经创建完成的单例Bean实例。当需要获取一个已创建的单例Bean时,会首先尝试从单例对象缓存中获取。
-
早期对象缓存:用于存储正在创建中的Bean实例。当Bean的实例化过程尚未完成时,会将对象放入早期对象缓存中,以便解决循环依赖问题。
-
原始对象缓存:用于存储通过工厂方法或构造函数创建的原始Bean对象。在Bean实例化完成后,会将对象放入原始对象缓存中,以便在后续使用的时候可以使用缓存的对象而不是每次都重新创建。
通过使用三级缓存,Spring框架能够处理循环依赖问题。当检测到循环依赖时,Spring会在早期对象缓存中查找并返回对象的代理,以满足对于未创建完成的Bean的依赖注入需求。
总结来说,循环依赖和三级缓存是Spring框架中不同概念的关键要素。循环依赖是指多个Bean之间相互依赖形成闭环,而三级缓存是Spring框架通过缓存机制解决循环依赖问题的关键工具。
14.setter循环依赖
Setter方法循环依赖指的是多个对象之间的依赖关系形成闭环,并且这些依赖关系是通过Setter方法进行设置的情况。
当存在Setter方法的循环依赖时,对象A依赖对象B,同时对象B又依赖对象A。在依赖注入过程中,如果没有适当的处理机制,会导致循环依赖无法解决,程序无法正常初始化。
在Spring框架中,存在一个解决Setter方法循环依赖的机制,称为"后处理器"(post-processor)。当Spring容器发现存在循环依赖时,会使用后处理器来解决问题。
解决Setter方法循环依赖的典型过程如下:
-
创建A对象的实例。
-
注入A对象的依赖,包括B对象的引用。
-
发现B对象的依赖需要A对象的引用,但A对象还未初始化完成,此时创建一个B对象的代理。
-
完成A对象的初始化。
-
设置B对象的A引用为已初始化的A对象。
-
B对象使用A对象的引用进行进一步初始化。
通过使用后处理器和代理对象,Spring能够在循环依赖的情况下,避免无限递归创建对象并解决循环依赖问题。
然而,需要注意的是,Setter方法循环依赖的存在往往意味着设计上的问题,可能导致代码结构复杂、难以理解和维护。因此,在开发过程中,建议尽量避免出现Setter方法循环依赖,进行合理的对象设计和依赖关系管理。
15.构造器循环依赖
构造器循环依赖是指多个对象之间的依赖关系形成闭环,并且这些依赖关系是通过构造器进行传递的情况。
当存在构造器的循环依赖时,对象A的构造器依赖于对象B,同时对象B的构造器又依赖于对象A。在对象初始化的过程中,如果没有适当的处理机制,会导致循环依赖无法解决,程序无法正常初始化,并可能会导致堆栈溢出。
在Java中,通常循环依赖是无法通过构造器解决的。因为构造器的调用是在对象实例化过程中进行的,在还没有创建出完整的对象之前,无法通过构造器进行依赖注入。
针对构造器循环依赖的情况,有一些解决方法和建议:
-
通过Setter方法解决:将循环依赖的对象作为Setter方法的参数进行注入,而不是在构造器中进行注入。这样可以避免在对象实例化过程中出现循环依赖的问题。
-
使用依赖查找:将循环依赖的对象通过依赖查找(Dependency Lookup)方式获取,而不是通过构造器进行依赖注入。这种方式需要在对象初始化完成后再进行依赖注入。
-
重新设计对象依赖关系:尝试重新审视对象之间的依赖关系,考虑是否可以降低或消除循环依赖。通过重新设计对象的拆分和组织,可以减少循环依赖的发生。
需要注意的是,虽然一些依赖注入框架(如Spring)提供了一些特殊处理来解决部分构造器循环依赖问题,但在设计和编码中,仍应当避免出现复杂的构造器循环依赖关系。良好的对象设计和依赖管理是减少循环依赖问题的关键。