🍉 目录
[CAS 的实现](#CAS 的实现)
[CAS 的工作原理](#CAS 的工作原理)
[CAS 的应用](#CAS 的应用)
1) 实现原子类 实现原子类)
[CAS 的 ABA 问题](#CAS 的 ABA 问题)
[synchronized 的 原理](#synchronized 的 原理)
[synchronized 基本特点](#synchronized 基本特点)
[1. 锁消除](#1. 锁消除)
[2. 锁粗化](#2. 锁粗化)
CAS(Compare-And-Swap,即 比较和交换),是用于实现同步原语的一种原子操作。在Java的并发编程中,CAS 操作是轻量级和无锁算法的基础,它允许线程在不使用传统互斥锁的情况下安全地更新共享变量。以下是 CAS 优化的详细解释。
CAS 操作的引入主要是为了在多线程环境下提供一种高效、低开销的同步机制。通过避免使用重量级锁,CAS 操作可以减少线程的上下文切换和锁竞争带来的性能损失。
CAS 的实现
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
-
Java的 CAS 利用的是 unsafe 这个类的提供的 CAS 操作
-
unsafe 的 CAS 依赖的是 JVM 针对不同的操作系统实现的 Atomic::cmpxchg
-
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 就会⾃动把锁粗化, 避免频繁申请释放锁。
🚩文化篇:真光之人,压抑愈久,深潜愈甚,其光华之绽放乃愈灿烂也。
以上就是本期的全部内容啦~希望对大家有帮助~~