悲观锁-Synchronized 和 ReentrantLock的异同
都是互斥锁、可重入性、原子性;适合写多读少的场景,保证数据的一致性。
1.Synchronized 的原理和实现机制
-
对象监视器(Monitor) :
Synchronized的实现依赖于对象的监视器锁(Monitor)。每个对象都有一个监视器,当一个线程获取锁后,其他线程必须等待该锁被释放。 -
加锁粒度:
- 实例方法锁:锁住当前对象实例。
- 静态方法锁:锁住当前类的
Class对象。 - 代码块锁:对某个对象加锁,锁住特定的代码块。
-
字节码层面的实现:Synchronized 是通过字节码中的
monitorenter和monitorexit指令来实现的。当进入同步方法或代码块时,monitorenter获取锁,退出时调用monitorexit释放锁。
锁升级过程:
- 无锁状态:没有线程竞争,无需加锁。
- 偏向锁:第一个线程获得锁,偏向该线程,后续获取不需要重新加锁。
- 轻量级锁:当其他线程竞争时,升级为自旋锁,避免线程挂起。
- 重量级锁:自旋失败时,升级为重量级锁,线程进入阻塞。
2.ReentrantLock 的原理与使用
-
可重入:同一个线程可以多次获取已经拥有的锁,而不会导致死锁。
-
显式锁定与解锁:与
Synchronized的隐式锁不同,ReentrantLock需要显式调用lock()获取锁,使用unlock()释放锁。这使得它的锁定范围更加灵活,可以跨方法和代码块使用。 -
公平锁与非公平锁:
- 公平锁:按等待的顺序获取锁,先等待的线程先获取锁,避免线程饥饿。
- 非公平锁:不保证先来先得,线程可能会“插队”获取锁,性能通常较好。
ReentrantLock默认是非公平锁。
常用方法:
lock() / unlock():获取和释放锁。tryLock():尝试获取锁,不会阻塞线程,如果无法获得锁则直接返回false。lockInterruptibly():可以响应中断的锁获取方式,支持线程被中断时放弃等待锁。
3.ReentrantLock怎么实现公平锁和非公平锁
-
ReentrantLock底层使用 AQS 实现线程的管理和锁机制。AQS 维护了一个 FIFO 队列,公平锁和非公平锁的实现依赖于这个队列。 -
公平锁:每次加锁时会检查等待队列是否为空,不允许插队。
-
非公平锁:线程可以在任何时候尝试获取锁,即使队列中有等待的线程。
4. AQS(AbstractQueuedSynchronizer)的原理
AQS(AbstractQueuedSynchronizer)是 JUC 中锁和同步器的基础框架,通过队列和状态管理实现了线程的等待、唤醒机制,几乎所有基于锁的实现,如 ReentrantLock、Semaphore、CountDownLatch,都依赖于 AQS。
工作原理:
-
FIFO 等待队列:AQS 通过一个 FIFO 双向队列 实现线程的等待和唤醒。线程获取锁失败时,会被加入到等待队列中进行等待,直到锁被释放并重新竞争。
-
独占锁与共享锁:
- 独占模式:一次只允许一个线程持有锁,其他线程需要等待。如
ReentrantLock。 - 共享模式:多个线程可以共享同一个锁资源。如
Semaphore、CountDownLatch。
- 独占模式:一次只允许一个线程持有锁,其他线程需要等待。如
-
状态管理:
- AQS 维护一个整数型的 state 状态来表示同步状态。0 表示未锁定,大于 0 表示锁定状态。
- 当线程尝试获取锁时,会调用
acquire(),AQS 根据当前的state判断是否能成功获取锁,不能获取则加入等待队列。
-
核心方法:
acquire():获取独占锁,无法获取时进入等待队列。release():释放独占锁。acquireShared():获取共享锁。releaseShared():释放共享锁。
队列节点:
每个等待线程都会被封装成一个 Node 节点,节点按照 双向链表 组织起来。当锁释放时,AQS 唤醒队列中的下一个节点,依次竞争锁。
如何通过 AQS 实现自定义锁?
- 通过继承 AQS 并重写
tryAcquire()、tryRelease()等方法,可以实现自定义的锁逻辑。
5. 乐观锁-CAS(Compare and Swap)的原理
CAS(Compare and Swap)是一种原子操作,用于无锁编程中解决竞争问题,保证线程安全。它的核心思想是“比较并交换”。适用于读多写少的场景,减少锁开销。
CAS 操作涉及三个操作数:
- V:要更新的变量的当前值。
- E:期望值,即你认为变量应该是的值。
- N:新值,如果变量当前值等于期望值,那么将其更新为新值。
执行步骤:
- 如果 V 的值与 E 相等,则将 V 的值更新为 N。
- 如果 V 的值与 E 不相等,则说明其他线程已经修改了 V,操作失败。
CAS 操作由硬件支持,CPU 提供专门的原子指令,如 cmpxchg,以保证操作的原子性。
优点:
- CAS 操作在硬件级别支持原子性,避免了加锁的开销,是一种轻量级的并发控制方式。
- 适合无锁编程,能显著提高多线程的性能。
缺点:
- ABA 问题:如果一个变量的值从 A 变成 B,又从 B 变成 A,那么 CAS 无法检测到这种变化。解决方法是使用 版本号 来解决,如
AtomicStampedReference。 - 自旋开销:如果 CAS 不成功,会不断重试,可能导致 CPU 高负载。
应用:
CAS 是 AtomicInteger、AtomicReference、AtomicStampedReference 等原子类的核心原理。
6. Semaphore(信号量)和 CountDownLatch 的区别
Semaphore(信号量) :
- 功能:控制同时访问某一资源的线程数量。
- 使用场景:如限制数据库连接池中的最大连接数。
- 工作方式:可以通过
acquire()和release()控制许可的获取和释放。
CountDownLatch:
- 功能:允许一个或多个线程等待其他线程完成操作后再继续执行。
- 使用场景:如等待多个服务启动完毕后再执行主线程任务。
- 工作方式:倒计时机制,
countDown()减少计数,await()等待计数归零。
6. 常见的并发容器
- ConcurrentHashMap:线程安全的哈希表,通过分段锁实现高效并发读写操作,Java 8 后采用了 CAS + 锁的方式提升性能。
- CopyOnWriteArrayList:基于写时复制的线程安全列表,适用于读多写少的场景。
- BlockingQueue:线程安全的队列,如
ArrayBlockingQueue、LinkedBlockingQueue。提供阻塞式的生产者-消费者模型支持。 - 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)
-
JMM的作用:解决多线程并发中内存可见性和指令重排序问题。它定义了线程与主内存的交互方式,确保多线程环境下共享变量的正确访问。
-
主内存与工作内存:JMM 抽象出主内存(共享内存)和线程的本地内存(私有缓存)。所有线程共享主内存中的变量,但每个线程会将主内存的变量拷贝到自己的本地内存中操作,操作完后再同步回主内存。
-
原子性、可见性、有序性:
- 原子性:通过
synchronized、Lock等保证操作不可分割。 - 可见性:使用
volatile保证变量对其他线程的可见性。 - 有序性:通过
happens-before原则控制指令顺序,防止重排序问题。
- 原子性:通过
-
内存屏障:在 JMM 中,通过内存屏障确保指令有序执行,防止缓存不一致和指令重排。
-
happens-before 原则:规定了两个操作的可见性顺序,比如对
volatile变量的写操作 happens-before 后续的读操作。