背景在开发过程中，为了实现逻辑复用，我们重写并抽离了一部分公共方法。这些方法通过 Feign 调用 Oms 系统，用于支持多客户导入功能。在此过程中，需要记录成功列表和错误列表，并最终返回给调用方。问题描述在重构代码时，为了去重，我们将 errorList 的类型从 List 改为 Set，并在方法内部使用了以下代码：new ArrayList<>(errorSet)来对 errorList 进行重新赋值。然而，这种操作导致了一个严重的问题：虽然 errorSet 中的数据被正确处理了，但 errorList 在上层方法中仍然是空的。原因分析引用问题：Java 中的集合（如 List 和 Set）是引用类型。使用 new ArrayList<>(errorSet) 会创建一个新的对象，生成新的内存地址和引用。上层方法持有的仍然是原来的 errorList 引用，而方法内部的操作并未修改该引用，因此上层方法无法感知到错误信息的变化。IDEA 警告：IDEA 在代码中给出了警告，但每当回事，未及时处理。警告内容可能类似于："Reassignment of method parameter 'errorList'" 或 "Possible unintended reference modification"。设计缺陷：方法签名中直接传递了 List<OmpProductExcel> errorList，并通过方法内部对其重新赋值。这种设计容易引发误解，因为调用者通常期望传入的参数不会被重新赋值。解决方案为了避免上述问题，可以采取以下改进措施：方法一：直接操作传入的集合[最终解决方案]不重新赋值 errorList，而是直接对其进行操作：private List<OmpProductExcel> batchValidCommon( AbstractExcelListener.SimpleCheckParam<OmpProductExcel> param, List<OmpProductExcel> targetList, List<OmpProductExcel> errorList, boolean customerFlag) { Set<OmpProductExcel> errorSet = new HashSet<>(); // 填充 errorSet 的逻辑 errorSet.addAll(...); // 直接将 errorSet 的内容添加到 errorList errorList.addAll(errorSet); // 返回成功列表或其他逻辑 return targetList; }这种方式保持了 errorList 的引用不变，确保上层方法能够感知到数据的变化。方法二：返回新的集合如果确实需要返回一个新的集合，可以通过方法的返回值传递，而不是直接修改传入的参数：private List<OmpProductExcel> batchValidCommon( AbstractExcelListener.SimpleCheckParam<OmpProductExcel> param, List<OmpProductExcel> targetList, boolean customerFlag) { Set<OmpProductExcel> errorSet = new HashSet<>(); // 填充 errorSet 的逻辑 errorSet.addAll(...); // 返回新的错误列表 return new ArrayList<>(errorSet); }调用方可以根据返回值获取错误列表，避免了引用问题。方法三：使用包装类如果需要同时返回多个结果，可以使用一个包装类：public class ValidationResult<T> { private List<T> successList; private List<T> errorList; // Getters and Setters } private ValidationResult<OmpProductExcel> batchValidCommon( AbstractExcelListener.SimpleCheckParam<OmpProductExcel> param, List<OmpProductExcel> targetList, boolean customerFlag) { Set<OmpProductExcel> errorSet = new HashSet<>(); // 填充 errorSet 的逻辑 errorSet.addAll(...); ValidationResult<OmpProductExcel> result = new ValidationResult<>(); result.setSuccessList(targetList); result.setErrorList(new ArrayList<>(errorSet)); return result; }这种方式更加清晰，避免了对方法参数的直接修改。总结引用的重要性：在 Java 中，集合是引用类型，操作时需要注意引用的变化。如果需要修改集合的内容，应直接操作原集合，而不是创建新的集合。IDEA 警告的价值：IDEA 的警告往往是潜在问题的提示，应尽量消除所有警告。如果警告无法消除，应明确其原因并添加注释说明。方法设计原则：避免在方法内部直接重新赋值传入的参数。如果需要返回新的结果，优先通过返回值或包装类传递。类似问题的预防：集合去重：在使用 Set 去重时，确保正确实现了 hashCode 和 equals 方法。引用传递：理解引用传递的机制，避免无意中创建新的对象。代码审查：在代码审查阶段重点关注方法参数的使用，避免不必要的副作用。
Java集合：普通集合：List（列表）底层实现：ArrayList 和 LinkedList。ArrayList 基于动态数组，支持快速随机访问元素，但插入和删除操作（特别是中间位置的操作）较慢。LinkedList 基于双向链表，允许在任何位置进行快速的插入和删除操作，但随机访问速度较慢。适用场景：当你需要一个可以容纳重复元素的有序集合，并且可能需要频繁地遍历或访问元素时使用。Set（集）底层实现：HashSet, LinkedHashSet 和 TreeSet。HashSet 使用哈希表实现，不保证元素的顺序，不允许重复元素，提供常数时间的基本操作（添加、移除、查询）。LinkedHashSet 维护了一个双向链表以保持元素的插入顺序，同时利用哈希表保证了性能。TreeSet 使用红黑树实现，能保持元素按自然顺序或者自定义顺序排序，不允许重复元素，但操作的时间复杂度为O(log n)。适用场景：当你需要确保集合中没有重复元素，并且/或者希望按照特定顺序处理这些元素时使用。Queue（队列）底层实现：LinkedList, PriorityQueue 等。LinkedList 可以作为双端队列使用，支持在两端进行插入和移除操作。PriorityQueue 使用堆实现，队列头部是最小（或最大，取决于构造函数）的元素。适用场景：当需要遵循“先进先出”（FIFO）原则或优先级队列（基于优先级处理元素）时使用。Deque（双端队列）底层实现：ArrayDeque 和 LinkedList。ArrayDeque 是基于循环数组实现的，允许在两端高效地添加和移除元素。LinkedList 同样可以用作双端队列。适用场景：当你需要在一个序列的两端进行高效地插入和删除操作时使用。Map（映射）底层实现：HashMap, LinkedHashMap 和 TreeMap。HashMap 使用哈希表实现，键值对之间无序，查找、插入和删除操作平均时间为常数时间。LinkedHashMap 在HashMap的基础上增加了双向链表来维护元素的插入顺序或访问顺序。TreeMap 基于红黑树实现，键值对会按键的自然顺序或者自定义比较器排序，操作的时间复杂度为O(log n)。适用场景：当你需要通过键来快速查找、插入、删除对应的值时使用。线程安全集合：ConcurrentHashMap设计与实现：基于哈希表实现，但使用了分段锁（Segment）技术来减少锁的竞争。每个Segment都继承自ReentrantLock，可以独立加锁，从而允许多个线程同时访问不同的段。线程安全策略：通过将数据分成多个段，使得不同段的数据可以被并发地修改和读取，极大地提高了并发性能。CopyOnWriteArrayList 和 CopyOnWriteArraySet设计与实现：写操作时进行复制整个底层数组，在副本上进行修改，完成后将引用指向新的数组。适用于读多写少的场景。线程安全策略：写操作是通过创建底层数组的新副本来实现的，这避免了传统同步列表在写操作时需要锁定整个列表的问题，因此读操作可以在不锁定的情况下进行。ConcurrentLinkedQueue 和 ConcurrentLinkedDeque设计与实现：基于无锁算法实现的先进先出（FIFO）队列，使用CAS（Compare-And-Swap）操作保证线程安全。线程安全策略：利用原子变量和非阻塞同步机制来确保线程安全性，特别适合于高并发环境下的队列操作。BlockingQueue 接口及其实现类（如 LinkedBlockingQueue, ArrayBlockingQueue）设计与实现：提供了一个线程安全的队列接口，其内部使用锁来保护状态，某些实现还提供了有界队列的支持。线程安全策略：通过显式的锁和条件对象来控制对队列的访问，允许生产者等待队列不满以及消费者等待队列不空的状态。SynchronousQueue设计与实现：不存储元素的队列，每一个put操作必须等待一个take操作，反之亦然。线程安全策略：直接在线程间传递数据，没有额外的缓冲区，确保了线程间的直接通信。Collections.synchronizedXXX 方法包装的标准集合例如 synchronizedList, synchronizedMap 等，通过对标准集合的方法加上 synchronized 关键字来达到线程安全的目的。线程安全策略：虽然简单易用，但由于所有的方法调用都需要获取同一个锁，所以在高并发情况下性能可能不如专门设计的并发集合。List使用注意事项：IO模型：BIO (Blocking I/O)描述：BIO 是传统的阻塞式I/O模型，所有的socket连接都是基于一个线程对应一个请求的方式进行处理。当一个线程执行读或写操作时，如果数据未准备好（对于读操作）或者发送缓冲区已满（对于写操作），该线程将会被阻塞，直到操作完成。【连接数少且固定】特点：编程模型简单，易于理解和使用。线程资源消耗大，每个连接都需要一个独立的线程来处理，不适合高并发场景。NIO (New I/O 或 Non-blocking I/O)描述：NIO 引入了通道（Channel）和缓冲区（Buffer）的概念，并提供了非阻塞I/O的支持。通过选择器（Selector），可以管理多个通道，这样单个线程就可以处理多个网络连接。【高并发网络】特点：支持非阻塞模式，允许线程在等待I/O操作完成的同时执行其他任务，提高了线程的利用率。使用更少的线程处理更多的连接，非常适合高并发场景。需要开发者自行管理复杂的逻辑，如注册、选择、取消等操作。AIO (Asynchronous I/O)描述：AIO 是异步非阻塞的I/O模型，它进一步扩展了NIO的功能，提供了真正的异步I/O支持。应用程序可以通过回调机制，在I/O操作完成后得到通知。【异步IO，回调函数】特点：最高级别的抽象，提供最简单的编程模型，因为所有的I/O操作都是由操作系统负责完成，并在完成后通知应用程序。相比于NIO，减少了开发者的负担，因为不需要直接管理多路复用的选择器。在某些情况下可能不如NIO性能优越，因为它的实现依赖于操作系统的支持情况。抽象类和接口：定义和设计：抽象类是使用abstract关键字定义的类，可以包含抽象方法和非抽象方法，可以有实例变量和构造方法；接口通过interface关键字定义，只能包含抽象方法、默认方法和静态方法，不包含实例变量或构造方法。继承关系：一个类只能继承自一个抽象类，但可以实现多个接口。继承抽象类体现的是"is-a"关系，而实现接口体现的是"can-do"关系。构造方法：抽象类可以有构造方法，子类可以通过super()调用父类的构造方法；接口没有构造方法。默认实现：抽象类可以包含非抽象方法，子类可以直接使用；接口可以包含默认方法，提供通用实现，子类可以选择重写或者使用默认实现。设计目的：抽象类的设计目的是提供类的继承机制，实现代码复用，适用于拥有相似行为和属性的类；接口的设计目的是定义一组规范或契约，实现类遵循特定的行为和功能，适用于不同类之间的解耦和多态性实现。设计模式：创造者模式：单例模式 (Singleton Pattern)思路：确保一个类只有一个实例，并提供一个全局访问点。应用场景：当系统只需要一个实例对象来协调动作时非常有用，比如日志记录、数据库连接等。工厂方法模式 (Factory Method Pattern)思路：定义一个创建对象的接口，但由子类决定要实例化的类是哪一个。这样，工厂方法将实例化推迟到子类。应用场景：当一个类无法预料要创建哪种类的对象或希望由其子类来指定所要创建的对象时使用。抽象工厂模式 (Abstract Factory Pattern)思路：提供一个创建一系列相关或相互依赖对象的接口，而无需指定它们具体的类。应用场景：当你需要为一个产品族提供一致的创建接口而不指定具体类时适用，例如GUI工具包在不同平台上的实现。建造者模式 (Builder Pattern)思路：将一个复杂对象的构建与其表示分离，使得同样的构建过程可以创建不同的表示。应用场景：当构造函数参数过多且很多参数都是可选的情况下，使用建造者模式可以让代码更清晰易读。结构型设计模式：适配器模式 (Adapter Pattern)思路：允许将不兼容的接口转换为可兼容的形式，使得原本由于接口不匹配而不能一起工作的类可以一起工作。应用场景：当你想要使用一些已经存在的类，但是它们的接口并不符合你的需求时，可以使用适配器模式。桥接模式 (Bridge Pattern)思路：将抽象部分与它的实现部分分离，使它们都可以独立地变化。应用场景：当一个类希望其子类能够选择不同的实现方式时，或者当需要在运行时动态选择实现时使用桥接模式。组合模式 (Composite Pattern)思路：允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端可以统一地对待单个对象和对象组合。应用场景：适用于那些需要处理简单元素和复杂结构相同的方式的场景，例如文件系统的目录结构管理。装饰器模式 (Decorator Pattern)思路：动态地给一个对象添加一些额外的职责。就扩展功能而言，装饰器模式相比生成子类更加灵活。应用场景：当你需要在不影响其他对象的情况下，以动态、透明的方式给单个对象添加职责时使用。享元模式 (Flyweight Pattern)思路：通过共享技术来实现相同或相似对象的支持，以减少内存占用和提高性能。应用场景：当一个应用程序使用大量的对象，而这些对象造成了很大的存储开销时；或者对象的大多数状态都可以外部化时适用。代理模式 (Proxy Pattern)思路：为其它对象提供一种代理以控制对这个对象的访问。应用场景：远程代理（代表远程的对象）、虚拟代理（根据需要创建开销很大的对象）、保护代理（基于权限控制对资源的访问）等。行为型模式：策略模式 (Strategy Pattern)思路：定义一系列算法，将每个算法封装起来，并使它们可以互换使用。应用场景：当存在多个算法家族且希望在运行时根据需要选择不同的算法族或算法变体时非常有用。模板方法模式 (Template Method Pattern)思路：定义一个操作中的算法骨架，而将一些步骤延迟到子类中去实现。模板方法使得子类可以在不改变算法结构的前提下重新定义算法的某些步骤。应用场景：当你有一个基础算法，但其中某些步骤的具体实现可能因情况而异时使用。观察者模式 (Observer Pattern)思路：定义了一种一对多的依赖关系，让多个观察者对象同时监听某一个主题对象。当主题对象发生变化时，它的所有依赖者（观察者）都会收到通知并自动更新。应用场景：适用于当一个对象的状态变化需要通知其他对象，而不需要这些对象紧密耦合的情况。迭代器模式 (Iterator Pattern)思路：提供一种方法顺序访问一个聚合对象中的各个元素，而又不暴露该对象的内部表示。应用场景：用于遍历集合对象，无需了解其底层结构（如数组、链表等），为不同的数据结构提供统一的遍历接口。责任链模式 (Chain of Responsibility Pattern)思路：避免请求发送者与接收者耦合在一起，让多个对象都有机会处理请求，将这些对象连接成一条链，并沿着这条链传递请求，直到有一个对象处理它为止。应用场景：适用于有多个对象可以处理同一请求，具体由哪个对象处理取决于运行时刻条件或优先级顺序。命令模式 (Command Pattern)思路：将请求封装为对象，从而使你可以用不同的请求对客户进行参数化；对请求排队或记录日志，以及支持可撤销的操作。应用场景：适合于需要执行远程调用、日志记录、事务管理等情况下的命令封装和调度。
CAP：CAP理论是分布式计算系统中的一个基本理论，由Eric Brewer提出，并由Seth Gilbert和Nancy Lynch正式证明。它指出在一个分布式系统中，不可能同时满足以下三个属性：一致性（Consistency）：每个读操作都能读到最新的写结果，即所有节点在同一时刻看到的数据是一致的。在分布式系统中，这意味着任何数据更新必须在所有相关的副本上生效后，才能被认为是成功的。可用性（Availability）：每个请求都能得到一个响应，不管这个响应成功与否。即使部分节点故障，系统整体仍然可以处理客户端的请求。分区容错性（Partition Tolerance）：尽管网络可能失败（如分隔成多个独立的子网），系统仍然能够运作。这意味着即使某些消息丢失或延迟，系统依然能够提供服务。考虑一个简单的例子，假设我们有两个节点A和B，它们之间需要保持一致性和应对网络分区。如果我们要求系统具有分区容错性（P），那么在网络分区发生时，A和B之间的通信可能会中断。如果在这个时候我们也要求系统具有一致性（C），那么当A接收到了一个写请求并更新了它的状态，由于无法与B通信，B将不会知道这次更新。因此，如果一个读请求到达B，它将返回过期的数据，这违反了一致性的原则。为了保持一致性，我们必须阻止B响应读请求直到它从A那里得到了更新的信息，这就牺牲了可用性（A）。另一方面，如果我们选择可用性（A），即使在网络分区的情况下，A和B都将继续接受读写请求。然而，在这种情况下，如果A和B不能同步它们的状态，系统就不能保证一致性（C）。因此，选择了可用性就意味着放弃了强一致性。所以，在设计分布式系统时，通常需要在这三者之间做出权衡，根据具体的应用场景来决定放弃哪一个属性。例如：选择CP（一致性+分区容错性）意味着在网络出现问题时，系统会选择等待直到确保所有节点都达到一致状态，这可能会导致某些节点暂时不可用。选择AP（可用性+分区容错性）则意味着即使在网络问题期间，系统也会继续响应请求，但可能会返回不完全一致的结果。选择CA（一致性+可用性）实际上是不可能实现的，因为在实际的分布式环境中，网络分区是一个不可避免的事实。如果要避免分区，那就意味着系统不能真正分布，也就不存在所谓的分布式系统了。网络分区问题：在网络分区（Network Partition）的情况下，分布式系统中的不同部分可能无法相互通信，导致原本作为一个整体运行的系统分裂成多个独立的部分。这种情况在Redis或Zookeeper这样的集群环境中被称为“脑裂”（Split Brain）。当脑裂发生时，每个独立的部分可能会继续处理读写请求，而不知道其他部分的存在或状态，这可能导致数据不一致、重复的数据更新等问题。脑裂现象以Zookeeper为例，它是一个为分布式应用提供协调服务的工具。如果一个由3个节点组成的Zookeeper集群中出现了网络分区，使得两个节点可以互相通信但与第三个节点失去联系，这时就可能发生脑裂。如果这两个能互相通信的节点认为它们构成了多数派，并继续进行选举和操作，那么剩下的那个孤立节点也可能尝试形成自己的多数派或者单独行动，这就形成了两个独立的工作组，各自认为自己是合法的领导者，这就是所谓的脑裂现象。对于Redis集群来说，脑裂可能导致主从复制出现问题，比如两个不同的节点都认为自己是当前的主节点，接受写操作，而这会导致数据不一致的问题。解决方法Quorum机制：这是最常用的解决脑裂问题的方法之一。通过设置一个大多数原则（如Paxos或Raft算法），确保只有拥有超过半数成员支持的分区才能进行写操作。例如，在一个5节点的集群中，至少需要3个节点的同意才能执行写入操作。这种方法有效地避免了脑裂问题，因为它保证了任何时候只有一个分区能够合法地处理写请求。TTL（Time To Live）或租约机制：在这种机制下，领导者会定期发送心跳信号给所有跟随者，并且每次都会附带一个新的TTL值。如果跟随者在TTL时间内没有收到心跳信号，则认为当前领导者失效，并触发新的领导者选举过程。这种方法可以帮助减少由于临时网络问题导致的脑裂风险。版本号或时间戳：为了防止不同分区上的数据冲突，可以使用版本号或时间戳来标记数据。当网络恢复后，可以通过比较版本号或时间戳来决定哪个副本是最新的，并以此为基础进行数据同步。手动干预：在一些情况下，可能需要管理员手动介入来解决脑裂问题。比如确定哪个分区应该被认为是权威的，并基于此来修复数据一致性问题。分布式事务：2PC：2PC：两阶段提交，将事务的提交过程分为资源准备和资源提交两个阶段，并且由事务协调者来协调所有事务参与者，如果准备阶段所有事务参与者都预留资源成功，则进行第二阶段的资源提交，否则事务协调者回滚资源。协调者向所有参与者发送事务内容，询问是否可以提交事务，并等待答复。各参与者执行本地事务操作，将 undo 和 redo 信息记入事务日志中（但不提交事务）。如参与者执行成功，给协调者反馈同意，否则反馈中止，表示事务不可以执行。协调者收到各个参与者的准备消息后，根据反馈情况通知各个参与者commit提交或者rollback回滚。协调者节点向所有参与者节点发出正式提交的 commit 请求。收到协调者的 commit 请求后，参与者正式执行事务提交操作，并释放在整个事务期间内占用的资源。参与者完成事务提交后，向协调者节点发送ACK消息。协调者节点收到所有参与者节点反馈的ACK消息后，完成事务。协调者向所有参与者发出 rollback 回滚操作的请求。参与者利用阶段一写入的undo信息执行回滚，并释放在整个事务期间内占用的资源。参与者在完成事务回滚之后，向协调者发送回滚完成的ACK消息。协调者收到所有参与者反馈的ACK消息后，取消事务。2PC缺陷：性能问题：执行过程中，所有参与节点都是事务阻塞性的，当参与者占有公共资源时，其他第三方节点访问公共资源就不得不处于阻塞状态，为了数据的一致性而牺牲了可用性，对性能影响较大，不适合高并发高性能场景。可靠性问题：2PC非常依赖协调者，当协调者发生故障时，尤其是第二阶段，那么所有的参与者就会都处于锁定事务资源的状态中，而无法继续完成事务操作（如果是协调者挂掉，可以重新选举一个协调者，但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题）数据一致性问题：在阶段二中，当协调者向参与者发送commit请求之后，发生了局部网络异常或者在发送commit请求过程中协调者发生了故障，这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。二阶段无法解决的问题：协调者在发出 commit 消息之后宕机，而唯一接收到这条消息的参与者同时也宕机了，那么即使协调者通过选举协议产生了新的协调者，这条事务的状态也是不确定的，没人知道事务是否被已经提交。3PC:3PC：三阶段提交协议，是二阶段提交协议的改进版本，三阶段提交有两个改动点，1）在协调者和参与者中都引入超时机制；2）在第一阶段和第二阶段中插入一个准备阶段，保证了在最后提交阶段之前各参与节点的状态是一致的。事务询问：协调者向所有参与者发出包含事务内容的 canCommit 请求，询问是否可以提交事务，并等待所有参与者答复。响应反馈：参与者收到 canCommit 请求后，如果认为可以执行事务操作，则反馈 yes 并进入预备状态，否则反馈 no。协调者根据参与者的反应情况来决定是否可以进行事务的 PreCommit 操作。根据响应情况，有以下两种可能：发送预提交请求：协调者向参与者发送 PreCommit 请求，并进入准备阶段。事务预提交 ：参与者接收到 PreCommit 请求后，会执行本地事务操作，并将 undo 和 redo 信息记录到事务日志中（但不提交事务）响应反馈 ：如果参与者成功的执行了事务操作，则返回ACK响应，同时开始等待最终指令。假如有任何一个参与者向协调者发送了No响应，或者等待超时之后，协调者都没有接到参与者的响应，那么就执行事务的中断。发送中断请求 ：协调者向所有参与者发送 abort 请求。中断事务 ：参与者收到来自协调者的 abort 请求之后（或超时之后，仍未收到协调者的请求），执行事务的中断。该阶段进行真正的事务提交，也可以分为以下两种情况：发送提交请求：协调接收到所有参与者发送的ACK响应，那么他将从预提交状态进入到提交状态，并向所有参与者发送 doCommit 请求。本地事务提交：参与者接收到doCommit请求之后，执行正式的事务提交，并在完成事务提交之后释放所有事务资源。响应反馈：事务提交完之后，向协调者发送ack响应。完成事务：协调者接收到所有参与者的ack响应之后，完成事务。中断事务：**任何一个参与者反馈 no，或者等待超时后协调者尚无法收到所有参与者的反馈，即中断事务发送中断请求：如果协调者处于工作状态，向所有参与者发出 abort 请求事务回滚：参与者接收到abort请求之后，利用其在阶段二记录的undo信息来执行事务的回滚操作，并在完成回滚之后释放所有的事务资源。反馈结果：参与者完成事务回滚之后，向协调者反馈ACK消息。中断事务：协调者接收到参与者反馈的ACK消息之后，执行事务的中断。3PC优缺点：与2PC相比，3PC降低了阻塞范围，并且在等待超时后，协调者或参与者会中断事务，避免了协调者单点问题，阶段三中协调者出现问题时，参与者会继续提交事务。数据不一致问题依然存在，当在参与者收到 preCommit 请求后等待 doCommit 指令时，此时如果协调者请求中断事务，而协调者因为网络问题无法与参与者正常通信，会导致参与者继续提交事务，造成数据不一致。【2PC和3PC都无法保证数据绝对的一致性，一般为了预防这种问题，可以添加一个报警，比如监控到事务异常的时候，通过脚本自动补偿差异的信息】TCC:TCC（Try Confirm Cancel）:是应用层的两阶段提交，所以对代码的侵入性强，其核心思想是：针对每个操作，都要实现对应的确认和补偿操作，也就是业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作，第一阶段由业务代码编排来调用Try接口进行资源预留，当所有参与者的 Try 接口都成功了，事务协调者提交事务，并调用参与者的 confirm 接口真正提交业务操作，否则调用每个参与者的 cancel 接口回滚事务，并且由于 confirm 或者 cancel 有可能会重试，因此对应的部分需要支持幂等。第一阶段：Tay，业务系统进行检测并且预留资源（加锁等），比如下单阶段会锁库存。第二阶段，根据一阶段结果决定confirm 还是cancel ，conform的话执行业务并释放锁，cancel释放try预留的资源（锁）。TCC如何保证最终一致：在Try阶段，因为在业务逻辑实现，保障最好，即使失败也可以进行cancel操作。定义为重要开始进行confirm操作，confirm一定成功。如果执行失败，TCC进行重试补偿。存在极低概率在CC环节失败，可能需要定时任务或人工介入。TCC注意事项：允许空回滚：可能由于try超时或丢包，导致二阶段回滚，触发cancel操作，此时事务参与者未收到try但是收到cancel，即使没有发现对应事务主键xid，也要返回回滚成功。防悬挂：网络原因导致cancel比一阶段try先执行（try超时导致事务管理器触发回滚，执行cancel）但是此时收到try操作，此时空回滚成功，但是会记录该事务的xid或业务主键，try检测到会放弃执行。幂等：由于网络或重试导致TCC重复执行，所以需要进行幂等控制。TCC优缺点：性能提升：性能提升了，锁粒度变小，不会锁整体资源。数据最终一致性：基于CC的幂等，保证数据最终完成确认或取消。可靠性：解决XA协议单点故障问题，由业务发起方控制业务，业务活动器也变成多点。缺点：TCC与业务耦合高，提升开发成本。Saga：Saga：Saga 事务核心思想是将长事务拆分为多个本地短事务并依次正常提交，如果所有短事务均执行成功，那么分布式事务提交；如果出现某个参与者执行本地事务失败，则由 Saga 事务协调器协调根据相反顺序调用补偿操作，回滚已提交的参与者，使分布式事务回到最初始的状态。每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。每个 Ti 都有对应的幂等补偿动作 Ci，补偿动作用于撤销 Ti 造成的结果。本地消息表：本地消息表：本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理，在该方案中主要有两种角色：事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表，并在本地事务中完成业务处理和记录事务消息，并轮询事务消息表的数据发送事务消息，事务被动方基于消息中间件消费事务消息表中的事务。避免消息不一致：1）业务处理成功，事务消息发送失败；2）业务处理失败，事务消息发送成功幂等性：同一个接口，多次发出同一个请求，请求的结果是一致的。（幂等和防重有些不同，防重强调的防止数据重复，幂等强调的是多次调用如一次，防重包含幂等）怎么解决：RequestId或版本号：insert前先select在保存数据的接口中，在insert前，先根据requestId等字段先select一下数据。如果该数据已存在，则直接返回，如果不存在，才执行 insert操作。【使用了乐观锁，更新操作必须都更新】唯一索引：加唯一索引是个非常简单但很有效的办法，如果重复插入数据的话，就会抛出异常，为了保证幂等性，一般需要捕获这个异常。如果是java程序需要捕获：DuplicateKeyException异常，如果使用了spring框架还需要捕获：MySQLIntegrityConstraintViolationException异常。悲观锁：可以加悲观锁，把对应用户的哪一行数据锁住。同一时刻只允许一个请求获得锁，其他请求则等待。（对于数据库for update）【这种方式有一个缺点，获取不到锁的请求一般只能报失败，比较难保证接口返回相同值。】分布式锁：数据库加锁可能有性能问题，可以使用分布式锁。限流算法：计数器（Counter）算法原理：计数器是最简单的限流算法之一。它通过设定一个时间窗口内的最大请求数来限制流量。每当有请求到来时，检查当前计数是否已达到设定的最大值，若未达到则允许请求并增加计数；反之则拒绝请求。实现思路：设置一个固定大小的时间窗口和该窗口内允许的最大请求数。每当收到请求时，增加计数器。如果计数超过设定的阈值，则拒绝请求。时间窗口结束后重置计数器。优点：简单易实现。缺点：存在临界问题，即在两个相邻的时间窗口交界处可能会出现短时间内接受过多请求的情况。漏桶（Leaky Bucket）算法原理：漏桶算法将请求看作水滴进入一个固定容量的桶中，桶底部有一个小孔以恒定速度漏水。如果流入的速度超过了流出的速度，桶会满溢，多余的请求将被丢弃。实现思路：设定一个队列作为“桶”，用来存储请求。新来的请求首先加入到队列中。按照固定的速率从队列中取出请求进行处理。如果队列满了，则新的请求会被直接拒绝或排队等待。优点：能够平滑突发流量，确保输出的请求速率稳定。缺点：不能完全利用系统的处理能力，因为在高峰期可能会有很多请求被延迟或丢弃。令牌桶（Token Bucket）算法原理：令牌桶算法是基于令牌生成与消耗的概念。系统按照一定的速率向桶中添加令牌，每个请求需要消耗一个令牌才能被处理。如果桶为空，则新的请求必须等待直到有足够的令牌可用。实现思路：定义一个固定大小的桶和令牌生成速率。每隔一定时间向桶中添加一定数量的令牌（不超过桶的容量）。当请求到达时，先尝试获取一个令牌，成功后才执行请求；如果没有令牌，则拒绝请求或让请求等待。优点：既能应对突发流量，又能保证长期平均速率不超过设定值。缺点：实现相对复杂一些。固定窗口（Fixed Window）与滑动窗口（Sliding Window）算法固定窗口：类似于计数器算法，但在每个固定大小的时间窗口内重新计算请求数量。滑动窗口：改进了固定窗口的问题，通过使用两个时间窗口的组合来更精确地控制速率，从而解决了临界点的问题。实现思路（滑动窗口为例）：将时间划分为多个小窗口，并记录每个小窗口内的请求数。当新请求到来时，更新最近一个小窗口的计数。根据所有相关小窗口的总请求数判断是否允许新请求。优点：滑动窗口能更好地处理临界情况下的流量波动。缺点：实现较为复杂，尤其是随着窗口细分程度增加时。Raft算法：Raft 是一个 一致性算法，和 Paxos 目标相同。但它还有另一个名字 - 易于理解的一致性算法。Paxos 和 Raft 都是为了实现 一致性 产生的。这个过程如同选举一样，参选者 需要说服 大多数选民 (Server) 投票给他，一旦选定后就跟随其操作。Paxos 和 Raft 的区别在于选举的 具体过程 不同。角色划分：Raft将系统中的每个节点分配为三种角色之一：Leader（领导者）：处理所有客户端请求，并负责将日志复制到其他节点。Follower（跟随者）：被动地接受来自Leader的日志条目或投票请求。Candidate（候选人）：在选举期间尝试成为新的Leader。初始状态下，所有节点都是Follower。如果一段时间内没有收到Leader的心跳信号（即超时），其中一个Follower会转变为Candidate并发起一次选举。领导选举当一个Follower在选举超时时间内未接收到Leader的心跳信号时，它会转换为Candidate状态，增加自己的任期号，并为自己投票，然后向集群中的其他节点发送请求投票的RPC。如果一个Candidate获得了大多数节点的投票（包括自己），它就会成为新的Leader。如果多个Candidates同时竞选导致票数分散，没有任何一个获得多数票，则等待下一个选举超时周期重新开始选举过程。日志复制：一旦选出Leader后，它就开始接收来自客户端的请求。每个请求都包含一条需要执行的命令。Leader首先将这条命令追加到自己的日志中，然后通过AppendEntries RPC将该日志条目复制给其他Follower节点。Follower成功复制了日志条目后，会回复确认信息给Leader。Leader收到大多数节点的成功响应后，应用这条日志条目到它的状态机，并指示Follower也这样做。安全性Leader完整性：如果某个日志条目已经在特定任期中被提交，则后续的所有Leader必须包含该日志条目。只允许Leader写入：只有Leader可以处理客户端请求并将新条目添加到日志中。任期检查：每个RPC消息都包含了发送者的当前任期号。如果接收者发现对方的任期号更大，则会立即转换角色（例如从Leader变为Follower）。paxos算法：Paxos算法是莱斯利·兰伯特（Leslie Lamport）提出的一种基于消息传递的一致性算法，用于解决分布式系统中如何就某个值达成一致的问题。它是许多现代分布式系统的基石，Paxos 有点类似前面说的 2PC，3PC，但比这两种算法更加完善。在很多大厂都得到了工程实践，比如阿里的 OceanBase 的 分布式数据库， Google 的 chubby 分布式锁 。Paxos算法的核心概念Paxos算法主要由以下几个部分组成：Proposer（提议者）、Acceptor（接受者）、和Learner（学习者）。它们共同工作以确保在分布式环境中即使面对节点故障或网络分区的情况下也能达成一致。基本流程阶段一：Prepare请求Proposer选择一个提案编号N，然后向大多数Acceptor发送Prepare请求。如果Acceptor收到的Prepare请求中的提案编号N大于之前接收的所有提案编号，则Acceptor作出承诺不再接受任何小于N的提案，并回复自己已经接受过的最大编号的提案（如果有的话）。否则忽略该请求。阶段二：Accept请求如果Proposer从大多数Acceptor那里收到了对Prepare请求的响应，它就可以发送Accept请求给这些Acceptor。这个请求包含了最初提出的值以及在Prepare阶段确定的提案编号N。如果在这个过程中没有发现已经有被接受的提案，那么Proposer可以自由选择自己的值；如果有，则必须选择Acceptor已经接受的最大编号提案中的值。Acceptor收到Accept请求后，如果确认提案编号N没有变化（即没有接收到更大编号的Prepare请求），则接受该提案。阶段三：Learn当一个提案被大多数Acceptor接受后，Learner可以被告知该提案已被接受，并将其作为最终决定值。确保一致性的关键点唯一性：在一个Paxos实例中，只能有一个值被选定。这通过要求Acceptor只接受具有最高提案编号的提案来保证。安全性：一旦一个值被选定，后续所有的提案都必须接受这个值。这意味着一旦一个值被大多数Acceptor接受，之后任何成功的提案都必须包含这个值。活跃性：在没有故障的情况下，系统能够继续进行决策过程并最终选定一个值。多Paxos与优化原始的Paxos算法适用于单次决策，而在实际应用中往往需要持续地做出一系列决策，这就引出了Multi-Paxos的概念。Multi-Paxos允许在一个Paxos实例上连续执行多个Paxos算法实例，从而支持一系列决策。为了提高效率，通常会引入Leader角色，让一个固定的Proposer负责提交所有提案，减少Prepare阶段的开销。此外，还有多种针对特定问题场景下的Paxos变种和优化方案，例如Fast Paxos、Cheap Paxos等，旨在进一步提高性能或降低复杂度。总之，虽然Paxos算法在理论上提供了一种强大的工具来处理分布式环境下的共识问题，但由于其实现细节较为复杂，实践中常采用更易理解和实现的Raft等算法作为替代。
注解：核心注解@Component：通用组件注解，用于标记一个类为Spring管理的组件（Bean）。它可以被@Service, @Repository, 和 @Controller等更特定的注解所替代。使用@Component注解时，在解析@Component注册为BeanDefinition时会调用AnnotationBeanNameGenerator.buildDefaultBeanName，此时正常情况下会将第一个字符从大写转为小写，但如果第一个和第二个都是大写，则不变。【FooBah->fooBah; URL->URL】@Service：标记服务层组件，通常用于业务逻辑层。它是一个特化的@Component，有助于分层架构的理解和维护。@Repository：标记数据访问层（DAO层）组件，用于持久层操作。同样也是@Component的一种具体形式，主要用于数据库交互。@Controller：用于标记Web控制器组件，通常与Spring MVC一起使用处理HTTP请求。@RestController：是@Controller和@ResponseBody的组合注解，专门用于创建RESTful Web服务。依赖注入相关注解@Autowired：自动装配依赖项，可以用于构造函数、字段或setter方法上。默认情况下，它会根据类型进行匹配。@Qualifier：与@Autowired结合使用，当有多个相同类型的Bean时，用来指定具体的Bean。@Value：用于注入外部配置文件中的属性值，或者直接指定字符串值。@Resource：类似于@Autowired，但它是基于名字而非类型的匹配，默认情况下。@Inject：JSR-330标准中定义的注解，功能类似@Autowired，但不支持一些高级特性如required=false。配置相关注解@Configuration：标识一个类为配置类，包含Bean定义的方法。@Bean：在@Configuration类中使用，标明一个方法返回的对象将被注册为Spring容器中的Bean。@ComponentScan：指示Spring扫描指定包下的组件（包括@Component, @Service, @Repository, @Controller等），以便将其注册为Spring Bean。@PropertySource：加载properties文件到Spring环境中。@Import：允许从另一个配置类导入Bean定义。@EnableAutoConfiguration：启用自动配置，内部实际上就去加载META-INF/spring.factories文件的信息，然后筛选出以EnableAutoConfiguration为key的数据，加载到IOC容器中，实现自动配置功能！AOP相关注解@Aspect：定义一个切面类。@Before, @After, @Around 等：这些注解用于定义切点处的增强逻辑。其他注解@Transactional：声明事务边界，通常用在服务层。@RequestMapping：用于映射web请求到处理器方法，包括GET, POST等请求方式。其变体有@GetMapping, @PostMapping等。@Transactional：@Transactional(readOnly = true)实际上就是利用数据库的SET TRANSACTION READ ONLY， 开启只读事务，这意味着在此事务内部，任何修改数据的操作（如 INSERT、UPDATE、DELETE）都将被禁止，只能执行读取操作（如 SELECT）；只读事务依然会运用上隔离级别（MVCC），需要事务隔离级别需要一定性能开销。当执行查询操作时，如果不需要事务的隔离级别和一致性保证，并且不需要使用事务管理的功能，那么不开启事务可能是更为合适的选择。这种情况下，查询操作将立即执行并返回结果，不会受到事务管理和隔离级别的开销影响。然而，需要注意的是，如果应用需要保证数据的一致性和隔离性，并且希望查询操作与其他事务的修改行为相互独立，那么开启只读事务是必要的。在这种情况下，虽然可能会有一些额外的性能开销，但可以保证数据的一致性和隔离性，避免了并发操作可能引起的数据不一致问题。Spring事务：Spring事务底层是基于数据库事务和AOP机制的。首先对于使用了@Transactional注解的Bean，Spring会创建一个代理对象作为Bean。当调用代理对象的方法时，会先判断该方法上是否加了@Transactional注解。如果加了，那么则利用事务管理器创建一个数据库连接。并且修改数据库连接的autocommit属性为false，禁止此连接的自动提交。然后执行当前方法，方法中会执行sql。执行完当前方法后，如果没有出现异常就直接提交事务。如果出现了异常，并且这个异常是需要回滚的就会回滚事务，否则仍然提交事务。Spring事务的隔离级别对应的就是数据库的隔离级别。Spring事务的传播机制是Spring事务自己实现的，也是Spring事务中最复杂的，Spring事务的传播机制是基于数据库连接来做的，一个数据库连接一个事务，如果传播机制配置为需要新开一个事务，那么实际上就是先建立一个数据库连接，在此新数据库连接上执行sql。Spring事务传播：REQUIRED(Spring默认的事务传播类型)：如果当前没有事务，则自己新建一个事务，如果当前存在事务，则加入这个事务SUPPORTS：当前存在事务，则加入当前事务，如果当前没有事务，就以非事务方法执行MANDATORY：当前存在事务，则加入当前事务，如果当前事务不存在，则抛出异常。REQUIRES_NEW：创建一个新事务，如果存在当前事务，则挂起该事务。NOT_SUPPORTED：以非事务方式执行,如果当前存在事务，则挂起当前事务NEVER：不使用事务，如果当前事务存在，则抛出异常NESTED：如果当前事务存在，则在嵌套事务中执行，否则和REQUIRED的操作一样（开启一个事务）Spring事务失效：方法内自调用：Spring事务是基于AOP的，只要使用代理对象调用某个方法时，Spring事务才能生效，而在一个方法中调用使用this.xxx()调用方法时，this并不是代理对象，所以会导致事务失效。解决方法1：把调用方法拆分到另外一个Bean中。解决方法2：自己注入自己。解决方法3：AopContext.currentProxy()+@EnableAspectJAutoProxy(exposeProxy = true)方法是private或final：Spring事务会基于CGLIB来进行AOP，而CGLIB会基于父子类来实现，子类是代理类，父类是被代理类，如果父类中的某个方法是private的，那么子类就没有办法重写它，也就没有办法额外增加Spring事务的逻辑。单独的线程调用方法：当Mybatis或JdbcTemplate执行SQL时，会从ThreadLocal中去获取数据库连接对象，如果开启事务的线程和执行SQL的线程是同一个，那么就能拿到数据库连接对象，如果不是同一个线程，那就拿到不到数据库连接对象，这样，Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL，此数据库连接的autocommit为true，那么执行完SQL就会提交，后续再抛异常也就不能再回滚之前已经提交了的SQL了。异常被吃掉：如果Spring事务没有捕获到异常，那么也就不会回滚了，默认情况下Spring会捕获RuntimeException和Error。没加@Configuration注解：如果用SpringBoot基本没有这个问题，但是如果用的Spring，那么可能会有这个问题，这个问题的原因其实也是由于Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接，但是ThreadLocal中存储的是一个MAP，MAP的key为DataSource对象，value为连接对象，而如果我们没有在AppConfig上添加@Configuration注解的话，会导致MAP中存的DataSource对象和Mybatis和JdbcTemplate中的DataSource对象不相等，从而也拿不到数据库连接，导致自己去创建数据库连接了。其他：类没有被Spring管理；数据库不支持事务。自动装配：自动装配方式：自动装配需要满足一定的条件，如Bean的定义必须在Spring容器中，且只能有一个匹配的依赖项。如果存在多个匹配的依赖项，可以使用@Qualifier注解或@Primary注解来指定具体的依赖项。ByName：在XML配置中，可以使用autowire="byName"来启用byName自动装配。ByType：在XML配置中，可以使用autowire="byType"来启用byType自动装配。构造函数：Spring容器会根据构造函数的参数类型自动将相应的依赖注入到构造函数中。@Autowired：使用@Autowired注解，Spring容器会自动将相应的依赖注入到标注了@Autowired的位置。@Autowired 和 @Resource区别：来源不同：@Resource 是 Java 定义的注解，它来自于 JSR-250（Java 250 规范提案）；@Autowired 是 Spring2.5 定义的注解。依赖查找顺序不同：依赖注入的功能，是通过先在 Spring IoC 容器中查找对象，再将对象注入引入到当前类中。@Autowired 注解在查找要注入的 bean 时，首先会按照类型进行匹配。如果有多个匹配的 bean，就会根据名称进行匹配。@Autowired 注解中名称匹配使用 @Qualifier 注解来指定要注入的bean的名称，如果不使用 @Qualifier 注解就会使用属性名。@Resource注解既没有指定name属性，也没有指定type属性，那么它会默认按照名称来查找对应的bean。否则@Resource会按配置的name或type去对应查找，并进行bean注入。参数不同：@Autowired 只支持设置required参数；@Resource支持name、lookup、type、mappedNames、description、authenticationType、shareable 7个参数。注入方式不同：@Autowired 支持属性注入、构造方法注入和 Setter 注入，而 @Resource 只支持属性注入和 Setter 注入其他：AOP：动态代理：Spring AOP是利用的动态代理机制，如果一个Bean实现了接口，那么就会采用JDK动态代理来生成该接口的代理对象，如果一个Bean没有实现接口，那么就会采用CGLIB来生成当前类的一个代理对象。代理对象的作用就是代理原本的Bean对象，代理对象在执行某个方法时，会在该方法的基础上增加一些切面逻辑，使得我们可以利用AOP来实现一些诸如登录校验、权限控制、日志记录等统一功能。单例模式：单例Bean：在Spring框架中，在整个应用程序中只存在一个实例的Bean对象。单例Bean的作用是共享和复用对象实例，以提高性能和减少资源消耗。Spring容器在启动时会创建单例Bean的实例，并在整个应用程序的生命周期中共享该实例单例Bean适用于需要共享和复用对象实例的场景，例如数据库连接池、线程池等。【同一个class可以在容器中存在2个不同name的实例， 这一点不符合单例设计模式的原则】单例模式：单例模式是一种设计模式，用于确保一个类只有一个实例，并提供一个全局访问点来获取该实例。单例模式的目的是限制类的实例化次数，以保证全局唯一性和避免资源浪费。常见的方式包括饿汉式（在类加载时就创建实例）、懒汉式（在第一次使用时创建实例）。单例模式适用于需要确保全局唯一性和避免资源浪费的场景，例如配置信息管理、日志记录器等。Bean的实例化和初始化：实例化：Spring在创建一个Bean对象时，会先创建出来一个Java对象，会通过反射来执行类的构造方法从而得到一个Java对象，而这个过程就是Bean的实例化。初始化：得到Java对象后，会进行依赖注入，依赖注入之后就会进行初始化了，而Bean的初始化就是调用前面创建出来的Java对象中特定的方法，比如Java对象实现了InitializingBean接口，那么初始化的时候就会执行Java对象的afterPropertiesSet()，Spring只会执行这个方法，并不关心方法做了什么。创建bean：推断构造方法-> 实例化-> 填充属性，也就是依赖注入-> 处理Aware回调-> 初始化前，处理@PostConstruct注解-> 初始化，处理InitializingBean接口-> 初始化后，进行AOPSpring SPI:spring.factories是SpringBoot SPI机制实现的核心，SPI机制表示扩展机制，所以spring.factories文件的作用就是用来对SpringBoot进行扩展的，SpringBoot在启动的过程中，会找出项目中所有的spring.factories文件，包括jar中spring.factories，从而向Spring容器中添加各个spring.factories文件中指定的ApplicationListener、ApplicationContextInitializer、配置类等组件，使得对SpringBoot做扩展非常容易了，只要引入一个jar，这个jar中有spring.factories文件，就可以把ApplicationListener等添加到Spring容器中。Spring 自动配置：在Spring中，我们通常需要去配置很多的Bean：比如用Mybatis，我们要配置SqlSessionFactory的Bean对象用AOP，我们需要配置@EnableAspectJAutoProxy注解用Spring事务，我们需要配置DataSourceTransactionManager的Bean对象用RabbitMQ，我们要配置RabbitTemplate的Bean对象用SpringBoot时，SpringBoot帮我们配置好了，我们在依赖了spring-boot-starter-web后，会间接的依赖到spring-boot-autoconfigure这个jar，这个jar中都包含了很多的自动配置类：RabbitAutoConfigurationAopAutoConfigurationElasticsearchDataAutoConfigurationDataSourceTransactionManagerAutoConfiguration等等。Spring循环依赖：构造器注入导致的循环依赖无法解决：如果Bean之间的依赖是通过构造器注入的方式定义的，那么Spring将无法解决这种循环依赖的问题，通常会抛出异常。字段注入或Setter方法注入：对于使用字段注入或者Setter方法注入造成的循环依赖，Spring可以有效地进行处理。Spring使用三级缓存（singletonObjects、earlySingletonObjects、singletonFactories）来解决单例Bean之间的循环依赖问题。当Spring正在创建一个单例Bean A时，它首先会创建这个Bean的一个原始实例，并将其放入“singletonFactories”缓存中。此时，Bean A还未完全初始化。如果在创建过程中发现需要依赖另一个未完全初始化的Bean B（可能是A依赖B，而B也依赖A），Spring会从缓存中获取Bean A的早期引用，并将其提前暴露给其他Bean（如Bean B）使用。一旦Bean B完成初始化并返回到Bean A的创建过程中，Spring会继续完成Bean A的初始化过程。原型(Prototype)范围的Bean：对于原型范围的Bean，Spring不会尝试去解决循环依赖的问题，因为每次请求原型Bean时都会创建一个新的实例，这使得循环依赖在这种情况下难以管理且没有实际意义。Spring MVC：创建在启动Tomcat过程中，会创建DispatcherServlet对象，并执行它的初始化逻辑。DispatcherServlet初始化过程中会创建Spring容器（根据用户的Spring配置）。然后初始化过程中还是初始化HandlerMapping、HandlerAdapter等等。SpringMVC中默认提供了好几个HandlerMapping，其中有一个为RequestMappingHandlerMapping。RequestMappingHandlerMapping的作用是去寻找Spring容器中有哪些加了@RequestMapping的方法。找到这些方法后，就会解析该注解上的信息，包含了指定的path，然后就把path作为key，Method作为value存到一个map中。执行请求到达DispatcherServlet：当一个HTTP请求到达时，它首先被Spring MVC的核心前端控制器DispatcherServlet接收。这个Servlet充当着请求的分发中心。HandlerMapping的作用：DispatcherServlet并不直接处理请求，而是查询一个或多个HandlerMapping实例来决定哪个控制器应该处理请求。HandlerMapping会根据请求的URL或者其他参数来确定合适的控制器。处理器执行：一旦确定了适当的控制器，DispatcherServlet将请求转发给该控制器。控制器（也称为处理器）负责处理请求，并返回一个包含视图名称和/或模型数据的ModelAndView对象。视图解析：得到ModelAndView后，DispatcherServlet接着查询ViewResolver来找到实际的视图对象。ViewResolver通过名字找到具体的视图实现，比如JSP页面或者Thymeleaf模板等。视图渲染：一旦找到了正确的视图，DispatcherServlet将模型数据传递给视图，然后由视图负责渲染结果并返回给客户端。响应返回客户端：最后，生成的视图结果会被作为HTTP响应返回给客户端。Spring拦截器和过滤器：区别概念与作用范围过滤器（Filter）：是Servlet规范的一部分，可以用来过滤所有到达Servlet容器的请求。它不仅适用于Spring应用，也适用于任何基于Servlet的应用程序。过滤器可以在请求到达Servlet之前或响应返回客户端之前进行处理，常用于日志记录、权限验证等。拦截器（Interceptor）：是Spring MVC框架特有的组件，主要用于拦截处理器（Handler）的执行，也就是控制层的方法调用前后的处理。它更适合于需要与Spring上下文交互的场景，如权限检查、参数校验等。配置方式过滤器通常通过web.xml文件或者Servlet 3.0+的注解方式进行配置。拦截器则是在Spring配置类中通过实现HandlerInterceptor接口并注册到InterceptorRegistry来配置。灵活性过滤器的作用范围更广，可以应用于任何类型的请求，而不仅仅是MVC请求。拦截器更加灵活，可以访问Spring MVC框架内部的数据结构，比如可以直接操作ModelAndView对象。执行顺序在一个典型的Spring MVC应用中，当一个HTTP请求到来时，其处理流程和执行顺序如下：过滤器链（Filter Chain）：首先，请求会经过一系列的过滤器（如果有的话）。这些过滤器按照它们在web.xml中定义的顺序或者通过注解声明的顺序依次执行。每个过滤器可以选择继续向下传递请求给下一个过滤器或者直接向客户端发送响应。DispatcherServlet：一旦所有前置过滤器都执行完毕且未终止请求，则请求会被传递给Spring MVC的核心前端控制器DispatcherServlet。拦截器链（Interceptor Chain）：DispatcherServlet接着会将请求分发给合适的处理器（Controller），但在实际调用处理器方法之前，会先执行与该请求匹配的所有拦截器的preHandle方法。如果所有的preHandle方法都返回true，则继续执行处理器方法；否则，跳过后续的拦截器和处理器方法，并可能直接返回响应。处理器方法：处理器（Controller中的方法）被执行。拦截器链（再次）：在处理器方法执行之后，拦截器链中的postHandle方法被调用，这是按拦截器添加顺序的逆序执行的。此时可以修改模型数据或视图对象。视图渲染：视图被渲染。完成阶段：最后，无论请求处理是否成功，拦截器的afterCompletion方法都会被调用，这也是按拦截器添加顺序的逆序执行的。这一步可以用来清理资源等。Spring注入：字段注入可能失效的情况代理问题：当使用CGLIB代理而非JDK动态代理时，由于CGLIB是通过继承的方式创建代理对象，对于私有字段的访问可能会遇到问题，尤其是在使用@Autowired进行字段注入时，如果代理类尝试直接访问目标类的私有成员变量而不是通过getter/setter方法，这可能导致依赖注入失败。循环依赖：虽然Spring能够处理大多数情况下基于setter或构造器注入的循环依赖问题，但字段注入在这种情况下通常会导致应用启动失败。这是因为字段注入是在对象实例化之后进行的，而Spring无法解决这种情况下产生的循环依赖。非单例作用域：对于原型（prototype）作用域的Bean，字段注入不会自动工作，因为Spring只在单例Bean初始化时执行依赖注入。Setter方法注入可能失效的情况：Setter方法注入相对稳定，但在某些特定情况下也可能出现问题：未调用Setter方法：理论上，只要定义了正确的setter方法并正确使用@Autowired注解，Spring会自动调用这些方法进行依赖注入。但是，如果你手动创建了一个Bean（即不通过Spring容器），那么Spring将没有机会调用这些setter方法，导致依赖注入失效。循环依赖：如前所述，在涉及循环依赖的情况下，如果两个或多个Bean相互依赖，且都使用setter方法注入，则Spring可以处理这种情况。但如果配置不当，比如忽略了必要的@Lazy注解，仍可能导致问题。构造器注入可能失效的情况：构造器注入被认为是最安全的依赖注入方式，因为它确保了所有必需的依赖都在对象创建时被提供，减少了空指针异常的风险。然而，它也有其局限性：循环依赖：构造器注入不支持直接解决循环依赖的问题。如果两个Bean相互依赖对方，并试图通过构造器注入对方，则会导致应用启动失败。不过，这个问题可以通过使用@Lazy注解延迟其中一个依赖的加载来缓解。不可变参数的变化：一旦对象被创建，通过构造器注入的依赖通常是不可变的（final修饰）。这意味着如果需要在对象生命周期内更改某个依赖，构造器注入就不适用了，这时候应该考虑使用setter注入。
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; public class MyLock { AtomicInteger state = new AtomicInteger(0); Thread owner = null; AtomicReference<Node> head = new AtomicReference<>(new Node()); AtomicReference<Node> tail = new AtomicReference<>(head.get()); void lock() { //这里支持可重入，现在是非公平的 //注释掉这里的，可以变为公平锁 //todo:可以拆一个tryLock方法 if (state.get() == 0) { if (state.compareAndSet(0, 1)) { System.out.println(Thread.currentThread().getName() + "直接拿到锁"); owner = Thread.currentThread(); return; } } else { if (owner == Thread.currentThread()) { System.out.println(Thread.currentThread().getName() + " 拿到了重入锁,当前重入次数为" + state.incrementAndGet()); return; } } Node current = new Node(); current.thread = Thread.currentThread(); while (true) { Node currentTail = tail.get(); if (tail.compareAndSet(currentTail, current)) { System.out.println(Thread.currentThread().getName() + "加入到了链表尾"); current.pre = currentTail; currentTail.next = current; break; } } while (true) { if (current.pre == head.get() && state.compareAndSet(0, 1)) { owner = Thread.currentThread(); head.set(current); current.pre.next = null; current.pre = null; System.out.println(Thread.currentThread().getName() + "被唤醒之后，拿到锁"); return; } LockSupport.park(); } } void unlock() { if (Thread.currentThread() != this.owner) { throw new IllegalStateException("当前线程并没有锁，不能解锁"); } int i = state.get(); if (i > 1) { state.set(i - 1); System.out.println(Thread.currentThread().getName() + "解锁了重入锁，重入锁剩余次数" + (i - 1)); return; } if (i <= 0) { throw new IllegalStateException("重入锁解锁错误！"); } Node headNode = head.get(); Node next = headNode.next; state.set(0); if (next != null) { System.out.println(Thread.currentThread().getName() + "唤醒了" + next.thread.getName()); LockSupport.unpark(next.thread); } } class Node { Node pre; Node next; Thread thread; } }
JVM基础：结构：程序计数器（Program Counter Register）：每个线程都有自己的程序计数器，是一个小内存空间。它记录了当前线程执行的字节码的地址，帮助CPU知道下一步要执行的指令是哪个。如果线程正在执行Java方法, 那么记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是Native方法,计数器值为空。 这也是唯一一个在JVM规范中不报内存溢出的区域。Java虚拟机栈（Java Virtual Machine Stack）：虚拟机栈是线程私有的，他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧，每一个方法在执行的时候都会创建一个栈帧，栈帧中用来存放（局部变量表、操作数栈 、动态链接 、返回地址）；在Java虚拟机规范中，对此区域规定了两种异常状况：如果线程请求的栈深度大于虚拟机所允许的深度，将会抛出StackOverflowError异常；如果虚拟机栈动态扩展时无法申请到足够的内存，就会抛出OutOfMemoryError异常。局部变量表：局部变量表是一组变量值存储空间，用来存放方法参数、方法内部定义的局部变量, 局部变量表存放了编译期可以知道的基本数据类型, 对象引用和returnAddress类型, 底层是变量槽（variable slot）。操作数栈：是用来记录一个方法在执行的过程中，字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了，当一个方法刚开始执行的时候，操作数栈中是空发的，在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。动态链接：因为字节码文件中有很多符号的引用，这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用，这种称为静态解析；另一部分会在运行期间转化为直接引用，称为动态链接。返回地址（returnAddress）：类型（指向了一条字节码指令的地址）JIT即时编译器（Just In Time Compiler），简称 JIT 编译器: 为了提高热点代码的执行效率，在运行时，虚拟机将会把这些代码编译成与本地平台相关的机器码，并进行各种层次的优化，比如锁粗化等。注意, HotSpot虚拟机只要申请栈空间成功就不会发生OOM(不允许动态拓展栈容量)本地方法栈（Native Method Stack）：与虚拟机栈类似，但用于处理本机方法。Java程序使用JNI（Java Native Interface）调用其他语言（如C/C++）编写的程序，此时本地方法栈会被使用。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的，同理它也会抛出StackOverflowError和OOM异常。堆（Heap）：JVM用来存储对象实例和数组，它是所有线程共享的区域，也是垃圾回收的主要区域。开启逃逸分析后，某些未逃逸对象可以通过标量替换在栈上分配。堆细分：新生代、老年代，对于新生代又分为：Eden区和Surviver1和Surviver2区。元空间（MetaSpace）：存储已经被java虚拟机加载的类信息、常量、静态变量。元空间不是堆的一部分，而是存放在本机内存中，不受JVM堆内存限制，更好利用内存。减少对应用程序行为依赖，更好管理类及元数据的内存分配，使类加载器更可靠的执行类的动态卸载。利用OS的内存，使得JVM启动阶段和内存管理更为高效，减少了永久代相关的Full GC。JVM参数：堆内存设置-Xms<size>：设置JVM启动时的初始堆大小（例如，-Xms512m表示初始堆大小为512MB）。-Xmx<size>：设置JVM允许的最大堆大小（例如，-Xmx2g表示最大堆大小为2GB）。-Xmn<size>：设置年轻代的大小（仅适用于某些垃圾收集器，如Serial, Parallel）。垃圾回收（GC）相关-XX:+UseG1GC：启用G1垃圾收集器。-XX:+UseConcMarkSweepGC：启用CMS（Concurrent Mark Sweep）垃圾收集器。-XX:+UseParallelGC：启用并行垃圾收集器（年轻代使用并行收集，老年代使用单线程标记-清除）。-XX:MaxGCPauseMillis=<value>：设置目标最大GC暂停时间（仅对G1有效）。-XX:+PrintGCDetails：打印详细的GC日志信息。性能调优-XX:PermSize=<size> 和 -XX:MaxPermSize=<size>：设置永久代（方法区）的初始大小和最大大小（在Java 7及之前版本中使用；Java 8之后被Metaspace取代）。-XX:MetaspaceSize=<size> 和 -XX:MaxMetaspaceSize=<size>：设置元空间的初始大小和最大大小（Java 8及以上版本）。-XX:+AggressiveOpts：开启激进的性能优化选项（包括一些实验性的优化）。调试与诊断-XX:+HeapDumpOnOutOfMemoryError：当发生OutOfMemoryError时自动生成堆转储文件。-XX:HeapDumpPath=<path>：指定堆转储文件的存储路径。-XX:+PrintFlagsFinal：打印所有JVM参数的最终值（可用于验证参数是否正确应用）。-verbose:gc：输出GC简要信息。其他-server 或 -client：选择JVM运行模式（-server模式通常具有更好的性能，但启动较慢；-client模式启动快但执行效率相对较低）。-D<name>=<value>：设置系统属性值，可以在程序中通过System.getProperty("<name>")获取。OOM:Java堆空间（Java heap space）当JVM尝试在堆上为新对象分配内存但没有足够的空间时发生。这通常是由于应用程序创建了太多对象而未能及时回收旧对象导致的。垃圾收集器过度（GC overhead limit exceeded）当JVM花费过多时间进行垃圾回收，并且回收的内存很少时会发生此错误。默认情况下，如果超过98%的时间用于GC并且回收了不到2%的堆，则会触发这个错误。永久代/元空间不足（PermGen space / Metaspace）在Java 7及之前版本中，使用的是“永久代”来存储类定义及其相关的元数据。而在Java 8及之后版本中，“永久代”被“元空间”取代，它直接使用本地内存。当类加载器加载的类信息超出分配给永久代或元空间的限制时，就会发生此类错误。方法区内存不足（Method area）方法区用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然现在大多数现代JVM已经转向使用元空间代替方法区，但在某些配置下仍然可能出现与此区域相关的内存问题。直接缓冲区内存不足（Direct buffer memory）当程序使用java.nio.ByteBuffer.allocateDirect()方法分配直接缓冲区，且请求的内存超出了JVM允许的最大值时发生。和普通的ByteBuffer用法相似，使用put和get方法来进行数据写入和读取，但是由于分配的是堆外内存，需要手动进行内存管理。栈溢出（Stack overflow）虽然严格来说这不是一种OOM错误，而是StackOverflowError，但它也与内存有关，通常由无限递归调用引起。【tips：虽然理论上可以通过try-catch块捕获OutOfMemoryError，但由于这种错误往往意味着系统资源已极度紧张，试图恢复可能不是最佳选择，因为后续操作也可能因缺乏足够资源而失败。】内存分代：优化内存分配：将不同生命周期的对象分开处理，减少老年代碎片化，避免频繁GC。提高垃圾回收效率：不同代可以使用不同的GC方式，如年轻代使用Scanvenge GC，老年代使用GMS或G1等。简化内存管理：年轻代创建销毁频繁，根据对象的不同生命周期特点，JVM可以更灵活地选择垃圾回收策略，例如，年轻代可以频繁进行快速的复制回收，而老年代可以使用更多时间进行标记-清除-压缩等操作以减少内存碎片。双亲委派：定义：在类加载过程中，先判断是否已经加载，是则直接返回，否则尝试进行加载。加载会将请求委派给父类加载器进行loadClass（），因此所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader中，父类无法加载才由自己加载，如果父加载器为null也会使用顶层启动类加载器作为父类加载器。优点：可以避免类的重复加载，也保证了 Java 的核心 API 不被篡改。破坏：自定义一个类加载器，重写loadClass方法；Tomcat可以加载自己路径下的class文件，而不会传递给父加载器；Java的SPI发起者已经是最上层BootstrapClassLoader。对Tomcat来说，先在本地cache查找该类是否已经加载过，看看 Tomcat 有没有加载过这个类。如果Tomcat 没有加载过这个类，则从系统类加载器的cache中查找是否加载过。如果没有加载过这个类，尝试用ExtClassLoader类加载器类加载，而不是使用AppClassLoader 来加载类（这里破坏了双亲委派，但是对于ExtClassLoader双亲委派仍然成立）比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader，这个加载链，就保证了 Object 不会被重复加载。如果 BoostrapClassLoader，没有加载成功，就会调用自己的 findClass 方法由自己来对类进行加载，findClass 加载类的地址是自己本 web 应用下的 class。加载依然失败，才使用 AppClassLoader 继续加载。都没有加载成功的话，抛出异常。对象创建：类加载检查：当执行new关键字创建对象时，首先会检查这个类是否已经被加载、解析和初始化。如果目标类还未被加载到JVM中，则需要先进行类加载过程。这包括加载（Loading）、链接（Linking，进一步分为验证Verification、准备Preparation和解析Resolution）和初始化（Initialization）。以下是类加载过程：通过类的全限定名来获取该类的二进制字节流：这通常意味着从文件系统读取.class文件，但也可能来自网络、数据库或其他来源。将字节流所代表的静态存储结构转换为方法区的运行时数据结构：即把类的信息存储到方法区中。在内存中生成一个代表该类的java.lang.Class对象，作为方法区这个类的数据访问入口。验证（Verification）：确保Class文件的字节流包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。主要包括文件格式验证、元数据验证、字节码验证和符号引用验证。准备（Preparation）：为类的静态变量分配内存，并设置默认初始值。注意这里的初始值通常不是代码中指定的值，而是类型的零值（例如int类型的0，boolean类型的false等）。对于final修饰的基本数据类型常量，如果它们在编译期就可以确定值，则会在准备阶段赋予正确的值。解析（Resolution）：将常量池内的符号引用替换为直接引用。符号引用是一个以字符串形式给出的名字，而直接引用可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。初始化：初始化是类加载过程的最后一步，它根据程序中的赋值语句以及静态代码块中的语句为类的静态变量赋予正确的初始值。这是执行类构造器 <clinit>() 方法的过程，这个方法由编译器自动收集类中的所有静态变量赋值动作和静态代码块合并而成。当首次主动使用某个类或接口时，就会触发该类或接口的初始化。分配内存：在确认了类已被正确加载后，接下来是在堆内存中为新对象分配空间。分配的内存量等于对象自身大小加上对象头的大小。对象头包含了元数据信息，如哈希码、GC分代年龄、锁状态标志等。内存分配有两种主要方式：指针碰撞（Bump the Pointer），适用于堆内存规整的情况；空闲列表（Free List），适用于堆内存碎片化的情况。初始化零值：内存分配完成后，分配给对象的这块内存空间会被初始化为零值（不包括对象头）。这一步确保了实例字段在没有显式初始化的情况下也能获得一个默认值（例如，数值类型为0，布尔型为false，引用类型为null）。设置对象头信息：初始化完零值之后，JVM会对对象头进行必要的设置。这可能包括记录对象的哈希码、GC信息（比如该对象属于哪个年代）、锁信息等。执行构造方法（方法）：最后一步是执行构造方法来初始化对象的成员变量，并执行其他初始化操作。当你使用new关键字并传递参数给构造函数时，这些参数将用于初始化对象的状态。返回引用：构造方法执行完毕后，一个新的对象就被成功创建了。此时，new表达式会返回对该对象的引用，可以赋值给一个变量或者作为参数传递给方法等。堆内存分配：确定对象大小：当一个对象被创建时，首先需要确定该对象所需的内存大小。这取决于对象的字段（包括基本类型和引用类型）以及对象头（Object Header）。对象头通常包含一些元数据信息，如锁状态、哈希码等。检查Eden区空间：大多数情况下，新的对象会首先尝试在年轻代的Eden区进行分配。如果对象所需的空间小于Eden区剩余的可用空间，那么对象将直接分配在此处。分配对象：指针碰撞（Bump the Pointer）：如果堆内存是规整的（即所有存活对象都紧密排列在一起），那么JVM可以通过维护一个指针来追踪下一个可用位置。每当有新对象分配时，只需要移动这个指针即可完成分配。空闲列表（Free List）：如果堆内存不是规整的（例如经过多次垃圾回收后产生了碎片），JVM会维护一个记录所有空闲块的列表。当需要分配对象时，从列表中找到合适的块分配给对象。触发垃圾回收：如果Eden区没有足够的空间来分配新对象，JVM会触发一次Minor GC（年轻代垃圾收集）。这次垃圾回收旨在清理Eden区和其中一个Survivor区中的无用对象，为新对象腾出空间。如果经过垃圾回收后，Eden区仍然无法满足分配需求，则可能将部分存活对象提前晋升到老年代。晋升到老年代：对于某些较大的对象，或者经历了几次垃圾回收仍然存活的对象，它们可能会直接或间接地被分配到老年代。老年代用于存放生命周期较长的对象，其垃圾回收频率低于年轻代。Full GC：如果老年代也没有足够的空间来容纳从年轻代晋升的对象，或者直接分配的大对象，将会触发Full GC。这是一个相对耗时的过程，因为它涉及整个堆的垃圾回收。TLAB：定义：TLAB会为每个线程预先分配一块内存空间，线程在分配对象时，会从自己的TLAB中进行分配，而不是直接在堆上进行分配。过小的TLAB可能会导致频繁的垃圾回收，而过大的TLAB可能会浪费内存空间。优点：减少线程同步：由于每个线程都有自己的TLAB，因此不需要进行线程同步操作，可以减少线程竞争和锁的开销。提高分配速度：由于对象分配是在TLAB中进行的，而不是在堆上进行，因此可以减少对象分配的开销，提高分配速度。提高局部性：由于每个线程都有自己的TLAB，对象分配在TLAB中进行，可以提高局部性，减少对共享内存的访问，提高缓存命中率。GC：存活算法和两次标记过程:引用计数法：给对象添加一个引用计数器，每当由一个地方引用它时，计数器值就加1；当引用失效时，计数器值就减1；任何时刻计数器为0的对象就是不可能再被使用的。优点：实现简单，判定效率也很高缺点：他很难解决对象之间相互循环引用的问题，基本上被抛弃可达性分析法：通过一系列的成为“GC Roots”(活动线程相关的各种引用，虚拟机栈帧引用，静态变量引用，JNI引用)的对象作为起始点，从这些节点ReferenceChains开始向下搜索，搜索所走过的路径成为引用链，当一个对象到GC ROOTS没有任何引用链相连时，则证明此对象时不可用的；两次标记过程：对象被回收之前，该对象的finalize()方法会被调用；两次标记，即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了，如果没有实现就直接判断该对象可回收；如果实现了就会先放在一个队列中，并由虚拟机建立的一个低优先级的线程去执行它，随后就会进行第二次的小规模标记，在这次被标记的对象就会真正的被回收了。三色标记：GC Root节点：局部变量的引用：在方法中定义的局部变量，包括方法的参数和局部变量，这些变量的引用指向了对象的实例。静态变量的引用：静态变量存在于类加载器的内存中，是类的一部分，它们的引用也被视为GC Roots。活动线程引用：正在运行的线程的引用和线程本地存储中的对象通常被视为GC Roots。JNI引用：通过Java Native Interface（JNI）创建的引用连接了Java堆内存和本地代码的内存，也可以被视为GC Roots。虚拟机引导类加载器：虚拟机内部使用的类加载器引用（通常是一些核心类或库）也是GC Root。三色标记：三色标记算法，用于垃圾回收器升级，将STW变为并发标记。STW就是在标记垃圾的时候，必须暂停程序，而使用并发标记，就是程序一边运行，一边标记垃圾。黑色： 代表对象已经检查过，且成员对象也被检查过了；如果有其他对象引用指向了黑色对象，无须重新检查一遍；黑色对象不可能直接（不经过灰色对象）指向某个白色对象。灰色：代表对象已经检查过，但成员还没全部检查完成。白色：代表对象没有被检查， 在可达性分析刚刚开始的阶段， 所有的对象都是白色的， 若在分析结束的阶段， 仍然是白色的对象， 即代表不可达。标记过程：并发标记，传统标记会STW，期间对象引用不会变化，但是并发标记可能出现漏标或错标。初始所有对象都是白色集合GC Root直接引用放入灰色集合从灰色集合获取对象，将本对象引用的其他对象也移动到灰色集合，本对象则移动到黑色集合重复步骤3，知道灰色集合为空结束后白色集合可以回收掉问题：浮动垃圾：标记过不是垃圾成为了垃圾（黑色或灰色突然变成垃圾），只能等待下次GC回收，问题较小。对象漏标：需要的被回收：一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾（删除引用），同时黑色对象引用了该对象（增加引用）（这两部可以不分先后顺序）；黑色对象的含义为其属性都已经被标记过了，重新标记也不会从黑色对象中去找，导致该对象被程序所需要，却又要被GC回收，此问题会导致系统出现问题。GC算法：垃圾回收算法：复制算法：复制算法将可用内存划分为两个等大小的区域，仅使用其中一个区域进行分配，另一个作为备用。当正在使用的区域满时，垃圾收集器会暂停程序执行，扫描整个使用的区域，并将所有存活的对象复制到备用区域，同时清理原区域。【Serial和Parallel Scavenge收集器】标记清除：标记出所有需要回收的对象，在标记完成后统⼀回收所有被标记的对象【CMS】标记整理：标记过程仍然与“标记-清除”算法⼀样，再让所有存活的对象向⼀端移动，然后直接清理掉端边界以外的内存；解决了产生大量不连续碎片问题垃圾回收器：Serial GC描述：Serial GC是一个单线程的垃圾收集器，意味着它在执行垃圾回收时会暂停所有应用程序线程（即所谓的“Stop the World”事件）。适用于单核处理器或小内存、低延迟要求的应用程序。Parallel GC (Throughput Collector)描述：Parallel GC使用多个线程进行垃圾回收，可以显著提高垃圾回收的速度。年轻代和老年代都可以并行处理。适用场景：适用于需要高吞吐量的应用程序，尤其是在拥有多个CPU核心的服务器环境中。优点：通过并行处理提高了垃圾回收效率，特别适合后台任务或对响应时间不太敏感的应用。缺点：虽然提高了吞吐量，但可能会导致较长的停顿时间，因为整个过程仍然是“Stop the World”的。CMS (Concurrent Mark Sweep) GC描述：CMS GC旨在减少停顿时间，特别是在老年代垃圾回收期间。它尽可能地与应用程序线程并发执行标记和清除阶段（一般这两个阶段耗时最长），但在某些阶段仍需暂停应用程序线程。【标记清除】初始标记：只是标记一下 GC Roots 能直接关联的对象，速度很快，STW。并发标记：进行 ReferenceChains跟踪的过程，和用户线程一起工作，不需要暂停工作线程。重新标记：为了修正在并发标记期间，因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录，STW。并发清除：清除 GC Roots 不可达对象，和用户线程一起工作，不需要暂停工作线程。适用场景：适用于交互式Web应用或其他对响应时间敏感的应用。优点：减少了长时间的“Stop the World”事件，提高了用户体验。缺点：CMS可能无法完全避免停顿，并且在高负载下可能导致碎片化问题，影响性能。G1 (Garbage First) GC描述：G1 GC将堆划分为多个大小相等的区域，并能独立于其他区域进行垃圾回收。它优先清理包含最多垃圾的区域（这也是G1名称的由来），旨在提供可预测的停顿时间。【标记整理】初始标记：Stop The World，仅使用一条初始标记线程对GC Roots关联的对象进行标记；并发标记：使用一条标记线程与用户线程并发执行。此过程进行可达性分析，速度很慢；最终标记：Stop The World，使用多条标记线程并发执行；筛选回收：回收废弃对象，此时也要 Stop The World，并使用多条筛选回收线程并发执行；适用场景：适用于大内存或多核处理器系统，特别是那些希望控制停顿时间的应用。优点：能够较好地平衡吞吐量和停顿时间，适合大型企业级应用。缺点：配置相对复杂，需要根据具体应用调整参数以获得最佳性能。ZGC (Z Garbage Collector)描述：ZGC是一个可扩展的低延迟垃圾收集器，专为满足现代硬件需求而设计。它的设计目标是在不影响应用性能的前提下支持非常大的堆（从几GB到数TB）。适用场景：非常适合需要极低延迟和大堆内存的应用场景。优点：几乎所有的操作都是并发执行的，极大地减少了停顿时间。缺点：相比其他收集器，ZGC还在不断发展中，某些功能可能不如传统收集器成熟。其他Java对象：对象的基本结构对象头（Object Header）：每个对象都有一个对象头，它包含了运行时数据如哈希码、GC分代年龄、锁状态标志等信息。对象头的大小依赖于JVM的实现和是否启用了压缩指针。在64位JVM上，默认情况下对象头是12字节（如果启用了指针压缩，则为12字节；否则为16字节）。在32位JVM上，对象头通常是8字节。实例数据（Instance Data）：实例数据部分包含了对象中实际存储的数据，即类中的字段（包括从父类继承下来的字段）。每个字段根据其类型占据不同的空间：boolean, byte - 1字节char, short - 2字节int, float - 4字节long, double - 8字节引用类型（如其他对象的引用）- 在32位JVM上通常为4字节，在64位JVM上默认为8字节（若开启指针压缩则为4字节）。对齐填充（Padding）：Java对象要求其大小必须是8字节的倍数以提高性能，因此可能会有额外的填充字节来满足这一要求。例如，如果一个对象的总大小（对象头+实例数据）为13字节，则需要再加3字节的填充使其达到16字节。堆内存设置原则：应用程序的需求对象创建频率和规模：如果您的应用程序频繁创建大量临时对象，或者处理大数据集，则可能需要更大的堆空间。长生命周期的对象：对于那些具有较长生命周期的对象，确保有足够的老年代空间来容纳它们，避免过早的老年代垃圾回收。系统硬件限制物理内存大小：确保分配给JVM的最大堆内存不会超出服务器的物理内存。否则，可能导致操作系统使用交换分区，这会严重影响性能。其他进程需求：除了JVM之外，服务器上可能还运行着其他服务或进程，预留足够的内存供这些进程使用是很重要的。垃圾回收行为GC频率与暂停时间：较大的堆空间可以减少垃圾回收的频率，但每次GC的时间可能会更长。较小的堆可能导致更频繁但更短暂的GC事件。根据应用对GC暂停时间的要求调整堆大小。GC算法的选择：不同的垃圾收集器有不同的优化策略。例如，G1 GC旨在提供可预测的停顿时间，而CMS则侧重于缩短停顿时间。选择合适的GC算法也会影响堆大小的选择。应用的并发性线程数量：每个线程都有自己的栈空间，默认情况下每个线程的栈大小可能是512KB到1MB不等。高并发应用中，大量的线程也会消耗相当多的内存。数据结构复杂度：复杂的嵌套数据结构会增加内存占用量，需据此调整堆大小。性能测试与监控基准测试：基于实际工作负载进行基准测试可以帮助确定最佳的堆大小配置。持续监控：利用工具如VisualVM、JConsole或其他监控解决方案，实时监控堆使用情况、GC活动等指标，以便根据实际情况动态调整堆大小。安全边际预留缓冲区：为了避免因内存突然激增而导致OOM错误，通常建议不要将所有可用内存都分配给堆。留出一定的安全边际以应对突发的内存需求。自定义类加载器：//自定义类加载器，打破双亲委派 //对于双亲委派机制而言，本质上是在Classloader这个抽象类中实现的，如果我们想打破双亲委派机制，主要继承Classloader重写里面的方法比如loadclass就能够实现我们自定义的加载逻辑 import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; public class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { // Define the class with a custom byte array, breaking the parent delegation return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { // Convert package name to directory structure String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try { FileInputStream inputStream = new FileInputStream(path); byte[] buffer = new byte[(int) new File(path).length()]; inputStream.read(buffer); inputStream.close(); return buffer; } catch (IOException e) { e.printStackTrace(); return null; } } public static void main(String[] args) { String classPath = "path/to/classes"; CustomClassLoader classLoader = new CustomClassLoader(classPath); try { // Load a class named "DynamicClass" from the specified class path Class<?> dynamicClass = classLoader.loadClass("DynamicClass"); System.out.println("Class " + dynamicClass.getName() + " loaded successfully."); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }堆外内存示例：import java.nio.ByteBuffer; public class OffHeapMemoryExample { public static void main(String[] args) { // 分配堆外内存 ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 1KB的直接内存 // 写入数据 buffer.put((byte) 'a'); buffer.put((byte) 'b'); // 翻转模式，将缓冲区从写模式切换到读模式 buffer.flip(); // 读取数据 while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } // 清除引用（帮助垃圾回收释放内存） buffer = null; // 切断引用 System.gc(); // 提示 GC 回收，但不保证 System.out.println("\nEnd of example"); } }
volatile底层实现：volatile可以保证数据的可见性，被volatile修饰的变量能够保证每个线程能够获取该变量的最新值，从而避免出现数据脏读的现象；同时禁止指令重排。加入volatile关键字时，汇编代码中会多出一个lock前缀指令，这个指令实际上相当于一个内存屏障（也成内存栅栏），内存屏障会提供3个功能：它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置，也不会把前面的指令排到内存屏障的后面；它会强制将对缓存的修改操作立即写入主存；如果是写操作，它会导致其他CPU中对应的缓存行无效。AQS：AQS，全称AbstractQueuedSynchronizer（抽象队列同步器），是Java并发包（java.util.concurrent）中用于构建锁和其他同步组件的基础框架。它提供了一个框架，用于实现依赖于先进先出（FIFO）等待队列的阻塞锁和相关同步器（信号量、事件等）。AQS支持独占和共享两种模式设计原理：状态管理：AQS使用一个int类型的状态变量表示当前同步状态。这个状态可以通过getState()、setState(int newState)和compareAndSetState(int expect, int update)方法进行查询和更新。FIFO队列：当一个线程尝试获取锁但失败时（即状态不符合获取条件），该线程会被封装成一个节点（Node），并被加入到同步队列的尾部。队列遵循FIFO原则，即先入队的线程优先获得锁。独占与共享模式：AQS支持两种模式来适应不同的同步需求。在独占模式下，每次只有一个线程能够成功获取锁；而在共享模式下，多个线程可能同时成功获取锁。自旋与阻塞：为了提高性能，AQS在某些情况下会让线程自旋（忙等）一段时间而不是立即进入阻塞状态，这取决于具体实现和系统配置。应用：ReentrantLock：一个可重入的互斥锁，提供了与使用synchronized关键字类似的同步语义，并扩展了诸如公平性选择、非块结构锁定操作等功能。Semaphore（信号量）：控制对某组资源的访问权限，限制同时访问某一资源的最大线程数。CountDownLatch：允许一个或多个线程一直等待，直到其他线程执行的一组操作完成为止。CyclicBarrier：允许一组线程互相等待，直到到达某个公共屏障点（所有线程都到达此点后才能继续执行）。ReadWriteLock：允许多个读操作并发执行，但在写操作发生时不允许任何读操作。【多用于缓存系统、配置管理、文档编辑等】锁优化：使用层面：减少锁的时间：不需要同步执行的代码，尽量放到同步块外边（循环的话，可能需要锁粗化，防止频繁加解锁）。减少锁的粒度：物理上的一个锁，拆分为逻辑上的多个锁，增加并行度，以空间换时间，例如java中的ConcurrentHashMap（使用多个segment分别进行加解锁）。使用读写锁：使用ReentranReadWriteLock，读加读锁，写加写锁。使用CAS：线程竞争不激烈，且执行快，可以考虑valatiled+cas。系统层面：自适应自旋锁：自动自旋，自旋时间可以有上一个在同一个锁上的自旋时间和锁的持有者状态决定。锁消除：如果编译器发现不可能存在共享数据的竞争，会进行锁消除【Netty中无锁设计的pipeline中的chnnelhandler会进行锁消除的优化】锁升级：偏向锁->轻量级锁->重量级锁ReentrantLock和synchronized：synchronized：synchronized基于监视器锁（Monitor Lock）实现，底层依赖于操作系统的互斥锁（Mutex）。每个对象在Java中都与一个隐式的监视器相关联，使用synchronized时，线程需要获得对象的监视器锁，然后才能进入同步代码块。synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 组成，其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息。同步代码块是利用 monitorenter 和 monitorexit 指令实现的，而同步方法则是利用 flags 实现的。使用：修饰实例⽅法: 作⽤于当前对象实例加锁，进⼊同步代码前要获得当前对象实例的锁修饰静态⽅法: 也就是给当前类加锁，会作⽤于类的所有对象实例，因为静态成员不属于任何⼀个实例对象，是类成员。修饰代码块: 指定加锁对象，对给定对象加锁，进⼊同步代码库前要获得给定对象的锁。偏向锁、轻量级锁、重量级锁：偏向锁：在锁对象的对象头中记录一下当前获取到该锁的线程ID，该线程下次如果又来获取该锁就可以直接获取到了。一旦有另一个线程尝试获取这个锁，偏向锁将被撤销，并可能升级为轻量级锁或者直接升级为重量级锁，取决于竞争的程度。轻量级锁：轻量级锁采用CAS操作来避免使用操作系统层面的互斥量，当一个线程尝试获取锁时，JVM会在当前线程的栈帧中创建锁记录（Lock Record），并将对象头中的Mark Word复制到该锁记录中。然后尝试通过CAS操作将对象头的Mark Word替换为指向该锁记录的指针。如果成功，则表示获得了锁；否则，表示存在竞争。多个线程同时竞争同一个锁时，可能会出现多次CAS失败的情况。在这种情况下，JVM通常会让未获得锁的线程短暂等待（即自旋）。重量级锁：重量级锁基于操作系统的互斥量实现，涉及到用户态和内核态的转换，开销较大。当一个线程获取重量级锁时，其他试图获取该锁的线程会被阻塞，直到持有锁的线程释放锁为止。这种方式虽然保证了线程安全，但代价较高。ReentrantLock：ReentrantLock是java.util.concurrent包下提供的一套互斥锁，需要lock()和unlock()方法配合try/finally语句块来完成。ReenTrantLock的实现是一种自旋锁，通过循环调用CAS操作来实现加锁，它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。区别：synchronized是Java语言的内置特性，是一个关键字，ReentrantLock是一个类。sychronized会自动的加锁与释放锁，ReentrantLock需要程序员手动加锁与释放锁。sychronized的底层是JVM层面的锁，ReentrantLock是API层面的锁。sychronized是非公平锁，ReentrantLock可以选择公平锁或非公平锁。sychronized锁的是对象，锁信息保存在对象头中，ReentrantLock通过代码中int类型的state标识来标识锁的状态。sychronized底层有一个锁升级的过程。在较低竞争时，synchronized会自动使用优化，比如锁消除和锁粗化，使得它的性能在某些情况下可能高于ReentrantLock。高竞争时，ReentrantLock可能更好，因为提供了更灵活的线程控制。ReentrantLock具有与之关联的Condition对象，可以搭配lock来更细粒度的控制线程通信；synchronized配合Object的wait()和notify()/notifyAll()来进行线程之间的通信，但不如Condition灵活。CountDownLatch和Semaphore：CountDownLatch表示计数器，可以给CountDownLatch设置一个数字，一个线程调用CountDownLatch的await()将会阻塞，其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一，当数字被减成0后，所有await的线程都将被唤醒。对应的底层原理就是，调用await()方法的线程会利用AQS排队，一旦数字被减为0，则会将AQS中排队的线程依次唤醒。Semaphore表示信号量，可以设置许可的个数，表示同时允许最多多少个线程使用该信号量，通过acquire()来获取许可，如果没有许可可用则线程阻塞，并通过AQS来排队，可以通过release()方法来释放许可，当某个线程释放了某个许可后，会从AQS中正在排队的第一个线程开始依次唤醒，直到没有空闲许可。伪共享：在现代处理器中，缓存行（Cache Line）是缓存的最小可分配单位，通常是64字节，当多个线程在不同CPU核心上操作缓存行中的不同变量时，如果这些变量位于同一个缓存行，修改其中一个变量会导致整个缓存行被标记为无效(cpu缓存一致性决定)。这种重复的缓存行无效化和重新加载的现象就是伪共享。public class FalseSharingExample implements Runnable { // 定义线程数 public static int NUM_THREADS = 4; public static final long ITERATIONS = 5000000000L; // 每个线程的迭代次数 private final int arrayIndex; // 此线程操作的共享数据索引 private static ValuePadding[] values; // 用于存储共享数据 public FalseSharingExample(int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; // 初始化共享变量数组 values = new ValuePadding[NUM_THREADS]; for (int i = 0; i < values.length; i++) { values[i] = new ValuePadding(); } final long start = System.nanoTime(); for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharingExample(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } System.out.println("Duration = " + (System.nanoTime() - start)); } @Override public void run() { long i = ITERATIONS + 1; while (0 != --i) { // 更新当前线程对应的共享变量 values[arrayIndex].value = i; } } // 内部类，带有缓存行填充以避免伪共享 public static class ValuePadding { protected long p1, p2, p3, p4, p5, p6, p7; // 填充字段 public volatile long value = 0L; // 实际操作的变量 protected long p9, p10, p11, p12, p13, p14, p15; // 填充字段 } // @Contended注释的使用演示，如果使用该注解，需要加启动参数启用 -XX:-RestrictContended /* public static class ValueWithContended { @Contended public volatile long value = 0L; } */ }
线程状态：wait和sleep：wait()属于Object方法，调用之后会强制释放当前对象锁，调用时必须拿到当前对象的监视器monitor对象，所以必须在 synchronized 保护的代码中使用，而 sleep 方法并没有这个要求。wait 方法会主动释放 monitor 锁，在同步代码中执行 sleep 方法时，并不会释放 monitor 锁。wait 方法意味着永久等待，直到被中断或被唤醒才能恢复，不会主动恢复，sleep 方法中会定义一个时间，时间到期后会主动恢复。wait/notify 是 Object 类的方法，而 sleep 是 Thread 类的方法。怎么创建线程：继承Thread类，重写run方法，并通过调用start方法启动线程。实现Runnable接口，并重写run方法。使用匿名内部类，或者Lambda表达式，适用于简单的任务。使用线程池，当然不一定会创建线程。class MyThread extends Thread { public void run() { // 线程任务逻辑 } } // 创建并启动线程 MyThread myThread = new MyThread(); myThread.start(); //Runnable class MyRunnable implements Runnable { public void run() { // 线程任务逻辑 } } //Callable MyCallable myCallable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(myCallable); Thread thread = new Thread(futureTask); futureTask.get(); //Runnable Thread thread = new Thread(new MyRunnable()); thread.start(); //匿名内部类 Thread thread = new Thread(new Runnable(){...run()}); //Lambda表达式 Thread thread = new Thread(()->{}); //线程池 ExecutorService executorService = Executors.newFixedThreadPool(5); executorService.submit(()->{});怎么停止线程：使用原子标志位，将逻辑嵌入业务代码。使用interrupt方法，在线程中检测Thread.currentThread().isInterrupted()进行处理。使用stop方法，不推荐，会导致强制停止，可能造成资源泄露。（会释放synchronized而不会自动缩放ReentranLock）如果是线程池，可以使用ExecutorService的shutdown或者shutdownNow。如果线程通过Future启动，可以使用cancel方法取消执行。线程池：参数：参数名称描述corePoolSize线程池中的核心线程数。即使线程处于空闲状态，也会保持在线程池中，除非设置了allowCoreThreadTimeOut。（核心数*（1+等待/运行））maximumPoolSize线程池中允许的最大线程数。当队列满了并且正在运行的线程数小于maximumPoolSize时，会创建新的线程。（可以设置压测值）keepAliveTime当线程数大于核心线程数时，多余的空闲线程在这个时间内没有任务可执行就会被终止。unitkeepAliveTime的时间单位，如秒、分钟等。workQueue用于保存等待执行的任务的阻塞队列。threadFactory每个线程创建都会调用的方法，可以用来设置线程的一些属性，比如是否是守护线程等。handler当线程和队列都满了之后，新提交的任务会触发这个拒绝策略处理器。常用线程池：线程池类型使用场景优点缺点参数FixedThreadPool适用于需要限定并发线程数量的场景，保证了任何时候最多有固定数量的线程同时运行。- 控制资源消耗；- 提供了对线程数量的精确控制。不适合处理大量耗时的任务，因为可能会导致内存溢出。允许队列长度为max intCachedThreadPool适用于执行很多短期异步任务的小程序，或负载较轻的服务器。- 自动回收旧的线程并创建新的线程；- 几乎无限制地创建新线程（注意可能导致内存溢出）。可能会创建大量的线程，导致系统资源耗尽。允许创建线程数为max intScheduledThreadPool适用于需要定时或周期性执行任务的场景。- 支持定时及周期性任务执行；- 能够灵活配置核心线程数。如果任务执行时间比计划间隔长，则会导致任务积压。允许创建线程数为max intSingleThreadExecutor适用于需要顺序执行每个任务，并且在任意时间点都不会有多个线程活动的场景。- 全部任务都在一个线程中按顺序执行，避免了上下文切换的开销。性能瓶颈明显，因为所有任务必须排队等待前一个任务完成。允许队列长度为max int线程池状态：状态处理RUNNING会接收新任务并且会处理队列中的任务SHUTDOWN不会接收新任务并且会处理队列中的任务，任务处理完后会中断所有线程STOP不会接收新任务并且不会处理队列中的任务，并且会直接中断所有线程TIDYING所有线程都停止了之后，线程池的状态就会转为TIDYING，一旦达到此状态，就会调用线程池的terminated()TERMINATEDterminated()执行完之后就会转变为TERMINATED转变前转变后转变条件RUNNINGSHUTDOWN手动调用shutdown()触发，或者线程池对象GC时会调用finalize()从而调用shutdown()RUNNINGSTOP手动调用shutdownNow()触发SHUTDOWNSTOP手动先调用shutdown()紧着调用shutdownNow()触发SHUTDOWNTIDYING线程池所有线程都停止后自动触发STOPTIDYING线程池所有线程都停止后自动触发TIDYINGTERMINATED线程池自动调用terminated()后触发拒绝策略：AbortPolicy：直接抛出异常，阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略。CallerRunsPolicy ：只要线程池未关闭，该策略直接在调用者线程中，运行当前被丢弃的任务。不会造成任务丢失，同时减缓提交任务的速度，给执行任务缓冲时间。DiscardOldestPolicy ：丢弃最老的一个请求，也就是即将被执行的任务，并尝试再次提交当前任务。DiscardPolicy ：该策略默默地丢弃无法处理的任务，不予任何处理。如果允许任务丢失，这是最好的一种方案。线程池流程：在使用execute()方法提交一个Runnable对象时会先判断当前线程池中的线程数是否小于corePoolSize如果小于，则创建新线程并执行Runnable如果大于等于，则尝试将Runnable加入到workQueue中如果workQueue没满，则将Runnable正常入队，等待执行如果workQueue满了，则会入队失败，那么会尝试继续增加线程如果当前线程池中的线程数是否小于maximumPoolSize如果小于，则创建新线程并执行任务（这里是非公平的，可能比队列中任务先执行）如果大于等于，则执行拒绝策略，拒绝此Runnable【tips：tomcat自己的线程池会优先启动线程，上限再将任务入队】【tips：execute执行，异常会移除异常线程并新建，submit是不会移除也不会创建】线程池是否完成：isTerminated：操作，执行pool.shutdown后判断pool.isTerminated是否成功，操作简单，但是需要有关闭线程池的操作。getCompletedTaskCount：使用getTaskCount()获取线程池中已经完成和正在执行的任务数量，getCompletedTaskCount返回以及执行完成的任务总数（这两个方法返回的都是近似的，因为任务和线程状态可能在计算中动态改变），这种方式不需要关闭线程池，但是需要确保不会有新任务进行提交。CountDownLatch：需要在线程池任务中执行CountDownLatch.countDown()方法对计数器减一，随后调用CountDownLatch.await()方法阻塞直到完成，这种方式需要提前知道线程数，性能较差，还需要任务加上异常判断，否则可能导致计数器无法完成闭环导致阻塞。计数器：类似CountDownLatch，自己维护一个原子变量，或者使用锁进行计数器操作。Future：使用Future获取线程池的任务状态，调用future.isDone()判断是否完成，需要每个线程池都关联一个future对象。Fork/Join:设计目的和策略Fork/Join框架：主要目的是通过并行化处理来加速计算密集型任务的执行。它采用分治法将大任务递归分解为更小的任务，直至这些子任务足够简单可以直接解决。这种策略特别适合那些可以自然分割的问题，如数组排序或搜索等。传统线程池：旨在提供一种有效管理线程生命周期的方法，减少频繁创建和销毁线程带来的开销，并且可以根据需要灵活调整并发级别。它可以适应多种不同类型的任务，无论是计算密集型还是I/O密集型。任务分配和执行Fork/Join框架：使用ForkJoinPool作为其执行环境。支持工作窃取算法，使得空闲线程可以从其他忙碌线程的工作队列中“偷取”任务执行，以此平衡负载。自动管理任务的分裂（fork）与合并（join），简化了开发者的负担。传统线程池：通常遵循先进先出（FIFO）原则来调度任务。开发者需手动管理任务的划分与协调，增加了复杂度但提供了更大的灵活性。集成与扩展性Fork/Join框架：内置了对阻塞队列的支持，能够自动处理任务队列的管理和线程的阻塞等待，这有助于提高效率和简化编程模型。传统线程池：虽然也支持使用阻塞队列，但这往往需要开发者根据具体需求进行配置或选择合适的第三方库实现，提供了更多的定制可能性但也要求更高的专业知识。适用场景Fork/Join框架：最适合于那些可以通过递归分解为独立子问题的任务，尤其是当任务之间几乎没有依赖关系时。例如，在数据处理、图像渲染等领域表现出色。传统线程池：适用于广泛的场景，包括但不限于计算密集型任务、I/O密集型任务、定时任务等。对于不需要或不适合细粒度并行化的任务，传统线程池可能是更好的选择。ThreadLocal：ThreadLocal 是一个泛型类，保证可以接受任何类型的对象，最终的变量是放在了当前线程的 ThreadLocalMap 中，并不是存在 ThreadLocal 上，ThreadLocal 可以理解为只是ThreadLocalMap的封装，传递了变量值。ThreadLocal是Java中所提供的线程本地存储机制，可以利用该机制将数据缓存在某个线程内部，该线程可以在任意时刻、任意方法中获取缓存的数据。ThreadLocal底层是通过ThreadLocalMap来实现的，每个Thread对象（注意不是ThreadLocal对象）中都存在一个ThreadLocalMap，Map的key为ThreadLocal对象，Map的value为需要缓存的值。在线程池中使用ThreadLocal可能会造成内存泄漏，而线程对象是通过强引用指向ThreadLocalMap，ThreadLocalMap也是通过强引用指向Entry对象，线程不被回收，Entry对象也就不会被回收，从而出现内存泄漏，解决办法是，在使用了ThreadLocal对象之后，手动调用ThreadLocal的remove方法，手动清楚Entry对象。内存泄露：ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用，⽽ value 是强引⽤。弱引用的特点是，如果这个对象持有弱引用，那么在下一次垃圾回收的时候必然会被清理掉。【如果key设计为强引用，问题会更严重，只要ThreadLocalMap还存在对ThreadLocal实例的引用，即使应用程序代码中已经不再使用该ThreadLocal实例，它也不会被垃圾回收器回收。】ThreadLocalMap实现中已经考虑了这种情况，在调用 set()、get()、remove() 方法的时候，会清理掉 key 为 null 的记录。如果说会出现内存泄漏，那只有在出现了 key 为 null 的记录后，没有手动调用 remove() 方法，并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使用完ThreadLocal 方法后，最好手动调用 remove() 方法。CompletableFuture：CompletableFuture实现了Future和CompletionStage接口，所以CompletableFuture首先是一个Future，除开拥有Future的功能外，还拥有CompletionStage接口所提供的功能，而CompletionStage表示异步执行整个过程中的某个步骤，表示我们可以利用CompletableFuture来对异步执行的多个任务进行编排（这是Future所不支持的）。//定义 public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {} //创建 public static CompletableFuture supplyAsync(Supplier supplier) { return asyncSupplyStage(asyncPool, supplier); } @FunctionalInterface public interface Supplier<T> { T get(); } public static CompletableFuture<Void> runAsync(Runnable runnable) { return asyncRunStage(asyncPool, runnable); } @FunctionalInterface public interface Runnable { public abstract void run(); } //示例 public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); Supplier<String> taskA = () -> { System.out.println("1:" + Thread.currentThread().getName()); return "Hello"; }; Function<String, String> taskB = s -> { System.out.println("2:" + Thread.currentThread().getName()); return s + " World"; }; CompletableFuture.supplyAsync(taskA, executorService) .thenApply(taskB); } //or CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { sleep(1000); return 42; }); // 非阻塞链式操作 CompletableFuture<Void> resultFuture = future.thenApply(result -> { System.out.println("Result: " + result); return result * 2; }).thenAccept(result -> System.out.println("Transformed Result: " + result)); resultFuture.get(); // 等待所有阶段完成 } }线程和进程：特性进程线程资源管理拥有独立的资源，如内存空间、文件描述符等。共享所属进程的资源，线程间可以通过全局变量等方式直接通信。开销创建和销毁的开销大，进程间的切换也需要较多的时间和资源。相对于进程，创建和销毁的开销较小，线程间的切换也更加高效。内存空间每个进程都有自己独立的地址空间。同一进程内的所有线程共享同一个地址空间。安全性进程间相互隔离，安全性高。因为共享资源，所以可能存在竞争条件，需要同步机制来保证数据一致性。通信方式进程间通信（IPC）较为复杂，通常使用管道、消息队列、信号量等方式。线程间可以直接访问同一进程内的任何数据，通信更为直接。故障影响一个进程崩溃不会直接影响其他进程。一个线程如果出现致命错误可能导致整个进程崩溃。