我是 javapub,一名 Markdown
程序员从👨💻,八股文种子选手。
面试官: 上个面试官对你的基础有了一定了解,听说你小子很不错!下面我们聊点有深度的。
面试官: 简单介绍下 CAS 你了解什么?
候选人: CAS是Compare And Swap的缩写,中文意思是比较与交换。它是一条 CPU 的原子指令,用于比较内存某个位置的值是否为预期值,如果是则更改为新的值。这一整个过程是原子的,也就是说它是线程安全的。
面试官: CAS 的用处是什么?
候选人: CAS 主要用于实现非阻塞算法。常见的使用场景有:
- 实现原子操作:像 Java 的 AtomicInteger,它使用 CAS 来原子更新变量值。
// AtomicInteger 中 CAS 的运用
private volatile int value;
public final int getAndIncrement() {
int next;
do {
next = get();
} while (!compareAndSet(next, next + 1));
return next;
}
- 实现锁的非阻塞式获取:像乐观锁。先假设可以获取锁,如果获取失败了再判断是否需要阻塞。
- 实现非阻塞的数据结构:像 ConcurrentLinkedQueue。使用 CAS 来实现链表节点的非阻塞追加等操作。
- 实现线程调度:像 Swing 里的 EDT(事件调度线程),通过 CAS 来实现对事件调度状态的修改。
面试官: 说说 CAS 的 ABA 问题?
候选人: CAS 操作包含三个操作数:内存位置(V)、旧的预期值(A)和新值(B)。如果当 CAS 操作开始时,V的确为A,但在 CAS 比较V和A之后,V的值变为了其他值,然后又变回A,就会产生ABA问题。
因为 CAS 操作只会在预期值A和当前值相同时更改为新值B,这时已经错失了一次更改的机会。
ABA 导致的问题是,当一个值原来是X,后来变成了Y,然后又变回X的时候,使用CAS进行检查时会发现它的值没有变化,但实际上却变动过了。这可能会对逻辑产生意料之外的影响。
常见的解决ABA问题的方法是使用版本号或者时间戳。在变量前面追加版本号version,每次变量更新的时候把version++,那么A-B-A 就会变成 1A-2B-3A,CAS 操作进行检查时就可以发现版本号不同,从而避免ABA问题。
面试官: 不错,CAS 是Java并发编程的基础之一,也是很重要的内容。能解释清楚CAS的ABA问题,且知道解决方法,未来会对并发编程有很大帮助。
候选人: 是的,CAS 是实现Java并发编程的基础工具之一,理解透彻CAS和ABA问题,对后续学习各种并发工具和框架,乃至设计并发系统会非常有帮助。我会继续深入学习CAS相关内容,了解更多实践应用的案例,并在项目中运用的更加娴熟。
谢谢面试官的提问,让我对CAS和ABA问题有了更全面和深入的认识,这些知识点确实对并发编程来说是基础中的基础。我一定会继续加深理解的!
面试官: 乐观锁和悲观锁了解吗?有什么区别?
候选人: 乐观锁和悲观锁是两种不同的锁机制:
悲观锁:总是假设会发生并发冲突,屏蔽一切可能违反数据完整性的操作。如synchronized关键词加在方法或者代码块上,会对该段代码采取排他锁,不允许其他线程同时执行。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。如果校验失败,就重试整个读取-修改-提交操作。典型的如CAS算法采用乐观锁。
两者主要区别在于对并发冲突的态度:
- 悲观锁试图防止并发冲突,乐观锁则容忍并发冲突,并在发生冲突时重试。
- 悲观锁会导致性能下降,因为任何时候只能有一个线程访问数据。而乐观锁可以让多个线程同时访问数据,只有在提交更新时才会检查并发冲突,所以性能更好。
- 悲观锁一般由同步机制实现,开销更大。而乐观锁由CAS这样的原子操作实现,开销更小。
所以,总体来说: - 读次数多、更新次数少,且更新不需要很强一致性的用乐观锁。
- 更新频繁,需要强一致性的用悲观锁。
- 两者也可以结合使用,先乐观锁重试几次,再悲观锁。
面试官: 你说的很详细,那你在项目中用过这两种锁吗?遇到什么问题?
候选人: 在项目中,我使用过synchronized作为悲观锁,和CAS + 版本号作为乐观锁。
使用synchronized时,由于锁定粒度太大,导致性能下降比较严重。后来在一些只读的方法上使用可重入锁ReentrantReadWriteLock,采用其读锁来提高并发度,性能得以提高。
使用CAS + 版本号时,由于业务的复杂性,导致版本号更新并不完全正确,产生过ABA问题。像在链表的删除操作,如果删除节点时版本号没有同步更新,这时线程B利用CAS把节点从A改成C,就产生了ABA问题,这时需要额外采取其他措施解决,比较棘手。
所以,总结来说使用锁机制时,需要考虑:
- 锁的粒度,尽量加在必要的范围内。
- 读写比例,如果读多,可以考虑读写锁。
- CAS使用时,要考虑清楚版本号的更新策略,避免ABA问题。
- 悲观锁和乐观锁也可以灵活结合,必要时采用悲观锁避免问题进一步扩大。
通过上述实践,让我对这两种锁有了更深的认识,今后在设计系统和使用锁机制时可以运用的更加娴熟和灵活。
面试官: 很好,你对锁机制的理解已经深入到能够在实践中运用并解决遇到的问题的地步。这是学以致用的好例子,也让我对你的能力有了更高的评价。
今天就先到这吧。
候选人: 谢谢面试官的肯定。锁机制作为并发编程的基础,我也花了不少时间去理解和实践。在项目我玩的贼6。
最近我在更新《面试1v1》系列文章,主要以场景化的方式,讲解我们在面试中遇到的问题,致力于让每一位工程师拿到自己心仪的offer,感兴趣可以关注公众号JavaPub追更!
🎁目录合集:
Gitee:https://gitee.com/rodert/JavaPub
GitHub:https://github.com/Rodert/JavaPub