并发之锁介绍

涉及并发操作时,必然会面临共享资源的抢占问题,而正是解决这一问题的核心同步机制。

核心原理就是把并发操作,改为顺序执行操作。

一、简介

        “锁”是一种用于控制对共享资源访问的机制,广泛应用于计算机系统、操作系统、数据库、多线程编程、分布式系统等领域。其核心目标是保证数据一致性、防止竞态条件(Race Condition)和避免并发错误

举个例子:

        想象一个公共洗澡间,一次只能容纳一个人使用。当有人进入时,会从内部上锁,防止其他人闯入;只有当前使用者洗完离开并解锁后,下一位人才能进入。

        这正是锁(Lock)的基本原理:当多个线程(或进程)竞争同一个有限且不可同时共享的资源(如内存、文件、设备等)时,通过“加锁”确保同一时刻仅有一个访问者,从而避免混乱和数据错误;使用完毕后“释放锁”,允许其他等待者继续使用。

软件锁的发展:

时间里程碑事件 / 技术贡献者 / 系统核心思想与意义
1965年信号量(Semaphore) 提出Edsger W. Dijkstra首次形式化解决进程互斥与同步问题,引入 P/V 操作(wait/signal),成为并发控制基石。
1967年管程(Monitor) 概念提出Per Brinch Hansen(后由 C.A.R. Hoare 完善)将共享数据与操作封装在一起,自动提供互斥访问,影响后续高级语言设计(如 Java)。
1970年代初临界区(Critical Section) 与 互斥锁(Mutex) 在操作系统中实现Unix、THE 操作系统内核提供基本互斥原语,用于保护共享资源,成为多任务系统标配。
1981年Peterson 算法发表Gary L. Peterson纯软件实现双线程互斥,无需硬件原子指令,在理论层面证明软件可解决互斥问题。
1980s自旋锁(Spinlock) 广泛用于多处理器内核BSD Unix、Linux 内核早期版本适用于短临界区,避免上下文切换开销,但消耗 CPU。
1980s–1990s读写锁(Read-Write Lock) 引入多种 Unix 变体支持多读者并发,提升读密集型场景性能。
1990s条件变量(Condition Variable) 成为标准同步原语POSIX Threads (pthreads)与互斥锁配合,实现线程等待/唤醒机制(如生产者-消费者模型)。
1995年Java 1.0 引入 synchronized 关键字Sun Microsystems基于 JVM 的内置管程机制,自动加锁/释放,简化线程安全编程。
2004年JDK 5 发布 java.util.concurrent 包Doug Lea 等引入 ReentrantLockReadWriteLockSemaphore 等高级锁,支持公平性、可中断、超时等特性。
2000s 中期无锁编程(Lock-Free Programming) 兴起多家高性能系统(如数据库、交易系统)利用 CPU 原子指令(如 CAS - Compare-and-Swap)构建无锁队列、计数器,避免锁开销与死锁。
2010s分布式锁成为微服务基础设施Redis、ZooKeeper、etcd解决跨节点资源竞争问题,如:Redis 的 SET key value NX PX + RedLock;ZooKeeper 临时顺序节点。
2010s–至今替代锁的并发模型普及Go(goroutine + channel)、Erlang(Actor)、Rust(所有权模型)不要通过共享内存来通信,而要通过通信来共享内存”,减少对传统锁的依赖。

二、锁分类

    2.1 按重量级分类

    重量级层级包含的锁 / 同步机制说明
    1. 无锁(Lock-Free / No Locking)• CAS(Compare-And-Swap)原子操作
    • AtomicInteger / AtomicReference 等原子类
    • 无锁队列(Lock-Free Queue)
    • 无锁栈、无锁哈希表
    • Disruptor 环形缓冲区
    不使用传统锁,依赖 CPU 提供的原子指令(如 x86 的 cmpxchg)实现线程安全。线程永不阻塞,但冲突时需重试。适用于简单状态更新或高性能并发数据结构。
    2. 轻量级锁(Lightweight Lock)• 自旋锁(Spin Lock)
    • JVM 轻量级锁(Lightweight Locking)
    • Ticket Lock(票锁)
    • MCS Lock
    • 读写自旋锁(Read-Write Spinlock)
    • 用户态互斥(User-space Mutex,无竞争时)
    在用户态通过忙等(自旋)或 CAS 实现互斥,不触发线程挂起或内核切换。适合临界区极短、竞争低的场景。高竞争时通常会“膨胀”为重量级锁。
    3. 重量级锁(Heavyweight Lock)• 操作系统 Mutex(如 pthread_mutex)
    • JVM 重量级锁(即 synchronized 在竞争激烈时的状态)
    • Linux futex(Fast Userspace Mutex)
    • 信号量(Semaphore,当用作互斥且涉及阻塞时)
    • 条件变量(Condition Variable)
    • ReentrantLock(默认非公平模式,在阻塞等待时)
    • 文件锁(flock, lockf)
    依赖操作系统内核调度,线程在无法获取锁时被挂起(阻塞),由 OS 唤醒。涉及 用户态 ↔ 内核态切换,开销大,但适合长时间持有锁或高竞争环境,避免 CPU 空

            根据具体场景的时候选择决定使用什么级别的锁。

    特性无锁轻量级锁重量级锁
    是否阻塞线程否(忙等)是(挂起)
    是否进入内核
    CPU 开销低(但冲突时重试多)中(自旋消耗 CPU)高(上下文切换)
    延迟极低低(短临界区)高(唤醒有延迟)
    适用场景简单原子操作、高频低冲突短临界区、低竞争长临界区、高竞争或必须阻塞
    典型代表AtomicInteger、CAS 操作JVM 轻量级锁、自旋锁synchronized(竞争时)、pthread_mutex

    2.2 按作用范围分类

            分类依据:竞争主体是谁?资源在哪个层级共享?

    作用范围决定了“锁如何被感知和传递”
            不同范围的锁,底层实现完全不同,不能混用。例如,Java 的 ReentrantLock 对分布式场景完全无效。

    类型说明典型场景
    线程级锁(Thread-level Lock)用于同一进程内多个线程之间的互斥访问,保护共享内存中的临界区,是多线程编程中最基础的同步机制。Java synchronized、C++ std::mutex
    进程级锁(Process-level Lock)用于同一台机器上不同进程之间协调对共享资源(如文件、设备)的访问,通常依赖操作系统提供的 IPC 机制实现。文件锁(flock)、System V 信号量、Windows Mutex
    分布式锁(Distributed Lock)用于跨多台服务器的分布式系统中,确保在集群环境下对全局唯一资源(如任务调度、库存扣减)的互斥访问,需解决网络延迟、节点故障等挑战。Redis 锁、ZooKeeper 临时节点、etcd lease

            按“作用范围”分类,是从系统架构层面回答:“这把锁能管住谁?”

    2.2.1 . 线程级锁(Thread-level Lock)

    用于同一进程内多个线程之间的互斥同步,保护共享内存中的临界区资源。

    原理

    • 所有线程共享同一地址空间,可直接访问同一把锁对象(如内存中的 mutex 变量)。
    • 加锁/解锁操作通常由语言运行时或操作系统线程库(如 pthread)提供。
    • 不涉及进程间通信或网络,开销最小。

    典型实现

    语言/平台实现方式
    JavasynchronizedReentrantLockReadWriteLock
    C++std::mutexstd::shared_mutex(C++17)
    Pythonthreading.LockRLock
    Go虽鼓励 channel,但提供 sync.MutexRWMutex
    POSIXpthread_mutex_t

     适用场景

    • 多线程修改全局变量、单例初始化、线程安全集合等。
    • 示例:Web 服务器中多个请求线程并发更新用户会话缓存。

    * 注意事项

    • 仅对同进程内的线程有效,跨进程无效。
    • 死锁、优先级反转等问题仍需防范。

     伪代码(java)

    // 共享资源
    int counter = 0;
    Object lock = new Object(); // 锁对象
    
    // 多个线程执行以下逻辑
    void increment() {
        synchronized (lock) {        // ← 获取线程级锁
            counter = counter + 1;   // ← 临界区(受保护)
        }                            // ← 自动释放锁
    }
    • synchronized 是 JVM 提供的线程级互斥锁。
    • 所有线程运行在同一个 JVM 进程内,共享 counter 和 lock
    • 跨进程无效(如另一个 Java 进程无法感知此锁)。

    2.2.2 进程级锁(Process-level Lock)

    用于同一台机器上多个独立进程之间协调对共享资源(如文件、设备、共享内存)的访问。

    原理

    • 进程内存空间隔离,无法直接共享普通锁变量。
    • 必须依赖操作系统提供的 IPC(进程间通信)机制实现锁语义。
    • 锁状态通常存储在内核或文件系统中,所有进程可感知。

    典型实现

    类型说明
    文件锁(File Lock)使用 flock()(BSD)或 fcntl()(POSIX)对文件加锁;简单可靠,常用于脚本防重复执行。
    System V / POSIX 信号量内核维护的计数器,可用于互斥或限流(如 sem_opensem_wait)。
    命名互斥体(Named Mutex)Windows 中通过名称创建跨进程 Mutex;Linux 可通过 /dev/shm + futex 模拟。
    共享内存 + 自旋锁高性能场景下,将锁变量放在共享内存中,配合原子操作实现(需谨慎处理崩溃恢复)。

    适用场景

    • 多个独立程序实例访问同一数据库文件(如 SQLite)。
    • 后台守护进程确保单实例运行(如 crond、日志收集器)。
    • 多进程 Web 服务器(如 Apache prefork 模式)共享监听套接字。

     *注意事项

    • 进程异常退出可能导致锁未释放(死锁),需配合超时或看门狗机制。
    • 性能低于线程级锁(涉及系统调用)。

    伪代码(python)

    import fcntl
    import time
    import sys
    
    LOCK_FILE = "/tmp/myapp.lock"
    
    def main():
        with open(LOCK_FILE, "w") as f:
            try:
                # 尝试获取排他文件锁(进程级)
                fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
                print("获得锁,开始执行任务...")
                time.sleep(10)  # 模拟任务
                print("任务完成")
            except IOError:
                print("另一个实例正在运行,退出。")
                sys.exit(1)
            finally:
                # 文件关闭时自动释放锁(或显式 flock(LOCK_UN))
                pass
    
    if __name__ == "__main__":
        main()
    • fcntl.flock() 是 Linux/Unix 的文件锁,由内核管理,对所有进程可见。
    • 即使脚本由不同用户启动,只要访问同一文件路径,锁就生效。
    • 若进程崩溃未释放锁,重启后可手动清理 /tmp/myapp.lock

    2.2.3. 分布式锁(Distributed Lock)

    用于跨多台服务器的分布式系统中,确保集群内对全局唯一资源的互斥访问。

     原理

    • 节点间无共享内存,必须依赖外部协调服务作为“第三方仲裁者”。
    • 锁状态存储在高可用、强一致(或最终一致)的中间件中。
    • 需解决:网络分区、节点宕机、时钟漂移、锁续期等分布式难题。

     典型实现

    中间件实现方式一致性模型
    RedisSET key value NX PX timeout + Lua 脚本释放;RedLock 算法增强可靠性最终一致(单机 Redis)
    强一致(Redis Cluster + RedLock 争议较大)
    ZooKeeper创建临时顺序节点(Ephemeral Sequential Node),最小序号者获得锁强一致(ZAB 协议)
    etcd利用 Lease(租约)+ Compare-and-Swap(CAS)实现锁强一致(Raft 协议)
    数据库SELECT ... FOR UPDATE 或唯一索引 + 插入竞争强一致(依赖 DB 事务)

     适用场景

    • 分布式任务调度(如 Quartz 集群模式):确保定时任务只在一个节点执行。
    • 秒杀系统:防止超卖,保证库存扣减原子性。
    • 分布式 ID 生成器:避免 ID 重复。
    • 配置中心变更通知:串行化处理配置更新。

    *注意事项

    • 必须设置超时(自动过期),防止客户端崩溃导致死锁。
    • 锁续期(Watchdog):长时间任务需后台线程定期延长锁有效期(如 Redisson 的 watchdog 机制)。
    • 释放锁需原子性:避免误删他人锁(Redis 中用 Lua 脚本校验 value)。
    • CAP 权衡:强一致锁(ZK/etcd)可能牺牲可用性;高性能锁(Redis)可能牺牲一致性。

    伪代码(redis+lua)

    import redis
    import time
    import uuid
    
    redis_client = redis.StrictRedis(host='redis-cluster', port=6379)
    
    LOCK_KEY = "inventory:product_123"
    LOCK_VALUE = str(uuid.uuid4())  # 唯一标识当前实例
    LOCK_TIMEOUT = 10000  # 10秒过期(毫秒)
    
    def acquire_lock():
        # SET key value NX PX timeout —— 原子加锁
        return redis_client.set(
            LOCK_KEY,
            LOCK_VALUE,
            nx=True,      # only set if not exists
            px=LOCK_TIMEOUT  # auto expire
        )
    
    def release_lock():
        # Lua 脚本:仅当 value 匹配时才删除(防误删)
        lua_script = """
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        """
        redis_client.eval(lua_script, 1, LOCK_KEY, LOCK_VALUE)
    
    def deduct_stock():
        if acquire_lock():
            try:
                stock = int(redis_client.get("stock:product_123") or 0)
                if stock > 0:
                    redis_client.decr("stock:product_123")
                    print("扣减成功")
                else:
                    print("库存不足")
            finally:
                release_lock()  # 必须释放
        else:
            print("获取分布式锁失败,可能正在处理")
    
    # 多个服务实例并发调用 deduct_stock()
    • fcntl.flock() 是 Linux/Unix 的文件锁,由内核管理,对所有进程可见。
    • 即使脚本由不同用户启动,只要访问同一文件路径,锁就生效。
    • 若进程崩溃未释放锁,重启后可手动清理 /tmp/myapp.lock
    • 锁状态存储在 Redis 集群 中,所有服务节点共享。
    • NX + PX 保证原子性;Lua 脚本保证释放安全性。
    • 必须设置超时,防止服务宕机导致死锁。
    • 更高可靠方案:ZooKeeper(临时顺序节点)、etcd(lease + CAS)。

    2.3 按功能与行为分类

            分类依据:锁在运行时表现出什么样的控制语义和调度行为?

            功能与行为决定了“锁如何管理访问顺序和并发粒度”

    类型说明适用场景
    互斥锁(Mutex / Exclusive Lock)最基本的独占锁,保证同一时刻只有一个线程能进入临界区,适用于所有需要排他访问的场景。保护临界区,如修改全局变量
    读写锁(Read-Write Lock)允许多个读操作并发执行,但写操作必须独占;在“读多写少”场景下显著提升并发性能读多写少场景(如缓存、配置)
    可重入锁(Reentrant Lock)允许同一线程多次获取同一把锁而不会死锁,内部通过持有者线程 ID 和计数器实现,适合递归调用或复杂嵌套逻辑。避免递归调用导致死锁(如 Java ReentrantLock
    公平锁 vs 非公平锁公平锁按请求顺序分配锁,避免线程饥饿;非公平锁允许新线程“插队”抢锁,吞吐更高但可能造成某些线程长期等待。公平锁防饥饿,非公平锁吞吐更高
    自旋锁(Spin Lock)获取失败时不挂起线程,而是循环检查(忙等),适用于临界区极短、上下文切换成本高于 CPU 空转的场景(如内核或高性能库)。临界区极短、上下文切换成本高(如内核)
    信号量(Semaphore)控制最多 N 个线程同时访问某类资源,本质是计数器,可用于限流、连接池管理或模拟互斥锁(当 N=1 时)。连接池、限流、资源池管理
    条件变量(Condition Variable)与互斥锁配合使用,让线程在特定条件不满足时主动挂起,并在条件达成时被唤醒,实现高效的线程协作(如生产者-消费者模型)。生产者-消费者、线程协作

            按“功能与行为”分类,是从编程模型层面回答:“这把锁怎么工作?”

    2.3.1互斥锁(Mutex / Exclusive Lock)

    项目说明
    定义最基本的独占锁,确保同一时刻仅一个线程可进入临界区。
    原理线程尝试获取锁 → 成功则执行临界区;失败则阻塞(或自旋)等待。释放后唤醒等待者。
    典型实现Java: synchronizedReentrantLock
    C++: std::mutex
    POSIX: pthread_mutex_t
    适用场景保护共享变量、单例初始化、资源独占访问等通用互斥需求。
    优点简单、通用、语义清晰。
    缺点串行化执行,高竞争时性能下降;可能死锁。

    所有并发控制的“基石”。

    伪代码:(java)

    // 共享资源
    int balance = 100;
    Object mutex = new Object();
    
    void withdraw(int amount) {
        synchronized (mutex) {          // ← 获取互斥锁
            if (balance >= amount) {
                balance -= amount;      // ← 临界区
            }
        }                              // ← 自动释放锁
    }

    本质:最基础的排他锁,synchronizedstd::mutex 即为此类。

    2. 读写锁(Read-Write Lock)

    项目说明
    定义支持多读单写:多个读线程可并发,写线程必须独占。
    原理内部维护两个锁:读锁(共享)和写锁(互斥)。写锁优先级通常更高(防写饥饿)。
    典型实现Java: ReentrantReadWriteLock
    C++: std::shared_mutex (C++17)
    Go: sync.RWMutex
    适用场景读多写少场景:缓存、配置中心、统计信息更新。
    优点显著提升读并发吞吐。
    缺点写操作可能被大量读阻塞(需注意写饥饿);实现比互斥锁复杂。

    为“读多写少”量身定制的高性能锁。

    伪代码(java)

    允许多读并发,写操作独占。

    ReadWriteLock rwLock = new ReentrantReadWriteLock();
    Map<String, String> cache = new HashMap<>();
    
    // 读操作
    String get(String key) {
        rwLock.readLock().lock();       // ← 获取读锁(可多个线程同时持有)
        try {
            return cache.get(key);      // ← 并发安全读
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 写操作
    void put(String key, String value) {
        rwLock.writeLock().lock();      // ← 获取写锁(独占)
        try {
            cache.put(key, value);      // ← 独占写
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    适用:缓存、配置等 读多写少 场景。

    3. 可重入锁(Reentrant Lock)

    项目说明
    定义允许同一线程多次获取同一把锁,不会导致自死锁。
    原理内部记录持有者线程 ID + 重入计数器。每次加锁计数+1,释放-1,归零才真正释放。
    典型实现Java: ReentrantLock(显式)、synchronized(隐式)
    Python: threading.RLock
    适用场景递归调用、嵌套方法同步、复杂业务逻辑中多次进入同一临界区。
    优点避免同一线程死锁,编程更安全。
    缺点略微增加内存开销(需存储线程ID和计数)。

    让“自己不卡自己”的智能锁。

    示例(java):

    同一线程可多次获取同一把锁,避免自死锁。

    ReentrantLock lock = new ReentrantLock();
    
    void outer() {
        lock.lock();
        try {
            System.out.println("outer");
            inner();                    // ← 递归调用
        } finally {
            lock.unlock();
        }
    }
    
    void inner() {
        lock.lock();                   // ← 同一线程再次加锁(成功!)
        try {
            System.out.println("inner");
        } finally {
            lock.unlock();
        }
    }

    关键:内部维护 线程ID + 重入计数器,计数归零才真正释放。

    4. 公平锁 vs 非公平锁

    项目公平锁(Fair Lock)非公平锁(Non-fair Lock)
    定义按请求顺序分配锁(FIFO)允许新线程“插队”抢锁
    原理维护等待队列,唤醒按序新线程先尝试直接获取,失败再入队
    典型实现Java: new ReentrantLock(true)Java: synchronizedReentrantLock()(默认)
    优点避免线程饥饿,行为可预测吞吐更高(减少上下文切换)
    缺点性能略低(需维护队列)可能导致某些线程长期等待
    适用场景对响应时间公平性要求高(如金融交易)追求高吞吐、低延迟(如 Web 服务)

    公平锁保“秩序”,非公平锁拼“效率”。

    示例(java):

    是否按请求顺序分配锁。

    // 公平锁:按 FIFO 顺序唤醒等待线程
    ReentrantLock fairLock = new ReentrantLock(true);
    
    // 非公平锁(默认):新线程可“插队”抢锁
    ReentrantLock unfairLock = new ReentrantLock(); // 默认 false
    
    void accessResource() {
        unfairLock.lock();
        try {
            // 临界区
        } finally {
            unfairLock.unlock();
        }
    }
    • 公平锁:吞吐略低,但无饥饿;
    • 非公平锁:吞吐高,但可能有线程长期等待。

    5. 自旋锁(Spin Lock)

    项目说明
    定义获取失败时不挂起线程,而是循环检查(忙等)直到成功。
    原理用户态 CPU 循环执行 CAS 或 test-and-set 指令,持续尝试获取锁。
    典型实现Linux 内核:spinlock_t
    Java:JVM 轻量级锁底层(短暂自旋)
    用户态库:DPDK、高性能中间件
    适用场景临界区极短(< 1μs)、低竞争、多核环境(如内核、网络驱动)。
    优点无上下文切换开销,响应快。
    缺点浪费 CPU(空转);高竞争或长临界区会导致严重性能问题。

    用 CPU 换时间,只适合“闪电战”。

    示例(c):

    忙等(不挂起线程),适合极短临界区。

    // C 伪代码(用户态原子操作)
    volatile int spinlock = 0;
    
    void spin_lock() {
        while (!__sync_bool_compare_and_swap(&spinlock, 0, 1)) {
            // 忙等:持续尝试 CAS(Compare-And-Swap)
            // 可加入 pause 指令优化 CPU
        }
    }
    
    void spin_unlock() {
        __sync_store(&spinlock, 0);  // 释放锁
    }
    
    // 使用
    void critical_section() {
        spin_lock();
        // 极短操作(如更新 flag)
        spin_unlock();
    }

    注意:仅适用于临界区 < 1μs 且低竞争场景,否则浪费 CPU。

    6. 信号量(Semaphore)

    项目说明
    定义控制最多 N 个线程同时访问某类资源的计数器(N ≥ 1)。
    原理内部维护一个整型计数器:
    acquire():计数 -1,若 <0 则阻塞
    release():计数 +1,唤醒等待者
    典型实现Java: Semaphore
    POSIX: sem_wait/sem_post
    Python: threading.Semaphore
    适用场景连接池、线程池、限流、生产者-消费者缓冲区大小控制。
    优点灵活控制并发度;可模拟互斥锁(N=1 时即为二值信号量)。
    缺点不记录持有者,无法实现可重入;易误用导致资源泄漏。

    不是“门锁”,而是“入场券发放机”。

    示例(java):

    控制最多 N 个线程同时访问资源。

    // 最多 3 个线程同时访问数据库连接池 为1的时候为互斥锁
    Semaphore dbPool = new Semaphore(3);
    
    void accessDatabase() {
        dbPool.acquire();              // ← 获取许可(若无则阻塞)
        try {
            // 使用数据库连接(最多3个并发)
        } finally {
            dbPool.release();          // ← 释放许可
        }
    }

    扩展:当 N=1 时,等价于互斥锁(二值信号量)。

    7. 条件变量(Condition Variable)

    项目说明
    定义与互斥锁配合,实现线程“等待-通知”协作机制。
    原理wait():原子释放锁 + 挂起线程
    signal()/notify():唤醒一个等待者
    broadcast()/notifyAll():唤醒所有
    典型实现Java: Object.wait()/notify()Condition
    POSIX: pthread_cond_wait
    Python: threading.Condition
    适用场景生产者-消费者、线程池任务调度、状态变更等待(如“等数据就绪”)。
    优点高效协作,避免轮询浪费 CPU。
    缺点必须与互斥锁配对使用;易出现“虚假唤醒”(需用 while 循环检查条件)。

    让线程“睡到条件满足再醒”,是协作的桥梁。

    示例(java):

    线程等待某条件成立后再继续(配合互斥锁使用)。

    ReentrantLock lock = new ReentrantLock();
    Condition notEmpty = lock.newCondition();
    Queue<String> queue = new LinkedList<>();
    
    // 消费者
    String take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();       // ← 释放锁 + 挂起,等待通知
            }
            return queue.poll();
        } finally {
            lock.unlock();
        }
    }
    
    // 生产者
    void put(String item) {
        lock.lock();
        try {
            queue.add(item);
            notEmpty.signal();          // ← 唤醒一个等待者
        } finally {
            lock.unlock();
        }
    }
    • 必须在持有锁时调用 await()
    • 使用 while 而非 if(防虚假唤醒);
    • signal() 唤醒一个,signalAll() 唤醒全部。

    2.4 按实现策略分类

            分类依据:系统对并发冲突的基本假设是什么?采用何种哲学来保证正确性?

            实现策略反映了系统对“并发本质”的理解与权衡(正确性 vs 性能 vs 复杂度)

    类型说明代表技术
    悲观锁(Pessimistic Locking)假设并发冲突频繁,每次访问共享数据前都先加锁,确保安全但可能降低并发效率;典型如数据库行锁、Java synchronized数据库行锁、synchronized
    乐观锁(Optimistic Locking)假设冲突较少,不加锁直接操作,仅在提交时通过版本号或 CAS 检查是否被修改;若冲突则重试,适用于低竞争高吞吐场景。数据库 version 字段、CAS(Compare-And-Swap)
    无锁(Lock-Free)不依赖传统锁机制,而是利用原子指令(如 CAS)构建数据结构,保证至少有一个线程能 progress,避免死锁和优先级反转。AtomicInteger、无锁队列(如 Disruptor)
    免锁(Wait-Free)无锁的更强形式,保证每个线程都能在有限步内完成操作,不受其他线程影响,实现复杂,多用于实时系统。更强的无锁保证,实现复杂

            按“实现策略”分类,是从并发理论和系统设计哲学层面回答:“我们相信世界是怎样的?”

    2.4.1. 悲观锁(Pessimistic Locking)

    项目说明
    核心思想“世界充满危险”——假设并发冲突一定会发生,因此在访问共享数据前先加锁,确保安全。
    工作方式- 读/写前获取排他锁
    - 操作完成后再释放锁
    - 其他线程必须等待锁释放
    典型实现- 数据库:SELECT ... FOR UPDATE
    - Java:synchronizedReentrantLock
    - 操作系统:互斥锁(Mutex)
    适用场景- 写操作频繁
    - 冲突概率高
    - 要求强一致性(如金融交易)
    优点- 逻辑简单,正确性高
    - 避免重试开销
    缺点- 串行化执行,吞吐低
    - 可能死锁、阻塞、优先级反转
    - 锁竞争激烈时性能急剧下降

    宁可错杀一千,不可放过一个——先锁再做事。

    示例(java):

    银行转账(强一致性要求)(数据库 + 应用层)

    // 开启事务
    beginTransaction();
    
    try {
        // 1. 加悲观锁(SELECT ... FOR UPDATE)
        Account from = db.query("SELECT * FROM accounts WHERE id = ? FOR UPDATE", fromId);
        Account to = db.query("SELECT * FROM accounts WHERE id = ? FOR UPDATE", toId);
    
        // 2. 检查余额
        if (from.balance < amount) throw new InsufficientFundsException();
    
        // 3. 执行转账
        from.balance -= amount;
        to.balance += amount;
    
        db.update("UPDATE accounts SET balance = ? WHERE id = ?", from.balance, fromId);
        db.update("UPDATE accounts SET balance = ? WHERE id = ?", to.balance, toId);
    
        commit(); // 提交事务,释放行锁
    } catch (Exception e) {
        rollback();
    }
    • 数据库在 FOR UPDATE 时对行加排他锁;
    • 其他事务读/写该行会被阻塞,直到当前事务提交;
    • 适合写多、冲突高、强一致场景

    2.4.2. 乐观锁(Optimistic Locking)

    项目说明
    核心思想“世界基本和平”——假设冲突很少发生,因此不加锁直接操作,仅在提交时检查是否被修改,冲突则重试。
    工作方式- 读取数据 + 版本号(或时间戳)
    - 在本地修改
    - 提交时比对版本号:
     ✓ 相同 → 更新成功
     ✗ 不同 → 失败重试
    典型实现- 数据库:version 字段 + UPDATE ... WHERE version = ?
    - Java:AtomicInteger.compareAndSet(expected, newValue)
    - MVCC(多版本并发控制):PostgreSQL、MySQL InnoDB
    适用场景- 读多写少
    - 冲突概率低
    - 高吞吐需求(如电商浏览、配置更新)
    优点- 无锁开销,高并发性能好
    - 避免死锁
    缺点- 冲突高时重试成本大(CPU 浪费)
    - 不适合长事务(重试代价高)
    - 无法保证实时一致性

    先做事,后验货;不行就重来。

    示例代码(java):

    商品信息编辑(多人可能同时查看,但少人同时修改)带 version 字段【不加锁,提交时检查版本是否被修改。】

    class Product {
        long id;
        String name;
        int version; // 乐观锁版本号
    }
    
    boolean updateProduct(Product newProd) {
        Product old = db.query("SELECT id, name, version FROM products WHERE id = ?", newProd.id);
        
        if (old == null) return false;
    
        // 尝试更新:仅当 version 未变时才成功
        int rows = db.update(
            "UPDATE products SET name = ?, version = version + 1 WHERE id = ? AND version = ?",
            newProd.name, newProd.id, old.version
        );
    
        if (rows == 0) {
            // 冲突!别人已修改
            throw new OptimisticLockException("数据已被他人修改,请重试");
        }
        return true;
    }
    
    // 调用方需处理重试
    void editProductSafely(long id, String newName) {
        int maxRetries = 3;
        for (int i = 0; i < maxRetries; i++) {
            try {
                Product p = new Product(id, newName, 0);
                if (updateProduct(p)) return; // 成功
            } catch (OptimisticLockException e) {
                if (i == maxRetries - 1) throw e;
                Thread.sleep(10); // 短暂等待后重试
            }
        }
    }
    • 无锁,高并发读性能好;
    • 冲突时需应用层重试
    • 适合读多写少、冲突低场景

    2.4.3. 无锁(Lock-Free)

    项目说明
    核心思想完全摒弃传统锁,利用原子指令(如 CAS) 构建数据结构,保证至少有一个线程能 progress(不会全部卡住)。
    工作方式- 使用 CPU 提供的原子操作(如 x86 的 cmpxchg
    - 多线程通过循环 + CAS 更新共享状态
    - 失败线程重试,但系统整体始终前进
    典型实现- Java:AtomicIntegerConcurrentLinkedQueue
    - C++:std::atomic
    - 高性能框架:Disruptor(无锁环形缓冲区)
    适用场景- 高频简单操作(计数器、指针切换)
    - 低延迟系统(交易引擎、实时日志)
    - 避免死锁/优先级反转的关键路径
    优点- 无死锁、无优先级反转
    - 高吞吐、低延迟(无上下文切换)
    缺点- 编程复杂,调试困难
    - ABA 问题需额外处理(如加版本戳)
    - 重试可能浪费 CPU

    没有门卫,但大家靠“原子礼仪”自觉排队。

    示例代码(java):

    用原子指令(如 CAS)实现线程安全,系统整体始终 progress。高并发计数器

    import java.util.concurrent.atomic.AtomicLong;
    
    AtomicLong counter = new AtomicLong(0);
    
    void increment() {
        long current, next;
        do {
            current = counter.get();      // 读当前值
            next = current + 1;
            // CAS:仅当值仍为 current 时才更新为 next
        } while (!counter.compareAndSet(current, next));
        // 失败则重试,但至少有一个线程会成功 → Lock-Free
    }
    
    // 或直接使用内置方法(内部也是 CAS 循环)
    void incrementSimple() {
        counter.incrementAndGet(); // 原子自增
    }
    • 无阻塞、无死锁;
    • 适用于简单状态更新(计数器、指针切换);
    • ABA 问题需注意(可用 AtomicStampedReference 解决)。

    2.4.4. 免锁(Wait-Free)

    项目说明
    核心思想比 Lock-Free 更强:每个线程都能在有限步内完成操作,不受其他线程影响。
    特点- 无任何等待、重试或依赖
    - 实时系统理想选择
    实现难度极高,通常仅用于特定数据结构(如某些队列、栈)
    典型应用航空航天、工业控制等硬实时系统
    人人独立,永不等待;

    示例代码(java):

    每个线程都能在有限步内完成操作,完全不受其他线程影响。Wait-Free 队列(理论模型)基于环形缓冲区 + 原子索引

    // Wait-Free Queue(简化版,假设单生产者单消费者)
    class WaitFreeQueue<T> {
        private final T[] buffer = (T[]) new Object[CAPACITY];
        private final AtomicLong head = new AtomicLong(0); // 出队位置
        private final AtomicLong tail = new AtomicLong(0); // 入队位置
    
        boolean enqueue(T item) {
            long currentTail = tail.get();
            long currentHead = head.get();
            
            if (currentTail - currentHead >= CAPACITY) {
                return false; // 队列满
            }
    
            // 直接写入,无需重试(因 tail 是原子递增)
            buffer[(int)(currentTail % CAPACITY)] = item;
            tail.lazySet(currentTail + 1); // 最终一致性写入
            return true;
        }
    
        T dequeue() {
            long currentHead = head.get();
            long currentTail = tail.get();
    
            if (currentHead == currentTail) {
                return null; // 队列空
            }
    
            T item = buffer[(int)(currentHead % CAPACITY)];
            buffer[(int)(currentHead % CAPACITY)] = null;
            head.lazySet(currentHead + 1);
            return item;
        }
    }
    • 此例为 单生产者-单消费者 的 Wait-Free 实现(无竞争);
    • 真正的多生产者 Wait-Free 队列(如 Michael-Scott)需更复杂算法;

    三、锁

    3.1 锁和临界区

            “临界区”和“锁”是并发编程中两个紧密关联的核心概念。它们共同解决多个执行单元(线程/进程)同时访问共享资源时可能导致的数据不一致或竞态条件(Race Condition)问题

             临界区:指程序中访问共享资源(如变量、文件、设备等)的一段代码,这段代码必须被互斥执行——即同一时刻只能有一个线程/进程在执行它。

            锁(Lock):是一种同步机制,用于控制对临界区的访问,确保同一时间只有一个执行单元能进入临界区。

            锁的作用就是保护临界区。没有锁,临界区就无法安全并发;没有临界区,锁就失去了存在的意义。

            在实际编程中,临界区的划分直接影响锁的性能与安全性。若临界区划分过小,可能导致频繁加锁解锁,增加开销;若划分过大,则会过度限制并发,降低程序吞吐量。例如,在处理用户订单数据时,若将“查询订单信息”和“修改订单状态”都纳入同一个临界区,会导致多个读请求无法并发;而合理的做法是将“修改订单状态”作为临界区加锁保护,“查询订单信息”通过读写锁的读模式允许并发访问,平衡安全性与效率。

            此外,临界区的原子性保障依赖锁的正确使用。必须确保锁的粒度与临界区完全匹配,即“进临界区前加锁,出临界区后解锁”,且加锁与解锁必须成对出现。若出现“加锁后未解锁”的情况,会导致锁永久持有,其他线程无法进入临界区,引发死锁;若“未加锁就进入临界区”,则无法避免竞态条件,导致数据错误。在 Java 中,synchronized 关键字会自动完成加锁与解锁(异常时也会释放锁),而 ReentrantLock 则需要手动通过 try-finally 块保证解锁,避免异常场景下的锁泄漏。

    3.2 锁的核心特性

            理解锁的特性是正确使用锁的前提,除了前文分类中提及的可重入性、公平性等,锁还具备以下核心特性:

            1. 互斥性:这是锁最基础的特性,确保同一时刻只有一个线程(或进程)能持有锁并进入临界区。互斥性是保证数据一致性的核心,无论是互斥锁、读写锁的写模式,还是分布式锁,都严格遵循互斥性原则。需要注意的是,读写锁的读模式不具备互斥性,其设计初衷是在读多写少场景下提升并发,通过“读-读兼容、读-写互斥、写-写互斥”的规则平衡互斥与并发。

            2. 可见性:锁不仅能控制临界区的访问权限,还能保证共享资源的可见性。在多线程环境中,线程会将共享变量缓存到本地工作内存,若未加锁,一个线程修改的变量可能无法及时被其他线程感知,导致脏读。而加锁操作会强制线程刷新本地缓存,将修改后的变量同步到主内存;解锁操作则会确保本地缓存中的修改已同步到主内存,从而保证后续获取锁的线程能读取到最新的变量值。例如,Java 中的 synchronized 和 ReentrantLock 都能保证可见性,这也是它们区别于普通变量的关键特性之一。

            3. 有序性:并发环境中,CPU 可能会对指令进行重排序以提升执行效率,这可能导致程序执行顺序与代码逻辑顺序不一致,引发潜在问题。锁能禁止指令重排序,确保临界区内的代码按逻辑顺序执行。例如,在单例模式的双重检查锁定实现中,若未使用 volatile 关键字或锁机制,可能因指令重排序导致创建出多个实例;而通过锁包裹实例创建逻辑,可强制指令有序执行,避免该问题。

    3.3 锁的常见风险与规避策略

            锁的使用虽能解决并发问题,但不当使用会引入新的风险,常见的有死锁、活锁、饥饿以及性能损耗等,需针对性采取规避策略。

    1. 死锁:

            指两个或多个线程互相持有对方所需的锁,且都不主动释放,导致所有线程永久阻塞。例如,线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1,此时两者陷入死锁。
            规避死锁的核心原则包括:
                    一是按固定顺序加锁,确保所有线程获取锁的顺序一致,避免循环等待;
                    二是设置锁超时时间,若线程在指定时间内未获取到锁,则主动释放已持有的锁,打破阻塞状态(如 ReentrantLock 的 tryLock 方法);
                    三是使用可中断锁,允许线程在等待锁的过程中响应中断信号,主动退出等待(如 ReentrantLock 的 lockInterruptibly 方法);
                    四是定期检测死锁,通过工具(如 Java 的 jstack)排查死锁线程,及时优化锁的使用逻辑。

    2. 活锁:

            指线程虽未阻塞,但因不断重试操作而无法推进执行。例如,两个线程同时获取锁失败后,都立即释放自身资源并重新尝试获取,导致两者反复重试却始终无法获取到锁。活锁与死锁的区别在于,活锁中的线程处于活跃状态,而死锁中的线程处于阻塞状态。
            规避活锁的策略包括:
                    一是引入随机延迟,线程获取锁失败后,随机等待一段时间再重试,避免多个线程同步重试;
                    二是设置重试次数上限,若超过次数则暂停重试或降级处理,避免无限循环;
                    三是优化锁的竞争逻辑,减少不必要的重试操作。

    3. 饥饿:
            指某个线程长期无法获取锁,始终处于等待状态。常见原因包括锁的非公平性、线程优先级不合理等。例如,在非公平锁机制下,新线程可能频繁“插队”获取锁,导致老线程长期等待;若高优先级线程持续竞争锁,低优先级线程可能永远无法获取锁。
            规避饥饿的策略包括:
                    一是使用公平锁,按线程请求锁的顺序分配锁,确保每个线程都有机会获取锁;
                    二是合理设置线程优先级,避免优先级倒置;
                    三是限制锁的持有时间,避免线程长时间占用锁,给其他线程获取锁的机会。

    4. 性能损耗
            锁的加锁、解锁操作以及线程阻塞/唤醒都会带来开销,过度使用锁或锁粒度不当会严重影响程序性能。
            优化策略包括:
                    一是减小锁粒度,将大临界区拆分为多个小临界区,降低锁的竞争概率(如 Java 中的 ConcurrentHashMap 采用分段锁机制,将哈希表分为多个段,每个段单独加锁,提升并发性能);
                    二是使用合适的锁类型,根据场景选择无锁、轻量级锁或重量级锁,读多写少场景优先使用读写锁;
                    三是避免锁的嵌套,减少重入锁的使用次数,降低锁管理开销;四是采用锁消除优化,编译器或 JVM 会自动消除不必要的锁(如对局部变量加锁,因局部变量不共享,锁无实际意义,会被自动消除)。

    3.4 使用场景

    典型业务场景核心并发问题推荐锁 / 同步机制推荐理由
    单例模式懒加载(双重检查)多线程同时初始化实例synchronized + volatile 或 ReentrantLock保证实例创建的原子性与可见性;JVM 对 synchronized 优化后性能良好
    高并发计数器(如 PV 统计)多线程对整型变量自增AtomicLong / LongAdder无锁 CAS 操作,避免重量级锁开销;LongAdder 在高竞争下性能更优
    缓存读多写少(如配置、热点数据)多读一写,需保证一致性ReentrantReadWriteLock(读写锁)允许多个线程并发读,写时独占,显著提升读吞吐
    数据库连接池 / 线程池资源管理限制同时使用的资源数量Semaphore(信号量)精确控制并发访问数(如最多 10 个连接),天然匹配资源池模型
    生产者-消费者模型(任务队列)生产满时阻塞生产者,消费空时阻塞消费者BlockingQueue(内部用 ReentrantLock + ConditionJDK 提供成熟实现(如 ArrayBlockingQueue),无需手写锁逻辑
    递归方法中的同步(如遍历树并修改)同一线程多次进入临界区ReentrantLock 或 synchronized两者均支持可重入,避免自死锁
    短临界区、低竞争(如更新标志位)需要极低延迟的互斥自旋锁(Spin Lock)或 JVM 轻量级锁避免线程挂起/唤醒开销,适合微秒级操作
    跨 JVM 实例的分布式任务调度多服务实例争抢唯一任务分布式锁(Redis + RedLock / ZooKeeper)确保集群中仅一个节点执行关键任务(如定时批处理)
    秒杀/库存扣减(高并发写)超卖风险,需强一致性数据库悲观锁(SELECT ... FOR UPDATE + 本地缓存预减悲观锁保证 DB 层原子性;缓存层用 Redis incr/decr + Lua 脚本防超卖
    日志写入(多线程追加)多线程写同一文件乱序synchronized 块 或 ReentrantLock保证写操作原子性;若性能敏感,可改用异步日志框架(如 Logback AsyncAppender)
    状态机变更(如订单状态流转)防止并发修改导致状态错乱乐观锁(version 字段)通过版本号校验更新是否成功,失败则重试,避免长时间持锁
    高性能无锁队列(如 Disruptor)极致吞吐,避免锁竞争无锁环形缓冲区(CAS + 内存屏障)适用于金融交易、高频事件处理等场景,延迟可降至纳秒级

    总结:

    没有“最好”的锁,只有“最合适”的锁
    综合考虑:

    • 资源特性(是否可共享、读写比例)
    • 竞争强度(低/高并发)
    • 临界区长度(短/长)
    • 系统架构(单机/分布式)
    • 一致性要求(强一致/最终一致)

    实际开发中,最常用的核心锁不超过 6 种如下:

    1. 互斥锁(Mutex)
    2. 读写锁(Read-Write Lock)
    3. 可重入锁(Reentrant Lock)
    4. 信号量(Semaphore)
    5. 自旋锁(Spin Lock)
    6. 分布式锁(Distributed Lock)
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值