首页 > 其他分享 >吾之蜜糖

吾之蜜糖

时间:2024-05-26 21:22:11浏览次数:7  
标签:蜜糖 事务 Java 对象 Spring 线程 内存

吾之蜜糖

本人整理的面试常见问答,But!!!My honey, your poison!

有些问题的回答可能不是很准确,欢迎大家指正。

这里并非最新文档,因为CSDN比较麻烦,最新文档请移步GitBook

基础

equals与hashCode

关联和约定

  1. 哈希表的使用: 在 Java 中,哈希表(如 HashMapHashSet 等)是基于哈希算法实现的数据结构,它通过哈希码(hash code)来快速定位对象的存储位置。
  2. 约定一:相等对象必须具有相等的哈希码: 如果两个对象根据 equals() 方法判断相等(即 obj1.equals(obj2) 返回 true),那么这两个对象的哈希码必须相等(即 obj1.hashCode() == obj2.hashCode())。
  3. 约定二:不相等的对象尽量有不同的哈希码: 如果两个对象根据 equals() 方法判断不相等,不要求它们的哈希码一定不相等,但是为了提高哈希表的性能,不相等的对象尽量有不同的哈希码,减少哈希冲突。

为什么需要重写 hashCode() 方法?

  • 保证哈希表性能: 如果重写了 equals() 方法,但没有重写 hashCode() 方法,那么两个根据 equals() 判断相等的对象可能会有不同的哈希码,这违反了约定一,会导致这两个对象在哈希表中无法正确被处理,甚至无法通过 HashMapHashSet 正确地进行存储和检索。
  • 确保对象一致性: 重写 hashCode() 方法可以保证根据 equals() 方法判断相等的对象拥有相等的哈希码,从而在使用哈希表时能够正确地识别和处理相等的对象。

抽象类和接口有什么区别?

  1. 成员方法实现:
    • 抽象类: 可以包含普通方法的实现,也可以包含抽象方法(没有具体实现,只有方法声明)。抽象类中的非抽象方法可以提供默认实现,子类可以选择性地覆盖这些方法。
    • 接口: 只能包含抽象方法的声明,不包含方法的实现。在 Java 8 及以后的版本中,接口可以包含默认方法和静态方法的实现。
  2. 多继承:
    • 抽象类: Java 中的类只能单继承,因此抽象类只能继承一个具体类或抽象类。但是抽象类可以实现多个接口。
    • 接口: 接口支持多继承,一个类可以实现多个接口,从而实现多重继承的效果。
  3. 构造方法:
    • 抽象类: 可以有构造方法,并且抽象类的构造方法在子类实例化时会被调用。
    • 接口: 不允许有构造方法,接口中不能定义实例字段,因为接口中的方法都是抽象的,没有实例变量可以初始化。
  4. 用途和设计:
    • 抽象类: 是对类的一种抽象,是一种模版设计。
    • 接口: 用于定义一种能力或行为,描述了一种规范或契约,实现接口的类需要提供接口中定义的所有方法的具体实现。接口适合用于不同类之间的行为规范和统一的契约。

Class#forName 和 ClassLoader 区别

  • Class#forName(...) 方法,除了将类的 .class 文件加载到JVM 中之外,还会对类进行解释,执行类中的 static 块。
  • ClassLoader 只干一件事情,就是将 .class 文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。

动态代理

CGlib(Code Generation Library)和JDK动态代理都是Java中实现动态代理的技术。

JDK动态代理

概念:JDK动态代理基于Java的反射机制,只能代理实现了接口的类。它在运行时生成代理类,该代理类实现了指定的接口,并将所有调用委派给一个InvocationHandler。

使用场景:适用于需要代理实现接口的类。

CGlib动态代理

概念:CGlib是一个开源的字节码生成库,通过使用ASM库操作字节码实现。CGlib可以代理没有实现接口的类,它生成目标类的子类,并覆盖其中的方法。

使用场景:适用于需要代理没有实现接口的类。

比较

  • 实现方式
    • JDK动态代理基于接口实现,必须通过实现接口来代理类。
    • CGlib动态代理通过生成目标类的子类进行代理,可以代理没有实现接口的类。
  • 性能
    • JDK动态代理在小规模代理情况下性能较好,但在大规模代理时性能不如CGlib。
    • CGlib通过直接操作字节码,性能较高,但生成代理类开销较大。
  • 使用限制
    • JDK动态代理要求目标类必须实现接口。
    • CGlib动态代理无法代理final类和final方法。

选择哪种代理方式主要取决于具体的应用场景和需求。

集合

快速失败(fail-fast)和安全失败(fail-safe)的区别

  • 快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个 ConcurrentModification 异常。 在 java.util 包下的都是快速失败。
  • 安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出 ConcurrentModification 异常。在 java.util.concurrent 包下的全是安全失败的。

Comparable 和 Comparator 的区别?

  • Comparable 接口,在 java.lang 包下,用于当前对象和其它对象的比较,所以它有一个 #compareTo(Object obj) 方法用来排序,该方法只有一个参数。
  • Comparator 接口,在 java.util 包下,用于传入的两个对象的比较,所以它有一个 #compare(Object obj1, Object obj2) 方法用来排序,该方法有两个参数。

ArrayList 与 LinkedList 区别?

ArrayList

  • 优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
  • 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。

LinkedList

  • 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作 add 和 remove ,LinedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
  • 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。

ArrayList 是如何扩容

  • 如果通过无参构造的话,初始数组容量为 0 ,当真正对数组进行添加时,才真正分配容量。每次按照 1.5 倍(位运算)的比率通过 copeOf 的方式扩容。
  • 在 JKD6 中实现是,如果通过无参构造的话,初始数组容量为10,每次通过 copeOf 的方式扩容后容量为原来的 1.5 倍。

HashMap 和 Hashtable 的区别

线程安全性

  • Hashtable: 是线程安全的类,它的方法都是同步的(synchronized),多个线程可以安全地访问一个 Hashtable 实例,但这也导致在多线程环境下性能相对较低。
  • HashMap: 是非线程安全的类,它的方法没有进行同步处理,因此多个线程同时访问 HashMap 可能导致数据不一致或其他问题。若需要在多线程环境下使用 HashMap,可以通过 Collections.synchronizedMap() 方法来创建同步的 HashMap

Null 键和值的处理

  • Hashtable: 不允许使用 null 作为键或值,否则会抛出 NullPointerException
  • HashMap: 允许使用 null 作为键和值,即 HashMap 中可以存储键或值为 null 的条目。

性能

  • HashMap: 由于 HashMap 非线程安全,不进行同步处理,因此在单线程环境下性能较高。
  • Hashtable: 由于 Hashtable 所有方法都进行了同步处理,因此在多线程环境下保证了线程安全,但性能相对较低。

现状

  • Hashtable: 继承自 Dictionary 类,已经被淘汰(Deprecated)。
  • HashMap: 实现了 Map 接口,是 AbstractMap 的子类,属于 Java Collections Framework 的一部分。

初始容量和扩容机制

  • HashTable 中数组默认大小是 11 ,扩容方法是 old * 2 + 1 ,HashMap 默认大小是 16 ,扩容每次为 2 的指数大小。

HashMap 和 ConcurrentHashMap区别

  1. 内部结构:在JDK1.8之前,ConcurrentHashMap使用分段锁(Segmentation)来提供并发性。每个段本质上是一个独立的HashMap,并且拥有一个锁。在JDK1.8之后,Segmentation被移除了。ConcurrentHashMap采用了一种不同的锁机制(synchronized和CAS操作)来提高并发性。
    • 当数组中当前位置为空时,使用CAS来把新的节点写入数组中对应的位置。
    • 当数组中当前位置不为空时,通过加锁(synchronized)来添加或删除节点。如果当前位置是链表,就遍历链表找到合适的位置插入或删除节点。如果当前位置是红黑树,就按照红黑树的规则插入或删除节点。
    • 当链表长度超过阈值(默认为8)时,就把链表转换为红黑树。当红黑树节点数小于阈值(默认为6)时,就把红黑树转换为链表。
  2. 线程安全:HashMap非线程安全,不能保证在多线程环境下的共享访问,而ConcurrentHashMap是线程安全的,设计用于多线程的环境中。
  3. 性能:由于ConcurrentHashMap的线程安全特性,它在多线程环境下比HashMap有更好的性能。它通过使用复杂的锁策略和CAS操作来最小化锁的竞争。
  4. 内存一致性:ConcurrentHashMap的读操作可以不加锁,并且其写操作可以延迟更新到主存,不同步其他读写操作,而HashMap在多线程下使用时需要外部同步。

HashMap 是 Java 中常用的哈希表实现的数据结构,用于存储键值对。在理解 HashMap 的工作原理和在 JDK 1.8 之后向其中添加元素可能发生的情况之前,我们先来了解 HashMap 的基本原理和核心概念。

HashMap 的工作原理

  1. 哈希表数组: HashMap 内部维护一个数组,数组的每个元素称为桶(bucket)。桶是存放键值对的基本单元,每个桶可能存放一个链表或红黑树,用来解决哈希冲突(即多个键映射到同一个桶的情况)。
  2. 哈希函数: 当向 HashMap 中添加键值对时,首先会根据键的 hashCode() 方法计算哈希值,然后通过哈希函数确定键值对应该存放在数组的哪个桶中。
  3. 解决哈希冲突: 如果多个键的哈希值映射到同一个桶中(即发生哈希冲突),HashMap 使用链表或红黑树来存储这些键值对。链表用于简单的存储,而当链表长度超过一定阈值(默认为8),链表会转换为红黑树,提高查找效率。

JDK 1.8 向 HashMap 添加元素可能发生的情况

在 JDK 1.8 中,向 HashMap 添加元素时可能会触发以下情况:

  1. 计算哈希值: 根据新加入键的 hashCode() 方法计算哈希值。

  2. 确定存放位置: 根据哈希值和当前数组的长度计算键值对应该存放在数组的哪个桶中(hashCode & capacity)。使用&而不是%是因为&的计算效率更高,这也是为什么hashMap扩容后是2的n次幂。

  3. 桶为空或非空处理:

    • 如果目标桶为空(即没有发生哈希冲突),直接将新的键值对存放在该桶中。
    • 如果目标桶已经有其他键值对:
      • 如果目标桶中的元素为链表,将新的键值对追加到链表的末尾。
        • 如果链表的长度超过8,如果桶数组的长度超过64则将链表修改为红黑树。
        • 如果链表的长度超过8,如果桶数组的长度没有超过64则出发扩容操作。
      • 如果目标桶中的元素为红黑树,按照红黑树的规则插入新的键值对。
  4. 链表转红黑树:

    • 当向一个桶中添加元素时,如果该桶中链表长度达到阈值(默认为8),会将链表转换为红黑树,提高查找效率。
  5. 扩容: 如果添加元素后 HashMap 中的元素数量达到数组容量的阈值(负载因子,默认为0.75),会触发扩容操作。扩容会将数组容量增加一倍,并重新计算每个键值对的存放位置。

    int index = hash(key) & (newCapacity - 1);
    

并发

非常推荐《Java并发编程的艺术》一书,虽然是15年出版的,某些知识已经过时,但由于jdk向下兼容,因此很多知识点还是没有变化的。

最重要的:思想永不过时!

简述线程、进程、程序的基本概念?

程序

程序,是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程

进程,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

线程

线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

你了解守护线程吗?它和非守护线程有什么区别?

Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。

守护线程一般由JVM自动创建,也可以通过Thread#setDaemon(boolean on)方法进行手动设置。

当用户线程执行完毕,只剩下守护线程时,JVM会自动退出。

什么是线程饥饿?

饥饿,一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

  • 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

线程的五状态与七状态模型

Thread 的线程状态

创建线程的方式

  • 方式一,继承 Thread 类创建线程类。
  • 方式二,通过 Runnable 接口创建线程类。
  • 方式三,通过 Callable 接口和 Future 创建线程。
  • 方式四,通过线程池创建线程。

一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。

Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread#getUncaughtExceptionHandler() 方法来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 #uncaughtException(exception) 方法进行处理。

Thread#sleep()与Object#wait()区别

  • sleep 方法,是线程类 Thread 的静态方法。调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)
  • wait 方法,是 Object 类的方法。调用对象的 #wait() 方法,会导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 #notify() 方法(或#notifyAll()方法)时,才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

为什么 wait 和 notify 方法要在同步块中调用?

  • Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。
  • wait和notify是用于线程间进行通信的,显然两线程通信过程中让其他线程横叉一脚是不合适的。

为什么你应该在循环中检查等待条件

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

// The standard idiom for using the wait method
synchronized (obj) {
    while (condition does not hold) {
        obj.wait(); // (Releases lock, and reacquires on wakeup)
    }
    ... // Perform action appropriate to condition
}

sleep(0) 有什么用途?

Thread#sleep(0) 方法,并非是真的要线程挂起 0 毫秒,意义在于这次调用 Thread#sleep(0) 方法,把当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread#sleep(0) 方法,是你的线程暂时放弃 CPU ,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作

单例模式的线程安全性?

  • 饿汉式单例模式的写法:线程安全
  • 懒汉式单例模式的写法:非线程安全
  • 双检锁单例模式的写法:线程安全

synchronized的实现原理与应用

总结:

  1. 可以被修饰的对象有哪些?
  2. 对象头中存储的内容?
  3. 锁有几种状态?
  4. 锁如何升级?
  5. 在轻量级锁和重量级锁中,会将对象头中原来的内容存放到哪里?

参考链接:

https://app.gitbook.com/o/kCU9nigbAxy9O5ghLetb/s/4nPAqAgKpdmjLNSFVzrX/java-bing-fa-de-yi-shu#synchronized-de-shi-xian-yuan-li-yu-ying-yong

推荐阅读:

《Java并发编程的艺术》第二章 第2节

https://weread.qq.com/web/reader/a1b42863643425f316430426755426757427657366e61366f5642696438626160ckaab325601eaab3238922e53?

同步方法和同步块,哪个是更好的选择

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

关于volatile你应该知道的事

总结:

volatile在两个方面发挥作用

  1. 以volatile修饰变量保证变量的内存可见性
  2. 以volatile修饰变量防止内存重排序

上述两点都与volatile底层的内存屏障有关,一条指令序列从编译到依次执行会经过三次重排序:

  1. 编译器优化重排序。在不改变单线程语义的前提下,可以重新安排语句的执行顺序,也就是说遵从as if serial规则。
  2. 处理器指令集并行重排序。处理器采用指令级并行技术将多条指令重叠执行。
  3. 内存系统重排序。与处理器使用缓存和读/写缓冲区有关。

正是由于处理器会使用缓存和读/写缓冲区,导致了多线程对同一共享变量读取的并发问题(即JMM中的工作内存和主内存),而volatile使用内存屏障来解决这个问题,volatile插入内存屏障的规则如下:

  • 在volatile写前插入StoreStore内存屏障,使得写之前的所有写指令的结果对本次写可见(底层是使工作内存保存的副本无效)。
  • 在volatile写后插入StoreLoad屏障,使得本次写的结果对之后的读指令可见。
  • 在volatile读后插入一个LoadLoad屏障,确保本次数据的装载先于后续指令数据的装载。
  • 在volatile读后插入一个LoadStore屏障,确保本次数据装载先于后续存储指令刷新到内存。

上面的内存屏障,既可以保证内存可见性,也可以防止指令重排序。

详细总结:

https://app.gitbook.com/o/kCU9nigbAxy9O5ghLetb/s/4nPAqAgKpdmjLNSFVzrX/java-bing-fa-de-yi-shu#huan-cun-yi-zhi-xing

推荐阅读:

《Java并发编程的艺术》第三章 第4节

ps:更推荐阅读第三章的全部内容,虽然有些内容可能已经过时,但是还是会称赞设计者的奇思妙想~

ps:比我写的好多了!

https://weread.qq.com/web/reader/a1b42863643425f316430426755426757427657366e61366f5642696438626160ck1c3321802231c383cd30bb3?

可以创建 volatile 数组吗?

结论:使用 volatile 修饰数组或对象引用可以确保对引用的写入操作对其他线程是可见的,但并不能保证引用指向的对象或数组内部的状态的可见性和一致性。如果需要保证数组或对象内部的状态在多线程环境中的可见性和一致性,需要考虑使用其他并发工具或技术,例如锁(synchronized)、并发集合类(如 ConcurrentHashMapCopyOnWriteArrayList 等)、Atomic 类等

Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组(即将该引用指向一个新数组),将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

同理,对于 Java POJO 类,使用 volatile 修饰,只能保证这个引用的可见性,不能保证其内部的属性。

long和double型变量的读写是否存在并发问题

在32位操作系统中存在并发问题,推荐使用volatile修饰。

32位操作系统的总线只有32位,而double和long类型的数据是64位,可能A线程刚将数据存放至寄存器的高32位,低32位还没有存放,B线程就开始读取该线程。

volatilesynchronized 的异同?

推荐先阅读关于volatile你应该知道的事

  1. 原理不同:volatile的底层原理是通过插入内存屏障与happens before规则来保证多线程之间的内存可见性与防止指令重排,工作在虚拟机和处理器级别。而Synchronize关键字的底层原理是通过对Mark word(对象头)的操作来实现多线程之间的同步,工作在虚拟机级别。

  2. 关键字修饰的内容不同:volatile修饰变量,Synchronize修饰变量,方法,类。

  3. 内存语义相同

    锁释放与volatile写有相同的内存语义。锁获取与volatile读有相同的内存语义。

    在线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。释放也与之类似。

    而当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。写一个volatile变量同理。

    由于他们的内存语义相同,因此对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

  4. 是否具有原子性:Synchronize修饰的内容具有原子性,而对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

  5. 是否造成线程阻塞:volatile不会,Synchronize会。

什么是 Java Lock 接口?

它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

image-20240415210310266

什么是AQS?

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作

锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

FIFO队列示意图:

image-20240415213756194

独占式获取同步状态示意图:

image-20240415213824758

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

推荐阅读:

《Java并发编程的艺术》第五章 第2节

https://weread.qq.com/web/reader/a1b42863643425f316430426755426757427657366e61366f5642696438626160cked332ca0262ed3d2c2191f2?

synchronized 和 ReentrantLock 异同?

  • 相同点
    • 都实现了多线程同步和内存可见性语义。
    • 都是可重入锁。
  • 不同点
    • 同步实现机制不同
      • synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。
      • ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)实现同步。
    • 可见性实现机制不同,即不同线程对同一代码块中共享变量的可见性。
      • synchronized 依赖 JMM保证共享变量的多线程内存可见性。
        • ps:synchronize关键字是通过happens before规则来保证内存可见性的。当线程释放锁时,JMM会将本地内存中共享变量刷新到共享内存;获取锁时,会使本地内存中的共享变量无效化。
      • ReentrantLock 通过 AQS 的 volatile state 保证共享变量的多线程内存可见性。
        • ps:例如在公平锁中,释放锁时会写volatile变量state;在获取锁时会读state。而根据volatilehappens before规则,释放锁的线程写state变量之前的共享变量,在获取锁的线程读取同一个state变量后会立即变得对获取锁的线程可见。本质上也是happens before规则。
      • 两者的本质上是利用同一套方法来保证内存可见性的,即happens before规则插入内存屏障防止指令重排序,以及刷新内存。
        • ps:如果实在不理解这一点,可以仔细阅读《Java并发编程的艺术》第三章内容
    • 使用方式不同
      • synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。
      • ReentrantLock 显示调用 tryLock 和 lock 方法(在try之前调用),需要在 finally 块中释放锁。
    • 功能丰富程度不同
      • synchronized 不可设置等待时间、不可被中断(interrupted)。
      • ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly),尝试非阻塞的获取锁、condition(提供 await、signal 等方法)等丰富功能
    • 锁类型不同
      • synchronized 只支持非公平锁。
      • ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。

什么是JMM?

有些面试官会把java运行时数据区域和JMM混淆。。。(我就yudao过...)

JMM即Java内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

关键词:工作内存,主内存,无效

什么是重排序?

为了提高性能,编译器和处理器会对指令序列进行重排序,排序的几种方式见关于volatile你应该知道的事

但是单线程环境中重排序不会改变程序运行的结果。

存在数据依赖关系的指令不允许重排序。例如int a = 3; int b = a;这样的指令序列。

什么是happens before?

结论:A happens before B即A指令序列的结果对B可见,但是A指令序列不一定要在B之前执行。

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
  • happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。

推荐阅读:《Java并发编程的艺术》第三章 第1.5节 第7.2、7.3节

https://weread.qq.com/web/reader/a1b42863643425f316430426755426757427657366e61366f5642696438626160ck1ff325f02181ff1de7742fc?

https://weread.qq.com/web/reader/a1b42863643425f316430426755426757427657366e61366f5642696438626160ck9f6326602389f61408e3715?

什么是阻塞队列?

BlockingQueue 接口,是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性:

  • 当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞。
  • 当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞。
  • 正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中 放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景,就是 Socket 客户端数据的读取和解析:

  • 读取数据的线程不断将数据放入队列。
  • 然后,解析线程不断从队列取数据解析。

Java有哪些阻塞队列?

  • 【最常用】ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

    此队列按照先出先进的原则对元素进行排序

  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

  • DelayQueue:支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。

  • SynchronousQueue:一个不存储元素的阻塞队列。

    每一个 put 必须等待一个 take 操作,否则不能继续添加元素。并且他支持公平访问队列。

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

    相对于其他阻塞队列,多了 tryTransfer 和 transfer 方法。

    • transfer 方法:如果当前有消费者正在等待接收元素(take 或者待时间限制的 poll 方法),transfer 可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
    • tryTransfer 方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回 false 。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    优势在于多线程入队时,减少一半的竞争。

CAS操作有什么缺点?

  1. ABA问题
  2. 循环时间长开销大,当存在资源竞争时,CAS会发生自旋,浪费CPU资源
  3. 只能保证一个共享变量的院子操作。

为什么使用 Executor 框架?

  1. 耗费性能与资源:每次执行任务创建线程 new Thread() 比较消耗性能,创建一个线程是比较耗时、耗资源的。
  2. 缺乏管理:调用 new Thread() 创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
  3. 不利于拓展:使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

使用Executors创建的线程池?

  • 普通任务线程池
    • #newFixedThreadPool(int nThreads)方法,创建一个固定长度的线程池。
      • 每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
      • 当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
    • #newCachedThreadPool()方法,创建一个可缓存的线程池。
      • 如果线程池的规模超过了处理需求,将自动回收空闲线程。
      • 当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制。
    • #newSingleThreadExecutor()方法,创建一个单线程的线程池。
      • 它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
      • 它的特点是,能确保依照任务在队列中的顺序来串行执行。
  • 定时任务线程池
    • 4、#newScheduledThreadPool(int corePoolSize) 方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
    • 5、#newSingleThreadExecutor() 方法,创建了一个固定长度为 1 的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。

如何使用 ThreadPoolExecutor 创建线程池?

Executors 提供了创建线程池的常用模板,实际场景下,我们可能需要更灵活的线程池,此时就需要使用 ThreadPoolExecutor 类。

核心参数:

  • corePoolSize 参数,核心线程数大小,当线程数 < corePoolSize ,会创建线程执行任务。
  • maximumPoolSize参数,最大线程数, 当线程数 >= corePoolSize 的时候,会把任务放入workQueue队列中。
  • keepAliveTime 参数,保持存活时间,当线程数大于 corePoolSize 的空闲线程能保持的最大时间。
  • unit 参数,时间单位。
  • workQueue参数,保存任务的阻塞队列。
  • handler 参数,超过阻塞队列的大小时,使用的拒绝策略。
  • threadFactory 参数,创建线程的工厂。

ThreadPoolExecutor 有哪些拒绝策略?

ThreadPoolExecutor 默认有四个拒绝策略:

  • ThreadPoolExecutor.AbortPolicy() ,直接抛出异常 RejectedExecutionException 。
  • ThreadPoolExecutor.CallerRunsPolicy() ,直接调用 run 方法并且阻塞执行。
  • ThreadPoolExecutor.DiscardPolicy() ,直接丢弃后来的任务。
  • ThreadPoolExecutor.DiscardOldestPolicy() ,丢弃在队列中队首的任务。

如果我们有需要,可以自己实现 RejectedExecutionHandler 接口,实现自定义的拒绝逻辑。当然,绝大多数是不需要的。

线程池中 submit 和 execute 方法有什么区别?

两个方法都可以向线程池提交任务。

  • #execute(...) 方法,返回类型是 void ,它定义在 Executor 接口中。
  • #submit(...) 方法,可以返回持有计算结果的 Future 对象。

虚拟机

非常推荐《深入理解Java虚拟机》一书,思想永不过时!

虚拟机是什么?

Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class )。

Java 被称为拥有平台无关性的语言。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

但是,跨平台的是 Java 程序(包括字节码文件),,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。

不同平台,不同 JVM

JVM由哪些部分组成?

img

类加载器:Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

运行时数据区域:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

执行引擎:负责执行字节码指令。

本地接口:调用C或C++实现的本地方法。

运行时数据区域

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。

虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

本地方法栈

本地方法栈则为虚拟机使用到的本地(Native)方法服务。

Java堆

此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。

ps:“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap)。

如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。(TLAB预分配,指针碰撞分配)

方法区

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区是《Java虚拟机规范》定义的抽象概念,而永久代和元空间是对该概念的两种不同实现方式。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

直接内存是什么?

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

直接内存(堆外内存)与堆内存比较?

  1. 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
  2. 直接内存 IO 的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。

为什么废弃永久代?

  1. 容易出现内存溢出。
  2. 降低GC的回收效率。

类的加载过程?

总结:加载、验证、准备、解析、初始化

记住关键的作用计科,在无序列表后为解释说明内容。

推荐阅读:《深入理解Java虚拟机》第七章 第3节

https://weread.qq.com/web/reader/cf1320d071a1a78ecf19254kf7632b60310af76640600fa?

  • 加载

    • 通过一个类的全限定名来获取定义此类的二进制字节流。(不仅仅是从class文件中获取)
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的数据存储格式完全由虚拟机实现自行定义。

    类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

  • 验证

    • 文件格式验证,验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
    • 元数据验证,对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息。
    • 字节码验证,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
    • 符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生

    验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

  • 准备

    • 为类中定义的变量(即静态变量)分配内存。
    • 为类中定义的变量设置类变量初始值。

    从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述

  • 解析

    • 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。

    符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

    直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

  • 初始化

    • 初始化阶段就是执行类构造器<clinit>()方法的过程

    <clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及<clinit>()方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作

    <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如代码清单7-5所示。

    <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。

    由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作

    <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

    接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

    Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

对象的创建过程?

  1. 检查是否执行类加载:当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。

    • 指针碰撞
    • 空闲列表

    分配内存时如何保证线程安全?

    • CAS失败重试
    • TLAB预分配,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
  3. 初始化零值:内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。

  4. 设置对象头:例如,是哪个类的实例、对象的GC分代年龄、对象哈希码(实际使用时彩绘进行计算)等。

  5. 执行构造函数<init>:new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

对象的访问定位?

句柄定位,对象移动时只需要修改句柄中的实例数据指针。

直接指针定位,快快快快快!

如何判断对象是否死亡?

  1. 引用技术:没有一个对象引用该对象,引用计数加1,释放则减1,引用计数为0说明可以回收。无法解决循环依赖问题。
  2. 可达性分析算法:从GC Roots向下搜索,不可达对象可以回收(三色标记)。

推荐阅读《深入理解Java虚拟机》第3.2节 第3.4.6节

为什么要有不同的引用类型?

在 Java 中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对 GC 回收时机不可控的妥协。例如,使用弱引用或软引用关联一些大对象,防止OOM。利用强引用加上Map构建告诉缓存等。

垃圾收集算法?

  • 标记清除:内存碎片
  • 标记整理:效率较低
  • 标记复制:浪费一般的内存空间
  • 分代收集:Eden,Survivor From,Survivor To,Old

什么是安全点?

SafePoint 安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread’s representation of it’s Java machine state is well described),比如记录OopMap 的状态,从而确定 GC Root 的信息,使 JVM 可以安全的进行一些操作,比如开始 GC 。

SafePoint 指的特定位置主要有:

  1. 循环的末尾 (防止大循环的时候一直不进入 Safepoint ,而其他线程在等待它进入 Safepoint )。
  2. 方法返回前。
  3. 调用方法的 Call 之后。
  4. 抛出异常的位置。

OopMap:一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。

垃圾收集器?

Serial收集器

单线程,新生代,标记复制算法

ParNew收集器

多线程版serial收集器,新生代,标记复制算法

Parallel Scavenge收集器

达到一个可控制的吞吐量,新生代,标记复制算法

Serial Old收集器

单线程,老年代,标记整理算法

Parallel Old收集器

Parallel Scavenge的老年代版本,吞吐量优先,老年代,标记整理算法

CMS收集器

Concurrent Mark Sweep收集器,获取最短回收停顿时间

  1. 初始标记
  2. 并发标记
  3. 重新标记:标记增量更新中更新的对象
  4. 并发清除

缺点:

  • 对资源敏感:多线程降低应用吞吐量
  • 无法处理并发清除阶段产生的浮动垃圾
  • 内存碎片

Garbage First收集器

  • 基于Region的内存布局:遵循分代收集理论,每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间。

    收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

  • 建立起“停顿时间模型”:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标

  • Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。

  • 优先处理回收价值最大的Region:让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。

  • 整体来看基于“标记-整理”算法,从局部来看(两个Region之间)基于“标记-复制”算法。

回收过程

  1. 初始标记
  2. 并发标记
  3. 最终标记:标记原始快照(SATB)记录下的在并发时有引用变动的对象。
  4. 筛选回收

image-20240421151552370

Shenandoah收集器

了解即可

  • 支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发
  • 没有实现分代
  • 不在使用记忆集记录跨Region引用

详细步骤参见《深入理解Java虚拟机》第3.6.1节

https://weread.qq.com/web/reader/cf1320d071a1a78ecf19254ka25329702bda2557a7b2ba9?

ZGC收集器

  • 低延迟垃圾收集器:ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

  • 基于Region的内存布局:ZGC的Region(在一些官方资料中将它称为Page或者ZPage)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有大、中、小三类容量:

    • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
    • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
    • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段)的,因为复制一个大对象的代价非常高昂。
  • 使用染色指针技术

    • HotSpot虚拟机的几种收集器有不同的标记实现方案,有的把标记直接记录在对象头上(如Serial收集器),有的把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息),而ZGC的染色指针是最直接的、最纯粹的,它直接把标记信息记在引用对象的指针上,这时,与其说可达性分析是遍历对象图来标记对象,还不如说是遍历“引用图”来标记“引用”了。
    • 染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储额外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节。实际上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶体管)的考虑,在AMD64架构中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚至只支持44位(16TB)的物理地址空间。
    • 尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到,如图3-20所示。当然,由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)image-20240421152616011

    染色指针的优势:

    • 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
    • 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。实际上,到目前为止ZGC都并未使用任何写屏障,只使用了读屏障(一部分是染色指针的功劳,一部分是ZGC现在还不支持分代收集,天然就没有跨代引用的问题)。内存屏障对程序运行时性能的损耗在前面章节中已经讲解过,能够省去一部分的内存屏障,显然对程序运行效率是大有裨益的,所以ZGC对吞吐量的影响也相对较低。
    • 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。现在Linux下的64位指针还有前18位并未使用,它们虽然不能用来寻址,却可以通过其他手段用于信息记录。如果开发了这18位,既可以腾出已用的4个标志位,将ZGC可支持的最大堆内存从4TB拓展到64TB,也可以利用其余位置再存储更多的标志,譬如存储一些追踪信息来让垃圾收集器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。

    染色指针如何解决寻址问题:多个虚拟内存对应一块真是的物理内存。

  • 并发收集过程:

    image-20240421153133854

    1. 并发标记,在染色指针上进行
    2. 并发预备重分配:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。
    3. 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
    4. 并发重映射:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
  • 支持“NUMA-Aware”的内存分配:NUMA(Non-Uniform Memory Access,非统一内存访问架构)是一种为多处理器或者多核处理器的计算机所设计的内存架构。由于摩尔定律逐渐失效,现代处理器因频率发展受限转而向多核方向发展,以前原本在北桥芯片中的内存控制器也被集成到了处理器内核中,这样每个处理器核心所在的裸晶(DIE)都有属于自己内存管理器所管理的内存,如果要访问被其他处理器核心管理的内存,就必须通过Inter-Connect通道来完成,这要比访问处理器的本地内存慢得多。在NUMA架构下,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。在ZGC之前的收集器就只有针对吞吐量设计的Parallel Scavenge支持NUMA内存分配,如今ZGC也成为另外一个选择。

分代收集理论中如何解决跨代引用问题?

讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构

这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是目前最常用的一种记忆集实现形式,一些资料中甚至直接把它和记忆集混为一谈。前面定义中提到记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。关于卡表与记忆集的关系,读者不妨按照Java语言中HashMap与Map的关系来类比理解。

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

对象分配规则?

  1. 对象优先在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  2. 大对象直接进入老年代:在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集
  3. 长期存活的对象将进入老年代:HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
  4. 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
  5. 空间分配担保:在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

类加载器是什么?

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

简单化:类加载器,负责读取 Java 字节代码(可能从.class文件,也可能动态生成或从网络获取),并转换成 java.lang.Class 类的一个实例。

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型是什么?

image-20240421155525531

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类

为什么破坏双亲委派模型?

  1. 双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

  2. 对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故。

SpringMVC

DispatcherServlet的工作流程?

流程示意图

  1. 发送请求

    用户向服务器发送 HTTP 请求,请求被 Spring MVC 的调度控制器 DispatcherServlet 捕获。

  2. 映射处理器

    DispatcherServlet 根据请求 URL ,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回。

  3. 处理器适配

    DispatcherServlet 根据获得的 Handler,选择一个合适的HandlerAdapter 。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 #preHandler(...) 方法)。

    提取请求 Request 中的模型数据,填充 Handler 入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:

    • HttpMessageConverter :会将请求消息(如 JSON、XML 等数据)转换成一个对象。
    • 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等。
    • 数据格式化:对请求消息进行数据格式化。如将字符串转换成格式化数字或格式化日期等。
    • 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中。

    Handler(Controller) 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象。

  4. 解析视图

    根据返回的 ModelAndView ,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的 ViewResolver),解析出 View 对象,然后返回给 DispatcherServlet。

  5. 渲染视图 + 响应请求

    ViewResolver 结合 Model 和 View,来渲染视图,并写回给用户( 浏览器 )。

如果我们的Controller类的方法有@ResponseBody注解时,会将对象进行转换并直接返回给客户端。

SpringMVC常见注解有哪些?

  • @Controller
  • @RestController
  • @RequestMapping
  • @GetMapping
  • @PathVariable

拦截器和过滤器有什么区别?

推荐阅读:https://blog.csdn.net/xinzhifu1/article/details/106356958

1、实现原理不同

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

2、使用范围不同

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于ApplicationSwing等程序中。

3、触发时机不同

过滤器拦截器的触发时机也不同,我们看下边这张图。

Filter

过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

4、拦截的请求范围不同

过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

5、控制执行顺序不同

先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

6、注入Bean情况不同

拦截器加载的时间点在springcontext之前,而过滤器加载时间在其之后。

SpringBoot

Spring Boot 是什么?

Spring Boot 是 Spring 的子项目,正如其名字,提供 Spring 的引导( Boot )的功能。

通过 Spring Boot ,我们开发者可以快速配置 Spring 项目,引入各种 Spring MVC、Spring Transaction、Spring AOP、MyBatis 等等框架,而无需不断重复编写繁重的 Spring 配置,降低了 Spring 的使用成本。

Spring Boot 提供了各种 Starter 启动器,提供标准化的默认配置。

SpringBoot的优缺点?

优点:使编码、配置、部署变得简单。

缺点:就像Java内存一样,墙里的人想出来,墙外面的人想进去。springboot提供了Bean自动注入的功能,但也正因如此,如果我们想自定义一些Bean时,就可能存在冲突。

Spring Boot 中的 Starter 是什么?

Starter POM 是一组方便的依赖描述符,我们可以将其引入项目以提供标准化的默认配置。

SpringBoot热部署?

或许可以使用spring-boot-devtools插件?我也没有试过,我嘛也不知道哇!

SpringBoot核心注解?

@SpringBootApplication注解,标注在该注解上的注解重要的有以下三个:

  1. @Configuration:指定类是Bean定义的配置类。
  2. @ComponentScan:扫描指定包下的Bean。
  3. @EnableAutoConfiguration:打开自动配置的功能。
    1. Spring Boot 在启动时扫描项目所依赖的 jar 包,寻找包含spring.factories 文件的 jar 包。
    2. 根据 spring.factories 配置加载 AutoConfigure 类。
    3. 根据 @Conditional 等条件注解的条件,进行自动配置并将 Bean 注入 Spring IoC 中。

Spring

Spring是什么?

"Spring" 在不同的语境中意味着不同的东西。它可以用来指代Spring Framework项目本身,它是一切的开始。随着时间的推移,其他Spring项目也被建立在Spring Framework之上。大多数时候,当人们说 "Spring" 时,他们指的是整个项目家族(全家桶)。这个参考文档的重点是基础:Spring框架本身。

上述是官方文档的翻译。

Spring Framwork核心模块?

Spring Framework

Core Container

  • Spring Core:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC)模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。

  • Spring Bean

  • Spring Context:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、事件机制、校验和调度功能。

  • SpEL (Spring Expression Language)

Data Accesss

  • JDBC:Spring 对 JDBC 的封装模块,提供了对关系数据库的访问。
  • ORM (Object Relational Mapping):Spring ORM 模块,提供了对 hibernate5 和 JPA 的集成。
    • hibernate5 是一个 ORM 框架。
    • JPA 是一个 Java 持久化 API 。
  • OXM (Object XML Mappers):Spring 提供了一套类似 ORM 的映射机制,用来将 Java 对象和 XML 文件进行映射。这就是 Spring 的对象 XML 映射功能,有时候也成为 XML 的序列化和反序列化。
  • Transaction:Spring 简单而强大的事务管理功能,包括声明式事务和编程式事务。

Web

  • WebMVC:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。
  • WebFlux:基于 Reactive 库的响应式的 Web 开发框架
  • WebSocket:
    • Spring 4.0 的一个最大更新是增加了对 Websocket 的支持。
    • Websocket 提供了一个在 Web 应用中实现高效、双向通讯,需考虑客户端(浏览器)和服务端之间高频和低延时消息交换的机制。
    • 一般的应用场景有:在线交易、网页聊天、游戏、协作、数据可视化等。

AOP

  • AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。
  • Aspects:该模块为与 AspectJ 的集成提供支持。
  • Instrumentation:该层为类检测和类加载器实现提供支持。

其它

  • JMS (Java Messaging Service):提供了一个 JMS 集成框架,简化了 JMS API 的使用。
  • Test:为JUnit和TestNG提供支持。
  • Messaging:该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。

使用 Spring 框架能带来哪些好处?

  • DIDependency Injection 方法,使得构造器和 JavaBean、properties 文件中的依赖关系一目了然。
  • 轻量级:与 EJB 容器相比较,IoC 容器更加趋向于轻量级。这样一来 IoC 容器在有限的内存和 CPU 资源的情况下,进行应用程序的开发和发布就变得十分有利。
  • 面向切面编程(AOP): Spring 支持面向切面编程,同时把应用的业务逻辑与系统的服务分离开来。
  • 集成主流框架:Spring 并没有闭门造车,Spring 集成了已有的技术栈,比如 ORM 框架、Logging 日期框架、J2EE、Quartz 和 JDK Timer ,以及其他视图技术。
  • 模块化:Spring 框架是按照模块的形式来组织的。由包和类的命名,就可以看出其所属的模块,开发者仅仅需要选用他们需要的模块即可。
  • 便捷的测试:要测试一项用Spring开发的应用程序十分简单,因为测试相关的环境代码都已经囊括在框架中了。
  • Web 框架:Spring 的 Web 框架亦是一个精心设计的 Web MVC 框架,为开发者们在 Web 框架的选择上提供了一个除了主流框架比如 Struts 、过度设计的、不流行 Web 框架的以外的有力选项。
  • 事务管理:Spring 提供了一个便捷的事务管理接口,适用于小型的本地事物处理(比如在单 DB 的环境下)和复杂的共同事物处理(比如利用 JTA 的复杂 DB 环境)。
  • 异常处理:Spring 提供一个方便的 API ,将特定技术的异常(由JDBC, Hibernate, 或 JDO 抛出)转化为一致的、Unchecked 异常。

什么是Spring IoC容器?

org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。

  • 容器通过读取[配置元数据](# 什么是配置元数据?)来获取要实例化、配置和组装哪些对象的指令。

例如,OpenFeign是流行的RPC框架,在它的源码里就是通过自定义客户端代理接口配置元数据,再由IoC容器进行组装,从而在标注@FeignClient注解的地方可以看到注册的Bean。

什么是配置元数据?

配置元数据表示开发者告诉Spring Container如何去实例化、配置和组装应用程序中的对象。

Spring容器写入配置元数据有以下几种方式:

  1. 基于XML
  2. 基于注解,例如@Service
  3. 基于java configuration,例如@Configuration、@Bean、@Import

IoC和DI有什么区别?

IoC是目的,DI是手段。

  • IoC控制反转,是指将用户创建对象的权利(例如通过传统方式new等硬编码方式)交给容器,由容器去创建和管理对象。

  • DI是IoC的一种特殊形式,对象仅通过构造函数参数、工厂方法参数或在对象实例上设置的属性来定义其依赖关系(XML,Java注解、Java配置),由工厂方法构造或返回。之后,IoC容器在创建bean时注入这些依赖项。

    程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。依赖注入是目前最优秀的解耦方式。

还是以Openfeign为例,它有一个方法org.springframework.cloud.openfeign.FeignClientsRegistrar#eagerlyRegisterFeignClientBeanDefinition,在这里,就是通过配置元数据,定义Bean Definition,再由容器创建管理该Bean。关键代码如下:

BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

依赖注入的两种方式?

  • 基于构造函数的DI
  • 基于Setter的DI

ApplicationContext 支持其管理的 bean 的基于构造函数和基于 setter 的 DI。在通过构造函数方法注入一些依赖项后,它还支持基于 setter 的 DI。您可以以 BeanDefinition 的形式配置依赖项,将其与 PropertyEditor 实例结合使用,将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即以编程方式),而是使用 XML bean 定义、带注释的组件(即用 @Component@Controller 等),或基于 Java 的 @Configuration 类中的 @Bean 方法。然后,这些源在内部转换为 BeanDefinition 的实例,并用于加载整个 Spring IoC 容器实例。

预实例化单例bean?

Spring框架在容器加载时能够检测到一些配置问题,比如引用了不存在的Bean或存在循环依赖。Spring会在实际创建Bean时设置属性和解析依赖,因此当请求对象时,如果创建对象或其依赖存在问题,可能会生成异常。为了及早发现这些配置问题,默认情况下ApplicationContext实现会提前实例化单例Bean,以便在创建ApplicationContext时就发现配置问题。但你仍然可以覆盖这种默认行为,使得单例Bean延迟初始化,而不是在实际需要时提前创建。

Spring的两种IoC容器?

BeanFactory ApplicationContext
它使用懒加载 它使用即时加载
它使用语法显式提供资源对象 它自己创建和管理资源对象
不支持国际化 支持国际化
不支持基于依赖的注解 支持基于依赖的注解

BeanFactory接口能够管理任何类型对象的高级配置机制。而ApplicationContext是BeanFactory的子接口。

简而言之, BeanFactory 提供了配置框架和基本功能, ApplicationContext 添加了更多企业特定的功能。

另外,BeanFactory 也被称为低级容器,而 ApplicationContext 被称为高级容器。

IoC的一些好处?

  • 它以最小的影响和最少的侵入机制促进松耦合

  • 它支持即时的实例化和延迟加载 Bean 对象。

  • 它将最小化应用程序中的代码量。

  • 它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或 JNDI 查找机制。

IoC的实现机制?

Spring 中的 IoC 的实现原理,就是工厂模式反射机制

我们在元数据中获取到Bean的全限定类名,在工厂使用反射机制动态加载类,并通过反射创建 Bean 的实例。再根据配置信息中定义的依赖关系(构造器注入、属性注入)将创建好的Bean实例装配到其他Bean中。而容器负责管理Bean的生命周期,应用程序通过容器获取需要的 Bean,而不需要自己创建对象,从而实现了控制反转。

interface Fruit {

     public abstract void eat();
     
}
class Apple implements Fruit {

    public void eat(){
        System.out.println("Apple");
    }
    
}
class Orange implements Fruit {
    public void eat(){
        System.out.println("Orange");
    }
}

class Factory {

    public static Fruit getInstance(String className) {
        Fruit f = null;
        try {
            f = (Fruit) Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
    
}

class Client {

    public static void main(String[] args) {
        Fruit f = Factory.getInstance("io.github.dunwu.spring.Apple");
        if(f != null){
            f.eat();
        }
    }
    
}

Bean的定义是什么?

在spring中,Bean是由Spring IoC容器实例化、组装和管理的对象,换而言之,Bean只是应用程序中的众多对象之一。Bean以及它们之间的依赖关系在容器使用的配置元数据中得到反映

在容器内,这些Bean定义表示为BeanDefinition对象,其中包括以下元数据:

  • 包限定类名:通常是Bean的实际实现类
  • Bean行为配置元素:说明Bean中容器中的行为方式(范围、生命周期回调等)
  • 对Bean完成其工作所需的其他Bean的引用。
  • 在新创建的对象中设置的其它配置参数,举例来说,如果你在Spring中创建了一个用于管理数据库连接池的bean,你可能需要设置一些配置参数,比如连接池的最大大小、最小大小、空闲连接超时时间等。

定义Bean的几种方式?

  1. XML配置文件
  2. 注解配置(@Service)
  3. Java Config配置(@Bean)
  4. 自定义Bean Definition(多在框架中使用,一般的crud根本用不到。。。)

关于第4点的补充:

除了包含有关如何创建特定 bean 的信息的 bean 定义之外, ApplicationContext 还允许注册在容器外部(由用户)创建的现有对象。这是通过 getBeanFactory() 方法访问 ApplicationContext 的 BeanFactory 来完成的,该方法返回 DefaultListableBeanFactory 实现。 DefaultListableBeanFactory 通过 registerSingleton(..)registerBeanDefinition(..) 方法支持此注册。然而,典型的应用程序仅使用通过常规 bean 定义元数据定义的 bean。

Bean 元数据和手动提供的单例实例需要尽早注册,以便容器在自动装配和其他自省步骤期间正确推理它们。

Spring 支持几种Bean Scope?

  • Singleton - 每个 Spring IoC 容器仅有一个单 Bean 实例。默认
  • Prototype - 每次请求都会产生一个新的实例。
  • Request - 每一次 HTTP 请求都会产生一个新的 Bean 实例,并且该 Bean 仅在当前 HTTP 请求内有效。
  • Session - 每一个的 Session 都会产生一个新的 Bean 实例,同时该 Bean 仅在当前 HTTP Session 内有效。
  • Application - 每一个 Web Application 都会产生一个新的 Bean ,同时该 Bean 仅在当前 Web Application 内有效。

Spring Bean初始化流程

关键词:实例化Bean,Aware属性设置,init初始化

  • 实例化 Bean 对象

    • Spring 容器根据配置中的 Bean Definition(定义)中实例化 Bean 对象。
    • Spring 使用依赖注入填充所有属性,如 Bean 中所定义的配置。
  • Aware 相关的属性,注入到 Bean 对象

    • 如果 Bean 实现 BeanNameAware 接口,则工厂通过传递 Bean 的 beanName 来调用 #setBeanName(String name) 方法。
    • 如果 Bean 实现 BeanFactoryAware 接口,工厂通过传递自身的实例来调用 #setBeanFactory(BeanFactory beanFactory) 方法。

    初学者可能会对Aware这个单词不太理解,Aware直译为“意识到的”,但是放在Spring的语境显然不合适。

    我们可以将其理解为表示某个对象具有意识(awareness)或感知(awareness)某些特定环境或情况的能力。例如,Spring 中的各种 "Aware" 接口,允许对象意识到(aware)或感知(aware)到容器的某些特定方面或功能。

    例如,ResourceLoaderAware 可以翻译为 "资源加载器感知器",表示对象是一个资源加载器的感知器。

  • 调用相应的方法,进一步初始化 Bean 对象

    • 如果存在与 Bean 关联的任何 BeanPostProcessor 们,则调用 #preProcessBeforeInitialization(Object bean, String beanName) 方法。
    • 如果 Bean 实现 InitializingBean 接口,则会调用 #afterPropertiesSet() 方法。
    • 如果为 Bean 指定了 init 方法(例如 <bean />init-method 属性),那么将调用该方法。
    • 如果存在与 Bean 关联的任何 BeanPostProcessor 们,则将调用 #postProcessAfterInitialization(Object bean, String beanName) 方法。

Spring Bean 的销毁流程如下:

  • 如果 Bean 实现 DisposableBean 接口,当 spring 容器关闭时,会调用 #destroy() 方法。
  • 如果为 bean 指定了 destroy 方法(例如 <bean />destroy-method 属性),那么将调用该方法。

流程图

什么是延迟加载?

  • 当容器启动之后,作用域为单例的 Bean ,不会立即创建。
  • 而是在获得该 Bean 时,才真正在创建加载。

方法注入是什么?

在大多数应用场景中,容器中的大部分bean都是单例的。当一个单例 bean 需要与另一个单例 bean 协作或一个非单例 bean 需要与另一个非单例 bean 协作时,通常可以通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 生命周期不同时就会出现问题。假设单例 bean A 需要使用非单例(原型)bean B,可能是在 A 上的每次方法调用上。容器仅创建单例 bean A 一次,因此只有一次设置属性的机会。每次需要 bean B 时,容器无法为 bean A 提供新的 bean B 实例。

在Spring中,方法注入(Method Injection)是一种依赖注入的方式,允许你在每次方法调用时动态地提供一个依赖对象。这对于解决上述提到的问题非常有用,其中一个单例Bean需要与一个非单例Bean协作,但需要在每次方法调用时获得一个新的非单例Bean实例。

方法注入可以通过在目标Bean中定义一个方法,该方法的参数类型是所需的依赖对象,然后由Spring容器动态调用该方法来注入依赖对象。

@Component
public class SingletonA {

    public void someMethod() {
        // 调用方法获取新的PrototypeB实例
        PrototypeB prototypeB = getPrototypeB();
        // 使用prototypeB进行一些操作
    }

    // 使用方法注入获取PrototypeB实例
    @Lookup
    protected PrototypeB getPrototypeB() {
        // Spring会在运行时动态生成子类来覆盖该方法,并提供所需的PrototypeB实例
        // do something...
        return ...;
    }
}
@Component
@Scope("prototype")
public class PrototypeB {
    // PrototypeB的一些属性和方法
}

Spring是如何解决循环依赖的?

推荐阅读:https://blog.csdn.net/qq_41907991/article/details/107164508

Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

Spring AOP是什么?

前置阅读

AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.
在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)

Aspect(切面)是什么?

aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面(Aspect)所定义的横切逻辑(advice)织入到切面所指定(point cut)的连接点(join point)中.
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

  1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
  2. 如何在 advice 中编写切面代码.

可以简单地认为, 使用 @Aspect 注解的类就是切面.

advice(增强)是什么?

由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.
许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.
例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.

连接点(join point)是什么?

a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.

切点(point cut)是什么?

匹配 join point 的谓词(a predicate that matches join points).
Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.

关于join point 和 point cut 的区别?

在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice

目标对象(Target)是什么?

织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

AOP proxy是什么?

一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类.
在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象.

织入(Weaving)是什么?

将 aspect 和其他对象连接起来, 并创建 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:

  • 编译器织入, 这要求有特殊的Java编译器.
  • 类装载期织入, 这需要有特殊的类装载器.
  • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
    Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

关于 AOP Proxy是什么?

Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理.
如果需要为一个类实现代理, 那么可以使用 CGLIB 代理. 当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.

Spring Translation是什么?

事务是逻辑上的一组操作,要么都执行,要么都不执行.

Spring Translation管理接口介绍

  • PlatformTransactionManager: (平台)事务管理器
  • TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)
  • TransactionStatus: 事务运行状态

所谓事务管理,其实就是“按照给定的事务规则来执行提交或者回滚操作”。

PlatformTransactionManager接口介绍

Spring并不直接管理事务,而是提供了多种事务管理器 ,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 Spring事务管理器的接口是: org.springframework.transaction.PlatformTransactionManager ,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

// 代码实现
/**
 * 这是Spring命令式事务基础结构中的中心接口,应用程序可以直接使用它,但它主要并不作为API使用:
 * 通常,应用程序将通过AOP与TransactionTemplate或声明式事务划分配合使用。
 *
 * 对于实现者,建议从提供的
 * {@link org.springframework.transaction.support.AbstractPlatformTransactionManager}
 * 类派生,该类预实现了定义的传播行为并处理事务同步。子类必须为底层事务的特定状态实现模板方法,
 * 例如:begin, suspend, resume, commit。
 *
 * 这个策略接口的经典实现是
 * {@link org.springframework.transaction.jta.JtaTransactionManager}。
 * 然而,在常见的单资源场景中,Spring特定的事务管理器
 * 比如JDBC, JPA, JMS是首选。
 */
public interface PlatformTransactionManager extends TransactionManager {

    /**
     * 根据指定的传播行为返回当前活动的事务或创建一个新事物。
     *
     * 请注意,隔离级别和超时等参数只会应用于新事物,因此在参与当前活动的事务时将会被忽略。
     *
     * 此外,并非所有事务定义设置都会被每个事务管理器支持:当遇到不受支持的事务时,一个正确的事务管理器实现应该抛出异常。
     *
     * 上述规则的一个例外是只读标志,如果不支持显式的只读模式,则应忽略该标志。本质上,只读标志只是潜在优化的一个提示。
     *
     * @param definition TransactionDefinition实例,可以为null,描述传播行为、隔离级别、超时等。
     * @return 表示新事务或当前事务的事务状态对象
  * @throws TransactionException 在查找、创建或系统错误情况下抛出
  * @throws IllegalTransactionStateException 如果给定的事务定义无法执行(例如,如果当前活动的事务与指定的传播行为冲突)
     */
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

    /**
     * 根据事务的状态提交给定的事务。如果事务已被程序标记为仅回滚,则执行回滚。
  * 
  * 如果不是新事务,则省略提交以正确参与周围的事务。如果之前的事务已被挂起以便创建一个新事务,则在提交新事务后恢复之前的事务。
  * 
  * 请注意,当提交调用完成时,无论是正常完成还是抛出异常,事务都必须完全完成并清理。在这种情况下,不应期望有回滚调用。
  * 
  * 根据具体的事务管理器设置,{@code commit}可能会传播{@link org.springframework.dao.DataAccessException},
  * 无论是在提交前刷新还是在实际提交步骤中。
  * @param status 由{@code getTransaction}方法返回的对象
  * @throws UnexpectedRollbackException 在事务协调器发起意外回滚的情况下抛出
  * @throws HeuristicCompletionException 在事务失败的情况下,由事务协调器的启发式决策引起
  * @throws TransactionSystemException 在提交或系统错误的情况下抛出
  * (通常由基本资源失败引起)
  * @throws IllegalTransactionStateException 如果给定的事务已经完成(即已提交或回滚)
     */
    void commit(TransactionStatus status) throws TransactionException;

    /**
     * 执行给定事务的回滚。
     * 如果事务不是新事务,只需将其设置为仅回滚即可正确参与周围事务。如果先前的事务已被暂停以便能够创建新事务,
     * 则在回滚新事务后恢复先前的事务。
     * 
     * 如果提交引发异常,则不要对事务调用回滚。即使出现提交异常,提交返回时事务也已经完成并清理完毕。
     * 因此,提交失败后的回滚调用将导致 IllegalTransactionStateException。
     * 根据具体的事务管理器设置, rollback也可能会传播org. springframework. dao. DataAccessException 。
     * @param:status – getTransaction方法返回的对象
     * @throws:
     * TransactionSystemException – 发生回滚或系统错误(通常由基本资源故障引起)
     * IllegalTransactionStateException – 如果给定事务已完成(即已提交或已回滚)
     */
    void rollback(TransactionStatus status) throws TransactionException;

}

请注意这句话`如果提交引发异常,则不要对事务调用回滚。即使出现提交异常,提交返回时事务也已经完成并清理完毕。

我们刚刚也说了Spring中PlatformTransactionManager根据不同持久层框架所对应的接口实现类,几个比较常见的如下图所示

图片

比如我们在使用JDBC或者iBatis(就是Mybatis)进行数据持久化操作时,我们的xml配置通常如下:

<!-- 事务管理器 -->
<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!-- 数据源 -->
    <property name="dataSource" ref="dataSource" />
</bean>

TransactionDefinition接口介绍

事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition类 ,这个类就定义了一些基本的事务属性。

事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面。下面便是这个五个属性的内容。

隔离级别

  1. 隔离级别带来的问题

    • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

    • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。

      例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

    • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

    • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

    ps: 不可重复读的重点是修改,幻读的重点在于新增或者删除。

    例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。

    例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。

  2. 隔离级别

    • TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
    • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
    • TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
    • TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    • TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务传播行为

支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

不支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

  • TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

这里需要指出的是,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。

事务超时属性(一个事务允许执行的最长时间)

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。

事务只读属性(对事物资源是否执行只读操作)

事务的只读属性是指,对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作,那么我们可以将事务标志为只读的,以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。

回滚规则(定义事务回滚规则)

这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与EJB的回滚行为是一致的)。 但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

TransactionStatus接口介绍

image-20240526205442340

TransactionStatus接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息.

PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。返回的TransactionStatus 对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事务)。

AbstractTransactionStatus接口接口内容如下:

/**
 * TransactionStatus接口的抽象基实现。
 * 预实现本地回滚和已完成标志的处理,并委托给底层SavepointManager 。还提供了在事务中保存保存点的选项。
 * 不假设任何特定的内部事务处理,例如底层事务对象,也没有事务同步机制。
 */
public interface AbstractTransactionStatus{
    boolean isNewTransaction();  // 是否是新的事物
    boolean hasSavepoint();   // 是否有恢复点
    void setRollbackOnly();    // 设置为只回滚
    boolean isRollbackOnly();   // 是否为只回滚
    boolean isCompleted;    // 是否已完成
}
```xxxxxxxxxx22 1@Component2public class SingletonA {34    public void someMethod() {5        // 调用方法获取新的PrototypeB实例6        PrototypeB prototypeB = getPrototypeB();7        // 使用prototypeB进行一些操作8    }910    // 使用方法注入获取PrototypeB实例11    @Lookup12    protected PrototypeB getPrototypeB() {13        // Spring会在运行时动态生成子类来覆盖该方法,并提供所需的PrototypeB实例14        // do something...15        return ...;16    }17}18@Component19@Scope("prototype")20public class PrototypeB {21    // PrototypeB的一些属性和方法22}java

标签:蜜糖,事务,Java,对象,Spring,线程,内存
From: https://www.cnblogs.com/jiuyou2020/p/18214313

相关文章