前言
之前分别详细的介绍过 synchronized 锁 和 锁策略,可以参考下边的链接
锁机制的“神锁“———synchronized [多线程系列4]-CSDN博客
历年必问高频面试题之——线程安全之常见的锁策略 ! ! !-CSDN博客
如果不了解 synchronized 锁 和 锁策略 的程序猿们,推荐先看看上边这两篇,会更好理解这里的内容。这里会介绍 java 官方的大佬们对 锁策略, cas 和 synchronized 进一步优化的详细过程。
一、锁策略 的优化
优化锁策略是多线程编程中重要的一环,它可以提高系统的并发性能和稳定性。以下是优化锁策略的一般过程:
- 分析并发访问模式: 了解并发访问模式是优化锁策略的第一步。通过分析哪些资源会被并发访问以及并发访问的频率和规模,可以确定哪些地方需要加锁以及何时加锁,选择哪种方式加锁。
- 选择合适的锁粒度: 锁粒度是指锁定的范围大小。通常情况下,锁的粒度越小,允许并发操作的程度就越高,但是管理多个锁也会增加开销。因此,需要根据具体情况选择合适的锁粒度,尽量减小锁的范围,提高并发性能。这里在上一篇 synchronized 锁 里提到过,对于锁的 粗化和细化。
实际开发过程中,使⽤细粒度锁,是期望释放锁的时候其他线程能使⽤锁. 但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会⾃动把锁粗化,避免频繁申请释放 锁.
- 使用读写锁(readers-writerlock): 如果资源的读操作远远多于写操作,可以考虑使用读写锁来提高并发性能。读写锁允许多个线程同时读取共享资源,但是只允许一个线程写入共享资源。
- 减少锁持有时间: 锁的持有时间越短,系统的并发性能就越高。因此,在使用锁的过程中,要尽量减少锁的持有时间,避免在锁的范围内进行耗时操作,可以将耗时操作放到锁的外部执行。
- 避免锁的嵌套: 锁的嵌套会增加代码的复杂性,容易导致死锁和性能问题。因此,尽量避免在一个锁的范围内获取其他锁,如果确实需要多个锁,可以尝试使用锁的顺序来避免死锁。
- 使用无锁数据结构: 对于高并发场景,可以考虑使用无锁数据结构,如CAS算法或者基于版本号的乐观锁。无锁数据结构能够减少锁的竞争,提高并发性能。
- 考虑并发容器: Java提供了一些并发容器,如
ConcurrentHashMap
、ConcurrentLinkedQueue
等,它们内部使用了复杂的锁策略和数据结构,能够提供较好的并发性能。对于上述的 ConcurrentHashMap容器的详细介绍请参考我的这篇博客。大厂面试必考之——— HashTable, HashMap, ConcurrentHashMap !!【多线程 5】-CSDN博客
通过以上优化过程,可以有效提升系统的并发性能,降低线程竞争带来的性能损失,提高系统的稳定性和可靠性。
二、CAS 的介绍和优化
什么是CAS
CAS: 全称Compareandswap,字⾯意思:”⽐较并交换“,⼀个CAS涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- ⽐较A与V是否相等。(⽐较)
- 如果⽐较相等,将B写⼊V。(交换)
- 返回操作是否成功
CAS是一种乐观锁的实现方式,它通过原子性的比较和交换操作来实现多线程并发的数据同步。CAS的优点是操作是原子性的,没有锁竞争,适用于对并发量较小的情况,性能较好。
CAS的应⽤
- 实现原⼦类 标准库中提供了 java.util.concurrent.atomic 包,⾥⾯的类都是基于这种⽅式来实现的. 典型的就是AtomicInteger类.其中的getAndIncrement相当于i++操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
示例:假设两个线程同时调⽤getAndIncrement
1. 两个线程都读取value的值到oldValue中.(oldValue是⼀个局部变量,在栈上.每个线程有⾃⼰的 栈)
2. 线程1先执⾏CAS操作.由于oldValue和value的值相同,直接进⾏对value赋值.
注意: • CAS是直接读写内存的,⽽不是操作寄存器.
• CAS的读内存,⽐较,写内存操作是⼀条硬件指令,是原⼦的.
3. 线程2再执⾏CAS操作,第⼀次CAS的时候发现oldValue和value不相等,不能进⾏赋值.因此需要 进⼊循环. 在循环⾥重新读取value的值赋给oldValue
4. 线程2接下来第⼆次执⾏CAS,此时oldValue和value相同,于是直接执⾏赋值操作.
5. 线程1和线程2返回各⾃的oldValue的值即可.
通过形如上述代码就可以实现⼀个原⼦类.不需要使⽤重量级锁,就可以⾼效的完成多线程的⾃增操作.
本来checkandset这样的操作在代码⻆度不是原⼦的.但是在硬件层⾯上可以让⼀条指令完成这个 操作,也就变成原⼦的了
实现⾃旋锁
基于CAS实现更灵活的锁,获取到更多的控制权.
⾃旋锁伪代码
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
}
}
while(!CAS(this.owner, null, Thread.currentThread())){
}
public void unlock (){
this.owner = null;
}
CAS的ABA问题
CAS(比较并交换)操作在解决并发问题时可能会遇到ABA问题。ABA问题指的是在使用CAS进行原子操作时,可能会出现这样的情况:
- 初始时,一个线程读取共享变量的值为A。
- 后来,另一个线程将共享变量的值从A改为B。
- 然后,又有一个线程将共享变量的值从B改回A。如下图
在这种情况下,如果某个线程在CAS操作之前和之后读取共享变量的值,并且在CAS操作中没有检查变量的值是否发生过变化,那么这个线程可能会错误地认为CAS操作是成功的,因为变量的值仍然是A,尽管实际上它的值已经经历了从A到B再到A的变化。
这种情况可能导致程序出现意外的行为,因为有些操作可能会基于不正确的假设而执行,比如误以为共享变量的值从未被修改过。
为了解决ABA问题,可以采取以下几种方法:
- 版本号: 使用版本号来跟踪共享变量的变化历史。每次修改共享变量时,都将版本号递增,这样在进行CAS操作时除了比较变量的值外,还要比较版本号,以确保不会遇到ABA问题。 • 真正修改的时候 ◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1. ◦ 如果当前版本号⾼于读 到的版本号.就操作失败(认为数据已经被修改过了).
- 引入额外的标记: 类似版本号,可以引入额外的标记位来跟踪变量的变化状态,从而避免ABA问题的发生。
- 使用带有ABA问题解决方案的CAS操作: 一些编程语言和库提供了带有ABA问题解决方案的CAS操作,比如Java中的
AtomicStampedReference
和AtomicMarkableReference
类。
通过以上方法,可以有效地解决或者减轻CAS操作中的ABA问题,确保程序在并发环境下的正确性。
相关⾯试题
1. 讲解下你⾃⼰理解的CAS机制
全称Compareandswap,即"⽐较并交换".相当于通过⼀个原⼦的操作,同时完成"读取内存,⽐较是 否相等,修改内存"这三个步骤.本质上需要CPU指令的⽀撑.
2. ABA问题怎么解决?
给要修改的数据引⼊版本号.
- 在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
- 如 果发现当前版本号和之前读到的版本号⼀致,就真正执⾏修改操作,并让版本号⾃增;
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
20291)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!