1. 简单做个自我介绍
  2. 线程池的核心参数

线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。

  1. corePoolSize 线程池核心线程大小

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。

  1. maximumPoolSize 线程池最大线程数量

当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

  1. keepAliveTime 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

  1. unit 空闲线程存活时间单位

keepAliveTime的计量单位

  1. workQueue 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

  1. threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

  1. handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略。

  1. 讲一下线程池的过期时间的参数
  2. 线程池是如何做到的过了一定时间把这个救急线程给销毁的

线程池的实现通常会在内部维护一个线程队列,用于存储可重复使用的线程,当需要执行任务时,线程池会从线程队列中取出一个空闲线程来执行任务。当任务执行完毕后,该线程并不会被销毁,而是重新加入到线程队列中等待下一次任务的到来,以提高线程的复用率和性能。

当线程池中的线程数量超过了一定阈值,而当前没有任务需要执行时,一些线程可能会被闲置,这时线程池就会考虑释放一些多余的线程以释放系统资源和减少线程的管理开销。线程池通常会设置一个线程存活时间的阈值,当一个线程空闲时间超过这个阈值时,就认为这个线程可以被销毁了。

线程池实现销毁线程的方法有多种,一种常见的方式是使用Java中的Thread类的interrupt()方法来中断线程,然后在线程的run()方法中捕获InterruptedException异常,释放线程所占用的资源并终止线程。另一种方式是使用线程池的shutdown()方法来关闭线程池,这个方法会等待所有任务执行完毕后再释放线程池中的线程,并且阻止新的任务提交。一旦所有任务执行完毕,线程池就会销毁所有线程并释放资源。

  1. 拒绝策略都有哪些呀
  • 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。

  • 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。

  • 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。

  • 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。

    • 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
    • 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
  1. 线程池会用到队列是吧,这些队列有什么特点

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

  1. 阻塞队列和非阻塞队列有什么特点,阻塞队列为什么会阻塞

阻塞队列和非阻塞队列是两种不同类型的队列,它们的主要区别在于当队列为空或已满时的行为表现。

  1. 阻塞队列:当队列为空时,从队列中获取元素的操作将被阻塞,直到队列中有可用元素;当队列已满时,向队列中添加元素的操作将被阻塞,直到队列中有空闲空间。
  2. 非阻塞队列:当队列为空时,从队列中获取元素的操作将立即返回空,不会阻塞;当队列已满时,向队列中添加元素的操作将立即返回失败,不会阻塞。

阻塞队列的阻塞行为是针对正在进行队列操作的线程的。也就是说,当一个线程尝试对一个阻塞队列进行插入或删除操作时,如果队列已经满了或者空了,这个线程将会被阻塞,直到队列中有空间可用或者有元素可取出。

因此,阻塞队列的阻塞行为是与正在进行队列操作的线程相关的,而不是针对整个线程池或应用程序。这也是阻塞队列被广泛用于多线程编程中的原因之一,因为它可以有效地控制线程的并发访问,从而避免了一些常见的线程同步问题。

阻塞队列之所以会阻塞,是因为它是一种线程安全的队列,在多线程环境下,可能存在多个线程同时对队列进行读写操作,而这些操作可能会引起竞态条件或线程安全问题。

为了保证线程安全,阻塞队列内部使用了一些同步机制,比如锁、条件变量等。当队列已满或者空时,对队列进行插入或删除操作的线程会被阻塞,直到队列的状态发生改变。

举个例子,如果一个线程尝试向已满的阻塞队列中插入一个元素,由于队列已满,这个线程就会被阻塞,直到有另一个线程从队列中取出了一个元素,腾出了空间,这个线程才能继续往队列中插入元素。

同样地,如果一个线程尝试从空的阻塞队列中取出一个元素,由于队列为空,这个线程也会被阻塞,直到有另一个线程向队列中插入了一个元素,使得队列中有元素可取出。

因此,阻塞队列之所以会阻塞,是因为它在内部使用了同步机制,以保证线程安全,并且在队列状态不满足操作需求时,能够阻塞相关线程,避免出现竞态条件或线程安全问题。

  1. 说一下Java中的lock锁

Java中的Lock是一种基于Java.util.concurrent(JUC)框架提供的锁机制。与synchronized关键字相比,Lock提供了更加灵活和精细的控制,可以更加方便地实现线程同步。

在Java中,Lock接口定义了一组用于获取和释放锁的方法,其中最常用的是lock()和unlock()方法。使用Lock锁的基本流程如下:

  1. 定义一个Lock对象:Lock lock = new ReentrantLock();
  2. 在需要加锁的代码块前调用lock()方法获取锁:lock.lock();
  3. 在代码块执行完毕后调用unlock()方法释放锁:lock.unlock();

Lock锁的使用可以有效避免多线程环境下的竞争条件和线程安全问题,从而提高程序的执行效率和性能。同时,Lock锁相比于synchronized关键字更加灵活,可以实现更加复杂的同步逻辑,例如公平锁、可重入锁、读写锁等。

需要注意的是,使用Lock锁时,必须保证每次获取锁后都能够正确释放锁,否则会导致死锁或其他严重的问题。因此,在使用Lock锁时,建议使用try-finally语句块,确保锁的正确释放。

  1. Reentrantlock和synchronized的用法有什么区别

ReentrantLock 和 synchronized 都可以用于实现多线程环境下的同步和互斥,但它们之间有以下几个区别:

  1. 可重入性:ReentrantLock 具备可重入性,同一个线程可以重复获取同一个锁。而 synchronized 也是可重入的,但只限于同一个线程在嵌套的同步块中获取锁时。
  2. 公平性:ReentrantLock 可以设置公平锁和非公平锁,而 synchronized 只能使用非公平锁。公平锁是指多个线程获取锁的顺序按照线程启动的顺序来,保证锁的获取是公平的。非公平锁则不保证锁的获取是公平的,可能会造成某些线程一直获取不到锁。
  3. 条件变量:ReentrantLock 提供了条件变量 Condition,可以让等待线程在满足特定条件之前一直等待,而 synchronized 没有类似的机制。
  4. 灵活性:ReentrantLock 的锁可以通过 tryLock() 方法进行非阻塞式的加锁操作,可以避免线程因等待锁而被阻塞。而 synchronized 只能进行阻塞式的加锁操作。
  5. 可中断性:ReentrantLock 的锁可以通过 lockInterruptibly() 方法支持可中断的获取锁操作,而 synchronized 不支持线程的中断操作。

总体来说,ReentrantLock 提供了更加灵活和强大的锁定机制,但也更加复杂,需要开发者手动管理锁的获取和释放。在并发量较大且需要更加细粒度控制的场景下,ReentrantLock 的灵活性和可扩展性更加优秀;而在简单的同步和互斥场景下,synchronized 的使用更加简单和方便。

  1. Reentrantlock和synchronized都可以在什么情况下使用,举几个实际业务的场景

ReentrantLock 和 synchronized 都可以用于实现线程安全的代码块,但它们有一些区别。

ReentrantLock 提供了更加灵活的锁定机制,它可以实现公平锁和非公平锁,而 synchronized 只能实现非公平锁。此外,ReentrantLock 还可以通过 tryLock() 方法进行非阻塞式的加锁操作,而 synchronized 只能进行阻塞式的加锁操作。因此,在某些情况下,使用 ReentrantLock 可以更好地控制线程的访问顺序和并发度。

下面是一些实际业务场景,可以使用 ReentrantLock 或 synchronized:

  1. 账户余额更新:当多个线程并发访问同一个银行账户时,需要保证每次更新余额时只有一个线程在进行,可以使用 synchronized 或 ReentrantLock 进行同步操作。

  2. 缓存管理:当多个线程并发访问同一个缓存时,需要保证只有一个线程在进行缓存更新操作,可以使用 ReentrantLock 实现公平锁或非公平锁。

  3. 线程池管理:当多个线程并发访问同一个线程池时,需要保证每次线程池中只有一个线程在进行任务执行,可以使用 ReentrantLock 进行同步操作。

  4. 生产者消费者模型:当多个线程并发访问同一个队列时,需要保证生产者和消费者之间的互斥,可以使用 synchronized 实现阻塞式的同步操作

  5. java中常见的集合类都有哪些

Java中常见的集合类主要包括以下几种:

  1. List接口:List是一个有序的集合,允许存储重复元素,它的实现类有ArrayList、LinkedList和Vector。其中,ArrayList和Vector底层都是基于数组实现的,而LinkedList则是基于双向链表实现的。

  2. Set接口:Set是一个不允许存储重复元素的集合,它的实现类有HashSet、TreeSet和LinkedHashSet。其中,HashSet是基于哈希表实现的,TreeSet是基于红黑树实现的,LinkedHashSet继承自HashSet,底层使用哈希表和双向链表来维护元素的插入顺序。

  3. Queue接口:Queue是一个先进先出(FIFO)的队列,它的实现类有LinkedList、PriorityQueue和ArrayDeque。其中,LinkedList既可以当做List,也可以当做Queue来使用,PriorityQueue是一个基于优先级堆的队列,ArrayDeque是一个基于数组实现的双端队列。

  4. Map接口:Map是一个键值对映射的集合,它的实现类有HashMap、TreeMap、LinkedHashMap和Hashtable。其中,HashMap是基于哈希表实现的,TreeMap是基于红黑树实现的,LinkedHashMap继承自HashMap,底层使用哈希表和双向链表来维护元素的插入顺序,Hashtable和Vector类似,是线程安全的,但不建议使用,因为其性能相对较差。

  5. hashmap它是怎么解决hash冲突的?

在 HashMap 中,当两个不同的 key 通过 hash 函数计算出相同的索引位置时,就会发生冲突。为了解决冲突,HashMap 使用了链表法(链地址法)这种解决方案解决方案。

  1. 链表法

当发生冲突时,HashMap 会在该位置上维护一个链表,将具有相同索引的元素链接在一起。这个链表的数据结构通常为单向链表或双向链表。在插入新元素时,只需要将其插入到该链表的末尾即可。在查找元素时,HashMap 首先通过 hash 函数计算出索引位置,然后遍历该位置的链表,找到对应的元素。

  1. 开放地址法

开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲位置然后把发生冲突的元素存入到这个位置,而在java中,ThreadLocal就用到了线性探测法来解决Hash冲突

  1. 再Hash法

就是通过某个Hash函数计算的key,存在冲突的时候,再用另外一个Hash函数对这个可以进行Hash,一直运算,直到不再产生冲突为止,这种方式会增加计算的一个时间,性能上呢会有一些影响

  1. 建立公共移除区

就是把Hash表分为基本表益处表两个部分,凡是存在冲突的元素,一律放到益处表

需要注意的是,链表法和开放地址法并不是绝对的优劣,它们各自的适用场景也不同。链表法适用于元素较多、链表长度较短的情况,而开放地址法适用于元素较少、冲突较少的情况。

简单总结一下HashMap是使用了c哪些方法来有效解决哈希冲突的:

\1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;

\2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;

\3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

  1. hashmap的扩容是怎么样的

  2. concurrentmap是怎么保证高并发的

ConcurrentMap是Java中的一个接口,它提供了一种线程安全的HashMap实现,能够在高并发环境下提供较好的性能。

ConcurrentMap的高并发能力是通过以下几个方面来实现的:

  1. 分段锁:ConcurrentMap内部将数据分成多个段,每个段上都有一个独立的锁,不同段之间的数据访问是相互独立的,因此多线程可以同时操作不同的段,提高了并发访问效率。

  2. CAS操作:ConcurrentMap中的putIfAbsent()、remove()、replace()等方法都是通过CAS(Compare and Swap)操作实现的,它们能够保证多线程并发访问时数据的一致性。

  3. ConcurrentHashMap的实现:ConcurrentMap的一个实现类是ConcurrentHashMap,它内部采用了一种类似于“分段锁”的机制,称为“分段锁桶”。在ConcurrentHashMap中,数据被分为多个桶(Segment),每个桶都有一个独立的锁来控制对该桶的访问,不同的线程可以同时访问不同的桶,从而提高了并发访问效率。

  4. 减小锁的粒度:ConcurrentMap在实现过程中,尽可能地减小锁的粒度,比如使用读写锁来保证读操作

  5. hashset是怎么实现的

HashSet是Java集合框架中的一种无序集合(Set)数据结构,它的实现方式如下:

  1. HashSet内部使用哈希表(Hash Table)来存储元素。哈希表是一种基于散列函数实现的数据结构,可以快速地插入、删除和查找元素。
  2. 在创建HashSet对象时,可以指定一个初始容量和一个负载因子。初始容量是哈希表中桶(Bucket)的数量,负载因子是哈希表在自动扩容之前可以容纳的最大元素数量与当前容量的比值。如果不指定这些参数,则默认初始容量为16,负载因子为0.75。
  3. 往HashSet中添加元素时,会先通过元素的hashCode()方法计算出元素的哈希码(Hash Code),然后根据哈希码找到元素在哈希表中对应的桶(Bucket)。如果该桶为空,则将元素直接插入到桶中;否则需要遍历桶中的元素,如果找到了与要插入的元素相同的元素,则不做任何操作,否则将元素插入到桶中。
  4. 在查找或者删除元素时,同样需要根据元素的哈希码找到对应的桶,然后遍历桶中的元素进行查找或者删除操作。查找和删除操作的时间复杂度都是O(1)或者O(n),其中n是桶中元素的个数。

需要注意的是,HashSet在遍历元素时,元素的顺序是不确定的,因为哈希表中元素的位置是根据元素的哈希码和桶的数量来计算的,而哈希码的值是随机的。如果需要有序集合,可以使用TreeSet等其他的集合类。另外,如果多个线程同时访问HashSet对象,可能会出现线程安全问题,可以使用Collections.synchronizedSet()方法或者ConcurrentHashMap等线程安全的集合类。

HashSet和HashMap都是Java集合框架中的数据结构,它们之间的关系如下:

  1. HashSet是基于HashMap实现的,它是使用HashMap的key来实现的。具体来说,HashSet中的元素都是HashMap的key,而HashMap中的key-value对中的key就是HashSet中的元素。
  2. HashSet底层的数据结构是一个HashMap对象,HashSet的构造方法也可以接受一个初始容量和负载因子等参数,这些参数也被用来初始化底层的HashMap对象。
  3. HashSet的add()方法实际上是调用了HashMap的put()方法,将元素作为key插入到HashMap中,value则是一个固定的Object对象(可以是任何对象)。
  4. 当我们使用HashSet中的contains()方法来判断某个元素是否存在时,实际上是调用了HashMap的containsKey()方法,检查该元素对应的key是否存在于HashMap中。

因此,可以看出HashSet和HashMap之间有着密切的关系,HashSet实际上就是一个利用HashMap的key来实现的无序集合

  1. treemap是怎么实现的?

TreeMap是Java集合框架中的一种基于红黑树实现的有序映射(SortedMap)数据结构。它的实现方式如下:

  1. TreeMap内部使用红黑树(Red-Black Tree)来存储键值对。红黑树是一种自平衡二叉搜索树,具有良好的平衡性和高效的查找、插入和删除操作。
  2. TreeMap中的所有键值对都按照键的自然排序(natural ordering)或者根据提供的比较器(Comparator)进行排序。因此,TreeMap中的键是有序的。
  3. 在创建TreeMap对象时,可以指定一个比较器(Comparator)来确定键的顺序。如果没有指定比较器,则默认按照键的自然排序(natural ordering)进行排序。
  4. 当往TreeMap中添加键值对时,会按照键的顺序将键值对插入到红黑树中。插入过程中,根据红黑树的特性,需要对树进行平衡操作,以保持树的平衡性。
  5. 在查找或者删除键值对时,同样会按照键的顺序进行操作。查找和删除操作的时间复杂度都是O(logN),其中N为TreeMap中键值对的个数。

需要注意的是,在多线程环境下,TreeMap的操作可能会导致线程安全问题。因为多个线程可能会同时对TreeMap进行添加、删除或者修改操作,这时候就需要对TreeMap进行同步处理,或者使用线程安全的集合类,例如ConcurrentSkipListMap等。

  1. arraylist的扩容是怎么实现的?

ArrayList是Java集合框架中的一种动态数组,它的扩容是通过以下步骤实现的:

  1. 在创建ArrayList对象时,会默认创建一个初始容量为10的数组。
  2. 当往ArrayList中添加元素时,如果当前元素个数已经等于数组的容量,那么就需要对数组进行扩容。
  3. 扩容的方式是创建一个新的数组,将原来数组中的元素复制到新数组中,并且新数组的容量是原来的1.5倍(可以通过源码中的扩容因子DEFAULT_CAPACITY_FACTOR进行设置)。
  4. 在将元素添加到新数组中之前,需要将原来的数组置空,这样就可以使得原来的数组成为垃圾对象,方便Java垃圾回收器进行回收。
  5. 最后,将新元素添加到新数组中,并且更新当前元素个数以及数组引用。

需要注意的是,在多线程环境下,ArrayList的扩容可能会导致线程安全问题。因为多个线程可能会同时对ArrayList进行添加或删除操作,这时候就需要对ArrayList进行同步处理,或者使用线程安全的集合类,例如Vector或者CopyOnWriteArrayList等。

  1. cas听说过吗?它在Java中有哪些工具类用用到cas呢

CAS是指“Compare and Swap”(比较和交换),是一种用于多线程编程中实现同步操作的算法。在Java中,CAS机制通常使用Atomic类的相关方法来实现,主要包括以下几个类:

  1. AtomicBoolean:提供原子性的boolean值操作。
  2. AtomicInteger:提供原子性的int值操作。
  3. AtomicLong:提供原子性的long值操作。
  4. AtomicReference:提供原子性的引用类型操作。

这些类的方法通常使用CAS机制来实现,以保证多线程情况下对共享变量的安全访问和修改。在Java的并发编程中,CAS机制是一个非常重要的技术,能够有效地避免线程安全问题,提高程序的并发性能。

  1. 说一下Java中的垃圾回收,它的full GC 老年代的回收有几种呀?有几种垃圾回收器呀?

Java中的垃圾回收是通过虚拟机自动完成的,它可以有效地回收程序中不再使用的对象,避免内存泄漏和内存溢出的问题。Java中的垃圾回收分为两种:新生代垃圾回收和老年代垃圾回收。

新生代垃圾回收通常采用的是复制算法,将新生代分为一块较大的Eden空间和两块较小的Survivor空间(通常为S0和S1)。当Eden空间填满时,会进行一次Minor GC,将存活的对象复制到其中一个Survivor空间中,然后清空Eden空间和另一个Survivor空间。当该Survivor空间也填满时,会再次进行一次Minor GC,将该Survivor空间中存活的对象复制到另一个Survivor空间中,同时清空该Survivor空间。这样来回复制几次后,仍然存活的对象就会被复制到老年代中。

老年代垃圾回收通常采用的是标记-清除或标记-整理算法,其中标记-清除算法会将不再使用的对象标记为垃圾,然后进行回收;而标记-整理算法则会将存活的对象移动到一端,然后清除另一端的所有垃圾对象。

当进行Full GC时,会同时回收新生代和老年代中的垃圾对象。老年代垃圾回收有两种方式:

  1. 标记-清除算法:标记不再使用的对象,然后清除它们。这种算法的问题在于,清除后会留下一些不连续的内存碎片,当需要分配大对象时,可能会无法找到连续的内存空间,从而触发一次Full GC。
  2. 标记-整理算法:标记不再使用的对象,然后将存活的对象向一端移动,清除另一端的所有垃圾对象,从而留出一段连续的内存空间。这种算法可以避免内存碎片的问题,但需要移动存活对象,造成一定的性能损失。

Java中有几种垃圾回收器,主要包括:

  1. Serial GC:单线程执行的垃圾回收器,适用于小内存环境和单线程应用。
  2. Parallel GC:多线程执行的垃圾回收器,适用于大内存环境和多核CPU应用。
  3. CMS GC:Concurrent Mark Sweep GC,
  4. G1 GC:Garbage First GC,采用标记-整理算法和分代收集思想,将堆内存划分为多个大小相等的Region,并按照垃圾密度高低,优先清理垃圾密度高的Region,适用于大内存环境和对响应时间有要求的应用。
  5. ZGC:Z Garbage Collector,是JDK 11引入的新的垃圾回收器,采用可并发的标记-整理算法,适用于大内存环境和对响应时间有极高要求的应用。
  6. Shenandoah GC:也是JDK 11引入的新的垃圾回收器,采用可并发的标记-整理算法和分代收集思想,适用于大内存环境和对响应时间有极高要求的应用。

不同的垃圾回收器适用于不同的场景和需求,开发者可以根据应用场景的内存大小、响应时间需求、系统配置等因素,选择合适的垃圾回收器来提高应用性能和稳定性。

  1. 为什么需要进行分代回收呢?

分代回收是一种将堆内存分为不同代(Generation)的垃圾回收方式,不同代之间采用不同的回收策略和频率,是现代垃圾回收器中常用的优化手段。

其主要原理是基于以下两个假设:

  1. 大部分对象的生命周期很短,很快就会被回收。
  2. 存活时间较长的对象往往会存活更久,即所谓的“年老代”。

基于以上假设,将堆内存分为不同代,可以针对不同对象的生命周期进行不同的回收策略和频率的优化,以提高垃圾回收的效率和性能。

具体来说,将堆内存分为年轻代(Young Generation)和年老代(Old Generation)两部分:

  1. 年轻代:是存放生命周期短暂的对象的区域,通常采用复制算法进行垃圾回收,即将存活的对象复制到另一块区域,然后清空当前区域的所有对象,从而达到快速回收垃圾的目的。
  2. 年老代:是存放生命周期较长的对象的区域,由于其中的对象寿命较长,因此采用标记-清除算法和标记-整理算法进行垃圾回收,以避免频繁地进行垃圾复制和移动,影响应用性能。

通过分代回收,可以针对不同对象的生命周期采用不同的回收策略和频率,从而提高垃圾回收的效率和性能,减少垃圾回收的影响,保证应用的稳定性和响应性能

  1. 老年代一般都存什么对象

1.大对象(字符串与数组),即超过了设定的值的对象,直接在老年代中分配。

2.长期存活的对象进入老年代。

  1. 栈中都存放什么东西

程序计数器、虚拟机栈、本地方法栈

  1. 如果老年代的垃圾回收特别频繁,那么这个问题应该怎么排查,用到了什么工具进行排查

老年代的垃圾回收特别频繁可能会导致应用程序的性能下降,甚至出现停顿等问题。排查这个问题可以遵循以下步骤:

  1. 分析GC日志,找出频繁GC的原因,如内存泄漏、堆空间不足等等。在日志中可以看到每次GC的原因、持续时间、频率等信息。
  2. 使用性能分析工具,如JProfiler、VisualVM等,观察应用程序在运行时的内存分配、对象创建、方法调用等情况,分析哪些代码或对象在造成内存问题。
  3. 使用堆分析工具,如Eclipse Memory Analyzer、MAT等,分析堆中的对象,找出占用内存较多的对象或类,以及它们的引用链,确定是否存在内存泄漏。
  4. 根据分析结果采取相应的措施,如调整堆大小、优化代码、修复内存泄漏等。

在排查过程中,可以使用JVM参数来打印GC日志,例如使用-Xloggc参数打印GC日志,使用-XX:+PrintGCDetails参数打印GC的详细信息。同时还可以使用JVM监控工具,如jstat、jmap等,获取JVM运行时的状态信息。

  1. 举一个例子:写一个内存泄漏的代码

https://zhuanlan.zhihu.com/p/368830445

不再会被使用的对象的内存不能被回收,这就是内存泄漏

/*
这段代码创建了一个静态的ArrayList对象,
并在一个无限循环中不断地向其中添加新的Object对象。
由于ArrayList对象是静态的,它的生命周期与程序的生命周期相同。
由于程序不断向ArrayList中添加新对象,
它会占用越来越多的内存。
由于这个ArrayList对象中的对象不会被释放,
这可能会导致内存泄漏问题。
*/
public class MemoryLeakDemo {
private static List<Object> list = new ArrayList<Object>();

public static void main(String[] args) throws InterruptedException {
while (true) {
Object obj = new Object();
list.add(obj);
}
}
}
/*
这段代码创建了一个HashSet对象,
并在一个循环中不断地向其中添加新的String对象。
由于HashSet对象中的对象不会被释放,这可能会导致内存泄漏问题。由于程序循环次数很大,
所以这个内存泄漏问题可能会导致程序占用越来越多的内存,并最终导致程序崩溃。
*/
public class MemoryLeakDemo {
private Set<String> set = new HashSet<>();

public void add(String str) {
set.add(str);
}

public static void main(String[] args) throws InterruptedException {
MemoryLeakDemo demo = new MemoryLeakDemo();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = String.valueOf(i);
demo.add(str);
}
}
}
  1. MySQL事务隔离级别都有什么
  • READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能 会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。
  • REPEATABLE-READ(可重复读) : 对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。MySQL默认使用的就是这种
  • SERIALIZABLE(可串⾏化) : 最⾼的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次 逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读 以及幻读
  1. 可重复读解决了什么问题
  2. 什么情况下可以出现这个不可重复读的现象

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

  1. 可重复读解决了幻读的问题吗

没有,是可串行化解决了这个问题

  1. 幻读是什么现象啊

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

  1. 行锁和间隙锁都是什么情况下可以产生
  2. 如果我写一个sql,我怎么看这个sql的性能好坏

要评估SQL语句的性能好坏,可以通过以下几种方式进行:

  1. 解析执行计划:执行计划是数据库为了执行SQL语句而创建的执行方案,它包含了SQL语句的各个操作所需要的资源、执行的顺序、使用的索引等等。可以通过数据库提供的解析执行计划的工具来查看SQL语句的执行计划,从而评估SQL语句的性能瓶颈。
  2. 评估索引的使用情况:索引是用来加速SQL语句的执行的,它可以减少全表扫描的次数,提高SQL语句的查询速度。可以通过查看执行计划来评估SQL语句是否使用了索引,如果没有使用索引,可能会导致SQL语句的性能变慢。
  3. 评估SQL语句的效率:可以通过执行SQL语句的时间来评估SQL语句的效率,如果执行时间比较长,可能是SQL语句的性能有问题。
  4. 评估SQL语句的复杂度:SQL语句的复杂度也会影响SQL语句的性能,可以通过查看SQL语句的语法结构、查询条件的复杂度等来评估SQL语句的复杂度。

总之,评估SQL语句的性能好坏需要综合考虑SQL语句的执行计划、索引的使用情况、SQL语句的执行时间、复杂度等多个方面。需要根据具体的场景进行分析和优化,以达到最优的SQL性能

  1. 怎么看这个sql是否是否用了没索引

要查看SQL是否使用了索引,可以通过执行计划来进行查看。在大多数关系型数据库中,执行计划可以通过 EXPLAIN

例如,在MySQL中,可以通过以下命令来获取SQL的执行计划:

EXPLAIN SELECT * FROM table_name WHERE column_name = ‘value’;

  1. 索引有几种结构

Hash索引,B+树索引

  1. spring cloud是一个什么样的框架

Spring Cloud是基于Spring Boot的微服务框架,它提供了一系列开发分布式系统的工具和服务,包括服务发现、配置中心、负载均衡、断路器、网关等。Spring Cloud基于Spring框架,可以与Spring Boot应用无缝集成,提供了快速开发和部署微服务应用的能力。

Spring Cloud的核心组件包括:

  1. 服务注册与发现(Eureka、Consul、Zookeeper等):提供了服务注册、服务发现和负载均衡的功能。
  2. 配置中心(Spring Cloud Config):提供了集中管理应用配置信息的能力,支持不同环境下的配置管理。
  3. 负载均衡(Ribbon):提供了客户端负载均衡的功能,可以根据配置的规则,自动将请求分发到不同的服务实例中。
  4. 断路器(Hystrix):提供了服务容错和熔断的能力,可以避免服务雪崩的风险。
  5. 网关(Zuul、Spring Cloud Gateway):提供了路由、过滤和安全控制等功能,可以作为整个系统的入口。

Spring Cloud还提供了一系列其他的功能,如分布式跟踪、分布式锁、分布式消息等,可以帮助开发者构建高可用、高性能、可扩展的分布式系统。

总之,Spring Cloud是一个非常强大、灵活的微服务框架,它能够大大简化分布式系统的开发和部署工作,让开发者可以更加专注于业务逻辑的实现。

  1. 一个微服务是怎么调用另一个微服务的

在微服务架构中,微服务之间通常使用HTTP或者RPC协议进行通信。一个微服务要调用另一个微服务,通常需要以下步骤:

  1. 服务发现:调用方需要知道被调用方的地址和端口,可以通过服务发现来获取被调用方的实例列表。常见的服务发现框架有Consul、Eureka、Zookeeper等。
  2. 负载均衡:如果被调用方有多个实例,调用方需要选择其中一个实例进行调用。负载均衡可以根据一定的策略,从多个实例中选择一个最优的实例进行调用。常见的负载均衡框架有Ribbon等。
  3. 调用:通过HTTP或者RPC协议,向被调用方发送请求,获取响应结果。在调用过程中,需要注意请求参数和响应格式的约定,以及异常处理和重试等问题。
  4. 链路追踪:在微服务架构中,一个请求往往会经过多个微服务,需要对整个请求的处理过程进行追踪和监控,以便及时发现问题。常见的链路追踪框架有Zipkin、SkyWalking等。

需要注意的是,微服务之间的调用需要考虑网络延迟、请求重试、服务熔断、服务降级等问题,以确保系统的稳定性和可靠性。同时,需要注意微服务之间的依赖关系,避免出现服务之间的循环依赖和版本不兼容等问题。

OpenFeign不是RPC框架,而是一个基于HTTP协议的声明式Web服务客户端

RPC(Remote Procedure Call)是一种远程过程调用协议,它可以让不同的进程或者服务之间通过网络进行通信和交互。RPC框架通常会将不同语言或者不同平台的数据类型进行序列化和反序列化,以便进行跨语言或者跨平台的通信。

而OpenFeign是一个轻量级的HTTP客户端框架,它可以帮助开发者快速定义和调用HTTP API接口。OpenFeign采用了声明式的接口定义方式,开发者只需要定义接口的方法和参数,而不需要关注底层的HTTP请求和响应细节。OpenFeign内部使用了Java标准的HTTP库和注解处理器,可以轻松实现负载均衡、服务发现和断路器等功能。

虽然OpenFeign不是RPC框架,但是它在微服务架构中扮演了类似RPC的角色,可以方便地实现微服务之间的通信和交互。

  1. openfeign是怎么实现远程调用的,具体步骤是什么,怎么调用另一个微服务的

OpenFeign底层是通过动态代理技术和HTTP客户端来实现远程调用的。具体步骤如下:

  1. 在OpenFeign中,定义一个接口,接口中包含需要远程调用的方法。
  2. OpenFeign会动态生成一个代理类,代理类实现了定义的接口。在代理类中,OpenFeign会将接口方法的信息(如方法名、参数、注解等)转化为HTTP请求,然后使用HTTP客户端发送请求到远程服务。
  3. OpenFeign通过集成HTTP客户端(如OkHttp或HttpClient)来发送HTTP请求,并接收响应。在发送HTTP请求之前,OpenFeign会根据接口方法中的注解信息,构造请求参数,包括请求头、路径参数、请求体等。
  4. 当HTTP客户端接收到响应后,OpenFeign会根据接口方法的返回值类型,将响应反序列化为Java对象,并返回给代理类。代理类再将Java对象返回给调用方。

如果要调用另一个微服务,需要在OpenFeign客户端中配置远程服务的URL地址。在配置URL地址之前,需要先在服务注册中心中注册被调用方的微服务,然后通过服务名来查找对应的URL地址。OpenFeign客户端可以通过配置文件、代码等方式来配置服务名和URL地址。在OpenFeign客户端中,也可以配置负载均衡策略、超时时间等参数。最后,在调用方的代码中,可以直接使用定义的接口来调用远程服务。

  1. 负载均衡和找服务都是在nacos里面做的吗

是的,Nacos提供了服务注册与发现的功能,同时也支持服务的负载均衡。服务提供者在启动时会向Nacos注册自己的服务实例信息(包括IP地址、端口、服务名等),服务消费者则可以通过Nacos查询服务提供者的地址信息,并通过负载均衡算法选择其中一个服务实例来调用。

Nacos支持多种负载均衡算法,如随机算法、轮询算法、加权随机算法、加权轮询算法等。服务消费者可以通过配置文件或代码来选择所使用的负载均衡算法。

此外,Nacos还提供了服务的健康检查功能,当服务实例不可用时,Nacos会自动剔除该实例,并通知服务消费者更新可用的服务列表。这些功能使得服务的发现和调用变得更加简单和可靠。

  1. Hystrix是做什么的呢

Hystrix是一款Netflix开源的熔断器(circuit breaker)框架,用于处理分布式系统中的服务降级、故障恢复和延迟容错等问题。

在分布式系统中,服务之间的调用是通过网络进行的。网络通信的不可靠性、服务之间的依赖关系复杂性、系统负载变化等因素都可能导致服务调用的失败或变慢,进而导致整个系统的崩溃。这时候就需要一种机制来处理这些问题,Hystrix就是为此而生。

Hystrix通过在每个服务之间添加熔断器的方式来处理服务调用失败的情况。熔断器是一种自动化的机制,可以在服务调用失败的情况下,立即返回一个错误响应,而不是一直等待响应超时。同时,熔断器还可以记录服务调用的失败率和延迟等指标,并根据这些指标来自动打开或关闭熔断器。

Hystrix还提供了以下功能:

  1. 服务降级:当服务出现故障或者超时时,可以通过降级策略返回一个预设的默认值或者Fallback方法,以保证系统的可用性。
  2. 隔离策略:通过线程池隔离和信号量隔离两种方式,对不同的服务进行隔离,防止服务之间的故障互相影响。
  3. 实时监控:Hystrix可以将服务调用的成功率、响应时间等指标实时上报到仪表盘中,方便运维人员进行实时监控和调整。

总之,Hystrix通过熔断器、服务降级、隔离策略和实时监控等多种机制,为分布式系统提供了一个高效可靠的服务调用框架,可以有效避免系统的崩溃和服务调用失败的问题。

  1. 你了解Sentinel的实现原理吗

是的,我可以简要介绍Sentinel的实现原理。

Sentinel是一款开源的分布式系统的流量控制、熔断降级框架。它的实现原理主要包括三个方面:资源定义、流量控制、熔断降级。

  1. 资源定义

Sentinel会将所有的系统资源都抽象为一个个独立的资源点,每个资源点都会分配一个唯一的资源名称,并通过配置中心(如Nacos)实现动态化管理。例如,对于一个HTTP请求,可以抽象出资源点为URL接口,对于一个Dubbo服务调用,可以抽象出资源点为服务名+方法名。

  1. 流量控制

Sentinel通过实时统计资源的流量、QPS、RT等指标,并基于规则和策略进行流量控制。Sentinel提供了三种流控模式,分别为直接、关联和链路,分别对应了基于单个资源点、关联资源点和链路的流量控制。通过流控模式,Sentinel能够灵活地对系统流量进行精确控制,以保证系统稳定运行。

  1. 熔断降级

在流量超出阈值或资源发生异常时,Sentinel会通过熔断降级策略自动触发熔断降级,避免系统崩溃。Sentinel提供了多种熔断降级策略,包括慢调用比例、异常比例、异常数等,可以根据不同的场景进行选择。同时,Sentinel还支持自适应熔断策略,根据当前系统的负载情况动态调整熔断阈值,以保证系统的稳定性和可靠性。

总之,Sentinel通过资源定义、流量控制和熔断降级等手段,实现了对分布式系统的流量控制和熔断降级功能。

  1. springboot和spring MVC有什么区别
  2. springboot的自动配置是怎么实现的

Spring Boot的自动配置(Auto-configuration)是通过在类路径下的META-INF/spring.factories文件中定义的自动配置类(auto-configuration classes)实现的。

这些自动配置类使用了Spring的条件注解(Conditional Annotation),当满足一定的条件时才会被加载和执行。这些条件可以是Classpath中是否存在某个类、是否存在某个Bean、是否存在某个配置文件、是否存在某个环境变量等等。

Spring Boot提供了一组内置的条件注解(@ConditionalOnClass、@ConditionalOnBean、@ConditionalOnProperty、@ConditionalOnMissingBean等),也可以自定义条件注解。

当应用程序启动时,Spring Boot会根据当前的Classpath、环境变量等情况,自动加载合适的自动配置类,并将它们应用于应用程序中。这些自动配置类可以自动配置数据源、缓存、日志、安全等方面的功能,从而简化了应用程序的开发和部署。

如果需要修改或禁用某个自动配置类,可以通过在应用程序中定义一个@Configuration类,并在其中使用@ImportResource或@EnableAutoConfiguration注解,或者在application.properties或application.yml中设置属性值,来控制自动配置的行为。

总的来说,Spring Boot的自动配置通过条件注解和自动配置类,根据当前环境和需要自动加载、配置一些常用的组件和功能,为开发者提供了极大的便利。

  1. 你自己有实现过一个starter吗

没有

spring中有一个事务注解,@Transactional注解是怎么保证事务的呀

在Spring框架中,@Transactional注解用于声明一个方法是一个事务性方法。当一个被@Transaction注解标注的方法被调用时,Spring框架会自动为这个方法创建一个事务,并在方法执行过程中管理这个事务的提交和回滚等操作,以保证事务的一致性、隔离性、持久性和原子性。

具体来说,@Transactional注解可以应用在类或者方法上,如果应用在类上,则表示该类的所有方法都是事务性方法。在方法上应用该注解时,可以通过指定属性来控制事务的行为,比如事务的隔离级别、事务的传播行为、事务的超时时间等。

Spring框架实现事务的原理是基于AOP(面向切面编程)技术实现的。具体来说,当Spring框架检测到一个方法被@Transaction注解标注时,它会通过AOP动态代理机制,在方法调用前后分别插入事务的开启和提交/回滚等操作,从而实现对事务的控制和管理。

总之,通过@Transaction注解,Spring框架可以在底层为开发者处理许多复杂的事务控制和管理的细节,让开发者专注于业务逻辑的实现,提高了开发效率和代码的可读性

  1. 如果使用@Transactional注解但是事务没生效,这是什么情况/为什么呢

如果使用@Transactional注解但是事务没有生效,可能是由于以下几个原因:

  1. 注解使用错误:可能是注解被错误地放置在了错误的位置,或者注解的属性配置不正确。例如,注解被放置在了一个没有被Spring管理的普通Java对象上,或者注解的隔离级别、传播行为等属性配置不正确。
  2. Spring AOP配置问题:事务是通过Spring AOP技术实现的,因此可能是Spring AOP的配置问题导致事务无法生效。例如,可能是Spring配置文件中没有正确地配置AOP的切入点表达式,或者AOP的配置存在语法错误。
  3. 数据库连接池配置问题:如果数据源或数据库连接池配置不正确,可能会导致事务无法生效。例如,连接池配置不足,导致无法获取数据库连接;或者连接池中的连接因为其他原因而被关闭,导致事务无法提交或回滚。
  4. 方法内部异常:如果方法内部发生了异常,可能会导致事务无法正常提交或回滚。例如,方法内部出现了未处理的运行时异常,导致事务没有被正常地提交或回滚。
  5. 不同类中的方法调用:如果@Transactional注解标注的方法是被其他类中的方法调用的,可能会因为Spring AOP的限制而无法生效。例如,如果事务注解标注在了一个私有方法上,由于Spring AOP只能对public方法进行代理,所以事务将不会被启用。
  6. 没有在Spring配置类上添加@EnableTransactionManagement注解,或者在XML配置文件中没有开启事务管理器的配置,那么使用@Transactional注解将无法启用事务。

总之,如果使用@Transactional注解但是事务没有生效,需要进行详细的排查和调试,找出原因并进行修复。可以通过查看日志、打开调试模式、检查Spring配置文件等方式来进行排查。

  1. spring中bean是如何解决循环依赖的?

在Spring容器中,如果存在循环依赖,也就是两个或多个Bean之间相互依赖,Spring采用了”提前暴露半成品Bean”的策略来解决这个问题。

具体来说,Spring容器在初始化Bean的过程中,会在创建Bean实例的过程中,对属性进行注入,如果发现某个属性的类型依赖了另一个Bean,那么Spring就会先创建被依赖的Bean,然后注入到依赖方中。但是,如果依赖方也依赖了该Bean,则这时就会出现循环依赖的情况。

为了解决这种循环依赖的问题,Spring采用了以下两种方式:

  1. 提前暴露半成品Bean
    在创建Bean时,当Spring检测到一个Bean的属性依赖了另一个尚未创建完成的Bean时,它会将该Bean标记为“正在创建中”,并将一个提前暴露的Bean实例(即尚未完成创建的Bean实例)提供给当前Bean,以便该Bean可以完成属性的注入,然后该Bean就可以被创建出来了。
  2. 通过代理实现
    当循环依赖的两个Bean中至少有一个是prototype作用域时,Spring无法通过提前暴露半成品Bean的方式解决循环依赖的问题,此时,Spring会通过代理的方式来解决循环依赖问题,即创建一个代理对象作为中间对象,完成Bean的创建和属性注入,然后再将代理对象替换为真正的Bean对象。

需要注意的是,Spring只能解决单例作用域的Bean之间的循环依赖,而对于prototype作用域的Bean,Spring无法使用提前暴露半成品Bean的方式解决循环依赖问题,需要采用代理的方式。

  1. Spring中Bean是怎么自动注入的?

Spring中的自动注入是通过依赖注入(Dependency Injection)实现的。在Spring容器中,当一个bean需要依赖另一个bean时,Spring会自动在容器中查找匹配的bean,并将其注入到需要的bean中。

Spring支持多种自动注入的方式,包括:

  1. 构造器注入(Constructor Injection):在bean的构造器中注入依赖。
  2. 属性注入(Property Injection):通过setter方法或直接注入属性的方式注入依赖。
  3. 接口注入(Interface Injection):在bean实现的接口中定义注入方法。
  4. 自动装配(Autowiring):Spring会根据自动装配的规则自动注入依赖。

在进行自动注入时,Spring会根据类型、名称等条件来查找匹配的bean。如果有多个符合条件的bean,Spring会根据指定的优先级或默认规则选择其中一个。

需要注意的是,在使用自动注入时,必须保证Spring容器中只有一个符合条件的bean,否则会抛出异常。可以通过使用@Primary注解或@Qualifier注解来指定注入的bean。

  1. Redis都能干什么用呀

分布式锁 : 通过 Redis 来做分布式锁是⼀种⽐较常⻅的⽅式。通常情况下,我们都是基于 Redisson 来实现分布式锁。

限流 :⼀般是通过 Redis + Lua 脚本的⽅式来实现限流。

消息队列 :Redis ⾃带的 list 数据结构可以作为⼀个简单的队列使⽤。Redis 5.0 中增加的 Stream 类型的数据结构更加适合⽤来做消息队列。它⽐较类似于 Kafka,有主题和消费组的概念,⽀持消息持久化以及 ACK 机制。

复杂业务场景 :通过 Redis 以及 Redis 扩展(⽐如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景⽐如:通过 bitmap 统计活跃⽤户、通过 sorted set 维护排⾏榜。

  1. 怎么保证MQ可以顺序消费呢?

多个有序的消息可能是一个大消息的拆分吧

话术:

其实RabbitMQ是队列存储,天然具备先进先出的特点,只要消息的发送是有序的,那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时,可能出现消息轮询投递给消费者的情况,而消费者的处理顺序就无法保证了。

因此,要保证消息的有序性,需要做的下面几点:

- 保证消息发送的有序性

- 保证一组有序的消息都发送到同一个队列

- 保证一个队列只包含一个消费者

  1. 如果有一个事务,这个事务中首先去操作了数据库,之后给mq发送了一条消息,之后又去操作了数据库,但是最后这个操作报错出异常了,就会导致事务的回滚,mq是如何做到事务的回滚呢

当涉及到事务的时候,消息队列(MQ)会和数据库一样参与到事务的处理中,以保证事务的一致性。如果在事务中向 MQ 发送消息,消息不会被立即发送,而是被缓存到事务日志中。只有当事务成功提交后,MQ 才会真正发送消息。

如果在事务提交之前,出现了异常导致事务被回滚,那么 MQ 中缓存的消息也会被回滚,即不会被发送出去。这是因为 MQ 会和数据库一样实现“分布式事务”机制,将 MQ 的操作纳入到事务的处理流程中。因此,如果事务回滚,MQ 中的消息也会被回滚,以保证事务的一致性。

需要注意的是,要想让 MQ 参与到事务中,需要使用支持事务的 MQ 客户端,并通过设置事务边界来控制 MQ 操作的范围。同时,要保证 MQ 的事务日志和数据库的事务日志同步提交,以确保 MQ 的操作和数据库的操作在事务上是一致的。

  1. 编程题 用Java代码写:一个数组举例为[1,2,1,3,5,6,5,7,6,1]输出没有重复出现的元素[2,3,7]
import java.util.*;

public class Main {
public static void main(String[] args) {
int[] arr = {1,2,1,3,5,6,5,7,6,1};
List<Integer> resultList = new ArrayList<>();

// 创建一个哈希集合来存储元素和它们的出现次数
Map<Integer, Integer> map = new HashMap<>();
for (int i : arr) {
map.put(i, map.getOrDefault(i, 0) + 1);
}

// 遍历哈希集合,将出现次数为 1 的元素添加到结果列表中
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() == 1) {
resultList.add(entry.getKey());
}
}

// 将结果列表转换为数组并输出
int[] resultArr = new int[resultList.size()];
for (int i = 0; i < resultList.size(); i++) {
resultArr[i] = resultList.get(i);
}
System.out.println(Arrays.toString(resultArr));
}
}