🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇
锁策略 cas synchronized 的优化
🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇
今日推荐歌曲: Peter Pan Was Right -- Anson Seabra 🎵🎵
前言
之前分别详细的介绍过 synchronized 锁 和 锁策略,可以参考下边的链接
锁机制的“神锁“———synchronized [多线程系列4]-优快云博客
历年必问高频面试题之——线程安全之常见的锁策略 ! ! !-优快云博客
如果不了解 synchronized 锁 和 锁策略 的程序猿们,推荐先看看上边这两篇,会更好理解这里的内容。这里会介绍 java 官方的大佬们对 锁策略, cas 和 synchronized 进一步优化的详细过程。
一、锁策略 的优化
优化锁策略是多线程编程中重要的一环,它可以提高系统的并发性能和稳定性。以下是优化锁策略的一般过程:
-
分析并发访问模式: 了解并发访问模式是优化锁策略的第一步。通过分析哪些资源会被并发访问以及并发访问的频率和规模,可以确定哪些地方需要加锁以及何时加锁,选择哪种方式加锁。
-
选择合适的锁粒度: 锁粒度是指锁定的范围大小。通常情况下,锁的粒度越小,允许并发操作的程度就越高,但是管理多个锁也会增加开销。因此,需要根据具体情况选择合适的锁粒度,尽量减小锁的范围,提高并发性能。这里在上一篇 synchronized 锁 里提到过,对于锁的 粗化和细化。
实际开发过程中,使⽤细粒度锁,是期望释放锁的时候其他线程能使⽤锁. 但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会⾃动把锁粗化,避免频繁申请释放 锁.
-
使用读写锁(readers-writerlock): 如果资源的读操作远远多于写操作,可以考虑使用读写锁来提高并发性能。读写锁允许多个线程同时读取共享资源,但是只允许一个线程写入共享资源。
-
减少锁持有时间: 锁的持有时间越短,系统的并发性能就越高。因此,在使用锁的过程中,要尽量减少锁的持有时间,避免在锁的范围内进行耗时操作,可以将耗时操作放到锁的外部执行。
-
避免锁的嵌套: 锁的嵌套会增加代码的复杂性,容易导致死锁和性能问题。因此,尽量避免在一个锁的范围内获取其他锁,如果确实需要多个锁,可以尝试使用锁的顺序来避免死锁。
-
使用无锁数据结构: 对于高并发场景,可以考虑使用无锁数据结构,如CAS算法或者基于版本号的乐观锁。无锁数据结构能够减少锁的竞争,提高并发性能。
-
考虑并发容器: Java提供了一些并发容器,如
ConcurrentHashMap
、ConcurrentLinkedQueue
等,它们内部使用了复杂的锁策略和数据结构,能够提供较好的并发性能。对于上述的 ConcurrentHashMap容器的详细介绍请参考我的这篇博客。大厂面试必考之——— HashTable, HashMap, ConcurrentHashMap !!【多线程 5】-优快云博客
通过以上优化过程,可以有效提升系统的并发性能,降低线程竞争带来的性能损失,提高系统的稳定性和可靠性。
二、CAS 的介绍和优化
什么是CAS
CAS: 全称Compareandswap,字⾯意思:”⽐较并交换“,⼀个CAS涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- ⽐较A与V是否相等。(⽐较)
- 如果⽐较相等,将B写⼊V。(交换)
- 返回操作是否成功
CAS是一种乐观锁的实现方式,它通过原子性的比较和交换操作来实现多线程并发的数据同步。CAS的优点是操作是原子性的,没有锁竞争,适用于对并发量较小的情况,性能较好。
CAS的应⽤
1) 实现原⼦类 标准库中提供了 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⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
- 如 果发现当前版本号和之前读到的版本号⼀致,就真正执⾏修改操作,并让版本号⾃增;
- 如果发现当前版 本号⽐之前读到的版本号⼤,就认为操作失败
三、 synchronized 的优化
锁机制的“神锁“———synchronized [多线程系列4]-优快云博客
上述博客中讲到了 synchronized 的锁优化,详细内容可以查看上述博客。
这里总结加补充:
优化synchronized
主要有以下几种方法:
-
减小同步块范围: 将
synchronized
关键字用于尽可能小的代码块,只在必要时同步关键部分,而不是整个方法或代码段。这可以减少线程争用的可能性,提高并发性能。 -
使用同步方法而不是同步块: 如果整个方法都需要同步,可以将方法声明为
synchronized
,而不是在方法内部使用同步块。这样可以简化代码,并且使得同步更加直观。 -
使用读写锁(ReentrantReadWriteLock): 如果有大量的读操作和少量的写操作,可以考虑使用读写锁来优化。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源,从而提高了并发性能。
-
使用并发集合类: Java提供了一系列线程安全的并发集合类,如
ConcurrentHashMap
、CopyOnWriteArrayList
等,它们内部使用了更加高效的锁机制来实现线程安全,可以替代使用synchronized
手动同步集合操作。 -
锁分离(Lock Striping): 对于某些高并发的情况,可以将数据分成多个部分,每个部分使用不同的锁来进行同步,从而减少锁的争用。这样可以提高并发性能,但需要注意避免死锁等问题。
-
使用并发工具类: Java并发包提供了一些高级并发工具类,如
CountDownLatch
、CyclicBarrier
、Semaphore
等,可以帮助实现更复杂的并发控制逻辑,避免过多的手工同步。
通过以上优化方法,可以提高synchronized
的性能和并发能力,使得程序在多线程环境下更加稳定和高效。
总结
以上就是今天要讲的内容,本文介绍了 锁策略 cas synchronized 的优化 ,通过选择合适的锁策略,并结合 CAS 和 synchronized
的优化技术,可以提高程序的并发性能和可维护性,确保在多线程环境下的稳定运行。
辛苦自己了,为了流量券赶在 23:55 写完了这篇博客。😀😀