余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

创建线程的方式

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长
  1. 继承Thread类创建线程:

    • 这种方式是通过创建一个新的类,该类继承自Thread类,并重写run方法。然后,通过创建该类的实例并调用其start方法来启动线程。
    • 优点:代码简单,易于理解。
    • 缺点:由于Java是单继承的,继承Thread类后就不能再继承其他类了,这在一定程度上限制了类的扩展性。
  2. 实现Runnable接口创建线程:

    • 这种方式是通过创建一个实现了Runnable接口的类,并实现其run方法。然后,将该类的实例作为参数传递给Thread类的构造器,创建Thread对象并调用其start方法来启动线程。
    • 优点:由于Java支持接口的多实现,因此这种方式更具灵活性,可以继承其他类并实现Runnable接口。
    • 缺点:相对于继承Thread类,代码稍显复杂。
  3. 通过Callable和FutureTask创建线程:

    • Callable接口类似于Runnable接口,但它可以返回结果并且可以抛出受检异常(checked exception)。
    • FutureTask是Future接口的一个实现,它包装了一个Callable对象,可以提交给Executor(如线程池)来执行。
    • 这种方式通常用于需要获取线程执行结果或处理受检异常的场景。
  4. 通过线程池创建线程:

    • 线程池是提前创建好一批线程,并保存在池中。当有任务需要执行时,从池中取出一个线程来执行任务。这种方式可以显著提高资源的利用率和性能。
    • Java中的ExecutorService接口提供了管理线程池的方法,如newFixedThreadPool、newCachedThreadPool等。
    • 优点:提高资源利用率、性能;简化线程管理。
    • 缺点:增加了代码的复杂性;需要合理配置线程池的大小以避免资源耗尽或性能下降。

关于Runnable和Callable的区别:

  • Runnable的run方法无返回值,而Callable的call方法有返回值。
  • Callable中可以抛出受检异常,而Runnable不可以。
  • Callable和Runnable都可以应用于executors(执行器),但Thread类只支持Runnable。

关于Future和FutureTask:

  • Future是一个接口,代表了一个异步执行的结果。它提供了检查执行是否完成、等待完成和获取执行结果的方法。
  • FutureTask是Future接口的一个实现,它实现了一个可以提交给Executor执行的任务,并且可以用来检查任务的执行状态和获取任务的执行结果。

最后,线程池是池化技术的一种典型实现,用于提高资源的利用率和性能。在编程中,线程池通常用于管理大量并发任务的执行。

并发笔记

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长

并发

synchronized

同一时刻只能一个线程执行代码块

可以修饰方法和代码块

如何实现可见性问题

  • 实现可见性的过程

    • 获取互斥锁

    • 清空本地代码,将主内存中的最新拷贝到本地内存

    • 将更改后共享变量值刷新到主内存

    • 释放互斥锁

如何实现同步

  • 都是使用mointorenter和monitorexit两个JVM指令实现

  • 什么是管程

    • 管理共享变量以及对共享变量操作过程,使得支持并发

    • 线程可以对monitor执行lock和unlock操作进行加锁和释放锁

    • 解决互斥问题的思路:将共享变量及对共享变量的操作统一封装起来

锁优化

  • 同步锁的四种状态:无锁、偏向锁、轻量级锁、重量级锁

  • 偏向锁:一个线程加锁

  • 轻量级锁:两个线程交替自旋

  • 同步锁锁定资源是对象

volatile

可见性:对变量的修改对所有线程可见

  • 可见性

    • volatile在写操作的时候,JVM会发一条lock前缀的指令,将这个缓存的变量会写到系统主存中;其他的使用的时候会从主存读取最新的数据。所以可见

内存屏障(Memory Barrier)是CPU的一种指令,用于控制特定条件下的重排序和内存可见性问题。

Java编译器会根据内存屏障的规则禁止重排序

  • 为了保障volatile变量的可见性和禁止指令重排序,java在字节码中插入内存屏障实现

    • 内存屏障解决指令重排

有序性:禁止指令重排,遵循happens-before原则

双重检验锁必须加volatile,因为内存屏障

  • 双重校验锁实现一个单例

  • 否则会出现空指针

存在问题

  • 不满足原子性

    • 解决方法

      • 使用syn

      • 使用可重入锁

      • 使用原子类

与syn的区别

  • volatile不需要加锁,不会阻塞线程

  • volatile是一种简单的同步机制

JUC

锁的分类

CAS:一条CPU的原子指令,可以保证共享变量修改的原子性

  • 使用unsafe类实现,其中都是native方法

  • 根据内存偏移量找到待更新的原值的准确内存地址,使用compareAndSwaplant将待更新的值和预期值进行比较

  • CAS缺陷

    • 循环时间太长

    • 只能保证一个共享变量原子操作

    • ABA问题

      • 解决方案:AtomicStampedReference(加个时间作为版本号)

问题:Thread 的join方法

与syn的比较

  • syn在以下情况下释放锁

    • 线程执行完释放

    • 线程执行时发生异常,JVM会自动释放

    • 锁方法执行了wait方法,进行释放锁

  • syn的问题

    • 无法控制阻塞时长——>JUC trylock()解决

    • 阻塞不可中断——>lockInterruptibly解决

    • syn不支持读写锁分离——>JUC的ReentrantReadWriteLock锁

AQS

  • 先进先出队列+CAS+volatile

    • 维护一个volatile的int类型的state变量,state=1是获取到锁;state的值变化是由CAS完成的
  • 实现

    • CAS操作提供原子性避免锁

    • volatile确保修改的可见性和内存操作的有序性

ThreadLocal

解决并发问题,在线程中传递数据

通过为每一个线程创建一份共享变量的副本保证各个线程之间的变量访问和修改互不影响

内存泄漏问题

  • 为什么

    • key ThreadLocal的引用缘

      • 栈上的ThreadLocal引用

      • ThreadLocalMap中的key对他的引用

    • value

      • 引用只有一条,从Thread过来的引用
    • 出现的问题

      • ThreadLocal栈上的引用不见了,但是Threadlocal对象因为还有一个引用,索引无法回收

        • 解决方法:ThreadLocal的key改成弱引用
      • Thread对象一直被使用,无法释放

        • 解决方法:对于value,Thread一直没有释放,只有在一个ThreadLocal用完的时候,手动调用一下remove方法
  • 解决

Java中几种集合的排序方式

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长
  • 包括实现Comparable接口、借助Comparator比较器进行排序,以及通过Stream API进行排序。同时还解释了Comparable和Comparator的区别、compareTo和equals的使用场景差异,以及Set集合的排序问题。
  1. 实现Comparable接口:
    • Java中的类可以通过实现Comparable接口来具备排序能力。
    • 实现Comparable接口的类需要重写compareTo方法,该方法定义了对象的排序规则。
    • 例如,学生类(Student)可以实现Comparable接口,并按照姓名和年龄进行排序。
  2. 借助Comparator比较器进行排序:
    • 当类本身没有实现Comparable接口,或者需要不同的排序规则时,可以使用Comparator接口。
    • Comparator是一个函数式接口,可以独立于原类之外定义排序逻辑。
    • 例如,可以使用Comparator对学生对象按照姓名和年龄进行排序。
  3. 通过Stream API进行排序:
    • Java 8引入了Stream API,可以方便地对集合进行排序操作。
    • Stream API的sorted方法可以接受一个Comparator作为参数来进行排序。
    • 例如,使用Stream对学生列表进行排序,可以简化排序的代码。
  4. Comparable与Comparator的区别:
    • Comparable用于使类本身具备排序能力,通过实现compareTo方法实现。
    • Comparator是一个独立的比较器,可以为不具备排序能力的类提供排序逻辑,或者提供不同的排序规则。
  5. compareTo与equals的使用场景:
    • compareTo主要用于排序和数值比较,如BigDecimal的比较。
    • equals主要用于判断两个对象在业务语义上是否相同,如String的比较通常使用equals来判断字面意义是否相同。
  6. Set集合的排序问题:
    • Set集合本身是无序的,即元素的插入顺序不保证。
    • 但是,SortedSet接口可以保证元素的排序,通过要求元素实现Comparable接口来实现。
    • LinkedHashSet类通过双向链表记录插入顺序,实现了插入有序的Set。

死锁

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

死锁的定义

死锁是指两个或两个以上的进程(或线程)在执行过程中,因竞争资源或彼此通信而造成的一种阻塞现象。当这些进程都在等待对方释放资源时,就会形成一个无法打破的僵局,若无外力作用,它们都将无法继续执行。此时,系统处于死锁状态。

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可抢占:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何解决死锁

解决死锁的方法主要分为预防死锁、避免死锁、检测死锁和解除死锁四种。

  1. 预防死锁:

    • 破坏互斥条件:允许资源被多个进程同时访问(但某些资源可能无法这样做)。
    • 破坏占有且等待条件:要求进程一次性申请所有所需资源,或者允许进程在持有资源的同时申请其他资源(但可能降低系统效率)。
    • 破坏不可抢占条件:允许进程被抢占已分配的资源(但可能导致数据不一致等问题)。
    • 破坏循环等待条件:对资源编号,要求进程按编号顺序申请资源(但可能增加资源管理的复杂性)。
  2. 避免死锁:

    • 使用银行家算法等算法来动态地检查资源分配的安全性,确保系统不会进入不安全状态。
    • 在资源分配过程中,采用资源预分配策略或资源按需分配策略,并监控系统的资源使用情况。
  3. 检测死锁:

    • 定期检查系统是否存在死锁现象,如使用资源分配图等方法。
    • 一旦发现死锁,立即采取措施进行解除。
  4. 解除死锁:

    • 终止一个或多个进程,以打破循环等待条件。
    • 回滚到安全状态,重新分配资源。
    • 在数据库系统中,可以采用自动回滚事务、重启事务等方法来解除死锁。

数据库死锁的发生与解决

在数据库中,死锁通常发生在多个事务并发执行时。当事务A持有资源A的锁并尝试获取资源B的锁时,而事务B持有资源B的锁并尝试获取资源A的锁时,就会发生死锁。

解决数据库死锁的方法包括:

  • 避免并发修改:尽量减少多个事务对同一资源的并发访问。
  • 保证操作顺序:确保多个事务按照相同的顺序访问资源。
  • 使用锁超时机制:设置锁的超时时间,当事务持有锁超过一定时间时自动释放锁。
  • 使用乐观锁或悲观锁等锁策略来管理资源访问。

综上所述,死锁是并发系统中常见的问题之一。通过理解死锁的产生条件、掌握解决死锁的方法以及合理设计资源访问策略,可以有效地预防和解决死锁问题。

线程

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

线程状态的详细解释

  1. 初始(NEW)

    线程对象被创建,但尚未调用start()方法。此时线程还未开始执行,只是作为一个对象存在于内存中。

  2. 运行(RUNNABLE)

    • 就绪(READY): 线程对象创建后,通过调用start()方法启动。此时线程进入就绪状态,等待操作系统的调度,以获取CPU时间片。
    • 运行中(RUNNING): 当就绪状态的线程获得CPU时间片时,开始执行程序代码,进入运行状态。

    在Java中,由于就绪和运行状态的切换非常频繁,且难以准确区分,因此将两者统称为“运行(RUNNABLE)”状态。

  3. 阻塞(BLOCKED)

    线程尝试获取某个对象的锁(如通过synchronized关键字),但锁已被其他线程持有。此时线程进入阻塞状态,直到锁被释放并成功获取。

  4. 等待(WAITING)

    线程通过调用Object类的wait()方法或其他等待方法(如Condition的await()方法)进入等待状态。此时线程需要等待其他线程的通知(通过notify()或notifyAll()方法)或中断来唤醒。

  5. 超时等待(TIMED_WAITING)

    线程通过调用带有超时参数的等待方法(如Thread.sleep(long millis)、Object.wait(long timeout)等)进入超时等待状态。此时线程在指定的时间内等待,如果超时时间到达或收到其他线程的通知,则线程会被唤醒。

  6. 终止(TERMINATED)

    线程执行完毕或由于异常等原因终止执行,进入终止状态。此时线程不再占用系统资源。

状态流转的细化

  • 从初始(NEW)到运行(RUNNABLE): 调用start()方法。
  • 从运行(RUNNABLE)到阻塞(BLOCKED): 尝试获取锁失败。
  • 从阻塞(BLOCKED)到运行(RUNNABLE): 成功获取锁。
  • 从运行(RUNNABLE)到等待(WAITING): 调用wait()等方法。
  • 从等待(WAITING)到运行(RUNNABLE): 收到其他线程的通知或中断。
  • 从运行(RUNNABLE)到超时等待(TIMED_WAITING): 调用带有超时参数的等待方法。
  • 从超时等待(TIMED_WAITING)到运行(RUNNABLE): 超时时间到达或收到其他线程的通知。
  • 从运行(RUNNABLE)到终止(TERMINATED): 线程执行完毕或异常终止。

关于RUNNING状态的缺失

如你所述,由于CPU时间片的分配和线程状态的频繁切换,很难准确区分线程是处于就绪状态还是运行状态。因此,Java将两者统称为“运行(RUNNABLE)”状态,以表示线程当前是可执行的,只要获得CPU时间片就能立即执行。

总结

了解线程的状态及其流转对于编写高效、可靠的并发程序至关重要。通过合理管理线程状态,可以确保程序的正确性和性能。

线程池

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

线程池的解释与实现原理

一、线程池的概念

线程池是池化技术的一种典型实现,所谓池化技术就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大提高资源的利用率,提升性能等。线程池,即提前创建好一批线程,并保存在线程池中。当有任务需要执行时,从线程池中选一个线程来执行任务。这样可以避免频繁地创建和销毁线程,从而提高系统的效率和响应速度。

二、线程池的实现

  1. Java中的线程池

    Java中的线程池通过实现ExecutorService接口来提供线程池的功能。Executors类提供了几种创建线程池的方法,如newFixedThreadPool(int Threads)创建固定数目线程的线程池,newCachedThreadPool()创建一个可缓存的线程池,newSingleThreadExecutor()创建一个单线程化的Executor,以及newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池。

  2. 线程池的主要参数

    • corePoolSize:核心线程数量,可以类比为正式员工数量,常驻线程数量。
    • maximumPoolSize:最大的线程数量,公司最多雇佣员工数量,包括常驻和临时线程数量。
    • workQueue:多余任务等待队列,当任务数量超过当前线程处理能力时,任务会放入此队列等待执行。
    • keepAliveTime:非核心线程空闲时间,即外包人员等待任务的时间,如果超过这个时间还没有任务执行,则会被销毁。
    • threadFactory:创建线程的工厂,可以在这里统一设置创建的线程的属性。
    • handler:线程池拒绝策略,当任务数量超过线程池的处理能力(包括核心线程、最大线程和任务队列)时,会执行此策略,默认是抛出异常。
  3. 线程池的工作原理

    线程池的工作流程大致如下:

    • 当有任务提交到线程池时,首先判断当前线程数量是否小于核心线程数量。如果是,则创建新的线程来执行任务。
    • 如果当前线程数量已经达到核心线程数量,但任务队列未满,则将任务放入任务队列等待执行。
    • 如果任务队列已满,但当前线程数量小于最大线程数量,则创建新的非核心线程来执行任务。
    • 如果当前线程数量已经达到最大线程数量,且任务队列已满,则根据拒绝策略处理新提交的任务。
  4. 线程池的execute方法

    execute方法是线程池的核心方法,用于向线程池中添加一个任务。该方法的实现逻辑相对复杂,但大致可以分为以下几个步骤:

    • 首先判断线程池的状态和当前线程数量,以及任务队列的状态。
    • 如果满足条件,则尝试创建新的线程来执行任务。
    • 如果不满足条件,则根据拒绝策略处理新提交的任务。
  5. 添加工作线程

    添加工作线程的过程是通过addWorker方法实现的。该方法首先判断线程池的状态和当前线程数量,然后尝试创建新的线程。创建线程的过程可能会受到多种因素的影响,如核心线程数量、最大线程数量、任务队列状态等。

三、总结

线程池是一种高效的并发处理机制,通过提前创建并保存一批线程,当有任务需要执行时,从线程池中选取一个线程来执行任务。这样可以避免频繁地创建和销毁线程,从而提高系统的效率和响应速度。Java中的线程池通过实现ExecutorService接口来提供线程池的功能,并提供了多种创建线程池的方法和参数配置选项。了解线程池的工作原理和实现机制对于编写高效、可扩展的并发程序具有重要意义。

java中的集合

发表于 2024-10-10 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长

Java中的集合类有哪些?如何分类的?

Java的集合框架中主要包含以下几种数据结构:

  1. List(列表):有序集合,可以包含重复元素。常见的实现类有ArrayList(基于动态数组实现)和LinkedList(基于链表实现)。
  2. Set(集合):无序集合,不包含重复元素。常见的实现类有HashSet(基于哈希表实现)和TreeSet(基于红黑树实现)。
  3. Queue(队列):一种先进先出(FIFO)的数据结构。常见的实现类有LinkedList(也可以作为队列使用)、ArrayDeque(双端队列)和PriorityQueue(优先队列)。
  4. Stack(栈):一种后进先出(LIFO)的数据结构。在Java中,Stack类继承自Vector类,但现在推荐使用Deque接口的实现类(如ArrayDeque)来代替Stack。
  5. Map(映射):存储键值对(K-V对)的数据结构。常见的实现类有HashMap(基于哈希表实现)和TreeMap(基于红黑树实现)。

从继承关系上讲,List、Set和Queue都是Collection接口的子接口,而Collection接口又继承了Iterable接口。这意味着这些集合都是可以遍历的。

从功能上讲:

  • List代表一个有序容器,元素可以重复。
  • Set是无序的(除了TreeSet,它是有序的),并且元素不可重复。
  • Map存储键值对,通过键来访问值。

从实现上讲:

  • List可以通过链表(如LinkedList)或数组(如ArrayList)实现。
  • Queue可以有不同的实现,如优先队列(PriorityQueue)和双端队列(ArrayDeque)。
  • Map的实现包括普通的HashMap和可以排序的TreeMap。

知识扩展:

Collection和Collections有什么区别?

  1. Collection是一个集合接口,提供了对集合对象进行基本操作的通用接口方法。它是List、Set等的父接口。
  2. Collections是一个包装类,包含各种有关集合操作的静态多态方法。它不能实例化,就像一个工具类,服务于Java的Collection框架。

Java中的Collection如何遍历迭代?

  1. 传统的for循环遍历,基于计数器。
  2. 迭代器遍历,使用Iterator接口。
  3. foreach循环遍历,内部也是采用了Iterator的方式实现。
  4. 迭代器遍历,使用Enumeration接口,这是Iterator的“古老版本”。
  5. Stream API,JDK 1.8中新增,使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。

Iterable和Iterator如何使用?

  • Iterator接口代表迭代的方式,包含next和hasNext方法。
  • Iterable接口代表的是是否可以迭代,如果可以迭代,会返回Iterator接口,即返回迭代方式。

为什么不把Iterable和Iterator合成一个使用?

  1. Iterable和Iterator并不是同时出现的,Iterator先于Iterable出现,目的是为了代替Enumeration。
  2. 将“是否可以迭代”和“迭代方式”抽出来,更符合单一职责原则,使得迭代方式可以被多个可迭代的集合复用,更符合面向对象的特点。

Autowired和@Resource的区别?

发表于 2024-09-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

@Autowired和@Resource的区别

口语化答案

好的,面试官,Autowired 和 Resource 都是依赖注入注解。一个是 spring 框架带的,一个是 javaee 框架带的。Autowired 主要是类型注入,Resource 是按照名称注入,名称找不到的话,会按照类型进行注入。Autowired 当存在多个的时候,可以配合Qualifier 来进行使用。一般在实际工作中比较常用 Resource。以上。

题目解析

常考题,面试官喜欢问,其实两者比较好区分,记住一个是类型,一个是名称即可。然后就是提供方的不同。

面试得分点

类型注入,名称注入,指定名称。

题目详细答案

@Autowired和@Resource都是用于依赖注入的注解。

@Autowired

来源:Spring 框架。

注入方式:默认按类型注入。

用法:可以用于字段、构造器、Setter 方法或其他任意方法。

可选性:可以与@Qualifier一起使用,以指定具体的 Bean。

处理机制:Spring 的AutowiredAnnotationBeanPostProcessor处理@Autowired注解。

@Resource

来源:由 Java EE 提供。

注入方式:默认按名称注入,如果按名称找不到,则按类型注入。

用法:可以用于字段或 Setter 方法。

属性:可以指定name和type属性。

处理机制:Spring 的CommonAnnotationBeanPostProcessor处理@Resource注解。

详细比较

  1. 注入方式:

@Autowired:默认按类型注入。如果需要按名称注入,可以结合@Qualifier注解使用。

@Resource:默认按名称注入。如果名称匹配失败,则按类型注入。

  1. 属性:

@Autowired:没有name和type属性,但可以使用@Qualifier指定名称。

@Resource:有name和type属性,可以明确指定要注入的 Bean 名称或类型。

  1. 兼容性:

@Autowired:是 Spring 框架特有的注解。

@Resource:是 Java 标准注解,兼容性更广,适用于任何支持 JSR-250 的容器。

  1. 使用场景:

@Autowired:在 Spring 应用中更为常见,尤其是在需要按类型注入的场景中。

@Resource:在需要兼容标准 Java EE 规范的应用中更为常见,或者在需要明确指定 Bean 名称时使用。

Transactional底层实现?

发表于 2024-09-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

@Transactional底层实现?

  1. 解析**@Transactional**注解:
    • Spring在启动时会扫描所有带有@Transactional注解的类和方法,并解析注解中的属性,生成TransactionAttribute对象。
  2. 创建代理对象:
    • Spring使用AOP创建代理对象,代理对象会拦截对目标方法的调用。
  3. 事务拦截器拦截方法调用:
    • 当代理对象的方法被调用时,TransactionInterceptor会拦截该调用。
  4. 获取事务属性:
    • TransactionInterceptor从TransactionAttributeSource获取当前方法的事务属性。
  5. 事务管理器处理事务:
    • 根据事务属性,TransactionInterceptor会通过TransactionManager开启一个新事务或加入一个现有事务。
  6. 执行目标方法:
    • 事务开始后,TransactionInterceptor会调用目标方法。
  7. 提交或回滚事务:
    • 如果目标方法执行成功,TransactionInterceptor会通过TransactionManager提交事务。
    • 如果目标方法抛出异常,根据事务属性中的回滚规则,TransactionInterceptor会决定是否回滚事务。
  8. 清理事务上下文:
    • 事务提交或回滚后,TransactionSynchronizationManager会清理事务上下文,确保线程的事务状态一致。

相关概念

1. AOP(面向切面编程)

Spring的声明式事务管理主要依靠AOP来实现。AOP允许在方法执行之前和之后添加额外的行为(如事务管理)。

2. 事务管理器(Transaction Manager)

Spring提供了多种事务管理器实现,如:

  • DataSourceTransactionManager:用于JDBC数据源的事务管理。
  • JpaTransactionManager:用于JPA的事务管理。
  • HibernateTransactionManager:用于Hibernate的事务管理。

这些事务管理器负责具体的事务处理逻辑。

3. 事务拦截器(Transaction Interceptor)

TransactionInterceptor是AOP的一个拦截器,用于拦截带有@Transactional注解的方法。它会在方法执行之前和之后执行事务管理逻辑。

4. 事务属性(Transaction Attributes)

@Transactional注解的属性(如传播行为、隔离级别、超时、只读等)会被解析为TransactionAttribute对象。这些属性定义了事务的具体行为。

5. 事务同步管理器(Transaction Synchronization Manager)

TransactionSynchronizationManager用于管理事务的同步状态。它维护了当前线程的事务状态,并负责在事务开始、提交或回滚时调用相应的回调。

6. 事务代理(Transaction Proxy)

Spring使用代理模式来实现声明式事务管理。代理对象会拦截对目标方法的调用,并在调用目标方法之前和之后执行事务管理逻辑。

7. 事务上下文(Transaction Context)

事务上下文包含了当前事务的状态信息,如事务是否已经开始、是否需要回滚等。Spring会在事务开始时创建事务上下文,并在事务结束时清理它。

LinkedList 真的比 ArrayList 添加元素快吗

发表于 2024-09-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌LinkedList 真的比 ArrayList 添加元素快吗?

题目详细答案

性能的比较需要根据具体的操作和数据分布来分析。

尾部插入

ArrayList:在尾部插入元素时,当数据量较小时,由于ArrayList需要频繁扩容,可能会稍显慢一些。但当数据量较大时,ArrayList的扩容策略(通常是当前容量的1.5倍)可以一次提供很多空间,减少了扩容的次数,从而在尾部插入效率上可能超过LinkedList。

LinkedList:在数据量较小时,尾部插入数据较快,因为每次添加只需要新建一个节点并调整指针。但当数据量大时,每次add()操作都会新建一个节点,这可能会增加时间消耗。

首部插入

ArrayList:在首部插入元素时,由于需要将原数组所有元素向后移动一个位置(通过System.arraycopy方法),效率相对较低。

LinkedList:在首部插入元素时,只需要调整首尾节点的指针,时间复杂度为O(1),因此效率较高。

中间插入

ArrayList:在中间插入元素时,同样需要将原数组的元素向后移动以腾出位置,时间复杂度为O(n),其中n为数组长度。但插入位置越往后,需要复制后移的数据越少,效率相对会高一些。

LinkedList:在中间插入元素时,需要遍历链表找到插入位置,然后从两端向中间搜索,index越往中间遍历越久,因此效率相对较低。但在数据量较小时,LinkedList的性能可能会超过ArrayList,因为ArrayList在数据量小时需要频繁扩容。

注意事项

内存消耗:LinkedList的每个节点都需要额外的空间来存储指针信息,因此在内存消耗上可能会比ArrayList稍大。

线程安全:ArrayList和LinkedList都不是线程安全的。如果需要在多线程环境下使用,需要考虑额外的同步措施。

综上所述,LinkedList和ArrayList在添加元素时的性能优劣取决于具体的操作和数据分布。在尾部插入大量数据时,ArrayList可能更优;在首部插入数据时,LinkedList更优;而在中间插入数据时,需要根据数据量的大小和插入位置来具体分析。

在 SQL 查询中,WHERE 和 HAVING 都可以用来过滤数据

发表于 2024-09-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

在 SQL 查询中,WHERE 和 HAVING 都可以用来过滤数据,但它们的工作机制不同:

WHERE:在分组(GROUP BY)之前过滤数据,属于预过滤。
HAVING:在分组(GROUP BY)之后过滤数据,属于后过滤。
将适合放在 WHERE 条件的过滤写在 HAVING 中,可能导致大量不必要的数据参与分组运算,从而增加查询的时间和资源消耗。

示例
原始表
假设有一个销售记录表 sales:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE sales (
id INT PRIMARY KEY,
product_id INT,
sale_date DATE,
amount DECIMAL(10, 2)
);
数据样例:

id product_id sale_date amount
1 101 2024-01-01 100.00
2 102 2024-01-02 200.00
3 101 2024-01-03 300.00
4 103 2024-01-04 400.00
5 101 2024-01-05 500.00
查询需求
我们需要统计每个产品的总销售金额大于 500 的记录。

错误示例:使用 HAVING

SELECT product_id, SUM(amount) AS total_amount
FROM sales
GROUP BY product_id
HAVING product_id = 101 AND total_amount > 500;
问题:

HAVING product_id = 101 是一个简单的等值条件,但它放在 HAVING 中,会导致所有数据都参与 GROUP BY 和 SUM 运算。
这增加了计算负担,尤其是当数据量很大时。
优化:使用 WHERE
sql

1
2
3
4
5
SELECT product_id, SUM(amount) AS total_amount
FROM sales
WHERE product_id = 101
GROUP BY product_id
HAVING total_amount > 500;

改进:

在 WHERE 中预先过滤出 product_id = 101 的数据,减少了参与 GROUP BY 和 SUM 运算的数据量。
查询效率显著提升。
性能对比
在数据量较大时,比如有 1,000,000 条记录:

HAVING 方式:所有记录都会参与分组计算,资源消耗高,时间长。
WHERE 方式:只有满足 product_id = 101 的数据参与分组,减少了无用的计算。
优化后,查询时间可能降低 30%-50% 或更多,具体视数据量和索引情况而定。

SQL 查询执行顺序
SQL 的逻辑执行顺序与书写顺序不同,逻辑执行顺序如下:

FROM:从数据源(表、视图等)加载数据。
ON:应用 JOIN 条件(仅适用于多表查询)。
JOIN:将符合 ON 条件的表进行连接。
WHERE:过滤不符合条件的行(行级过滤)。
GROUP BY:将数据分组。
HAVING:对分组后的结果进行过滤(基于聚合条件)。
SELECT:选择所需的列或表达式。
DISTINCT:去重。
ORDER BY:对结果集排序。
LIMIT:返回指定数量的行。

执行顺序分析
优化后的 SQL 调整了条件位置:

WHERE 先执行:WHERE product_id = 101 在 GROUP BY 之前过滤掉了不符合条件的行,减少了参与分组的记录数量。
减少分组和聚合运算的负担:GROUP BY 和 SUM 只需处理过滤后的数据。
HAVING 用于聚合结果过滤:HAVING total_amount > 500 只对已经分组后的结果应用。
效果:大幅减少了分组和聚合运算的记录数,提升了性能。

区别:WHERE:在分组之前过滤数据,作用于行。
HAVING:在分组之后过滤数据,作用于聚合结果。

mysql索引失效的场景

  • 建立联合索引,id-name-create_time-account_date(入账时间)
    建立了id-name索引,但是查询条件中有create_time 没有使用索引下推,需要建立一个id-name-create_time索引,会走索引下推;
  • 其中order by 两个时间的排序方式不一致。索引失效

解决hash碰撞的方法

发表于 2024-09-16 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

解决hash碰撞的方法?

题目详细答案

链地址法(Chaining)

链地址法是最常见的解决哈希碰撞的方法之一。在这种方法中,每个桶(bucket)包含一个链表(或树结构,Java 8 及以上版本)。当发生哈希碰撞时,新的键值对被添加到相应桶的链表中。

优点:

简单易实现。

动态调整链表长度,不需要提前知道元素数量。

缺点:

当链表长度增加时,查找效率下降。

需要额外的存储空间来存储指针。

1
2
3
4
5
6
7
8
9
10
class HashMapNode<K, V> {
K key;
V value;
HashMapNode<K, V> next;

HashMapNode(K key, V value) {
this.key = key;
this.value = value;
}
}

开放地址法(Open Addressing)

开放地址法不使用链表,而是在哈希表本身寻找空闲位置来存储碰撞的元素。常见的开放地址法有以下几种:

线性探测(Linear Probing)

当发生哈希碰撞时,线性探测法在哈希表中向后依次查找下一个空闲位置。

优点:实现简单,不需要额外的存储空间。

缺点:当哈希表接近满时,查找效率急剧下降(称为“主群集”问题)。

1
2
3
4
5
int hash = key.hashCode() % table.length;
while (table[hash] != null) {
hash = (hash + 1) % table.length;
}
table[hash] = new Entry(key, value);

二次探测(Quadratic Probing)

二次探测法在发生哈希碰撞时,按照平方序列查找空闲位置(如 1, 4, 9, 16, …)。

优点:减少主群集问题。

缺点:实现较复杂,可能会导致二次群集问题。

1
2
3
4
5
6
7
int hash = key.hashCode() % table.length;
int i = 1;
while (table[hash] != null) {
hash = (hash + i * i) % table.length;
i++;
}
table[hash] = new Entry(key, value);

双重散列(Double Hashing)

双重散列法使用两个不同的哈希函数。当第一个哈希函数发生碰撞时,使用第二个哈希函数计算新的索引。

优点:减少群集问题。较好的查找性能。

缺点:实现复杂。需要设计两个有效的哈希函数。

1
2
3
4
5
6
int hash1 = key.hashCode() % table.length;
int hash2 = 1 + (key.hashCode() % (table.length - 1));
while (table[hash1] != null) {
hash1 = (hash1 + hash2) % table.length;
}
table[hash1] = new Entry(key, value);

再哈希法(Rehashing)

再哈希法在发生碰撞时,使用不同的哈希函数重新计算哈希值,直到找到空闲位置。

优点:减少群集问题。

缺点:实现复杂。需要设计多个有效的哈希函数。

分离链接法

在 Java 8 及以上版本中,当链表长度超过一定阈值(默认是 8)时,链表会转换为红黑树,以提高查找效率。

优点:在高冲突情况下性能较好,动态调整链表和树的长度。

缺点:实现复杂,需要额外的存储空间。

其他方法

Cuckoo Hashing:使用两个哈希表和两个哈希函数,如果插入时发生冲突,将原来的元素“踢出”并重新插入到另一个哈希表中。

Hopscotch Hashing:类似于线性探测,但在插入时会调整元素的位置,使得查找路径更短。

链地址法是最常见的解决哈希碰撞的方法,适用于大多数情况。开放地址法在空间利用率上有优势,但在高负载情况下性能可能下降。再哈希法和其他高级方法适用于特定的高性能需求场景。

synchronized

发表于 2024-09-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

synchronized

1. synchronized 的使用方法

  • 同步方法:在方法声明时加上 synchronized 关键字,这样当某个线程调用这个方法时,会先获取到该方法的锁(通常是该方法所属对象的锁),其他线程必须等待锁被释放后才能调用这个方法。
  • 同步代码块:使用 synchronized(对象) 来定义一个同步代码块,这里的对象就是锁对象。当线程进入这个代码块时,会先尝试获取这个对象的锁,获取到锁后才能执行代码块中的代码。

2. synchronized 的实现机制

  • 方法级同步:对于同步方法,JVM 在方法的常量池中添加一个 ACC_SYNCHRONIZED 标志。当线程调用这个方法时,会检查这个标志,如果设置了该标志,则需要先获取到方法的锁(通常是该方法所属对象的监视器锁),然后开始执行方法,方法执行完毕后再释放锁。
  • 代码块级同步:对于同步代码块,JVM 使用 monitorenter 和 monitorexit 两条字节码指令来实现同步。monitorenter 指令用于获取锁,monitorexit 指令用于释放锁。每个对象都有一个监视器锁(monitor),当线程执行到 monitorenter 指令时,会尝试获取对象的监视器锁,如果获取成功,则计数器加一;当线程执行到 monitorexit 指令时,计数器减一。当计数器为 0 时,表示锁已经被释放,其他线程可以获取锁。

3. Monitor(监视器)

  • Monitor 是 Java 中用于实现同步的一种机制,它可以看作是一个特殊的对象,这个对象包含了一个特殊的房间(Entry Set)和一个等待房间(Wait Set)。
  • 当线程尝试获取对象的锁时,它会在 Entry Set 中等待,直到锁被释放。
  • 如果线程在持有锁的过程中因为某些原因被挂起(比如调用了 wait() 方法),那么它会被移到 Wait Set 中,等待其他线程唤醒它(比如调用 notify() 或 notifyAll() 方法)。
  • Monitor 保证了同一时间只有一个线程可以访问被保护的数据和代码。

4. synchronized 的特性

  • 互斥性:同一时间点,只有一个线程可以获得锁,获得锁的线程才能处理被 synchronized 修饰的代码片段。
  • 阻塞性:只有获得锁的线程才能执行被 synchronized 修饰的代码片段,未获得锁的线程只能阻塞,等待锁释放。
  • 可重入性:如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的。这是因为 JVM 会维护一个锁计数器,当同一个线程多次获取锁时,计数器会递增;当释放锁时,计数器会递减,直到计数器为 0 时,锁才会被真正释放。

综上所述,synchronized 通过在方法或代码块级别添加同步机制,利用对象的监视器锁来保证线程安全。它是 Java 中实现线程同步的一种简单而有效的手段。

synchronized 是 Java 中用于保证线程安全的关键字,它通过一系列的机制来保证原子性、可见性和有序性。下面是对这三个方面的详细解释:

synchronized特性

1. 原子性

原子性是指一个操作是不可中断的,即该操作要么全部执行,要么全部不执行。在并发编程中,原子性用于保证某个操作在执行过程中不会被其他线程打断。

在 Java 中,synchronized 通过 monitorenter 和 monitorexit 这两个字节码指令来保证原子性。当一个线程进入 synchronized 修饰的方法或代码块时,它会先尝试获取锁(通过 monitorenter 指令)。如果获取成功,则继续执行后续的代码;如果获取失败(因为锁已被其他线程持有),则该线程会被阻塞,直到锁被释放(通过 monitorexit 指令)为止。

由于 synchronized 保证了同一时间只有一个线程能够持有锁并执行相应的代码,因此它也就保证了这段代码在执行过程中的原子性。即使由于时间片耗尽或其他原因导致线程被中断,只要锁还没有被释放,该线程在下一次获得时间片时仍然会继续执行剩余的代码,直到完成。

2. 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。

Java 内存模型(JMM)规定了所有的变量都存储在主内存中,而每个线程都有自己的工作内存(也称为线程本地存储)。线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存。这可能导致线程1修改了某个变量的值,但线程2由于还没有从主内存中刷新该变量的副本,因此看不到修改后的值。

synchronized 关键字通过确保在进入同步块或同步方法时获取锁,并在退出时释放锁,来实现对变量的可见性保证。具体来说,当一个线程持有锁并执行同步代码块时,它会将自己工作内存中的变量副本更新到主内存中(在写操作时)。当其他线程尝试进入该同步代码块时,它们会先获取锁,并在获取锁后从主内存中读取最新的变量值到自己的工作内存中(在读操作时)。这样,就保证了线程之间对共享变量的可见性。

3. 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。然而,由于硬件和编译器的优化,指令可能会被重排序以提高性能。这种重排序在单线程环境下通常不会改变程序的执行结果,但在多线程环境下可能会导致问题。

Java 提供了 as-if-serial 语义来确保单线程程序的有序性。该语义要求编译器和处理器在优化时不能改变单线程程序的执行结果。然而,在多线程环境下,as-if-serial 语义并不能完全保证有序性。

为了解决这个问题,synchronized 关键字通过确保同一时间只有一个线程能够执行同步代码块来提供有序性保证。由于同步代码块在同一时间只能被一个线程执行,因此可以认为该代码块内的指令是按照它们在代码中出现的顺序执行的。这避免了由于指令重排序而导致的多线程问题。

总结来说,synchronized 通过确保同一时间只有一个线程能够执行同步代码块来提供原子性、可见性和有序性保证。这些特性使得 synchronized 成为 Java 中实现线程安全的一种重要手段。

锁升级

这篇文章的核心内容是介绍了Java中synchronized关键字的锁升级过程,主要包括无锁、偏向锁、轻量级锁和重量级锁四种状态。以下是对这些核心内容的简要概述:

1.无锁状态:

  • 当一个线程第一次访问一个对象的同步块时,JVM会在对象头中设置该线程的Thread ID,并将对象头的状态位设置为“偏向锁”。

2.偏向锁:

  • 当一个synchronized块被线程首次进入时,锁对象会进入偏向模式。
  • 偏向锁模式下,锁会偏向于第一个获取它的线程,JVM会在对象头中记录该线程的ID作为偏向锁的持有者。
  • 如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程ID相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态。
  • 触发条件:首次进入synchronized块时自动开启,假设JVM启动参数没有禁用偏向锁。
  • 注意:在JDK 15中,偏向锁已被废除。

3.轻量级锁:

  • 当有另一个线程尝试获取已被偏向的锁时,偏向锁会被撤销,锁会升级为轻量级锁。
  • 在轻量级锁状态中,JVM为对象头中的Mark Word预留了一部分空间,用于存储指向线程栈中锁记录的指针。
  • 当一个线程尝试获取轻量级锁时,JVM会:
    1. 将对象头中的Mark Word复制到线程栈中的锁记录(Lock Record)。
    2. 尝试通过CAS操作更新对象头的Mark Word。
  • 如果替换成功,则该线程获取锁成功;如果失败,则表示已经有其他线程获取了锁,则该锁状态就会升级到重量级锁状态。
  • 触发条件:当有另一个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。

4.重量级锁:

  • 当轻量级锁的CAS操作失败,即出现了实际的竞争,锁会进一步升级为重量级锁。
  • 当锁状态升级到重量级锁状态时,JVM会将该对象的锁变成一个重量级锁,并在对象头中记录指向等待队列的指针。
  • 如果一个线程想要获取该对象的锁(当前对象已被其他线程锁定时),则需要先进入等待队列,等待该锁被释放。当锁被释放时,JVM会从等待队列中选择一个线程唤醒,并将该线程的状态设置为“就绪”状态,然后等待该线程重新获取该对象的锁。
  • 触发条件:当轻量级锁的CAS操作失败,轻量级锁升级为重量级锁。

Stream

发表于 2024-09-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

Stream

Stream API的深入理解

Stream API是Java 8中引入的一个关键抽象概念,它允许你以声明性方式处理数据集合(包括数组等)。Stream通过一系列的操作(如筛选、转换、聚合等)来构建复杂的数据处理流水线。

特性及优点详解

  1. 无存储:Stream本身不存储数据,它只是数据源的一个视图。这意味着你可以对原始数据集合进行多次Stream操作,而不会改变原始数据。

  2. 为函数式编程而生:Stream API的设计符合函数式编程的范式,它鼓励使用不可变的数据和纯函数(即没有副作用的函数)。这使得代码更加简洁、易于理解和维护。

  3. 惰式执行:Stream上的操作是惰式执行的,这意味着只有在需要结果时才会执行操作。这有助于优化性能,因为它允许Stream API在可能的情况下延迟计算,直到真正需要结果为止。

  4. 可消费性:Stream只能被消费一次,一旦遍历过就会失效。这是为了保持流的不可变性,并防止在多个线程中同时修改流。

关键性操作

  • 流的创建:可以通过集合的stream()或parallelStream()方法、Stream.of()方法或Stream.Builder来创建流。

  • 中间操作:这些操作会返回一个新的流,并允许你进行链式调用。常见的中间操作包括filter()(筛选)、map()(转换)、flatMap()(扁平化映射)、sorted()(排序)、distinct()(去重)等。

  • 最终操作:这些操作会触发流的计算,并返回一个结果或副作用。常见的最终操作包括forEach()(遍历)、collect()(收集到集合)、reduce()(归约)、count()(计数)等。

示例分析

你给出的示例很好地展示了Stream API的使用场景。在这个示例中,你首先通过filter()筛选出非红色的球,然后通过map()将它们融化成随机的三角形,再通过另一个filter()筛选出非小的三角形,最后通过某种方式计算出剩余图形的周长。

这个示例展示了如何使用Stream API来构建一个复杂的数据处理流水线,并通过链式调用中间操作和最终操作来得到最终结果。

实际应用

Stream API在实际应用中非常有用,特别是在处理大型数据集时。它允许你以声明性方式编写代码,而不需要关心底层实现细节。此外,Stream API还支持并行处理,这使得它能够在多核处理器上更有效地利用计算资源。

总之,Stream API是Java 8中一个非常重要的特性,它极大地提高了Java程序员的生产力,并使得编写高效、简洁和易于维护的代码变得更加容易。

Stream

发表于 2024-09-10 | 更新于 2025-06-22 | 分类于 笔记
字数统计 | 阅读时长

Stream的并行流是如何实现的

Stream的并行流是如何实现的?

在Java中,Stream API提供了一种高效且声明式的方式来处理数据集合。并行流(parallel stream)是Stream API中的一个重要特性,它允许开发者利用多核处理器的并行处理能力来提高数据处理的效率。

  1. 获取并行流:

    • 使用Collection接口的parallelStream方法,或者通过Stream接口的parallel方法将顺序流转换为并行流。
  2. 底层实现:

    • 并行流底层使用了Java 7中引入的Fork/Join框架。
    • Fork/Join框架旨在将一个大任务分割(fork)成多个小任务,这些小任务可以并行执行,然后再将这些小任务的结果合并(join)成最终结果。
    • 这种分治策略非常适合处理可以递归分解的大规模计算任务。
  3. 执行方式:

    • 并行流通过并发运行的方式执行流的迭代及操作,从而充分利用多核处理器的性能。

ForkJoinPool和ThreadPoolExecutor的区别

ForkJoinPool和ThreadPoolExecutor都是Java中用于管理线程和并行任务的工具,但它们在实现方式和适用场景上有所不同。

  1. 实现方式:

    • ForkJoinPool:基于工作窃取(Work-Stealing)算法实现的线程池。它适用于处理可以递归分解的大规模计算任务。每个线程都有自己的双端队列(deque)来存储任务,当某个线程的任务队列为空时,它会从其他线程的队列中窃取任务来执行。
    • ThreadPoolExecutor:基于任务队列和线程池的实现。它适用于处理不同类型的任务,包括计算密集型、IO密集型和混合型任务。ThreadPoolExecutor允许更灵活的任务调度和线程管理。
  2. 适用场景:

    • ForkJoinPool:更适合处理可以递归分解的大规模计算任务,如排序、归并、搜索等。
    • ThreadPoolExecutor:适用于更广泛的场景,包括异步执行、定时执行和周期性执行等。
  3. 性能:

    • ForkJoinPool通过工作窃取算法来减少线程之间的空闲时间,提高CPU利用率,但在任务划分不均衡时可能导致线程饥饿。
    • ThreadPoolExecutor的性能取决于任务类型、线程数量和任务队列的配置。

总结

Stream的并行流是Java中处理大规模数据集合的高效工具,它底层使用了Fork/Join框架来实现并行处理。而ForkJoinPool和ThreadPoolExecutor则是Java中用于管理线程和并行任务的两种不同实现方式,它们在实现方式、适用场景和性能上有所不同。开发者应根据具体的应用场景和需求选择合适的工具来优化程序的性能。

微服务教程笔记

发表于 2024-09-01 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长

CouponType 枚举类

public static CouponType convert(String code) {
    return Stream.of(values())
    .filter(bean -> bean.code.equalsIgnoreCase(code))
    .findFirst()
    .orElse(UNKNOWN);
}
  • 使用Long表示金额,金额通常以分为单位,比如100代表100分,比使用Double转 BigDecimal 有效

  • JPA支持一对多、多对多的级联关系,但是过深的级联层级带来的DB层压力可能在洪峰流量下太大,所以尽量减少级联配置,用单表查询取代;如果一个查询需要join多张表,最好的做法是通过重构业务逻辑来简化DB查询的复杂度;

思考:java动态更新枚举类;

  • stream 根据门店分组计算每个门店下商品价格(key=shopId,value=门店商品总价)
    public Map<String,Long> getTotal(List products){
    Map<String, Long> collect = products.stream()
    .collect(Collectors.groupingBy(x -> x.getProductId(),
    Collectors.summingLong(x -> x.getPrice() * x.getCount()))
    );
    return collect;
    }

InnoDB引擎的4大特性?

发表于 2024-02-22 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌InnoDB引擎的4大特性?

插入缓冲(insert buffer)

插入缓冲(Insert Buffer/Change Buffer):提升插入性能,change buffering是insert buffer的加强,insert buffer只针对insert有效,change buffering对insert、delete、update(delete+insert)、purge都有效

只对于非聚集索引(非唯一)的插入和更新有效,对于每一次的插入不是写到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,如果在则直接插入;若不在,则先放到Insert Buffer 中,再按照一定的频率进行合并操作,再写回disk。这样通常能将多个插入合并到一个操作中,目的还是为了减少随机IO带来性能损耗。

二次写(double write)

Doublewrite缓存是位于系统表空间的存储区域,用来缓存InnoDB的数据页从innodb buffer pool中flush之后并写入到数据文件之前,所以当操作系统或者数据库进程在数据页写磁盘的过程中崩溃,Innodb可以在doublewrite缓存中找到数据页的备份而用来执行crash恢复。数据页写入到doublewrite缓存的动作所需要的IO消耗要小于写入到数据文件的消耗,因为此写入操作会以一次大的连续块的方式写入

在应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是double write。

自适应哈希索引(Adaptive Hash index)

Adaptive Hash index属性使得InnoDB更像是内存数据库。该属性通过innodb_adapitve_hash_index开启,也可以通过—skip-innodb_adaptive_hash_index参数关闭;

Innodb存储引擎会监控对表上二级索引的查找,如果发现某二级索引被频繁访问,二级索引成为热数据,建立哈希索引可以带来速度的提升

经常访问的二级索引数据会自动被生成到hash索引里面去(最近连续被访问三次的数据),自适应哈希索引通过缓冲池的B+树构造而来,因此建立的速度很快。哈希(hash)是一种非常快的等值查找方法,在一般情况下这种查找的时间复杂度为O(1),即一般仅需要一次查找就能定位数据。而B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般3-4层,故需要3-4次的查询。

innodb会监控对表上个索引页的查询。如果观察到建立哈希索引可以带来速度提升,则自动建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。AHI有一个要求,就是对这个页的连续访问模式必须是一样的。

预读(read ahead)

InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)为了区分这两种预读的方式,我们可以把线性预读放到以extent为单位,而随机预读放到以extent中的page为单位。线性预读着眼于将下一个extent提前读取到buffer pool中,而随机预读着眼于将当前extent中的剩余的page提前读取到buffer pool中。

(1)线性预读(linear read-ahead)

方式有一个很重要的变量控制是否将下一个extent预读到buffer pool中,通过使用配置参数innodb_read_ahead_threshold,可以控制Innodb执行预读操作的时间。如果一个extent中的被顺序读取的page超过或者等于该参数变量时,Innodb将会异步的将下一个extent读取到buffer pool中,innodb_read_ahead_threshold可以设置为0-64的任何值,默认值为56,值越高,访问模式检查越严格例如,如果将值设置为48,则InnoDB只有在顺序访问当前extent中的48个pages时才触发线性预读请求,将下一个extent读到内存中。如果值为8,InnoDB触发异步预读,即使程序段中只有8页被顺序访问。你可以在MySQL配置文件中设置此参数的值,或者使用SET GLOBAL需要该SUPER权限的命令动态更改该参数。在没有该变量之前,当访问到extent的最后一个page的时候,Innodb会决定是否将下一个extent放入到buffer pool中。

(2)随机预读(randomread-ahead)

随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中,由于随机预读方式给Innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃。要启用此功能,请将配置变量设置innodb_random_read_ahead为ON。

MVCC解决的问题

发表于 2024-02-22 | 更新于 2025-06-22 | 分类于 面试
字数统计 | 阅读时长

👌MVCC解决了什么问题?

1.提高并发性

MVCC 允许多个事务同时执行读写操作,而无需互相阻塞或等待。这极大地提高了数据库的并发处理能力,特别是在高并发环境中。

2.减少锁争用

在传统的锁机制中,读写操作需要加锁,容易导致锁争用和死锁问题。MVCC 通过版本控制,使得读取操作不需要加锁,从而减少了锁争用的频率。

3.提供一致性视图

MVCC 为每个事务提供一个一致性的快照视图,使得事务在执行过程中看到的数据是固定的,不会受到其他并发事务的影响。这简化了应用程序的开发,因为开发者无需担心数据在事务执行过程中发生变化。

4.避免读写冲突

在 MVCC 中,读操作不会阻塞写操作,写操作也不会阻塞读操作。这避免了读写冲突,提高了系统的整体性能和响应速度。

5.减少死锁

由于读操作不需要加锁,MVCC 减少了死锁的可能性。死锁通常发生在多个事务互相等待资源释放的情况下,而 MVCC 的无锁读操作减少了这种情况发生的机会。

6.提高读性能

MVCC 提供了无锁的读操作,读操作直接读取数据的快照版本,不需要等待其他事务完成。这极大地提高了读操作的性能,尤其是在读操作频繁的场景下。

7.实现更高的隔离级别

MVCC 支持实现更高的隔离级别,如快照隔离(Snapshot Isolation),在这种隔离级别下,事务可以看到一个一致的快照视图,同时避免了脏读和不可重复读等问题。

假设有一个银行账户表accounts:

1
2
3
4
5
+----+--------+
| id | balance|
+----+--------+
| 1 | 1000 |
+----+--------+

场景 1:高并发读写

  • 事务 A:读取账户余额
  • 事务 B:更新账户余额

在传统的锁机制下,事务 A 和事务 B 可能会互相阻塞。然而在 MVCC 中:

  1. 事务 A 开始,读取balance = 1000。
  2. 事务 B 开始,更新balance为 1200。
  3. 事务 A 继续读取,仍然看到balance = 1000。
  4. 事务 B 提交后,新的事务可以看到balance = 1200。

这种情况下,事务 A 和事务 B 可以并发执行,互不影响。

场景 2:避免读写冲突

  • 事务 C:读取账户余额
  • 事务 D:更新账户余额

在 MVCC 中:

  1. 事务 C 开始,读取balance = 1000。
  2. 事务 D 开始,更新balance为 1500。
  3. 事务 D 提交后,新的事务可以看到balance = 1500。
  4. 事务 C 继续读取,仍然看到balance = 1000。

MVCC,读写并发,读写不冲突(读不加锁,读写不冲突)

dependencyManagement与dependencies的区别

发表于 2022-07-31 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长
  • dependencyManagement
    • 用于父类的管理,一般写在顶层父类的pom.xml中
    • 只是做声明依赖,不做具体引入,只有在子项目中使用到是才会实现依赖
    • 子类声明了version,就用子类自己的,否则都继承父类的version和scope
  • dependencies
    • 默认被子类全部继承

日期类

发表于 2022-07-31 | 更新于 2025-06-22 | 分类于 java
字数统计 | 阅读时长
1
2
3
4
5
6
7
8
9
10
System.out.println(new Date());
System.out.println(new Date().getTime());
System.out.println(new DateTime());
System.out.println(new LocalDate());
Mon May 30 11:18:29 CST 2022
1653880709973
2022-05-30 11:18:29
2022-05-30
Mysql数据库存储日期dateTime(年月日时分秒)
Mysql数据库存储日期date(年月日)
<i class="fa fa-angle-left"></i>1…101112<i class="fa fa-angle-right"></i>

232 日志
18 分类
28 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini