面试题答案
一键面试Java中synchronized的锁升级过程
- 无锁状态:当一个对象刚创建时,还没有任何线程访问它,此时处于无锁状态,对象头中的Mark Word记录对象的一些基本信息,如哈希码、分代年龄等。
- 偏向锁:
- 当第一个线程访问同步块时,会在对象头的Mark Word中记录该线程的ID,以后该线程再次进入同步块时,只需检查Mark Word中的线程ID是否与自己的一致,若一致则无需进行CAS操作等加锁动作,直接进入同步块,这种锁的状态称为偏向锁。它是为了在只有一个线程频繁访问同步块的场景下减少锁竞争开销。
- 偏向锁的撤销:当有其他线程尝试访问同步块时,偏向锁会被撤销。撤销过程需要在全局安全点(所有线程都暂停执行)下进行,检查持有偏向锁的线程是否还存活,如果不存活,则直接撤销偏向锁恢复到无锁状态;如果存活,则升级为轻量级锁。
- 轻量级锁:
- 当偏向锁撤销后,或者多个线程交替访问同步块但竞争不激烈时,会升级为轻量级锁。线程在自己的栈帧中创建锁记录(Lock Record),将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果CAS操作成功,当前线程获得轻量级锁,进入同步块。
- 自旋:如果CAS操作失败,说明有其他线程已经获得了轻量级锁。此时当前线程不会立即阻塞,而是会进行自旋操作,即在线程的用户态进行忙循环,尝试在一段时间内再次获取锁。自旋的目的是避免线程频繁的上下文切换带来的开销。如果自旋一定次数后仍未获取到锁,则轻量级锁会膨胀为重量级锁。
- 重量级锁:
- 当自旋次数达到一定阈值(可以通过
-XX:PreBlockSpin
参数调整,默认值在不同JDK版本有所不同),或者有线程在自旋时持有锁的线程退出同步块,轻量级锁会升级为重量级锁。重量级锁会使未获取到锁的线程进入阻塞状态,被挂起在操作系统的线程队列中,当持有锁的线程释放锁后,会唤醒队列中的线程竞争锁。重量级锁通过操作系统的互斥量(Mutex)实现,涉及用户态到内核态的切换,开销较大。
- 当自旋次数达到一定阈值(可以通过
synchronized和Lock接口在公平性策略上的具体实现与区别
- synchronized:
- 公平性实现:synchronized是非公平锁。在锁竞争时,新到来的线程有可能直接获取到锁,而不是按照等待的先后顺序获取锁。例如,当一个线程释放锁后,处于等待队列头部的线程可能还未来得及被唤醒,新到来的线程就有机会竞争并获取到锁。
- 原因:这种非公平策略可以减少线程切换的开销,提高系统的吞吐量。因为如果是公平锁,每次锁释放后都需要唤醒等待队列中最前面的线程,这涉及到线程上下文切换等开销。而在非公平锁下,新到来的线程有机会直接获取锁,减少了线程唤醒和上下文切换的次数。
- Lock接口:
- 公平性实现:Lock接口可以通过
ReentrantLock
类实现公平锁和非公平锁。通过构造函数ReentrantLock(boolean fair)
来设置公平性,当fair
为true
时,ReentrantLock
为公平锁;当fair
为false
时,ReentrantLock
为非公平锁(默认)。 - 公平锁实现:公平锁在每次锁获取时,会检查等待队列中是否有线程在等待,如果有,则新线程会进入等待队列尾部等待,按照FIFO(先进先出)的顺序获取锁。这种策略保证了等待时间最长的线程优先获取锁,避免了线程饥饿问题。
- 非公平锁实现:非公平锁在每次锁获取时,新线程会尝试直接获取锁,如果获取失败,再进入等待队列。与synchronized类似,新线程有机会在锁刚释放时直接获取锁,而不考虑等待队列中的线程顺序,从而提高了系统的吞吐量。
- 公平性实现:Lock接口可以通过
在不同业务需求下如何选择合适的锁机制来保障系统性能和数据一致性
- 高并发读多写少场景:
- 选择:可以使用
ReadWriteLock
(ReentrantReadWriteLock
实现了该接口)。读操作可以共享锁,多个线程可以同时进行读操作,而写操作需要独占锁。这样在读多写少的场景下,可以大大提高系统的并发性能。例如,在缓存系统中,大量的读操作获取缓存数据,偶尔有写操作更新缓存,使用ReadWriteLock
可以在保证数据一致性的同时提高系统的并发处理能力。 - 不选synchronized原因:如果使用
synchronized
,无论是读还是写都需要获取同一把锁,会导致读操作的并发度降低,影响系统性能。
- 选择:可以使用
- 竞争不激烈且对吞吐量要求高场景:
- 选择:使用
synchronized
或ReentrantLock
的非公平锁。因为它们在竞争不激烈时,非公平锁的特性可以减少线程上下文切换开销,提高系统吞吐量。例如,在一个内部业务系统中,并发访问量不大且对公平性要求不高,使用synchronized
简单方便且性能较好。 - 不选公平锁原因:公平锁虽然保证了公平性,但由于每次锁获取都要考虑等待队列顺序,会增加线程上下文切换和调度的开销,在这种场景下会降低系统性能。
- 选择:使用
- 对公平性要求高场景:
- 选择:使用
ReentrantLock
的公平锁。例如,在一些资源分配系统中,每个请求都需要公平地获取资源,公平锁可以保证每个请求按照顺序获取锁,避免某些请求长时间等待。 - 不选synchronized原因:synchronized是非公平锁,无法满足这种对公平性有严格要求的场景。
- 选择:使用
- 锁需要可中断、可超时获取场景:
- 选择:使用
Lock
接口。Lock
接口提供了lockInterruptibly()
方法可以响应中断,tryLock(long timeout, TimeUnit unit)
方法可以在指定时间内尝试获取锁。例如,在一个任务调度系统中,当一个任务等待获取锁的时间过长时,可以中断等待或者超时放弃等待,使用Lock
接口可以方便地实现这种需求。 - 不选synchronized原因:synchronized在获取锁时无法响应中断,也没有超时获取锁的机制。
- 选择:使用