CAS 和 synchronized 的优化过程

🍉 目录

[CAS 的实现](#CAS 的实现)

[CAS 的工作原理](#CAS 的工作原理)

优化过程

[CAS 的应用](#CAS 的应用)

1) 实现原子类 实现原子类)

2)实现自旋锁

[CAS 的 ABA 问题](#CAS 的 ABA 问题)

[synchronized 的 原理](#synchronized 的 原理)

[synchronized 基本特点](#synchronized 基本特点)

加锁工作过程

其他优化操作

[1. 锁消除](#1. 锁消除)

[2. 锁粗化](#2. 锁粗化)


CAS(Compare-And-Swap,即 比较和交换),是用于实现同步原语的一种原子操作。在Java的并发编程中,CAS 操作是轻量级和无锁算法的基础,它允许线程在不使用传统互斥锁的情况下安全地更新共享变量。以下是 CAS 优化的详细解释。

CAS 操作的引入主要是为了在多线程环境下提供一种高效、低开销的同步机制。通过避免使用重量级锁,CAS 操作可以减少线程的上下文切换和锁竞争带来的性能损失。

CAS 的实现

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  1. Java的 CAS 利用的是 unsafe 这个类的提供的 CAS 操作

  2. unsafe 的 CAS 依赖的是 JVM 针对不同的操作系统实现的 Atomic::cmpxchg

  3. Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 CPU 硬件提供的 lock 机制保证其原子性。

简而言之,当硬件层面予以支持,软件层面才得以实现。

CAS 的工作原理

CAS 操作的基本思想是比较并交换。它包含三个参数:内存位置(V)、预期值(A)和新值(B)。CAS 操作会检查内存位置 V 的值,与预期值 A 是否相等,如果相等则将 V 替换为 B,否则不进行任何操作。CAS 操作是原子的,即它在硬件层面上是不可分割的,这确保了操作的线程安全性。

优化过程

🍉减少锁的使用

在许多情况下,CAS 操作可以替代传统的锁机制,从而避免锁带来的开销和竞争。通过使用 CAS 操作,线程可以在不阻塞的情况下尝试更新共享变量,这提高了系统的并发性能。

🍉自旋等待

当 CAS 操作失败时(即内存值与预期值不符时),即没有拿到锁对象时,线程也不会立即进入阻塞状态而是会开始自旋等待状态。在自旋等待期间线程会不断的重新尝试 CAS 操作,直到成功或者到某个自旋时间的阀阈值。这种自旋等待机制减少了线程上下文切换的开销,并提高了系统的相应速度。

🍉减少内存开销

CAS 操作通常只需要对少量的内存位置进行操作,这减少了内存带宽的消耗。相比之下,传统的锁机制需要维护一个复杂的等待队列和锁状态,这会增加内存开销。

🍉提高可扩展性

CAS操作是基于硬件原语的,因此它可以很好地扩展到多核处理器环境。在多核处理器上,CAS操作可以并行执行,而传统的锁机制可能需要跨核进行复杂的同步操作。

🍉避免死锁

由于CAS操作不涉及锁的持有和释放,因此它避免了死锁问题的发生。死锁是传统锁机制中常见的问题之一,它会导致线程永久性地阻塞在等待锁的状态下。

CAS 的应用

1) 实现原子类

Java 标准库库中提供了java.util.concurrent.atomic包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类,其中的 getAndIncrement 相当于 i++ 操作。

AtomicInteger atomicInteger = new AtomicInteger ( 0 );
// 相当于 i++
atomicInteger.getAndIncrement();

原子类伪代码

class AtomicInteger {
private int value;
public int getAndIncrement () {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+ 1 ) != true ) {
oldValue = value;
}
return oldValue;
}
}

CAS 操作修改同一个变量是,直接读取内存而不是寄存器,修改也是直接修改的内存。是一条硬件指令,是原子的。

代码实现

java 复制代码
public class Demo22 {
    public static AtomicInteger count=new AtomicInteger(0);//设置初始值
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement(); //操作是原子的
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement(); //操作是原子的

            }
        });

        //启动线程
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);

    }
}

结果显示

通过形如上述代码就可以实现一个原子类,不需要使用重量级锁,就可以高效完成自增操作。

2)实现自旋锁

基于 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 问题

ABA 问题 :

假设存在两个线程 t1 和 t2 。如果有一个共享变量 num,初始值为A。

线程 t1 需要将原始 A 的值修改为 B(如果被其他线程修改了也没关系,一共修改为 B 只需要修改一次) ,在 t1 刚刚读取到 A 的值(value = A,oldvalue = A ),这时穿插了线程 t2 的执行,线程 t2 将 A 修改为 B 后,又将 B 修改为 A(value = A)。现在值被修改为 B 执行了一次,但是 t1 现在进行判断时,发现 value = oldvalue ,那么此时意味着 t1 也会修改 A 变为 B ,但是此时的修改是第二次,此时是第二次操作修改是错误的。(具体执行如下)

针对上面的 ABA 问题的解决方案

为了解决这个问题,可以使用版本号或时间戳来跟踪内存位置的变化。

针对上述修改值这个问题,我们引入一个版本号,每次判断 value 和 oldvalue 的时候也需要判断版本号,查看版本号是否和每次操作时读取的版本号一致。 在上面的 ABA 问题中引入版本号,当线程 t1 第一次读取的时候,版本号为1,后来经过 t2 的两次修改,虽然 num 的值变为了 A ,但是版本号不等于1,说明在 t1 未执行这段期间 t2 已经执行了(假设执行的就是 A 转变为了 B)。

如下图

synchronized 的 原理

synchronized 基本特点

1)开始为乐观锁,如果锁冲突频繁,转变为悲观锁

2)开始是轻量级锁实现,如果锁被持有的时间较长,转变为重要量级锁

3)实现轻量级锁的时候大概率需要用到自旋锁策略

4)synchronized 是一种可重入锁

5)synchronized 是一种不公平锁

6)synchronized 不是读写锁

加锁工作过程

🍉 偏向锁状态

工作原理:当只有一个线程(偏向线程)访问同步代码块或方法时,JVM会在对象的对象头中设置一个偏向锁标志,并将线程ID记录在对象头中。后续该线程再次访问时,只需检查对象头中的线程ID是否与其自身ID一致,若一致则无需进行任何同步操作,直接进入同步代码块。

撤销与升级: 当有其他线程(竞争线程)尝试获取锁时,JVM会检测到偏向锁状态,并尝试撤销偏向锁,将锁升级为轻量级锁。

🍉 轻量级锁状态(轻量级锁是为了在线程交替执行同步块时提高性能而设计的)
随着其他线程进⼊竞争, 偏向锁状态被消除, 进⼊轻量级锁状态(⾃适应的⾃旋锁).
此处的轻量级锁就是通过 CAS 来实现.
• 通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤)
• 如果更新成功, 则认为加锁成功
• 如果更新失败, 则认为锁被占⽤, 继续⾃旋式的等待(并不放弃 CPU)

⾃旋操作是⼀直让 CPU 空转, ⽐较浪费 CPU 资源.
因此此处的⾃旋不会⼀直持续进⾏, ⽽是达到⼀定的时间/重试次数, 就不再⾃旋了.
也就是所谓的 "⾃适应"

🚩自旋等待:自旋等待期间,线程会在一个小的循环中重复尝试获取锁,直到锁被释放或自旋次数超过阈值。

🍉 重量级锁状态(当锁竞争非常激烈,轻量级锁的自旋尝试无法快速获取锁时,JVM会将锁膨胀为重量级锁)

重量级锁使用操作系统提供的互斥量(mutex)机制来确保线程间的同步。线程会进入阻塞状态,并被放入等待队列(如Contention List Queue)中等待锁被释放。

🚩锁释放与唤醒:当持有锁的线程执行完同步代码块并释放锁时,JVM会随机唤醒等待队列中的一个线程。

如下图:

其他优化操作

JVM 根据配置和实现对 synchronized 锁的优化操作还有 锁消除、锁粗化。

1. 锁消除

编译器+ JVM 判断锁是否可消除,如果可以,就直接消除。什么意思呢???(一脸问号)
举个栗子:

StringBuffer sb = new StringBuffer ();
sb.append( "a" );
sb.append( "b" );
sb.append( "c" );
sb.append( "d" );
此时每个 append 的调⽤都会涉及加锁和解锁. 但如果只是在单线程中执⾏这个代码, 那么这些加锁解锁操作是没有必要的, ⽩⽩浪费了⼀些资源开销。

2. 锁粗化

⼀段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会⾃动进⾏锁的粗化。
锁的粒度:粗和细

实际开发过程中, 使⽤细粒度锁, 是期望释放锁的时候其他线程能使⽤锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会⾃动把锁粗化, 避免频繁申请释放锁。

🚩文化篇:真光之人,压抑愈久,深潜愈甚,其光华之绽放乃愈灿烂也。

以上就是本期的全部内容啦~希望对大家有帮助~~

相关推荐
禁默6 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood12 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑15 分钟前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb421528718 分钟前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶18 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_4336184421 分钟前
shell 编程(二)
开发语言·bash·shell
zfoo-framework26 分钟前
【jenkins插件】
java
风_流沙32 分钟前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
charlie11451419135 分钟前
C++ STL CookBook
开发语言·c++·stl·c++20