侧边栏壁纸
博主头像
兰若春夏 博主等级

一日为坤,终生为坤

  • 累计撰写 23 篇文章
  • 累计创建 13 个标签
  • 累计收到 6 条评论

目 录CONTENT

文章目录

并发编程

奥德坤
2025-08-26 / 0 评论 / 0 点赞 / 20 阅读 / 0 字

并发编错三大特性

原子性

一个或多个操作,在CPU执行的过程中,看起来就像一个单一的、不可中断的操作。要么所有操作都执行成功,要么都不执行,不存在执行了一半的中间状态被其他线程看到。

但是在多线程情况下,线程切换过程中会强制挂起当前线程,并切换到另一个线程去执行

经典的i++操作

public class CounterDemo {

    private static int count;

    @SneakyThrows
    public static void increment() {
        TimeUnit.MILLISECONDS.sleep(100);
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
最后输出结果可能不等于200

在Java端保证原子性一般有三种方式

CAS、synchronized、ReentrantLock

CAS

public class CounterDemo {

    private static AtomicInteger count = new AtomicInteger(0);

    @SneakyThrows
    public static void increment() {
        TimeUnit.MILLISECONDS.sleep(100);
        count.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
它内部使用了CAS (Compare-And-Swap) 这种更轻量级的、基于CPU原子指令的机制来保证原子性。

Synchronized

public class CounterDemo {

    private static int count ;

    @SneakyThrows
    public static synchronized void increment() {
        TimeUnit.MILLISECONDS.sleep(100);
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
通过monitorenter和monitorexit指令,确保同一时间只有一个线程能执行同步代码块,从而保证了原子性。

ReentrantLock

public class CounterDemo {

    private static int count ;

    private static ReentrantLock lock = new ReentrantLock();
    @SneakyThrows
    public static void increment() {
        TimeUnit.MILLISECONDS.sleep(100);
        lock.lock();
        count++;
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100; j++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}
可见性

当一个线程修改了一个共享变量的值,其他线程能够立即看到这个修改

现代CPU为了弥补CPU与主内存(RAM)之间巨大的速度差异,引入了多级高速缓存(L1, L2, L3 Cache)。

  • 线程在执行时,会先把主内存中的数据拷贝一份到自己的工作内存(即CPU缓存)中。
  • 所有的计算和修改都是在工作内存中进行的。
  • 修改完成后,在某个不确定的时机,才会将工作内存的数据**写回(刷新)**到主内存。

这就导致了缓存不一致性问题:一个线程在自己的缓存里修改了变量,但还没来得及写回主内存,另一个线程从主内存读到的就是旧的、过期的“脏数据”。

public class StoppableTaskDemo {

    public static boolean stop = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (stop){

            }
            System.out.println("线程结束");
        }).start();
        Thread.sleep(100);
        stop = false;
        System.out.println("线程开始");
    }
}

通过volatile解决

public class StoppableTaskDemo {

    public static volatile boolean stop = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (stop){

            }
            System.out.println("线程结束");
        }).start();
        Thread.sleep(100);
        stop = false;
        System.out.println("线程开始");
    }
}

通过synchronized解决

public class StoppableTaskDemo {

    public static boolean stop = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (stop){
            //内部有一个synchronized
                System.out.println("线程运行中");
            }
            System.out.println("线程结束");
        }).start();
        Thread.sleep(100);
        stop = false;
        System.out.println("线程开始");
    }
}

通过Lock解决

public class StoppableTaskDemo {

    public static boolean stop = true;

    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (stop){
                lock.lock();
                lock.unlock();
            }
            System.out.println("线程结束");
        }).start();
        Thread.sleep(100);
        stop = false;
        System.out.println("线程开始");
    }
}

Java中的解决方案

  • volatile 关键字:这是保证可见性的主要手段。当一个变量被声明为 volatile
    • 写操作:会强制将当前线程工作内存中的值刷新到主内存。
    • 读操作:会强制让当前线程的工作内存失效,重新从主内存中读取。
  • synchronized 和 ****Lock:它们也能保证可见性。在解锁(unlock)前,会强制把修改过的变量刷新到主内存;在加锁(lock)后,会清空工作内存,强制从主内存加载。
  • final 关键字:被 final修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this引用泄露出去,那么在其他线程中就能保证可见。
有序性

程序执行的顺序与代码中定义的顺序一致。

为了提升性能,编译器处理器通常会对输入的指令进行乱序执行优化。它们会保证在单线程环境下,重排序后的结果与代码顺序执行的结果是一致的。但在多线程环境下,这种优化可能会导致意想不到的后果。

经典DCL单例模式

class Singleton {
    // 如果没有volatile,可能因指令重排序而出错
    private static volatile Singleton instance; 

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

instance = new Singleton() 这行代码不是原子的,它大致包含三个步骤:

  1. memory = allocate(); // 1. 分配对象的内存空间
  2. ctorInstance(memory); // 2. 初始化对象
  3. instance = memory; // 3. 设置instance指向刚分配的内存地址

编译器或CPU可能会将步骤2和3重排序,变成 1 -> 3 -> 2。

如果发生重排序:

  1. 线程A执行到 instance = new Singleton(),按 1 -> 3 -> 2 的顺序执行。
  2. 当执行完第3步 instance = memory 时,instance 已经不为 null 了,但对象还没初始化。
  3. 此时发生线程切换,线程B进入 getInstance() 方法。
  4. 线程B执行第一个 if (instance == null),发现 instance 不为 null,直接返回 instance
  5. 但这个 instance 是一个半初始化的对象,使用它可能会导致程序崩溃。

Java中的解决方案:

  • volatile 关键字:它包含禁止指令重排序的语义。通过插入内存屏障来阻止编译器和处理器的重排序优化。
  • synchronized 和 ****Lock:同样能保证有序性。一个锁的解锁操作 happens-before 于后续对这个锁的加锁操作。
  • Java内存模型 (JMM) 的 Happens-Before 原则:这是Java语言层面定义的有序性规则,比如“程序次序规则”、“监视器锁规则”、“volatile变量规则”等,它们天然地保证了一些操作的有序性。

Java中的锁

锁分类
悲观锁

共享资源每次只给一个线程使用,其他线程阻塞,只有用完后其他线程才能获取

  • 思想:总是假设最坏的情况,认为数据在操作期间一定会被其他线程修改。因此,在每次操作数据之前,都会先加锁,确保在自己操作的整个过程中,数据不会被外界修改。
  • 实现方式:Java中所有传统的锁,如 synchronizedReentrantLock,都属于悲观锁。
  • 应用场景写多读少,并发冲突激烈的场景。它能有效防止数据冲突,但加锁和释放锁的开销较大,在冲突不激烈时会影响性能。
乐观锁

乐观锁总是设想最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停的执行,无需加锁和等待,只有提交修改的时候会验证数据是否被其他资源修改(CAS)

  • 思想:总是假设最好的情况,认为数据在操作期间不会被其他线程修改。因此,它不会上锁,而是在更新数据时去判断,在此期间数据有没有被其他线程修改过。
  • 实现方式:通常通过 CAS (Compare-And-Swap) 机制实现。CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当 V 符合预期值 A 时,处理器才会用 B 更新 V 的值,否则不执行任何操作。整个过程是原子的。
  • 典型代表java.util.concurrent.atomic 包下的所有原子类,如 AtomicInteger
  • 应用场景读多写少,并发冲突不激烈的场景。它避免了加锁和解锁的开销,性能更高。但如果冲突频繁,会导致CAS操作不断失败和重试,反而会消耗更多CPU资源。
可重入锁

一个线程在持有某个锁的情况下,可以再次、多次地请求同一个锁,并且每次都能成功获取。

为了支持重入,锁的内部必须维护两个关键信息:

  1. 当前持有锁的线程 (owner):记录是谁拿了锁。
  2. 重入计数器 (holdCount):记录这个线程拿了多少次锁。
  • 绝对的主流和默认选择。Java中的 synchronizedReentrantLock都是可重入的,这使得我们可以在一个同步方法中安全地调用另一个同步方法,或者在子类重写父类的同步方法时,通过 super调用父方法,而不会产生死锁。
  • 递归调用:如果一个递归函数需要加锁,那么它必须是可重入的。
  • 绝大多数并发场景:只要你需要加锁,99.9%的情况下你需要的都是可重入锁。
不可重入锁

一个线程在持有某个锁的情况下,不能再次请求同一个锁。如果尝试再次获取,线程会被阻塞,导致死锁

  • 极为罕见。它的使用场景非常有限,通常是作为一个理论上的概念,用于对比和理解可重入锁的重要性。
  • 在某些非常特定的场景下,你可能想强制避免某个线程重复进入一段代码(防止意外的递归调用),可能会考虑使用它。但这通常被认为是一种糟糕的设计,更好的方式是通过代码逻辑本身来控制,而不是依赖锁的特性。
公平锁

每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。

  • 缺点:整体效率可能较低。因为每次都必须严格按顺序唤醒等待队列中的第一个线程,这涉及到大量的线程挂起和唤醒操作,开销较大。
  • 当公平性是业务的硬性要求时
  • 适用场景
    • 任务调度系统:需要确保长时间等待的任务最终能被执行,防止重要但不紧急的任务被“饿死”。
    • 资源队列:比如一个打印机任务队列,你希望严格按照提交的先后顺序进行打印。
    • 任何要求“先来后到”业务逻辑的场景
非公平锁

每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。synchronized,ReentrantLock()默认 都是非公平锁

  • 缺点可能导致饥饿 (Starvation)。某些线程可能运气一直不好,总是抢不到锁,从而长时间无法执行。
  • 绝大多数场景。在对性能要求极高,且能容忍偶尔的线程饥饿情况下,非公平锁是最佳选择。
  • 它的高吞吐量特性,使得它成为 ReentrantLocksynchronized的默认实现。
  • 适用场景:追求最高性能的后台服务、Web服务器等。
排他锁

同一时间点,只能有一个线程持有当前的锁资源

如果一个线程获取了排他锁,那么在它释放该锁之前,其他任何线程(无论想读还是想写)都无法获取该锁,只能进入等待状态。synchronized,ReentrantLock

适用场景:适用于任何需要保证数据一致性的写操作,或者读-写-改这类复合操作。在这些场景下,资源状态的修改过程不希望被任何其他线程(即使是读线程)观察到。

共享锁

同一时间点,可以有多个线程同时持有当前的锁资源

如果一个线程获取了共享锁,其他线程也可以成功获取共享锁并访问资源。但是,如果有线程想要获取排他锁,它必须等待所有共享锁都被释放。

适用场景读多写少 (Read-Mostly) 的场景。当一个共享资源的读取频率远高于写入频率时,使用读写锁 (ReentrantReadWriteLock) 可以极大地提升并发性能。

CAS

CAS的全称是 Compare-And-Swap (比较并交换)

它是一种原子操作,通常由CPU指令直接支持(例如x86架构的 CMPXCHG指令),这意味着它的执行过程不会被任何其他线程中断。

CAS操作涉及三个操作数:

  1. V (Memory Location):要操作的内存地址(即变量)。
  2. A (Expected Value):线程预期的、该地址当前应该存放的值。
  3. B (New Value):准备要写入的新值。

执行逻辑: 当且仅当内存地址 V处的值与预期值 A相等时,处理器才会原子地将该地址的值更新为新值 B。否则,它什么也不做。无论成功与否,它都会返回 V处操作前的真实值。

优点

  1. 高性能 (非阻塞):相比于 synchronized这种悲观锁,CAS是非阻塞的。它对于JVM不会导致线程被挂起和恢复,没有线程上下文切换的开销。在并发冲突不激烈的情况下,性能远超悲观锁。
  2. 乐观锁实现:它体现了乐观锁的思想,即“假设没有冲突,先尝试操作再说”。

缺点

  1. ABA问题
    1. 问题描述:CAS只检查“当前值”和“预期值”是否相等,但无法感知这个值是否被“动过手脚”。一个值可能从A变为B,然后又变回了A。CAS检查时会误认为它从未变过。
    2. 例子:线程1读取值为A。线程2将值从A改为B,然后又改回A。线程1进行CAS时,发现值仍然是A,于是操作成功。但在某些业务场景下(例如链表的节点操作),这可能会导致严重问题。
    3. 解决方案:Java提供了 AtomicStampedReference,它在CAS的基础上增加了一个“版本号”(stamp)。每次修改,版本号都会加1。这样,即使值变回A,版本号也不同了,CAS会失败。
  2. 自旋开销大
    1. 如果并发冲突非常激烈,会导致大量线程反复地、长时间地自旋(循环重试)。这会持续消耗CPU资源,性能反而可能不如让线程进入等待状态的悲觀锁。
  3. 只能保证一个共享变量的原子操作
    1. CAS一次只能对一个内存地址进行原子操作。如果需要同时保证多个变量的原子性,就需要使用 synchronizedLock,或者将多个变量封装成一个对象,然后使用 AtomicReference来对这个对象的引用进行CAS操作。
特性 CAS (乐观锁) synchronized (悲观锁)
思想 假设不会冲突,直接尝试,失败再重试 假设总会冲突,先加锁再操作
实现 CPU原子指令,通常伴随自旋 操作系统互斥量 (Mutex),涉及线程上下文切换
性能 低/中度冲突时性能高 高冲突时性能稳定,避免CPU空转
问题 ABA问题、CPU空转开销 线程阻塞、上下文切换开销
synchronized

synchronized 的核心作用是提供一种**互斥(Mutual Exclusion)**的机制,它确保在同一时刻,只有一个线程能够执行被它修饰的代码块或方法。这块被保护的代码区域被称为“临界区”。

  1. 修饰实例方法
  • 锁对象:当前类的实例对象 (this)。
  • 作用范围:整个方法体。
  • 示例
  • codeJava
public class BankAccount {
    private int balance;

    // 锁是 this,即 BankAccount 的实例对象
    public synchronized void deposit(int amount) {
        balance += amount;
    }
}
  • 当一个线程调用 deposit方法时,它会尝试获取 this对象的锁。如果成功,其他任何线程都无法同时调用该实例的任何 synchronized方法(比如 withdraw方法),直到该线程退出 deposit方法并释放锁。
  1. 修饰静态方法
  • 锁对象:当前类的 Class 对象 (例如, BankAccount.class)。
  • 作用范围:整个方法体。
  • 示例
  • codeJava
public class Bank {
    private static int totalAssets;

    // 锁是 Bank.class 对象
    public static synchronized void addAssets(int amount) {
        totalAssets += amount;
    }
}
  • 静态方法的锁是类级别的。一个线程进入 addAssets方法后,其他线程无法进入任何该类的 synchronized静态方法,但不影响它们调用非静态的 synchronized方法(因为锁的对象不同)。
  1. 修饰代码块
  • 锁对象手动指定的任何对象(包括 thisXxx.class或任何其他对象实例)。
  • 作用范围{} 包围的代码块。
  • 示例
  • codeJava
public class Worker {
    private final Object lock = new Object(); 
    // 通常使用一个专用的锁对象
    public void doWork() {
        // ... 一些不需要同步的代码 ...
        // 只对关键部分加锁,减小锁的粒度
        synchronized (lock) {
            // ... 临界区代码 ...
        }
  
        // ... 另一些不需要同步的代码 ...
    }
}
  • 这是最灵活、也是最推荐的使用方式,因为它可以精确地控制锁的粒度,只锁定必要的代码,从而提高程序的并发性能。
synchronized 的底层原理:Monitor

synchronized 关键字的实现是基于 JVM 层面的 **Monitor(监视器锁)**机制。每个 Java 对象都可以看作是一个 Monitor。

当一个线程试图获取一个对象的锁时,它实际上是在尝试获取该对象关联的 Monitor 的所有权。

Monitor 内部主要包含几个关键部分:

  • _owner: 一个指针,指向当前持有该 Monitor 的线程。
  • _entryList: 一个等待队列,存放所有尝试获取锁但失败、进入**阻塞 (BLOCKED)**状态的线程。
  • _waitSet: 另一个等待队列,存放调用了该对象 wait() 方法、进入**等待 (WAITING)**状态的线程。
  • _recursions: 一个计数器,用于支持锁的可重入

执行流程(简化版)

  1. monitorenter: 线程执行到 synchronized 代码块的入口时,JVM 会插入一条 monitorenter 字节码指令。线程尝试获取 Monitor 的所有权。
    1. 如果 Monitor 的 _ownernull,线程成功获取锁,将 _owner 指向自己,计数器 _recursions 设为1。
    2. 如果 _owner 是当前线程自己,说明是重入_recursions 计数加1。
    3. 如果 _owner 是其他线程,线程获取锁失败,被放入 _entryList 队列中阻塞等待。
  2. monitorexit: 线程执行完同步代码块后(无论是正常退出还是异常退出),JVM 会插入一条 monitorexit 字节码指令。
    1. 线程将 _recursions 计数减1。
    2. 如果计数变为0,线程释放 Monitor 的所有权(将 _owner设为 null),并唤醒 _entryList 中的一个等待线程来竞争锁。
锁消除

锁消除是一种非常激进的优化。JIT 编译器在运行时,通过对代码进行逃逸分析,判断一个锁对象是否只被一个线程访问,从未发布到其他线程

如果编译器能证明一个锁对象不会“逃逸”出当前线程的作用域,也就是说,不可能有其他线程来竞争这个锁,那么对这个对象的加锁操作就是完全多余的。这时,编译器就会直接消除这些加锁和解锁的指令,就好像它们从未存在过一样。

逃逸分析:简单来说,就是分析一个对象的动态作用域。如果一个对象在方法中被定义后,可能被外部方法所引用(比如作为返回值、或者赋值给类变量),则认为该对象“逃逸”了。反之,则认为它“未逃逸”。

  1. 经典例子

一个最典型的例子就是 StringBufferVector 这类线程安全的类在方法内部作为局部变量使用。

public class LockElisionDemo {

    // 这个方法看起来有锁,但JIT可能会把它消除掉public String createAndAppendString(String s1, String s2, String s3) {
  
        // sb 是一个局部变量,它的引用不会“逃逸”出这个方法// 其他线程根本不可能访问到这个 sb 对象
        StringBuffer sb = new StringBuffer(); 

        // StringBuffer的append方法是synchronized的// 理论上每次调用都会加锁
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);

        return sb.toString();
    }
}

分析

  1. StringBufferappend 方法是 synchronized 方法,锁对象是 sb 实例。
  2. createAndAppendString 方法中,sb 是一个方法内的局部变量
  3. JIT 编译器通过逃逸分析发现,sb 这个对象的引用从未离开过 createAndAppendString 方法的作用域。它没有被赋值给类的成员变量,也没有作为返回值返回(返回的是 sb.toString()创建的新字符串)。
  4. 结论:这个 sb 对象是线程私有的,永远不可能被其他线程访问到。因此,对 sb 的所有 synchronized 加锁操作都是不必要的。
  5. 优化:JIT 编译器会大胆地将 append 方法内部的 monitorentermonitorexit 指令全部移除,这个过程就叫锁消除

优化后的代码,在运行时就等同于使用非线程安全的 StringBuilder,性能得到了极大的提升

锁粗化

如果 JIT 编译器发现有一系列连续的操作都在反复地、频繁地同一个对象进行加锁和解锁,即使这些操作之间没有竞争,频繁的加锁/解锁本身也会带来性能开销。

在这种情况下,编译器会认为这种“细粒度”的锁反而成了累赘。于是,它会智能地将这些连续的加锁/解锁操作合并成一个更大范围的锁,只在整个操作序列的开始加一次锁,在结束时解一次锁。这个过程就叫锁粗化

public class LockCoarseningDemo {

    private final Object lock = new Object();
    private int count = 0;

    // 未优化前的代码逻辑
    public void processInLoop_Before() {
        for (int i = 0; i < 10000; i++) {
            // 在循环内部,每次都加锁和解锁
            synchronized (lock) {
                count++;
            }
        }
    }

    // JIT编译器可能将其粗化为下面的样子
    public void processInLoop_After() {
        // 将锁的范围扩大到整个循环之外
        synchronized (lock) {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    }
}

分析

  1. processInLoop_Before 方法中,synchronized 块在循环体内部。
  2. 这意味着程序将执行 10000次加锁10000次解锁 操作。
  3. JIT 编译器检测到这种模式后,会判断将锁的范围扩大到整个循环之外,并不会改变程序的同步语义。
  4. 优化:编译器会将 synchronized (lock) 这行代码“移动”到 for 循环的外面,形成 processInLoop_After 的效果。
  5. 结果:加锁和解锁操作从 10000 次骤降到 1 次,性能显著提升。

这似乎与我们常说的“尽量减小锁的粒度”相违背。实际上,这两者并不矛盾:

  • 减小锁粒度:是程序员在编写代码时应该遵循的原则,目的是为了减少线程不必要的等待时间,提高并发度。
  • 锁粗化:是 JIT 编译器在运行时进行的一种优化,它针对的是“没有并发竞争,但加解锁开销过大”的特定场景,目的是为了减少加解锁操作本身的性能损耗。
锁升级:从“轻”到“重”的性能优化

早期的 synchronized 被诟病性能差,因为它直接依赖操作系统的互斥量(Mutex),每次加锁都会涉及用户态到内核态的切换,开销巨大。

从 JDK 1.6 开始,JVM 引入了锁升级机制,这是一种自适应的优化,目的是根据锁的竞争情况,动态地选择合适的锁状态,以最小的代价实现同步。

锁的状态记录在 Java 对象头的 Mark Word 中,升级路径是单向的,只能升级不能降级。

  1. 无锁状态 (Unlocked)
    1. 对象刚创建,没有任何线程竞争。
  2. 偏向锁 (Biased Locking)
    1. 适用场景:绝大多数情况下,锁不仅没有竞争,而且总是由同一个线程反复获取。
    2. 机制:当第一个线程获取锁时,JVM 会把线程 ID 记录在 Mark Word 中。之后,该线程再次进入同步块时,无需任何同步操作,只需简单检查一下线程 ID 是否匹配。这是最高效的状态。
    3. 升级:当有另一个线程尝试获取该锁时,偏向锁模式被撤销,锁升级为轻量级锁。
  3. 轻量级锁 (Lightweight Locking)
    1. 适用场景:存在锁竞争,但竞争不激烈,线程持有锁的时间很短。
    2. 机制:线程通过 CAS (Compare-And-Swap) 操作尝试获取锁。失败的线程不会立即阻塞,而是会进行自旋 (Spinning),即执行一个空循环,期待锁能很快被释放。自旋避免了线程上下文切换的开销。
    3. 升级:如果自旋一定次数后(或竞争加剧),锁仍未被释放,锁就会膨胀为重量级锁。
  4. 重量级锁 (Heavyweight Locking)
    1. 适用场景:锁竞争激烈,线程等待时间长。
    2. 机制:这就是传统的 Monitor 锁。获取不到锁的线程会被放入等待队列并阻塞,交由操作系统调度。虽然开销最大,但它不会让线程空转消耗 CPU。

0

评论区