4.多线程

5.1 多线程的几种实现方式,什么是线程安全。

  多线程可以通过继承Thread类、实现Runnable接口、实现Callable接口,以及线程池等方式创建多线程。

  线程安全是指多个线程访问类,不会因为多线程同时操作类导致数据不一致或其他错误行为。可通过同步代码块(synchronizes block、synchronized method)、Lock、原子类、JUC工具包解决

5.2 volatile的原理,作用,能代替锁么。

  volatile的作用是保证线程对主内存的可见性、以及防止指令重排。它的原理是通过内存屏障来防止指令之间的重排、保证代码执行的顺序,防止数据出错。

5.3 画一个线程的生命周期状态图。

5.4 sleep和wait的区别。

  • sleep没有释放锁、wait会释放锁
  • sleep是线程暂停执行,wait用于线程交互/通信
  • sleep方法执行后,线程自动苏醒;wait方法调用后,线程需要另一个线程调用notify或notifyAll方法才能苏醒
  • sleep是Thread类的本地方法、wait是Object的本地方法

5.5 sleep和sleep(0)的区别。

  在操作系统中,sleep() 函数通常指的是让当前线程或进程暂停执行一段时间。而 sleep(0) 是调用 sleep() 函数时传入的一个具体参数值,即让线程睡眠时间为0秒。在Java中主要用于提示当前线程放弃CPU,让其他线程执行,是提高并发性能的一种方式

 

5.6 Lock与Synchronized的区别 。

  Lock和Synchoronized都是Java中的线程同步机制。它们在实现方式、灵活性、异常处理、性能和并发、锁粒度上有所区别

synchronized和ReentrantLock使用
  1. 实现方式:synchronized是Java内置的关键字,可直接在方法或代码块上使用;Lock是JUC包下的一个接口(常用的ReentrantLock),是一种可拓展的互斥锁,使用时实例化一个ReentrantLock对象便可加锁解锁。
  2. 灵活性:synchronized适用于简单的互斥场景、Lock可提供公平锁、尝试获取锁、定时锁、可中断锁,控制锁的获取和释放。
  3. 异常处理:synchronized在遇到异常时会自动释放锁,也可能因异常无法释放所。Lock必须在finally中手动调用unlock()方法释放锁,若不释放则会导致死锁。
  4. 性能和开发:两者性能相差不大,Lock在非公平锁时并发性能更高,无竞争情况下锁获取更快
  5. 锁粒度:synchronized关键字锁粒度基于对象,Lock支持读写锁,允许读操作并发执行,写操作互斥。

5.7 synchronized的原理是什么,一般用在什么地方(比如加在静态方法和非静态方法的区别,静态方法和非静态方法同时执行的时候会有影响吗),解释以下名词:重排序,自旋锁,偏向锁,轻

量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁。

  synchronized关键字主要用于线程同步,确保同一时刻只有一个线程访问被保护的方法或代码块。原理是基于对象头中的锁状态和Monitor机制。对象头中的锁状态存储在对象头中的MarkWord中,MarkWord包括GC年龄,锁状态,hashcodeMonitor由Owner、EntryList、WaitSet组成Owner存放正在执行的synchronized(obj)代码块的线程,EntryList存放获取不到对象阻塞的线程、WaitSet存放获取过锁,但因资源不足而等待的线程

执行流程:当线程试图获取一个对象锁时,会先尝试获取对象关联的Monitor,若Monitor未被其它线程持有,则线程获取Monitor进入临界区执行代码,执行完毕后释放Monitor,若被其他线程持有,则在Monitor的EntryList中排队等待。

synchronized标注的地方:

  • synchronized加在静态方法上,锁的是类对象,同一时刻只允许一个线程访问静态同步方法。
  • synchronized加在非静态方法上,锁的是实例对象,一个类可以有多个实例对象,多个实例对象可以独立执行同步的非静态方法,互不影响。
  • 静态方法和非静态方法同时执行互不影响,因为它们持有的锁(类对象锁和实例对象锁)不同

重排序:JMM允许在不改变单线程程序的执行结果前提下,编译器和处理器优化程序性能对指令进行重新排序。多线程环境下,为保证内存可见性,可通过volatile关键字的内存屏障机制限制重排序

自旋锁:JDK1.6引入的一种轻量级锁表现形式。线程在获取锁失败,不会挂起,而是原地循环等待(自旋),短时间内不断尝试获取锁,减少线程上下文切换开销

偏向锁:它会偏向于第一个获取锁的线程后续在该线程再次获取锁时,无需进行CAS操作,从而降低线程间切换的开销。当有其他线程尝试获取锁时偏向锁才会升级为轻量级锁或重量级锁

轻量级锁:在无竞争情况下,通过CAS和自旋获取和释放锁,避免操作系统层面的线程调度,减小锁开销。适用于无竞争和竞争程度较低场景

可重入锁:可称为递归锁,同一线程在外层方法获取锁后,进入内存方法仍然可再次获取锁而不会引起死锁。synchronized关键字支持可重入锁。

公平锁:分配锁时按照请求先后顺序分配锁,反之不按照请求先后顺序分配锁。synchronized时非公平锁,ReentrantLock可指定公平/非公平锁。

乐观锁:总认为发生并发冲突时,获取资源时直接加锁

悲观锁:乐观认为在部分情况下不会发生并发冲突。在访问资源时不加锁,但在更新时会检查此期间资源是否被其他线程修改,若未被修改则更新成功,否则重试。可通过版本号或CAS实现

5.8 用过哪些原子类,他们的原理是什么。

  AtomicInteger(JNI调用底层C++实现的CAS指令实现无锁操作)、AtomicLong、AtomicBoolean、AtomicReference、AtomicStampedReference(可通过时间戳解决ABA问题)、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater、AtomicMarkableReference等原子类。

原理:这些原子类都依赖于CAS操作或者其他低级别的硬件同步原语来实现无锁并发编程。CAS操作是一个CPU级别的指令,它可以检查内存中的某个值是否与预期相符,如果相符则替换新的值,这个过程是原子性的,不会受到线程上下文切换的影响。由于避免了使用传统的互斥锁(synchronized关键字),因此在高并发环境下,原子类通常能提供更好的性能和可扩展性。

5.9 JUC下研究过哪些并发工具,讲讲原理。

  1. ReentrantLock: ReentrantLock是一个可重入的互斥锁,它提供了比synchronized关键字更灵活的锁机制。原理上,ReentrantLock基于AQS(AbstractQueuedSynchronizer)实现,通过CAS操作和线程挂起/唤醒机制,保证了线程安全的同步。它支持公平锁和非公平锁,以及可中断、不可中断和超时获取锁等功能。

  2. Semaphore: Semaphore是一个信号量工具类,用于控制同时访问特定资源的线程数量。通过计数信号量来限制并发线程数,超过许可数目的线程将会被阻塞。其原理同样是基于AQS,通过调整许可证的数量来控制并发。

  3. CountDownLatch: CountDownLatch允许一个或多个线程等待其他线程完成一组操作后才能继续执行。它通过一个计数器来控制,当计数器为0时,所有等待的线程会被释放。原理上,也是基于AQS,通过countDown()方法递减计数器,当计数器为0时,调用await()方法的线程得以继续执行。

  4. CyclicBarrier: CyclicBarrier允许一组线程等待所有线程都到达某个屏障点(或者说完成某个阶段)后再全部执行下去。当所有线程都调用await()方法到达屏障时,所有线程会被释放并继续执行。CyclicBarrier支持循环使用,当所有线程都到达后,计数器会重置。

  5. ConcurrentHashMap:ConcurrentHashMap是线程安全的哈希表,它通过分段锁(Segment)实现并发控制,后来在Java 8中进行了重构,使用了CAS算法和数组加链表加红黑树的结构,降低了锁的粒度,进一步提高了并发性能。

5.10 用过线程池吗,如果用过,请说明原理,并说说newCache和newFixed有什么区别,构造函数的各个参数的含义是什么,比如coreSize,maxsize等。

  Java中线程池是通过JUC并发包中的ThreadPoolExecutor实现的,它能够有效管理线程的创建和销毁,复用已存在的线程,减少线程创建和销毁的开销,还可控制并发线程数量防止过多线程导致资源浪费

  1. corePoolSize: 核心线程数,线程池中的常驻线程数。当线程池中的线程数目少于corePoolSize时,即使线程池中的线程都处于空闲状态,线程池也会优先创建新线程去处理任务,而不是直接放入队列。

  2. maximumPoolSize: 线程池所能容纳的最大线程数。当线程池中的线程数达到corePoolSize,且工作队列已满时,线程池会继续创建新线程,直到线程数达到maximumPoolSize。

  3. keepAliveTime: 空闲线程存活时间。当线程池中的线程数目超过corePoolSize时,多余的线程会在空闲keepAliveTime时间后被终止,以减少资源消耗。

  4. unit: keepAliveTime参数的时间单位,例如 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。

  5. workQueue: 工作队列,用于存放等待执行的任务。可以选择无界队列(如LinkedBlockingQueue)、有界队列(如ArrayBlockingQueue)或其他类型的工作队列,不同类型的工作队列会影响线程池的工作模式和性能。

  6. threadFactory: 创建新线程的工厂,用于定制线程的创建逻辑,如设置线程的名称、优先级等。

  7. RejectedExecutionHandler: 拒绝策略,当线程池和工作队列均满时,新提交的任务无法被接受时,将调用此拒绝策略处理新任务。例如AbortPolicy(抛出异常)、CallerRunsPolicy(调用者线程自己执行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,尝试执行新任务)等。

  newFixedThreadPool创建核心线程数和最大线程数相等且固定数目的线程池,一旦创建,线程池大小不变,超过线程数的任务会被放在队列中等待执行。该线程池适用于任务数目固定且执行时间可预测的场景。

  newCachedThreadPool创建一个可缓存的线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE。线程池中的线程在闲置一段时间(默认60s)就会被回收。当有新任务提交时,线程池会创建新线程或复用空闲线程执行任务,适用于处理大量短生命周期任务的场景

5.11 线程池的关闭方式有几种,各自的区别是什么。

  线程池关闭方式有两种,分别是shutdown()和shutdownNow()方法;

shutdown()方法会停止新任务的提交,但依然会执行完队列中等待的任务,直到所有任务执行完毕;即shutdown()方法调用前提交的任务会继续执行完,shutdown()方法调用后的任务会停止。

shutdownNow()方法会尽快停止所有任务,并返回一个尚未开始执行任务的任务列表。

5.12 假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同时调用它,如何做到?

  可以使用令牌算法或信号量Semaphore控制并发访问

Semaphore控制并发访问

5.13 spring的controller是单例还是多例,怎么保证并发的安全。

  默认是单例的,因此在该类中定义的非静态实例变量在多个请求间会被共享,可能导致线程安全问题。

解决方法:

  1. 避免在Controller类中使用实例变量
  2. 使用synchronized关键字保证关键临界区资源在同一时刻,只有一个线程访问

5.14 用三个线程按顺序循环打印abc三个字母,比如abcabcabc。

ReentrantLock实现abcabcabc

5.15 ThreadLocal用过么,用途是什么,原理是什么,用的时候要注意什么。

  ThreadLocal是一种线程绑定的局部变量工具类,它可为每个线程提供独立的变量副本,各个线程互不影响,能解决多线程环境下的数据共享问题

用途

  • 存储线程私有数据在处理HTTP请求时,每个线程保存一个与当前请求相关的session或Request对象
  • 数据库连接管理:同一个请求生命周期内,为每个线程分配并复用一个数据库连接,而不是每次操作都创建新的连接
  • 日志记录上下文信息:记录每条日志的线程特定上下文信息,使得日志输出能够包含针对当前线程的信息

原理:ThreadLocal内部维护一个Map,键为当前执行线程对象,值为存储的对象

使用注意事项

  1. 内存泄漏:ThreadLocal为每一个线程保留一份线程副本,我们在使用完后需要通过remove()方法移除关联对象
  2. 使用场景:适用于线程内部使用,不适用于跨线程间的数据共享
  3. 线程池中的使用:在线程池中使用时,要清除ThreadLocal变量,防止线程复用,导致不同任务数据交叉污染,可通过Thread.UncaughtExceptionHandler接口来清理线程池中的ThreadLocal

5.16 如果让你实现一个并发安全的链表,你会怎么做。

  为保证链表的并发安全,可以通过synchronized关键字,Lock锁,原子类,COW(写少读多,写时复制整个链表)、无锁数据结构和CAS操作。

ReentrantLock实现

5.17 有哪些无锁数据结构,他们实现的原理是什么。

  无锁数据结构是指多线程环境下,读写操作不需要显示获取和释放锁,通过原子操作、CAS机制保证数据的一致性和正确性。

  1. 无锁栈:

    • 实现原理:通常采用链表结构,并结合CAS操作实现节点的入栈(push)和出栈(pop)操作。当尝试修改栈顶指针时,使用CAS检查栈顶是否发生变化,如果未变,则更新栈顶;若已变,则重新尝试。
  2. 无锁队列:

    • Michael-Scott非阻塞队列:使用双端链表和CAS操作,插入和删除操作都在尾部进行,同时保持一个“哑元”节点作为哨兵,以简化边界条件处理。
    • Treiber Stack-Based Queue:基于栈的数据结构实现无锁队列,每个元素是一个指向下一个元素的指针,插入和删除都通过CAS来竞争修改这个指针。
  3. 无锁哈希表:

    • 例如Hazard Pointers或基于CAS的扩展如ConcurrentHashMap (Java):通过划分内部数组为段(segment),每个段都有自己的锁,大部分操作在一个段内是无锁的,部分跨段操作需要加锁。另外,也利用了CAS操作更新桶(bucket)中的链表节点。
  4. 无锁计数器:

    • 基于CAS的原子整型变量,每次更新计数值时都会比较当前值与预期值,只有在两者相等的情况下才会成功更新,否则会一直循环重试直到成功为止。
  5. 无锁链表节点删除:

    • 通过CAS来改变前一个节点的next指针指向待删除节点的后继节点,从而完成节点的删除操作。
  6. 无锁环形缓冲区(Ring Buffer):

    • 利用原子操作更新生产者和消费者索引,确保在多线程环境下安全地进行数据的生产和消费。

  这些无锁数据结构的核心思想是利用现代处理器提供的原子指令集来代替传统的互斥锁,尽量减少甚至消除由于锁竞争导致的性能瓶颈和死锁等问题。但需要注意的是,无锁数据结构的设计和实现相对复杂,且存在ABA问题(即一个值被多次修改后又恢复原值的情况),有时需要引入版本号或其他机制来解决这个问题。

5.18 讲讲java同步机制的wait和notify。

  wait和notify方法都是Object类中的方法,用于线程间通信和同步。

  • wait()方法:在调用wait()方法之前会先去获取对象的监视器(线程占用Monitor的Ower),否则报错IllegalMonitorStateException异常。当一个线程调用wait()方法时,该线程会释放监视器,进入等待状态(Monitor中的WaitSet),直到有一个线程调用noti()或notifyAll()方法唤醒该线程,重新获取监视器,若没有获取到则阻塞(Monitor的EntryList)
  • notify()方法:一个线程调用notify()是为了唤醒另一个调用了wait()方法的线程,使其重新获取对象的监视器。

5.19 CAS机制是什么,如何解决ABA问题。

  CAS是一种无锁算法,CAS包含三个操作数,内存位置V,预期值A和新值B。执行原子操作时,当V的值与预期值相同时,才会将V的值更新为B,反之说明其他线程对V的值进行了修改CAS不会执行任何操作并返回一个是否成功的状态

ABA问题:在多线程环境下,内存位置V上有一个初始值A,线程1准备更新它,但在其更新前,线程2将A修改为了B,线程3又将B修改为了A。这时线程1检查内存位置V的值还是A,CAS操作会认为没有冲突,实际在此期间变量发生了变化

解决方法:版本号

版本号:每次修改值,不仅改变其值,还增加版本号。进行CAS操作时,不仅比较内存位置V与预期值,还要比较版本号是否递增。可通过内部维护一个版本号引用的AtomicStampedRefernce类解决该问题。

AtomicStampedRefernce

5.20 多线程如果线程挂住了怎么办。

  当多个线程挂起时,可以先定位问题、接着排查问题、然后根据真正问题所在搜寻解决方案。

  • 定位问题:日志分析:检查应用程序日志,是否包含有异常信息、死锁或其他导致线程挂起的情况;堆栈跟踪:通过jstack或者其他类似工具获取线程堆栈信息,查看挂起线程的状态以及它在执行哪个方法或代码块时被阻塞
  • 排查问题:死锁、无限循环、IO操作/网络堵塞、同步对象等待(wait())、外部资源限制
  • 解决方案:修复代码逻辑、设置超时机制、资源管理优化

5.21 countdowlatch和cyclicbarrier的内部原理和用法,以及相互之间的差别

Countdowlatch内部原理与用法:

内部原理: CountDownLatch通过抽象队列同步器AQS(AbstractQueuedSynchronizer)实现,它维护了一个计数器。当构造一个CountDownLatch时,需要传入一个初始的计数值每当调用countDown()方法时,计数器减1;当调用await()方法时,如果计数器值大于0,则当前线程会被阻塞,直到计数器为0为止一旦计数器为0,所有等待的线程都会被唤醒并继续执行

用法:通常用于主线程等待多个子线程完成任务后才继续执行的情况,例如初始化阶段有多个资源需要加载主线程需要等待所有资源加载完毕才能开始后续工作

CountDownLatch

CyclicBarrier内部原理与用法:

内部原理: CyclicBarrier也基于AQS实现,但它的目标是让一组线程到达某个屏障点时一起执行某项操作,而不是简单的计数到零。每个线程调用await()方法时会阻塞在那里直到所有参与的线程都到达了这个屏障点

用法:当需要一组线程同时达到某个状态或者完成各自的任务后再共同执行下一个任务时,可以使用CyclicBarrier。

CyclicBarrier

差别:

  • 行为差异:CountDownLatch是一次性的,计数器递减到0后就不可重用;而CyclicBarrier是可以循环使用的,当所有线程到达屏障后,计数器自动重置,并且可以指定一个Runnable作为屏障动作,在所有线程到达后运行。
  • 目标不同:CountDownLatch主要用于一个或多个线程等待其他线程完成一系列操作后才能继续执行;CyclicBarrier则是为了实现多个线程间的同步,确保所有线程都达到某一状态后,再集体进行下一步操作

5.22 countdownlatch的await方法和是怎么实现的。

   CountdownLatch中的await()方法被调用时会判断计数器是否为0,如果是则直接返回。反之则调用该方法的线程在AQS维护的队列中等待,直到其它线程调用countdown()方法,计数器中的值为0,AQS才会释放等待的线程,这些线程重新竞争CPU资源继续执行。

await()方法的实现流程:

  • 调用AQS的tryAcquireShared(int arg)方法尝试获取资源,这里arg通常为0,表示需要计数器为0才能成功获取
  • 如果不能立即获取资源(即计数器大于0),则通过AQS的doAcquireSharedInterruptibly(int arg)方法将当前线程加入同步队列并挂起等待
  • 当其他线程调用countDown()导致计数器归零时,AQS会触发相应逻辑,唤醒等待队列中的线程,使其能够从await()方法中返回并继续执行。

5.23 对AbstractQueuedSynchronizer了解多少,讲讲加锁和解锁的流程,独占锁和公平所加锁有什么不同。

   AQS时一种构建所和同步器的组件,其基于FIFO和CAS实现线程的阻塞和唤醒,以及状态管理。

获取锁:

独占式获取锁:当线程调用tryAquire(int args)获取锁时,会通过CAS尝试将AQS内部的状态变量设置为指定值,如果设置成功则成功获取锁;反之则线程会被封装成节点加入同步队列的尾部,并进入等待状态。被阻塞线程会在其他线程释放或特定条件时被唤醒,并再次尝试获取锁

释放锁:线程执行完临界区代码会调用release(int args)释放锁,更新状态变量,并检查是否有现成被唤醒;AQS会选择合适的线程从等待队列中移除并将其转为可运行状态,使其有机会竞争资源。

独占锁与公平锁的区别:

  • 非公平锁(默认): 在非公平锁下,获取锁的操作并不保证按照线程请求锁的顺序进行,即使有其他线程已经在等待队列中,新到达的线程也可能直接尝试获取锁。这样可以减少上下文切换开销,提高系统的吞吐量,但可能导致某些线程长期得不到锁资源,即“饥饿”现象

  • 公平锁: 公平锁在设计上遵循FIFO原则,当锁空闲时,它总是优先分配给等待时间最长的线程。这意味着新到达的线程不会立即获得锁,而是必须等到所有比它更早请求锁的线程都获得过锁之后才轮到它。这种策略有助于避免饥饿问题,但也可能降低系统的整体性能,因为频繁的线程上下文切换会导致更高的开销

  AQS通过维护一个抽象的等待队列和状态变量配合CAS原子操作,实现了高效的线程同步机制,并且可以根据实际需求选择不同的加锁策略(如公平锁与非公平锁)

5.24 使用synchronized修饰静态方法和非静态方法有什么区别。

  synchronized修饰静态方法,锁住的时类对象,synchronized修饰非静态方法锁住的是类的实例对象,两者互不影响,可并发进行。

5.25简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处。

ConcurrentLinkedQueue

  • 用途:它是一个无界、非阻塞的并发队列基于链表结构实现,适用于高并发环境下生产者消费者模式,尤其适合处理大量快速入队和出队操作,并且不需要考虑队列满的情况。
  • 特点:
    • 无界:这意味着它可以无限增长,除非系统资源耗尽,否则不会因为队列满而导致插入操作失败。
    • 非阻塞:使用了 CAS(Compare and Swap)等无锁算法来保证线程安全,因此在大多数情况下,添加或移除元素时不会引起线程阻塞,提高了性能。
    • FIFO:遵循先进先出的原则。

LinkedBlockingQueue

  • 用途:同样用于生产和消费场景,但它是有界的、支持阻塞的并发队列,可以根据需要设置队列容量
  • 特点:
    • 有界:可以设定队列的最大容量,当队列已满时,尝试向其中插入元素的线程会阻塞,直到其他线程从队列中移除了元素;同理,当队列为空时,尝试获取元素的线程也会被阻塞,直到有新的元素加入。
    • 阻塞:通过使用ReentrantLock和Condition进行线程同步,实现了插入和删除操作的阻塞等待机制,确保线程间的协调和资源的有效利用。
    • FIFO:同样遵循先进先出的原则。

5.26导致线程死锁的原因?怎么解除线程死锁。

  导致死锁有四种原因:互持资源、请求并等待、不可剥夺、循环等待

解决办法:

  1. 破坏请求并保持状态:一次性申请所有资源
  2. 破坏不可剥夺条件:占有资源的线程申请不到其他资源时,主动释放其占有的资源
  3. 破坏循环等待条件:按需申请资源

5.27 非常多个线程(可能是不同机器),相互之间需要等待协调,才能完成某种工作,问怎么设计这种协调方案。

  1. 分布式锁: 使用分布式锁服务如ZooKeeper、Redis或etcd等实现资源的互斥访问。每个参与任务的线程在执行关键操作前获取锁,完成后释放锁。这样可以确保同一时间只有一个线程(或一个机器上的线程)执行特定的操作

  2. 消息队列/中间件: 利用RabbitMQ、Kafka或其他消息队列系统,让各个线程将任务结果或状态发布到队列中其他线程根据接收到的消息进行下一步操作(发布订阅)。通过持久化和顺序保证特性,消息队列能很好地处理异步协作与顺序依赖问题

  3. 分布式事务/两阶段提交(2PC) /三阶段提交(3PC): 如果是涉及数据库操作的场景,可以考虑使用分布式事务技术来确保多个参与者的一致性

5.28 用过读写锁吗,原理是什么,一般在什么场景下用。

  读写锁时多线程编程中常见的一中线程同步机制,它允许多个线程同时读取共享资源,,只允许同一时间只有一个线程更新共享资源。

  • 读锁(Read Lock):当一个线程获取到读锁后,其他线程仍然可以获取读锁并进行并发读取操作

  • 写锁(Write Lock):一旦某个线程获得了写锁,其他任何线程(包括读线程和写线程)都不能再获得任何锁,直至该写锁被释放。这样就保证了在进行写操作期间不会有读操作或其它写操作发生,确保数据一致性

适用场景:适合读多写少的场景,如缓存系统、数据库读写操作

通过合理地使用读写锁,可以在不牺牲正确性的前提下,最大化地利用CPU和内存资源,提高系统的并发处理能力和吞吐量。

5.29 开启多个线程,如果保证顺序执行,有哪几种实现方式,或者如何保证多个线程都执行完再拿到结果。

  可以使用Thread.join()、Object.wait(),notify()、lock.await()、release()方法都可以实现多线程顺序执行

5.30 延迟队列的实现方式,delayQueue和时间轮算法的异同。

  延迟队列中是一种队列中的元素在特定时候后才能消费的优先队列。Delayed接口定义了一个方法getDelay(TimeUnit unit)用于获取剩余延迟时间,并且每个元素都有一个可比较的到期时间。当尝试从队列中获取元素时,只有已经到期的元素才会被成功取出

时间轮算法(Timing Wheel): 时间轮算法是一种空间换时间的数据结构,用于高效地处理定时任务。它将时间划分为一系列固定的时间间隔(称为槽或桶),形成一个环形数组或链表结构。每一个槽代表一定时间范围内的定时事件。每当时间流逝,指针会顺时针移动到下一个槽,处理该槽内所有到期的任务。

  • 相同点:

    • 都是为了处理具有延迟特性的任务
    • 都可以支持多个线程安全地进行任务的添加和移除
  • 不同点:

    • 数据结构与实现复杂度DelayQueue基于优先队列实现,其内部排序逻辑较为复杂;而时间轮算法通常使用环形数组或者链表,实现相对简单,查找和删除操作有固定的复杂度

    • 效率与资源占用DelayQueue由于采用堆结构,插入和删除元素时可能需要做堆调整,对于大量短生命周期任务可能会有一定性能损耗;时间轮算法则通过“滚动”机制来处理过期事件,对于密集的、周期性调度任务更为高效,特别是小步长的时间间隔,因为可以通过索引直接定位。

    • 内存消耗:时间轮算法可以根据时间间隔大小选择合适的槽位数量,对内存空间有一定的预估和控制能力;而DelayQueue没有固定的内存限制,随着元素增多,其内存消耗也会增加。

    • 扩展性:时间轮算法可以设计为多级时间轮,以支持更长时间跨度的延迟任务;而DelayQueue本身不支持这种层级结构,但可以通过其他手段(如分层处理)实现类似效果。

  DelayQueue适合于处理任意延迟时间的任务,并且无需额外自定义复杂的定时器数据结构;而时间轮算法更适合于处理大量周期性或离散时间间隔的定时任务,在高并发场景下具有更好的性能表现。

参考链接

Java并发常见面试题总结(上) | JavaGuide

posted @   求知律己  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示