【多线程 6】锁策略, cas 和 synchronized 优化过程

🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇

                     锁策略 cas synchronized 的优化               

🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇

今日推荐歌曲:   Peter Pan Was Right    -- Anson Seabra   🎵🎵


前言

之前分别详细的介绍过 synchronized 锁 和 锁策略,可以参考下边的链接

锁机制的“神锁“———synchronized [多线程系列4]-CSDN博客

历年必问高频面试题之——线程安全之常见的锁策略 ! ! !-CSDN博客

如果不了解 synchronized 锁 和 锁策略 的程序猿们,推荐先看看上边这两篇,会更好理解这里的内容。这里会介绍 java 官方的大佬们对 锁策略, cas 和 synchronized 进一步优化的详细过程。


一、锁策略 的优化

优化锁策略是多线程编程中重要的一环,它可以提高系统的并发性能和稳定性。以下是优化锁策略的一般过程:

  1. 分析并发访问模式: 了解并发访问模式是优化锁策略的第一步。通过分析哪些资源会被并发访问以及并发访问的频率和规模,可以确定哪些地方需要加锁以及何时加锁,选择哪种方式加锁。

  2. 选择合适的锁粒度: 锁粒度是指锁定的范围大小。通常情况下,锁的粒度越小,允许并发操作的程度就越高,但是管理多个锁也会增加开销。因此,需要根据具体情况选择合适的锁粒度,尽量减小锁的范围,提高并发性能。这里在上一篇 synchronized 锁 里提到过,对于锁的 粗化和细化。实际开发过程中,使⽤细粒度锁,是期望释放锁的时候其他线程能使⽤锁. 但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会⾃动把锁粗化,避免频繁申请释放 锁.

  3. 使用读写锁(readers-writerlock): 如果资源的读操作远远多于写操作,可以考虑使用读写锁来提高并发性能。读写锁允许多个线程同时读取共享资源,但是只允许一个线程写入共享资源。

  4. 减少锁持有时间: 锁的持有时间越短,系统的并发性能就越高。因此,在使用锁的过程中,要尽量减少锁的持有时间,避免在锁的范围内进行耗时操作,可以将耗时操作放到锁的外部执行。

  5. 避免锁的嵌套: 锁的嵌套会增加代码的复杂性,容易导致死锁和性能问题。因此,尽量避免在一个锁的范围内获取其他锁,如果确实需要多个锁,可以尝试使用锁的顺序来避免死锁。

  6. 使用无锁数据结构: 对于高并发场景,可以考虑使用无锁数据结构,如CAS算法或者基于版本号的乐观锁。无锁数据结构能够减少锁的竞争,提高并发性能。

  7. 考虑并发容器: Java提供了一些并发容器,如ConcurrentHashMapConcurrentLinkedQueue等,它们内部使用了复杂的锁策略和数据结构,能够提供较好的并发性能。对于上述的 ConcurrentHashMap容器的详细介绍请参考我的这篇博客。大厂面试必考之——— HashTable, HashMap, ConcurrentHashMap !!【多线程 5】-CSDN博客

通过以上优化过程,可以有效提升系统的并发性能,降低线程竞争带来的性能损失,提高系统的稳定性和可靠性。


二、CAS 的介绍和优化

什么是CAS

CAS: 全称Compareandswap,字⾯意思:”⽐较并交换“,⼀个CAS涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. ⽐较A与V是否相等。(⽐较)
  2. 如果⽐较相等,将B写⼊V。(交换)
  3. 返回操作是否成功

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进行原子操作时,可能会出现这样的情况:

  1. 初始时,一个线程读取共享变量的值为A。
  2. 后来,另一个线程将共享变量的值从A改为B。
  3. 然后,又有一个线程将共享变量的值从B改回A。如下图

在这种情况下,如果某个线程在CAS操作之前和之后读取共享变量的值,并且在CAS操作中没有检查变量的值是否发生过变化,那么这个线程可能会错误地认为CAS操作是成功的,因为变量的值仍然是A,尽管实际上它的值已经经历了从A到B再到A的变化。

这种情况可能导致程序出现意外的行为,因为有些操作可能会基于不正确的假设而执行,比如误以为共享变量的值从未被修改过。

为了解决ABA问题,可以采取以下几种方法:

  1. 版本号: 使用版本号来跟踪共享变量的变化历史。每次修改共享变量时,都将版本号递增,这样在进行CAS操作时除了比较变量的值外,还要比较版本号,以确保不会遇到ABA问题。 • 真正修改的时候                                                                                                                            ◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.                                            ◦ 如果当前版本号⾼于读 到的版本号.就操作失败(认为数据已经被修改过了).

  2. 引入额外的标记: 类似版本号,可以引入额外的标记位来跟踪变量的变化状态,从而避免ABA问题的发生。

  3. 使用带有ABA问题解决方案的CAS操作: 一些编程语言和库提供了带有ABA问题解决方案的CAS操作,比如Java中的AtomicStampedReferenceAtomicMarkableReference类。

通过以上方法,可以有效地解决或者减轻CAS操作中的ABA问题,确保程序在并发环境下的正确性。


相关⾯试题

1. 讲解下你⾃⼰理解的CAS机制

全称Compareandswap,即"⽐较并交换".相当于通过⼀个原⼦的操作,同时完成"读取内存,⽐较是 否相等,修改内存"这三个步骤.本质上需要CPU指令的⽀撑.

2. ABA问题怎么解决?

给要修改的数据引⼊版本号.

  1. 在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
  2. 如 果发现当前版本号和之前读到的版本号⼀致,就真正执⾏修改操作,并让版本号⾃增;
  3. 如果发现当前版 本号⽐之前读到的版本号⼤,就认为操作失败


三、 synchronized 的优化

锁机制的“神锁“———synchronized [多线程系列4]-CSDN博客

上述博客中讲到了 synchronized 的锁优化,详细内容可以查看上述博客。

这里总结加补充:

优化synchronized主要有以下几种方法:

  1. 减小同步块范围:synchronized关键字用于尽可能小的代码块,只在必要时同步关键部分,而不是整个方法或代码段。这可以减少线程争用的可能性,提高并发性能。

  2. 使用同步方法而不是同步块: 如果整个方法都需要同步,可以将方法声明为synchronized,而不是在方法内部使用同步块。这样可以简化代码,并且使得同步更加直观。

  3. 使用读写锁(ReentrantReadWriteLock): 如果有大量的读操作和少量的写操作,可以考虑使用读写锁来优化。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源,从而提高了并发性能。

  4. 使用并发集合类: Java提供了一系列线程安全的并发集合类,如ConcurrentHashMapCopyOnWriteArrayList等,它们内部使用了更加高效的锁机制来实现线程安全,可以替代使用synchronized手动同步集合操作。

  5. 锁分离(Lock Striping): 对于某些高并发的情况,可以将数据分成多个部分,每个部分使用不同的锁来进行同步,从而减少锁的争用。这样可以提高并发性能,但需要注意避免死锁等问题。

  6. 使用并发工具类: Java并发包提供了一些高级并发工具类,如CountDownLatchCyclicBarrierSemaphore等,可以帮助实现更复杂的并发控制逻辑,避免过多的手工同步。

通过以上优化方法,可以提高synchronized的性能和并发能力,使得程序在多线程环境下更加稳定和高效。


总结

以上就是今天要讲的内容,本文介绍了 锁策略  cas  synchronized  的优化 ,通过选择合适的锁策略,并结合 CAS 和 synchronized 的优化技术,可以提高程序的并发性能和可维护性,确保在多线程环境下的稳定运行。

辛苦自己了,为了流量券赶在 23:55 写完了这篇博客。😀😀

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值