目录

Java中的锁,锁的究竟是什么?

锁的使用

Jvm中有两种线程同步方案:synchronized 和 Lock。

synchronized 的使用

java 复制代码
public class Counter {

    private int count = 0;

    // 同步方法
//    public synchronized void increment() {
//        count++;
//    }

    // 非同步方法
    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        // 等待两个线程执行完
        t1.join();
        t2.join();

        System.out.println("最终计数: " + counter.getCount()); // 期望输出 2000
    }
}

synchronized使用场景:

使用场景 锁对象 适用情况
同步方法 this 保护整个实例方法
同步代码块 自定义对象 保护部分代码,灵活性更高
同步静态方法 Class 对象 保护类级别的静态资源
同步类对象 Class 对象 在实例方法中保护静态资源
同步不同实例 各自的 this 实例级独立同步,不干扰其他实例

Lock 的使用

java 复制代码
public class Counter {

    private static final ReentrantLock lock = new ReentrantLock();

    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        // 等待两个线程执行完
        t1.join();
        t2.join();

        System.out.println("最终计数: " + counter.getCount()); // 输出 2000
    }
}

CAS 的使用

CAS 是一种无锁的原子操作:只有当某个值等于预期值时,才将其更新为新值。

java 复制代码
if (内存中的值 == 期望值) {
    更新为新值;
    返回 true;
} else {
    不更新;
    返回 false;
}

例如:

java 复制代码
AtomicInteger atomicInt = new AtomicInteger(0);
boolean success = atomicInt.compareAndSet(0, 1); // 如果当前值是0,则更新为1

通常会被自旋重试,直到成功::

java 复制代码
while (!compareAndSwap(expected, newValue)) {
    expected = getCurrent(); // 重新获取
}

java源码:AbstractQueuedSynchronizer#compareAndSetState

CAS的优点:在多线程环境下减少上下文切换和阻塞。

锁的本质

锁的本质:通过原子性的对用一个共享变量进行修改,记录是否获取锁的状态。

MESA 模型

Mesa 模型源自 Mesa 编程语言,而 Mesa 是 Xerox PARC 为开发 Alto 计算机系统设计的语言。当时,研究人员需要一种可靠的方式来处理多线程程序中的并发问题。Mesa 模型基于"监视器"(Monitor)概念,但对其进行了优化,解决了早期监视器实现中的一些局限性。

管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发

Lock 锁住的是什么?

java 复制代码
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;
}

synchronized 锁住的是什么?

synchronized 锁的是对象 (而不是代码本身),具体来说是某个对象的监视器 (monitor)

每个 Java 对象都有一个与之关联的监视器(monitor),它是一个内部的锁结构,用于协调多线程访问。当一个线程进入 synchronized 块时,它必须先获取该对象的监视器,执行完后释放监视器。

监视器

监视器和字节码

以如下代码为例:

java 复制代码
public class SynchronizedTest {

    private static final Object object = new Object();

    public static void main(String[] args) {
        synchronized (object) {
            System.out.println("synchronized 代码块");
        }
    }
}

这个类的字节码:

java 复制代码
Classfile /SynchronizedTest.class
  Last modified Apr 7, 2025; size 542 bytes
  MD5 checksum ...
public class SynchronizedTest
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER

Constant pool:
   #1 = Class              #7             // SynchronizedTest
   #2 = Fieldref           #1.#8          // SynchronizedTest.object:Ljava/lang/Object;
   #3 = Fieldref           #9.#10         // java/lang/System.out:Ljava/io/PrintStream;
   #4 = String             #11            // synchronized 代码块
   #5 = Methodref          #12.#13        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #14            // java/lang/Object
   #7 = Utf8               SynchronizedTest
   #8 = NameAndType        #15:#16        // object:Ljava/lang/Object;
   #9 = Class              #17            // java/lang/System
  #10 = NameAndType        #18:#19        // out:Ljava/io/PrintStream;
  #11 = Utf8               synchronized 代码块
  #12 = Class              #20            // java/io/PrintStream
  #13 = NameAndType        #21:#22        // println:(Ljava/lang/String;)V
  #14 = Utf8               java/lang/Object
  #15 = Utf8               object
  #16 = Utf8               Ljava/lang/Object;
  #17 = Utf8               java/lang/System
  #18 = Utf8               out
  #19 = Utf8               Ljava/io/PrintStream;
  #20 = Utf8               java/io/PrintStream
  #21 = Utf8               println
  #22 = Utf8               (Ljava/lang/String;)V
  ...

{
  private static final java.lang.Object object;
    descriptor: Ljava/lang/Object;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL

  static {};
    Code:
       0: new           #6                  // class java/lang/Object
       3: dup
       4: invokespecial #23                 // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field object:Ljava/lang/Object;
      10: return
    LineNumberTable:
      line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
       0: getstatic     #2                  // Field object:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #4                  // String synchronized 代码块
      11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           5    16    19   any
    LineNumberTable:
      line 6: 0
      line 7: 6
      line 8: 14
      line 9: 24
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      25     0  args   [Ljava/lang/String;
          5      20     1   obj   Ljava/lang/Object;

}

JVM 在底层通过 monitorentermonitorexit 指令实现 synchronized 的同步机制。这些指令操作对象的监视器。下面是原理的简化和对应的伪代码解释:

  • 当线程进入 synchronized (object) 时:
    1. 执行 monitorenter 指令,尝试获取 object 的监视器。
    2. 如果监视器未被占用,线程成功获取并进入代码块。
    3. 如果监视器已被其他线程持有,当前线程阻塞等待。
  • 当线程离开 synchronized 块时:
    1. 执行 monitorexit 指令,释放 object 的监视器。
    2. 其他等待的线程可以竞争获取监视器。

监视器的C++代码:

c++ 复制代码
class ObjectMonitor {
private:
    volatile intptr_t _header;         // 对象头的 Mark Word
    void* _object;                     // 指向被锁的 Java 对象
    pthread_mutex_t _mutex;            // 互斥锁,用于保护监视器
    pthread_cond_t _cond;              // 条件变量,用于线程等待和唤醒
    Thread* _owner;                    // 当前持有锁的线程
    ObjectWaiter* _waiters;            // 等待队列(等待锁的线程)
    int _recursions;                   // 重入次数(支持锁重入)

public:
    ObjectMonitor() {
        _header = 0;
        _object = nullptr;
        _owner = nullptr;
        _waiters = nullptr;
        _recursions = 0;
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ~ObjectMonitor() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    void enter(Thread* self);          // 进入监视器(获取锁)
    void exit(Thread* self);           // 退出监视器(释放锁)
    void wait(Thread* self);           // 等待
    void notify(Thread* self);         // 通知一个等待线程
    void notifyAll(Thread* self);      // 通知所有等待线程
};

字段说明

  • _mutex:互斥锁,确保同一时刻只有一个线程操作监视器。
  • _cond:条件变量,用于实现 wait() 和 notify() 的等待/唤醒机制。
  • _owner:记录当前持有锁的线程。
  • _recursions:支持锁的可重入性(同一个线程可以多次获取锁)。
  • _waiters:等待队列,存储因锁竞争而阻塞的线程。
监视器池

当 synchronized 锁升级为重量级锁时,JVM 需要一个机制来管理锁的竞争,包括记录持有锁的线程、维护等待队列、处理线程的阻塞和唤醒等。由于锁竞争可能发生在多个对象上,JVM 不可能为每个对象都预先分配一个完整的监视器结构(内存开销太大)。因此,JVM 使用一个 ObjectMonitor 池 来动态分配和重用监视器对象,以优化内存使用和性能。

在 HotSpot JVM 中,ObjectMonitor 是一个 C++ 类,定义在 hotspot/src/share/vm/runtime/objectMonitor.hpp 和 objectMonitor.cpp 中。JVM 通过一个池(或类似的内存管理机制)来维护这些 ObjectMonitor 实例。

监视器池的实现
  • 全局池:JVM 维护一个全局的 ObjectMonitor 池,通常是一个链表或类似的数据结构,用于存储空闲的 ObjectMonitor 实例。

    • 在源码中,这由 ObjectSynchronizer 类管理(位于 synchronizer.cpp)。

    • 示例字段(简化):

      C++ 复制代码
      static ObjectMonitor* gFreeMonitorList; // 空闲监视器链表
      static volatile intptr_t gMonitorFreeCount; // 空闲监视器数量
  • 初始化

    • JVM 启动时会预分配一定数量的 ObjectMonitor 实例,放入空闲池中。
    • 数量通常由 JVM 参数(如 -XX:MonitorBound)控制,默认值取决于系统资源。

    动态分配

    • 当需要新的 ObjectMonitor 时,JVM 从池中取出一个空闲实例。
    • 如果池为空,则动态分配一个新的实例(通过 new ObjectMonitor())并初始化。

    回收

    • 当锁释放且不再需要某个 ObjectMonitor 时,它会被清理并放回池中,以便重用。
监视器池分配步骤
  1. 检查锁状态:

    • JVM 检查目标对象(如 MyClass.class)的 Mark Word。
    • 如果是轻量级锁且竞争加剧,进入锁膨胀流程。
  2. 从池中获取:

    • 调用 ObjectSynchronizer::inflate:

      c++ 复制代码
      ObjectMonitor* ObjectSynchronizer::inflate(oop obj) {
          if (gFreeMonitorList != NULL) {
              // 从空闲池中取出一个监视器
              ObjectMonitor* monitor = gFreeMonitorList;
              gFreeMonitorList = monitor->next_free();
              gMonitorFreeCount--;
              monitor->recycle(); // 重置状态
              return monitor;
          } else {
              // 池为空,分配新实例
              return new ObjectMonitor();
          }
      }
    • 参数 obj 是被锁的 Java 对象(这里是 MyClass.class)。

  3. 初始化监视器:

    • 将 ObjectMonitor 的 _object 字段设置为 MyClass.class 的指针。
    • 清空 _owner、_EntryList 等字段,准备接收线程。
  4. 将 ObjectMonitor 地址写入 Mark Word

在分配 ObjectMonitor 后,JVM 需要将其地址与 Class 对象关联起来,这一过程通过更新 Mark Word 完成。

Mark Word 与监视器

Mark Word 是一个动态结构,其内容根据锁状态变化:

  • 无锁:存储哈希码或 GC 信息。
  • 偏向锁:存储线程 ID 和偏向标志。
  • 轻量级锁:指向线程栈中的锁记录。
  • 重量级锁:存储指向 ObjectMonitor 的指针。

在 64 位 JVM 中,Mark Word 通常是 64 位,格式如下(简化):

text 复制代码
|-----------------------------------------------|
| 锁状态 | 内容                                      |
|--------|------------------------------------------|
| 无锁   | hash:31 | age:4 | 0 | 01                |
| 偏向锁 | thread:54 | epoch:2 | 1 | 01            |
| 轻量级 | ptr_to_lock_record:62 | 00              |
| 重量级 | ptr_to_monitor:62 | 10                  |
|-----------------------------------------------|
  • 重量级锁状态:最后两位是 10,其余位存储 ObjectMonitor 的地址。

写入过程

  1. 锁膨胀:

    • ObjectSynchronizer::inflate 返回 ObjectMonitor 实例后,JVM 更新 Mark Word。

    • 示例代码(简化):

      c++ 复制代码
      void inflate_and_associate(oop obj, ObjectMonitor* monitor) {
          markOop mark = obj->mark(); // 获取当前 Mark Word
          if (mark->is_neutral()) {   // 无锁状态
              markOop new_mark = (markOop)(monitor | WEIGHTED_LOCK_FLAG);
              if (Atomic::cmpxchg_ptr(new_mark, obj->mark_addr(), mark) == mark) {
                  monitor->set_object(obj); // 关联对象
              }
          } else if (mark->has_locker()) { // 轻量级锁
              // 撤销轻量级锁,更新为重量级锁
              markOop new_mark = (markOop)(monitor | WEIGHTED_LOCK_FLAG);
              Atomic::cmpxchg_ptr(new_mark, obj->mark_addr(), mark);
          }
      }
    • 使用 CAS(cmpxchg_ptr)原子地将 ObjectMonitor 地址写入 Mark Word。

  2. 标志位设置:

    • 将 Mark Word 的低两位设置为 10,表示重量级锁。
    • 高位存储 ObjectMonitor 的内存地址。
  3. 关联完成:

    • 此时,MyClass.class 的 Mark Word 指向 ObjectMonitor,线程通过该指针访问监视器。
监视器的后续管理
  • 线程竞争:
    • 第一个线程获取锁,ObjectMonitor 的 _owner 设置为该线程。
    • 其他线程加入 _EntryList,通过 pthread_mutex_t 和 futex 阻塞。
  • 锁释放:
    • 线程退出 synchronized 块,调用 monitorexit。
    • JVM 检查 _recursions,若为 0,则释放 ObjectMonitor,唤醒 _EntryList 中的线程。
  • 回收:
    • 如果 ObjectMonitor 不再需要,JVM 将其放回空闲池(gFreeMonitorList)。

pthread_mutex_t

pthread_mutex_t 是 POSIX 线程库 (Pthreads) 提供的一种互斥锁类型,用于在多线程程序中实现同步。它的源码实现依赖于具体的操作系统和 C 库(如 glibc 或 musl),通常是用 C 语言编写,并结合底层系统调用(如 Linux 的 futex)实现的。由于 Pthreads 是标准接口,其具体实现因平台而异,这里以 Linux 上 glibc 的实现为例。

在 glibc(GNU C Library)中,pthread_mutex_t 是一个结构体,定义在 pthread.h 或相关的内部头文件中。以下是其简化形式(基于 glibc 2.34 的源码,路径如 nptl/sysdeps/pthread/pthread.h):

c 复制代码
typedef union {
    struct __pthread_mutex_s {
        int __lock;           // 锁的状态(0 表示未锁,>0 表示已锁)
        unsigned int __count; // 重入计数(对于递归锁)
        int __owner;          // 持有锁的线程 ID
        unsigned int __nusers;// 使用计数(调试用)
        int __kind;           // 锁的类型(普通、递归、错误检查等)
        short __spins;        // 自旋计数(优化用)
        short __elision;      // 锁消除标志(硬件优化)
        __pthread_list_t __list; // 等待队列(指向等待线程)
    } __data;
    char __size[__SIZEOF_PTHREAD_MUTEX_T]; // 固定大小的字节数组
    long int __align;                      // 对齐用
} pthread_mutex_t;

字段说明

  • __lock:核心锁状态,通常与 futex 系统调用交互。
  • __count:记录重入次数(仅对递归锁有效)。
  • __owner:记录持有锁的线程 ID(用于调试或错误检查锁)。
  • __kind:锁的类型(PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_RECURSIVE 等)。
  • __list:等待队列,用于阻塞等待的线程。
  • __size:确保结构体大小和内存对齐。
  • __align:确保结构体大小和内存对齐。

pthread_mutex_lock

pthread_mutex_lock是pthread中的函数。

C++ 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex) {
    struct __pthread_mutex_s *imutex = &mutex->__data;
    pid_t tid = gettid(); // 获取当前线程 ID

    // 检查锁类型并处理重入
    if (imutex->__kind == PTHREAD_MUTEX_RECURSIVE && imutex->__owner == tid) {
        imutex->__count++;
        return 0;
    }

    // 快速路径:尝试获取锁
    if (atomic_compare_and_swap(&imutex->__lock, 0, 1) == 0) {
        imutex->__owner = tid;
        imutex->__count = 1;
        return 0;
    }

    // 慢路径:锁被占用,使用 futex 等待
    while (atomic_compare_and_swap(&imutex->__lock, 1, 2) != 0) {
        // 这里说的就是内核态
        syscall(SYS_futex, &imutex->__lock, FUTEX_WAIT, 2, NULL, NULL, 0);
    }

    // 成功获取锁
    imutex->__lock = 1;
    imutex->__owner = tid;
    imutex->__count = 1;
    return 0;
}
  • 逻辑
    1. 快速路径:通过原子操作(CAS)尝试将 __lock 从 0 改为 1,若成功则获取锁。
    2. 慢路径:如果锁被占用,进入竞争状态,使用 futex 系统调用阻塞线程。
    3. 重入支持:对于递归锁,检查线程 ID 并增加 __count。

pthread_mutex_unlock

c++ 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex) {
    struct __pthread_mutex_s *imutex = &mutex->__data;
    pid_t tid = gettid();

    // 检查是否为持有者
    if (imutex->__owner != tid) {
        return EPERM; // 权限错误
    }

    // 处理重入
    if (imutex->__kind == PTHREAD_MUTEX_RECURSIVE && --imutex->__count > 0) {
        return 0;
    }

    // 释放锁
    imutex->__owner = 0;
    imutex->__count = 0;
    if (atomic_swap(&imutex->__lock, 0) == 2) {
        // 如果有等待者,唤醒一个线程
        syscall(SYS_futex, &imutex->__lock, FUTEX_WAKE, 1, NULL, NULL, 0);
    }

    return 0;
}

futex

什么是 futex

futex 的全称是 Fast Userspace Mutex ,它是 Linux 系统中的一个系统调用接口 ,可以通过 syscall 访问,属于 Linux 提供给用户态线程库、语言运行时(JVM、glibc、Go runtime等)用来实现锁的底层原语

当在用户态调用 syscall(SYS_futex, ...) 后,操作系统都做了些什么事?

text 复制代码
用户程序
   |
   | syscall(SYS_futex, ...)
   ↓
CPU 切换到内核态(特权级 0)
   ↓
内核根据 syscall 编号查找对应函数(如 sys_futex)
   ↓
执行 futex 内核代码(内核栈、调度、睡眠、唤醒等)
   ↓
返回结果,切换回用户态

通过监视器获取锁

监视器获取锁:

c++ 复制代码
void ObjectMonitor::enter(Thread* self) {
    // 如果当前线程已经是持有者,增加重入计数
    if (_owner == self) {
        _recursions++;
        return;
    }

    // 尝试获取互斥锁
    pthread_mutex_lock(&_mutex);

    // 如果锁未被占用,设置当前线程为持有者
    if (_owner == nullptr) {
        _owner = self;
        _recursions = 1;
    } else {
        // 锁被占用,将当前线程加入等待队列并阻塞
        ObjectWaiter waiter(self);
        _waiters->append(&waiter);
        pthread_cond_wait(&_cond, &_mutex); // 等待被唤醒
    }

    pthread_mutex_unlock(&_mutex);
}

逻辑

  1. 检查是否重入:如果是当前线程,直接增加 _recursions。
  2. 使用 pthread_mutex_lock 保护操作。
  3. 如果锁空闲,占有锁;否则加入等待队列并阻塞。
c++ 复制代码
void ObjectMonitor::exit(Thread* self) {
    pthread_mutex_lock(&_mutex);

    // 确保是持有者调用
    if (_owner != self) {
        pthread_mutex_unlock(&_mutex);
        throw "IllegalMonitorStateException";
    }

    // 减少重入计数
    _recursions--;
    if (_recursions == 0) {
        _owner = nullptr; // 完全释放锁
        if (_waiters != nullptr) {
            // 唤醒一个等待线程
            ObjectWaiter* waiter = _waiters->removeFirst();
            pthread_cond_signal(&_cond);
        }
    }

    pthread_mutex_unlock(&_mutex);
}

逻辑

  1. 检查调用者是否持有锁。
  2. 减少重入计数,若为 0,则释放锁并唤醒等待队列中的线程。

如何实现CAS?

硬件的实现CAS

__atomic_compare_exchange_n 是编译器内置函数,其实现依赖于硬件指令:

  • x86/x86_64:使用 cmpxchg(Compare and Exchange)指令。

    • 示例汇编:

      asm 复制代码
      mov eax, [expected]    ; 加载期望值
      lock cmpxchg [ptr], desired ; 比较并交换
  • ARM:使用 ldrex 和 strex(Load-Exclusive 和 Store-Exclusive)指令对。

  • 编译器生成:GCC 根据目标架构自动生成对应的原子指令。

不是所有的硬件都天然支持原子指令,尤其是早期的简单处理器或某些嵌入式系统。不过,现代通用处理器(如 x86、ARM、RISC-V 等)通常都提供原子指令支持,因为多线程和并发编程的需求日益增加。如果硬件不支持原子指令,__atomic_compare_exchange_n 等原子操作的实现需要依赖软件模拟或操作系统支持。

硬件是否都提供原子指令?

  • 支持原子指令的硬件:
    • x86/x86_64:提供 cmpxchg(Compare and Exchange)、lock 前缀等指令。
    • ARM:提供 ldrex/strex(Load-Exclusive/Store-Exclusive)指令对(ARMv6 及以上)。
    • RISC-V:提供原子扩展(A 扩展),包括 amo(Atomic Memory Operation)指令。
    • PowerPC:提供 lwarx/stwcx(Load and Reserve/Store Conditional)。
  • 不支持原子指令的硬件:
    • 早期的简单处理器(如 8086、某些 8 位微控制器)没有原生的原子指令。
    • 一些低端嵌入式系统(如老式 AVR 或 PIC 微控制器)缺乏硬件支持。
    • 某些特殊用途处理器可能故意省略复杂指令以简化设计。
  • 趋势:现代处理器几乎都支持原子指令,因为多核和并发是标配。但在极低功耗或极简设计的场景中,硬件可能不提供。

硬件支持原子指令时的实现

当硬件提供原子指令时,__atomic_compare_exchange_n 直接映射到这些指令:

  • x86 示例:

    asm 复制代码
    mov eax, [expected]         ; 加载期望值
    lock cmpxchg [ptr], desired ; 原子比较并交换
    setz al                    ; 设置返回值(成功为 1,失败为 0)

    lock 确保操作不可中断。

  • ARM 示例:

    asm 复制代码
    ldrex r1, [ptr]         ; 加载当前值
    cmp r1, expected        ; 比较
    bne fail                ; 不相等则失败
    strex r2, desired, [ptr]; 尝试存储新值
    cmp r2, #0              ; 检查是否成功
    beq success             ; 成功跳转
    fail:

这种实现效率高,直接利用硬件的原子性。

硬件如何保障原子性的?

原子性的硬件实现:三大关键机制

1. 原子指令(Atomic Instructions)

x86 示例:LOCK CMPXCHG

asm 复制代码
LOCK CMPXCHG [mem], reg
  • CMPXCHG: 比较并交换
  • LOCK 前缀:告诉 CPU 保证这个指令是原子的
  • CPU 会自动协调以下:
    • 禁止其他核心访问目标地址所在缓存行
    • 保证整个操作不可中断(包括异常或中断)

ARM 示例:LDREX / STREX

  • LDREX:加载一个值并设置"本地监视器"
  • STREX:尝试写入,如果期间该地址被其他核修改,则写入失败
  • 这是一种乐观锁 + 回退机制,通常配合自旋使用
2. 总线锁(Bus Locking)机制(较早期)

早期 CPU 使用一种粗暴方式来实现原子性:

  • 在执行带 LOCK 的指令时,锁住整个内存总线
  • 其他 CPU 在此期间无法发出内存访问请求
  • 实现方式:
    • 设置 LOCK# 引脚,阻止其他核访问共享内存

缺点:影响整个平台的并发性能。

3. 缓存一致性协议(MESI)

现代多核 CPU 更优雅地用缓存系统配合原子指令:

什么是 MESI 协议?

  • 一种多核处理器缓存一致性协议,保证各核心缓存的数据一致
  • 每个缓存行的状态有 4 种:
    • Modified(已修改)
    • Exclusive(独占)
    • Shared(共享)
    • Invalid(失效)

原子性通过"缓存行独占"实现

  • 原子指令(如 LOCK CMPXCHG)执行时,会:
    1. 把目标地址所在的缓存行标记为"独占"或"已修改"
    2. 临时禁止其他核心读写此缓存行(通过总线探测或总线广播)
    3. 确保操作完成前没有人能访问

所以:现代 CPU 不再锁总线,而是锁缓存行,提高了并发性能。

示例:一次 CAS 的原子流程(现代 CPU)

假设有两个核心同时执行:

java 复制代码
compareAndSwap(address, expected, newValue)

核心 A 的流程:

  1. 从内存把 address 处的值读到本地缓存
  2. LOCK CMPXCHG 尝试更新值
  3. CPU 使用缓存一致性协议通知其他核**"我要独占这行缓存"**
  4. 其他核心必须让出该缓存行(失效状态)
  5. 核心 A 修改完成后,写入主内存(或延迟写)

如果期间有其他核也尝试修改,就会失败(触发重试)

关键支持技术(背后的底层机制)

技术 作用说明
原子指令(如 CMPXCHG) 执行原子读-比较-写
LOCK 前缀 确保缓存一致性协议激活
总线锁(老机制) 临时阻断其他访问
MESI 协议 保证各核心对缓存一致理解
内存屏障(Memory Barrier) 防止 CPU 指令重排序打乱操作顺序
本地监视器(ARM) 检测共享内存是否被其他核写入

总结:硬件原子性的实现机制

层级 技术 作用
指令层 CMPXCHG, LDREX/STREX 提供原子读改写
微架构层 LOCK 前缀 / 内存屏障 保证原子执行,不被打断
缓存层 MESI 协议 多核环境下的缓存一致性
总线层 总线锁(早期) 粗暴保障内存独占访问权

软件模拟实现CAS

硬件不支持原子指令时,__atomic_compare_exchange_n 如何实现?

如果硬件没有原子指令,GCC 或其他编译器需要通过软件手段模拟原子性。以下是可能的实现方式:

示例方法 :忙等待(自旋锁,简单但低效)

  • 思路:通过循环检查和修改变量,模拟原子性。

  • 伪代码:

    c 复制代码
    static volatile int spinlock = 0;
    
    bool __atomic_compare_exchange_n(int *ptr, int *expected, int desired,
                                     bool weak, int success_memorder, int failure_memorder) {
        while (spinlock != 0) {} // 忙等待
        spinlock = 1;            // 获取锁
        bool success = false;
        if (*ptr == *expected) {
            *ptr = desired;
            success = true;
        } else {
            *expected = *ptr;
        }
        spinlock = 0;            // 释放锁
        return success;
    }
  • 缺点:效率低下,浪费 CPU 资源,仅适用于简单场景。

锁实现原理总结

synchronized 通过JVM指令自动获取锁和释放锁,底层通过Monitor对象中的属性pthread_mutex_t 的字段__lock进行CAS操作实现。

Lock 通过对AQS中的字段state进行CAS操作实现。

两者本质的区别:

  • synchronized 由JVM实现了MESA模型,Lock 通过Java代码实现了MESA模型
  • synchronized 会调用 syscall 进入内核等待,Lock不会
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
字节源流23 分钟前
【微服务】基础概念
后端·spring·spring cloud
uhakadotcom2 小时前
归因工具:了解国内外顶级产品
算法·面试·github
菜鸟起航ing4 小时前
【Java面试系列】Spring Boot中自动配置原理与自定义Starter开发实践详解 - 3-5年Java开发必备知识
java·spring boot·面试·自动配置·自定义starter
Asthenia04124 小时前
手搓Redis之为Zset而作的跳表实现逻辑梳理与面试问题解析
后端
uhakadotcom4 小时前
了解Dapr:构建分布式应用的强大工具
后端·面试·github
橘猫云计算机设计4 小时前
ASP.NET图书馆借阅系统(源码+lw+部署文档+讲解),源码可白嫖!
java·数据库·后端·爬虫·小程序·毕业设计·asp.net
uhakadotcom4 小时前
Kibana:数据分析和可视化的强大工具
后端·面试·github
摆烂工程师4 小时前
Grok3 支持 DeeperSearch 的免费可用次数的查询了
前端·后端·程序员
uhakadotcom4 小时前
Grafana:数据可视化和监控平台的强大工具
后端·面试·github