当前位置: 首页 > news >正文

Java【多线程】(7)常见的锁策略


目录

1.前言

2.正文

2.1悲观锁和乐观锁

2.2重量级锁和轻量级锁

2.3挂起等待锁和自旋锁

2.4互斥锁与读写锁

2.5可重入锁与不可重入锁

2.6公平锁与不公平锁

2.7synchronized优化

2.7.1锁升级

2.7.2锁消除

2.7.3锁粗化

 3.小结


1.前言

哈喽大家好,今天来给大家分享Java多线程中常见的锁策略,锁策略不是和Java强相关,但凡涉及到并发编程涉及到锁都会涉及锁策略,概念较多但都很重要,废话不多说让我们开始吧。

2.正文

首先在这里声明一点:以下讲的各种锁,不是针对某一种具体的锁,而是某个具体锁具有“悲观"特性或者“乐观”等特性~~

2.1悲观锁和乐观锁

悲观锁

  • 定义:假设并发冲突一定会发生,因此在操作数据前先加锁,确保同一时刻只有一个线程能访问资源。

  • 特点:强一致性,但性能开销大。

乐观锁

  • 定义:假设并发冲突很少发生,操作数据时不加锁,只在提交修改时检查是否被其他线程修改过。

  • 特点:高性能,但可能需重试。


悲观锁的现实场景

  • 你去银行取钱,柜员会说:"稍等,我先锁上保险箱再给你拿钱。"

  • 为什么?银行默认你会和别人争抢取钱这个操作,必须锁住资源。

乐观锁的现实场景

  • 你和同事改同一份Docs,直接编辑,保存时系统提示:"有冲突,请解决。"

  • 为什么?默认你们不会同时改同一段落,冲突了再处理。

根本区别在于对接下来锁竞争的是否激烈。


对比维度悲观锁乐观锁
默认态度"肯定会有人抢,先锁再说!""应该没人抢,冲突了再说~"
实现方式synchronizedReentrantLock版本号、CAS(如AtomicInteger
性能开销高(上下文切换、阻塞)低(无阻塞,可能重试)
适用场景写多读少(如支付、转账)读多写少(如商品库存、点赞计数)
失败处理线程阻塞等待回滚或自动重试

2.2重量级锁和轻量级锁

重量级锁

  • 定义:依赖操作系统内核的互斥量(Mutex Lock)实现的锁机制,线程竞争时会直接进入阻塞状态,由操作系统负责线程调度。

  • 特点:功能完备(支持公平性、超时等),但性能开销大(涉及用户态到内核态的切换)。

轻量级锁

  • 定义:基于CAS(Compare-And-Swap)自旋实现的锁机制,线程通过循环尝试获取锁,避免直接进入阻塞状态。

  • 特点:性能高(无系统调用),但长时间自旋会浪费CPU资源。


重量级锁的现实场景

  • 你去医院挂号,发现窗口排队的人很多,直接去休息区睡觉(线程阻塞),等护士叫号(操作系统唤醒)。

  • 为什么?因为你知道要等很久,不如让出资源。

轻量级锁的现实场景

  • 你在便利店排队结账,发现前面只有一个人,于是站在原地不停张望(自旋),等对方结束立马抢位置。

  • 为什么?因为等待时间短,不值得去找座位。


  • 重量级锁,当悲观的场景下,此时就要付出更多的代价。(更低效)
  • 轻量级锁,应对乐观的场景,此时付出的代价就会更小。(更高效)

对比维度重量级锁轻量级锁
实现原理通过操作系统内核的互斥量(Mutex)用户态的CAS自旋(如AtomicInteger
线程状态阻塞(挂起)运行中(自旋等待)
性能开销高(上下文切换约1-10μs)低(自旋耗时约0.1-1ns/次)
适用场景高竞争、长临界区低竞争、短临界区
失败处理线程进入等待队列继续自旋或升级为重量级锁

2.3挂起等待锁和自旋锁

挂起等待锁就是重量级锁的典型实现,而自旋锁就是轻量级锁的典型实现。

挂起等待锁(阻塞锁)

  • 定义:当线程获取锁失败时,立即释放CPU资源,进入阻塞状态(挂起),等待被唤醒

  • 核心机制:依赖操作系统调度,涉及线程上下文切换

  • 典型实现:Java的synchronized在重量级锁状态、ReentrantLock.lock()

自旋锁

  • 定义:当线程获取锁失败时,不放弃CPU,而是循环重试(自旋),直到成功获取锁

  • 核心机制:通过CPU空转(忙等待)避免上下文切换

  • 典型实现:Java的AtomicIntegerCAS操作、ReentrantLock.tryLock()自旋版本(以后会详细讲解Java中的CAS)


挂起等待锁场景

  • 你去热门餐厅取号,服务员说:"现在没位,去旁边商场逛2小时再回来"(线程挂起)

  • 为什么合理:等待时间长时,干等着(自旋)反而浪费精力

自旋锁场景

  • 你在便利店排队,收银员说:"稍等1分钟马上好",你选择站着玩手机等待(自旋)

  • 为什么合理:短暂等待时,来回走动(上下文切换)更耗能


对比维度挂起等待锁自旋锁
等待机制立即释放CPU进入阻塞状态保持CPU占用循环检测
系统开销高(上下文切换约1-10μs)低(但浪费CPU周期)
实现复杂度高(需OS支持线程调度)低(CAS即可实现)
适用场景锁持有时间长(>1ms)锁持有时间短(<1μs)
线程状态BLOCKED/WATINGRUNNABLE
公平性通常可实现公平通常是非公平的
典型应用数据库事务、文件IO计数器、状态标志

2.4互斥锁与读写锁

分析下这个读写锁:多个线程读取一个数据,是本身就线程安全的。多个线程读取,一个线程修改,肯定会涉及到线程安全问题。如果你把读和写都加上普通的互斥锁,意味着锁冲突将会非常严重,读锁和读锁之间不互斥,读锁和写锁互斥,写锁和写锁之间也互斥。于是乎读写所的存在,保证线程安全的前提下,降低锁冲突概率提高效率。


互斥锁(Mutex Lock)

  • 定义:独占锁,同一时刻只允许一个线程访问共享资源

  • 特点:强排他性,读/写操作同等对待

  • 典型实现:Java的synchronizedReentrantLock

读写锁(ReadWrite Lock)

  • 定义:分离锁,将读操作和写操作区别对待

  • 特点:允许多个读线程并发,写线程独占

  • 典型实现:Java的ReentrantReadWriteLock


互斥锁场景

  • 图书馆自习室规则:"每次只允许一人进入,无论你是看书(读)还是做笔记(写)"

  • 结果:即使多人只想看书,也得排队轮流进

读写锁场景

  • 改进后的规则:"看书的人可以一起进,但做笔记的人必须单独使用房间"

  • 结果:读书效率提升,写作时仍保证独占


对比维度互斥锁读写锁
并发粒度完全互斥读读并发,读写/写写互斥
吞吐量低(所有操作串行)高(读操作可并行)
实现复杂度简单复杂(需维护读/写状态)
适用场景读写操作耗时相近读多写少(≥5:1)
线程饥饿风险写线程可能被读线程长期阻塞
锁升级不支持读锁不能升级为写锁(会死锁)
公平性可公平/非公平可公平/非公平

2.5可重入锁与不可重入锁

之前文章中讲解过这里不过多展开了喔~简单总结~


可重入锁(Reentrant Lock)

  • 定义:同一个线程可以多次获取同一把锁,锁会维护一个持有计数(hold count)

  • 关键特性:防止线程自己造成死锁

  • 典型实现:Java的synchronizedReentrantLock

不可重入锁(Non-reentrant Lock)

  • 定义:线程获取锁后,再次尝试获取会立即阻塞/失败

  • 关键特性:严格线性获取锁

  • 典型实现:早期的简单锁实现、某些特定场景的自旋锁


可重入锁场景

  • 你家大门装了智能锁,你(线程)进入时:

  1. 第一次进门:验证指纹(获取锁)
  2. 进卧室时:不再验证(重入计数+1)
  3. 离开卧室:不真正锁门(计数-1)
  4. 最终出门:才真正上锁(计数归零)

不可重入锁场景

  • 老式钥匙锁的尴尬情况:

  1. 你进门后锁上门(获取锁)
  2. 想进里屋时发现需要钥匙(再次获取锁)
  3. 钥匙插在外门锁上(死锁形成)
  4. 最终困在门厅里(线程阻塞)

对比维度可重入锁不可重入锁
死锁预防避免同一线程自我死锁可能因递归调用导致自我死锁
实现复杂度高(需维护线程ID和计数)低(只需布尔状态)
性能开销略高(计数器操作)略低
适用场景递归调用、回调函数简单线性流程
锁释放必须完全释放(计数归零)单次解锁即释放
典型应用Java同步机制、数据库事务某些内核锁、特殊优化场景

2.6公平锁与不公平锁

公平锁(Fair Lock)

  • 定义:按照线程请求锁的先后顺序分配锁,遵循FIFO(先进先出)原则

  • 特点:避免线程饥饿,保证公平性

  • 实现方式:通过队列维护等待线程(如ReentrantLock(true)

非公平锁(Non-fair Lock)

  • 定义:允许线程插队获取锁,不保证请求顺序

  • 特点:吞吐量高,但可能导致线程饥饿

  • 实现方式:直接尝试CAS获取锁(如synchronizedReentrantLock()默认模式)


公平锁场景

  • 银行VIP窗口叫号系统:"请A001号到3号窗口"(严格按取号顺序服务)

  • 优点:先来的人一定能先办业务

  • 缺点:即使窗口空闲,也必须叫号

非公平锁场景

  • 地铁早高峰排队:"车门一开,所有人挤着上车"(谁快谁上)

  • 优点:车厢利用率高(减少空置时间)

  • 缺点:可能有人永远挤不上去


对比维度公平锁非公平锁
排队机制严格FIFO允许插队(新线程可直接竞争)
吞吐量较低(约降低30%)较高
线程饥饿不会发生可能发生
实现复杂度高(需维护等待队列)低(直接CAS)
响应时间稳定但较长不稳定(可能极快或极慢)
适用场景交易系统、计费系统高并发缓存、计数器
JVM实现ReentrantLock(true)synchronizedReentrantLock()

公平锁:

public class FairLockDemo {
    private static final ReentrantLock lock = new ReentrantLock(true); // 公平模式
    
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                } finally {
                    lock.unlock();
                }
            }, "Thread-" + i).start();
        }
    }
}
// 输出保证按线程启动顺序获取锁

非公平锁:

public class NonFairDemo {
    private static final ReentrantLock lock = new ReentrantLock(); // 默认非公平
    
    public static void main(String[] args) {
        // 先让主线程持有锁
        lock.lock();
        
        new Thread(() -> {
            System.out.println("子线程尝试获取锁");
            lock.lock();  // 这里会插队!
            System.out.println("子线程获取成功");
            lock.unlock();
        }).start();
        
        Thread.sleep(100); // 确保子线程启动
        System.out.println("主线程释放锁");
        lock.unlock();  // 释放后子线程可能抢到,即使有其他等待线程
    }
}

2.7synchronized优化

2.7.1锁升级

JVM根据竞争情况,动态调整synchronized的锁状态,从低开销到高开销逐步升级,避免一刀切使用重量级锁。 JVM没有提供锁降级。

graph LR
    A[无锁] -->|首次获取| B[偏向锁]
    B -->|有竞争| C[轻量级锁]
    C -->|竞争加剧| D[重量级锁]
  1. 偏向锁(Biased Locking)

    • 场景:单线程反复访问同步块

    • 原理:在对象头记录线程ID(无需CAS)

  2. 轻量级锁(Thin Lock)

    • 场景:多线程交替执行(无真正竞争)

    • 原理

      • 栈帧中创建Lock Record

      • 通过CAS将对象头指向Lock Record

  3. 重量级锁(Heavyweight Lock)

    • 场景:高并发竞争

    • 原理:通过ObjectMonitor实现

    • 开销:上下文切换约1-10μs


补充以下何为偏向锁:
刚一上来,不是真加锁, 而是只是简单做一个标记,进行synchronized。这个标记,非常轻量, 相比于加锁解锁来说,效率高很多~如果没有其他线程来竞争这个锁,最终当前线程执行到解锁代码,也就只是简单清除上标记即可~~(不涉及真加锁,真解锁)如果有其他线程来竞争,就抢先一步,在另一个线程拿到锁之前,抢先拿到锁真假锁了,偏向锁 =>轻量级锁,其他线程只能阻塞等待。

2.7.2锁消除

这也是编译器优化的一种体现。


概念:编译器会判定,当前这个代码逻辑是否真的需要加锁,如果确实不需要加锁,但是你写了 synchronized,,就会自动把synchronized给去掉,像一些判定不清楚的情况,不会触发锁消除。

如果到处有synchronized,意味着优化机制,只能把其中一部分,他能明确判定的给优化掉,还会有很多不应该使用, 但是编译器也优化不调。

2.7.3锁粗化

概念
将相邻的多个细粒度锁合并为单个大锁,减少锁申请/释放开销。(加锁和解锁之间,包含的代码越多,就认为锁的粒度就越粗)

触发场景

// 原始代码
for (int i = 0; i < 100; i++) {
    synchronized(obj) { // 每次循环都加锁
        doSomething();
    }
}

// 优化后等效代码
synchronized(obj) { // 合并为单个锁
    for (int i = 0; i < 100; i++) {
        doSomething();
    }
}

以上三种做个总结:

优化手段适用场景性能提升幅度实现层级
锁升级所有synchronized场景10-100倍JVM运行时
锁消除线程私有对象2-5倍JIT编译期
锁粗化密集短锁操作1.5-3倍字节码优化阶段

 3.小结

今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!

相关文章:

  • 【S32M244 RTD200P04 LLD篇8】S32M244 PWM ADC LLD demo
  • (蓝桥杯)动态规划蓝桥杯竞赛指南:动态规划解决最少钞票数问题(超详细解析+代码实现)
  • LabVIEW 开发如何降本增效
  • 数据库分表算法详解:原理、实现与最佳实践
  • FPGA状态机设计:流水灯实现、Modelsim仿真、HDLBits练习
  • FogFL: Fog-Assisted Federated Learning for Resource-Constrained IoT Devices
  • 车载联网终端4G汽车TBOX介绍定义与概述
  • Oracle迁移翻车,数据校验没做好...
  • 前端工具方法整理
  • Redis持久化之AOF
  • 百度的deepseek与硅基模型的差距。
  • 原理图输出网表及调入
  • 无耳 Solon AI v3.1.2 发布(兼容 Java 8 ~ 24),支持 SpringBoot2,jFinal,Vert.X 等第三方框架
  • 电池分选机:新能源时代的品质守护者|深圳比斯特自动化
  • 若依原理笔记
  • 8-运算符
  • Ubuntu16.04配置远程连接
  • java基础 数组Array的介绍
  • 版本控制工具——Git
  • 买不起了,iPhone 或涨价 40% ?
  • wordpress 找不到版权/seo提高关键词
  • 鼓楼公司网站建设费用/网站优化推广
  • 广告公司业务有哪些/关键字优化用什么系统
  • B2B网站做不出排名跟流量/商城系统开发
  • 网站带搜索功能怎么做/宁波网站制作优化服务公司
  • ps做阿里网站分辨率设置/看啥网一个没有人工干预的网