本篇文章接上一篇文章(一些和多线程相关的面试考点),主要讲的是多线程中一些加锁的策略和面试考点,同样的,如果读者可以认真看完和总结积累,相信对面试会有很大帮助。这一部分文字居多,读者要组织好语言以面对面试官的提问。
重量级锁与轻量级锁是并发编程中用于实现线程同步的两种重要锁机制,它们在锁的获取和释放的开销、效率以及应用场景上有所不同。
重量级锁:
轻量级锁:
锁升级:
重量级锁 | 轻量级锁 | |
---|---|---|
开销 | 较大,涉及操作系统层面的调度和上下文切换 | 较小,主要通过CAS操作实现,不涉及操作系统层面的切换 |
效率 | 较低,特别是在竞争较少的情况下 | 较高,在竞争较少时能够显著提高性能 |
适用场景 | 适用于高度竞争的情况,确保资源的独占性 | 适用于竞争较少或没有竞争的情况,减少不必要的开销 |
实现方式 | 基于操作系统的互斥量或信号量实现 | 基于CAS操作实现 |
锁升级 | 不存在锁升级的概念,一旦获取就是重量级锁 | 在竞争激烈时,轻量级锁可能会升级为重量级锁 |
乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是两种处理并发控制的策略,它们在数据库管理系统(DBMS)、分布式系统以及多用户环境中广泛应用,以确保数据的一致性和完整性。这两种策略的主要区别在于它们对数据并发访问的假设以及如何处理可能的冲突。
SELECT ... FOR UPDATE
语句在数据库中锁定选定的数据行,直到当前事务结束(提交或回滚)。选择乐观锁还是悲观锁,取决于具体的应用场景和性能需求。在冲突不频繁且对性能要求较高的系统中,乐观锁可能是一个更好的选择;而在冲突频繁或需要严格保证数据一致性的系统中,悲观锁可能更为合适。此外,还可以根据业务需求和数据库特性,结合使用两种锁策略来达到最佳效果。
读写锁(Readers-Writer Lock)是一种用于并发编程中的同步机制,它允许多个线程同时读取共享资源,但在写入时需要独占访问权限。这种锁分为读锁和写锁两部分,各自具有不同的特性和使用场景。
在Java中,读写锁通常通过ReentrantReadWriteLock
类实现。这个类提供了读锁(ReadLock
)和写锁(WriteLock
)的获取和释放方法。
ReentrantReadWriteLock
支持非公平性(默认)和公平性的锁获取方式。非公平锁的吞吐量较高,但可能导致线程饥饿;公平锁则按照请求锁的顺序来授予锁,但可能降低性能。读写锁特别适用于读多写少的场景,如缓存系统、数据库访问、文件系统操作和消息队列等。在这些场景中,读操作远远超过写操作,使用读写锁可以显著提高并发性能。
以下是一个简单的Java示例,演示了如何使用ReentrantReadWriteLock
:
import java.util.concurrent.locks.ReentrantReadWriteLock; public class DataContainer { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private int data = 0; public void writeData(int newValue) { lock.writeLock().lock(); try { data = newValue; System.out.println("写入数据: " + data); } finally { lock.writeLock().unlock(); } } public void readData() { lock.readLock().lock(); try { System.out.println("读取数据: " + data); } finally { lock.readLock().unlock(); } } }
在这个示例中,DataContainer
类包含了一个整数data
作为共享资源,以及一个ReentrantReadWriteLock
对象来控制对data
的访问。writeData
方法使用写锁来确保在写入data
时没有其他线程可以读取或写入它,而readData
方法则使用读锁来允许多个线程同时读取data
。
自旋锁:
挂起等待锁:
公平锁(Fair Lock)
非公平锁(Unfair Lock)
应用场景与选择
锁粗化(Lock Coarsening)是一种编译器优化技术,主要用于改进多线程程序中的锁性能。这种技术通过将多个连续的、独立的锁操作合并为一个更大的锁操作,从而减少锁的竞争次数,提高程序的执行效率。以下是对锁粗化的详细解释:
锁粗化通常用于以下场景:
假设有以下Java代码:
public void processData(List dataList) { for (Data data : dataList) { synchronized (this) { // 处理数据的代码块1 } // 一些无关的代码 synchronized (this) { // 处理数据的代码块2 } } }
在这个例子中,每个循环迭代中都对this
对象进行了两次加锁和解锁操作。编译器可能会将这些连续的锁操作合并为一个大的锁操作,从而优化代码性能。
锁消除(Lock Elimination)是一种用于优化多线程程序中锁性能的技术,主要通过编译器或运行时系统在代码优化阶段,通过静态分析或动态优化检测到某些情况下不需要进行同步的代码块,并将其对应的锁操作去除。以下是关于锁消除的详细解释:
锁消除是编译器或运行时系统在代码优化阶段,识别并消除不必要的锁操作的一种优化技术。其目标是通过减少不必要的同步操作来提高程序的性能。
锁消除通常应用于以下场景:
假设有以下Java代码片段:
public class Counter { private int count = 0; public synchronized void increment() { count++; } public void process() { Counter counter = new Counter(); for (int i = 0; i < 1000000; i++) { counter.increment(); } } }
在这个例子中,increment
方法被synchronized
关键字修饰,意味着每次调用时都会进行加锁和解锁操作。然而,如果编译器或运行时系统通过静态分析发现Counter
对象在process
方法内部被创建且不会被外部线程访问,那么它就可以推断出increment
方法中的锁操作是不必要的,从而进行锁消除。
锁消除的实现主要依赖于编译器或运行时系统的优化能力。编译器通过静态分析代码,识别出不需要进行同步的代码块,并在优化阶段将其中的锁操作去除。运行时系统则可能通过动态监控程序的运行情况,收集相关信息以辅助优化决策。
synchronized 关键字在Java中被JVM划分成4个状态:无锁,偏向锁,轻量级锁,重量级锁。这4个状态其实就是锁升级的过程,且该过程是不可逆的,如无法从重量级锁转变成轻量级锁,但可以从轻量级锁转变成重量级锁。
在以上加锁过程中可以总结出以下 synchronized 的特性:
ReentranLock 是可重⼊互斥锁,和synchronized定位类似,都是⽤来实现互斥效果,保证线程安全。
区别:
ReentrantLock 类有独立的加锁(lock),超时加锁(trylock,尝试加锁一段时间后得不到锁就放弃)和解锁(unlock)操作,加锁解锁需要手动设置,而 synchronized 关键字是全自动的。
synchronized是非公平锁,ReentrantLock默认是⾮公平锁,但是可以通过构造方法传入⼀个 true 开启公平锁模式。
// ReentrantLock 的构造⽅法 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
更强⼤的唤醒机制。synchronized 是通过 Object 的 wait/notify 实现等待-唤醒。每次唤醒的是⼀个随机等待的线程。ReentrantLock 搭配 Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。
总结:
在锁竞争不太激烈的情况下使用 synchronized 关键字可以提高效率,但是在锁竞争激烈的情况下,使用ReentrantLock 可以更加灵活的调整加锁行为,防止因死等而拖慢程序运行效率。
还是一样,相关面试题不会给出具体答案,需要读者自己总结。
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 介绍下读写锁?
- 什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
- 什么是偏向锁?
- synchronized 实现原理是什么?