JUC常考知识点

113 阅读9分钟

悲观锁-SynchronizedReentrantLock的异同

都是互斥锁可重入性原子性;适合写多读少的场景,保证数据的一致性。

image.png

1.Synchronized 的原理和实现机制

  • 对象监视器(Monitor)Synchronized 的实现依赖于对象的监视器锁(Monitor)。每个对象都有一个监视器,当一个线程获取锁后,其他线程必须等待该锁被释放。

  • 加锁粒度

    • 实例方法锁:锁住当前对象实例。
    • 静态方法锁:锁住当前类的 Class 对象。
    • 代码块锁:对某个对象加锁,锁住特定的代码块。
  • 字节码层面的实现:Synchronized 是通过字节码中的 monitorentermonitorexit 指令来实现的。当进入同步方法或代码块时,monitorenter 获取锁,退出时调用 monitorexit 释放锁。

锁升级过程:

  1. 无锁状态:没有线程竞争,无需加锁。
  2. 偏向锁:第一个线程获得锁,偏向该线程,后续获取不需要重新加锁。
  3. 轻量级锁:当其他线程竞争时,升级为自旋锁,避免线程挂起。
  4. 重量级锁:自旋失败时,升级为重量级锁,线程进入阻塞。

2.ReentrantLock 的原理与使用

  • 可重入:同一个线程可以多次获取已经拥有的锁,而不会导致死锁。

  • 显式锁定与解锁:与 Synchronized 的隐式锁不同,ReentrantLock 需要显式调用 lock() 获取锁,使用 unlock() 释放锁。这使得它的锁定范围更加灵活,可以跨方法和代码块使用。

  • 公平锁与非公平锁

    • 公平锁:按等待的顺序获取锁,先等待的线程先获取锁,避免线程饥饿。
    • 非公平锁:不保证先来先得,线程可能会“插队”获取锁,性能通常较好。ReentrantLock 默认是非公平锁。
常用方法
  • lock() / unlock():获取和释放锁。
  • tryLock():尝试获取锁,不会阻塞线程,如果无法获得锁则直接返回 false
  • lockInterruptibly():可以响应中断的锁获取方式,支持线程被中断时放弃等待锁。

3.ReentrantLock怎么实现公平锁和非公平锁

  • ReentrantLock 底层使用 AQS 实现线程的管理和锁机制。AQS 维护了一个 FIFO 队列,公平锁和非公平锁的实现依赖于这个队列。

  • 公平锁:每次加锁时会检查等待队列是否为空,不允许插队。

  • 非公平锁:线程可以在任何时候尝试获取锁,即使队列中有等待的线程。


4. AQS(AbstractQueuedSynchronizer)的原理

AQSAbstractQueuedSynchronizer)是 JUC 中锁和同步器的基础框架,通过队列和状态管理实现了线程的等待、唤醒机制,几乎所有基于锁的实现,如 ReentrantLockSemaphoreCountDownLatch,都依赖于 AQS。

工作原理
  • FIFO 等待队列:AQS 通过一个 FIFO 双向队列 实现线程的等待和唤醒。线程获取锁失败时,会被加入到等待队列中进行等待,直到锁被释放并重新竞争。

  • 独占锁与共享锁

    • 独占模式:一次只允许一个线程持有锁,其他线程需要等待。如 ReentrantLock
    • 共享模式:多个线程可以共享同一个锁资源。如 SemaphoreCountDownLatch
  • 状态管理

    • AQS 维护一个整数型的 state 状态来表示同步状态。0 表示未锁定,大于 0 表示锁定状态。
    • 当线程尝试获取锁时,会调用 acquire(),AQS 根据当前的 state 判断是否能成功获取锁,不能获取则加入等待队列。
  • 核心方法

    • acquire():获取独占锁,无法获取时进入等待队列。
    • release():释放独占锁。
    • acquireShared():获取共享锁。
    • releaseShared():释放共享锁。
队列节点

每个等待线程都会被封装成一个 Node 节点,节点按照 双向链表 组织起来。当锁释放时,AQS 唤醒队列中的下一个节点,依次竞争锁。

如何通过 AQS 实现自定义锁?

  • 通过继承 AQS 并重写 tryAcquire()tryRelease() 等方法,可以实现自定义的锁逻辑。

5. 乐观锁-CAS(Compare and Swap)的原理

CASCompare and Swap)是一种原子操作,用于无锁编程中解决竞争问题,保证线程安全。它的核心思想是“比较并交换”。适用于读多写少的场景,减少锁开销。

CAS 操作涉及三个操作数:

  • V:要更新的变量的当前值。
  • E:期望值,即你认为变量应该是的值。
  • N:新值,如果变量当前值等于期望值,那么将其更新为新值。

执行步骤:

  1. 如果 V 的值与 E 相等,则将 V 的值更新为 N。
  2. 如果 V 的值与 E 不相等,则说明其他线程已经修改了 V,操作失败。

CAS 操作由硬件支持,CPU 提供专门的原子指令,如 cmpxchg,以保证操作的原子性。

优点
  • CAS 操作在硬件级别支持原子性,避免了加锁的开销,是一种轻量级的并发控制方式。
  • 适合无锁编程,能显著提高多线程的性能。
缺点
  1. ABA 问题:如果一个变量的值从 A 变成 B,又从 B 变成 A,那么 CAS 无法检测到这种变化。解决方法是使用 版本号 来解决,如 AtomicStampedReference
  2. 自旋开销:如果 CAS 不成功,会不断重试,可能导致 CPU 高负载。
应用

CAS 是 AtomicIntegerAtomicReferenceAtomicStampedReference 等原子类的核心原理。


6. Semaphore(信号量)和 CountDownLatch 的区别

Semaphore(信号量)

  • 功能:控制同时访问某一资源的线程数量。
  • 使用场景:如限制数据库连接池中的最大连接数。
  • 工作方式:可以通过 acquire()release() 控制许可的获取和释放。

CountDownLatch

  • 功能:允许一个或多个线程等待其他线程完成操作后再继续执行。
  • 使用场景:如等待多个服务启动完毕后再执行主线程任务。
  • 工作方式:倒计时机制,countDown() 减少计数,await() 等待计数归零。

6. 常见的并发容器

  • ConcurrentHashMap:线程安全的哈希表,通过分段锁实现高效并发读写操作,Java 8 后采用了 CAS + 锁的方式提升性能。
  • CopyOnWriteArrayList:基于写时复制的线程安全列表,适用于读多写少的场景。
  • BlockingQueue:线程安全的队列,如 ArrayBlockingQueueLinkedBlockingQueue。提供阻塞式的生产者-消费者模型支持。
  • ConcurrentLinkedQueue:基于 CAS 实现的无锁并发队列,适用于高并发场景。

线程池

维护一个线程队列来管理并复用多个线程,避免频繁的线程创建和销毁,提升性能和系统资源的利用率。

1. 核心参数

  • 核心线程数(corePoolSize) :线程池中始终保留的最小线程数,即使线程处于空闲状态,也不会被销毁。
  • 最大线程数(maximumPoolSize) :线程池能够容纳的最大线程数,超过这个数量时会触发拒绝策略。
  • 任务队列(workQueue) :当核心线程数已满,任务被放入队列中等待执行。
  • 线程存活时间(keepAliveTime) :当线程池中线程数超过核心线程数时,空闲线程超过指定时间会被销毁。
  • 线程工厂(ThreadFactory) :创建线程的工厂,允许自定义线程创建(如设置线程名、优先级等)。
  • 拒绝策略(RejectedExecutionHandler) :当线程池无法处理更多任务时,如何处理拒绝的任务。

2. 线程池的工作原理

  • 当任务提交时,如果核心线程未满,则创建新的线程执行任务。
  • 如果核心线程满了,任务进入等待队列。
  • 当等待队列满了,且未达到最大线程数,则会创建新的线程处理任务。
  • 当达到最大线程数且队列也满了,触发拒绝策略。

3. 线程池的常见实现

  • FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相等,任务多余时进入任务队列。
  • CachedThreadPool:动态线程池,线程数量根据需求动态调整,适合处理大量短时间任务。
  • SingleThreadExecutor:单线程池,只有一个线程,所有任务按顺序执行。
  • ScheduledThreadPool:支持定时和周期性任务执行的线程池。

4. 线程池的拒绝策略

  • AbortPolicy(默认) :直接抛出异常,拒绝任务。
  • DiscardPolicy:丢弃任务,不抛异常。
  • DiscardOldestPolicy:丢弃队列中最早的任务,尝试执行当前任务。
  • CallerRunsPolicy:由调用线程直接执行任务,避免任务丢失。

5. 常见问题与优化

  • 如何设置线程池大小:根据 CPU 密集型任务和 I/O 密集型任务的特性,合理配置线程池大小。通常,CPU 密集型任务可配置为 CPU 核心数 + 1,I/O 密集型任务则需要更多线程。
  • 任务积压和过载问题:使用适当的任务队列长度和拒绝策略,避免任务过多导致系统崩溃。
  • 资源泄露问题:线程池使用完后要调用 shutdown() 方法关闭,避免线程池泄露。

6. 线程池的生命周期管理

  • new:线程池刚创建,尚未执行任务。
  • running:线程池可接受任务并处理队列中的任务。
  • shutdown:不再接受新任务,但会处理已提交任务。
  • terminated:所有任务完成,线程池彻底关闭。

7. 线程池的优缺点

  • 优点:提高了系统的并发能力、减少了频繁创建销毁线程的开销、提供了任务队列和拒绝策略。
  • 缺点:如果不合理配置,可能会导致线程数过多或过少,影响性能或导致任务积压。

JMM(Java Memory Model)

  1. JMM的作用:解决多线程并发中内存可见性和指令重排序问题。它定义了线程与主内存的交互方式,确保多线程环境下共享变量的正确访问。

  2. 主内存与工作内存:JMM 抽象出主内存(共享内存)和线程的本地内存(私有缓存)。所有线程共享主内存中的变量,但每个线程会将主内存的变量拷贝到自己的本地内存中操作,操作完后再同步回主内存。

  3. 原子性、可见性、有序性

    • 原子性:通过 synchronizedLock 等保证操作不可分割。
    • 可见性:使用 volatile 保证变量对其他线程的可见性。
    • 有序性:通过 happens-before 原则控制指令顺序,防止重排序问题。
  4. 内存屏障:在 JMM 中,通过内存屏障确保指令有序执行,防止缓存不一致和指令重排。

  5. happens-before 原则:规定了两个操作的可见性顺序,比如对 volatile 变量的写操作 happens-before 后续的读操作。