【高薪程序员必看】万字长文拆解Java并发编程!(4-2):悲观锁底层原理与性能优化实战

简介: 获取锁,不可被打打断:释放锁:可打断,获取锁:获取锁失败进入阻塞状态时,可以被其他线程的interrput方法打断:尝试获得锁,返回值是boolean,true获取锁成功,false获取锁失败:尝试获得锁,获取不到锁等待n单位时间,时间到了还没获取到锁就返回false,时间内获取到锁还是返回true,支持可打断:设置公平锁,默认为falseCondition condition = lock.newCondition()//Condition可以创建多个也就是支持多个条变量。

 

image.gif 编辑

Hello大家好!👋 我是摘星✨,今天我们来深度拆解Java并发编程中最经典的「悲观锁」🔒设计。

在多线程环境下,当你的转账操作被重复提交💸、库存被超卖📉、计数器结果离奇错误❌时,背后往往是因为缺乏合理的锁控制。而悲观锁作为Java并发中最「简单粗暴」的解决方案,从JDK1.0时代的重量级锁⛓️,到如今JVM层级的锁升级优化⚡,其底层实现堪称一部高性能并发的发展史📜

本文将带你穿透**synchronized关键字**的表面语法,直击三大核心问题💡:

  1. **🤔 为什么悲观锁能保证线程安全?**(从Java对象头到Monitor的硬件级协作)
  2. **⚡ JDK1.6后synchronized如何实现性能飞跃?**(偏向锁/轻量级锁的取舍智慧)
  3. **🚫 高并发场景下如何规避锁的性能陷阱?**(从字节码层面理解锁膨胀的条件)

📌 举个真实案例:某电商平台在秒杀活动中使用synchronized导致TPS从8000📈暴跌到300📉,最终通过缩小锁粒度+锁分离优化提升15倍性能🚀——我们将在文中用代码还原这个优化过程。

下面我们直接切入正题,从操作系统与JVM的协作契约🤝开始讲起!

目录

4.5. 锁记录

4.6. 偏向锁

4.7. 轻量级锁

4.8. 锁膨胀

4.9. ReentrantLock

4.9.1. ReentrantLock与synchronized的对比

4.9.2. API介绍


4.5. 锁记录

锁记录(Lock Record)是jvm用来支持轻量级锁的数据结构,是线程私有的,用于记录锁的状态,锁拥有者的信息以及竞争情况等等.

  1. 指向拥有者的指针(Owner Pointer):记录当前拥有锁的线程ID。在加锁的过程中,将对象头部的指针指向锁记录,以便快速定位拥有锁的线程。
  2. 标记位(Displacement):记录了对象头部的偏移量,用于在解锁的过程中恢复对象头部的标记字段。
  3. 锁状态(Lock State):记录了当前线程锁的状态,包括偏向锁、轻量级锁、重量级锁等。

在竞争情况下,多个线程会通过锁记录进行协调,实现了对轻量级锁的竞争和重量化的升级。当多个线程竞争同一把锁时,会通过锁记录中的信息协调竞争关系,避免无谓的自旋和竞争。

4.6. 偏向锁

偏向锁:当没有没有锁竞争时可以用偏向锁来优化

偏向锁的加锁过程:

  • 偏向锁加锁:当线程第一次访问的同步代码块时,jvm会将该对象的锁标记为偏向锁01,同时将对象头中的mark word指向线程ID
  • 偏向锁检查:当线程再次访问同一个加锁对象的synchronized(obj)代码块时,会先检查该对象的锁标记是否为偏向锁,是偏向锁且线程ID与当前线程ID一致,则无需加锁,直接进入代码块

偏向锁撤销过程:

  • 当其他线程访问这个同步代码块时,发现线程ID不一致,偏向锁就会被撤销,升级为轻量级锁
  • 调用wait/notify方法是也会撤销偏向锁,因为wait/notify方法只有重量锁才有

4.7. 轻量级锁

轻量级锁:如果有多个线程使用synchronized对一个对象加锁,但是加锁的时间是错开的,不会发生竞争时,可以使用轻量级锁来优化.

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        method2()
    }
}
public static void method2(){
    synchronized(obj){
    }
}

image.gif

轻量级锁的加锁过程:

  1. 检测偏向锁:线程进入同步代码块时,首先会尝试偏向锁,如果对象的对象头中的锁标识位是01(偏向锁),且当前线程是偏向锁的拥有者,则直接进入同步代码块执行,不需要加轻量级锁.如果对象没有偏向锁,则会进入下一步CAS操作尝试获取轻量级锁.
  2. CAS操作尝试获取轻量级锁:线程进入同步代码块时会尝试通过CAS操作将对象头的markword指向锁记录
  • CAS成功:当前线程成功获取了轻量级锁,对象锁标识位更新为00(轻量级锁)
  • CAS失败-竞争:说明有其他线程也在竞争该对象锁,那么当前线程会进行自旋,尝试在短时间内快速获取锁
  • CAS失败-重入:说明当前线程执行了一次加锁过程,会添加一条锁记录

轻量级锁的解锁过程:

  1. CAS操作尝试释放轻量级锁:当持有轻量级锁的线程退出同步块时,会尝试通过CAS操作将对象头中的markword恢复
  • CAS成功:当前线程成功释放了轻量级锁m对象锁标识位更新为01(无锁状态),同时重置锁记录
  • CAS失败:说明有其他线程正在竞争锁,此时轻量级锁会膨胀为重量级锁

4.8. 锁膨胀

轻量级锁竞争:当前线程加轻量级锁的CAS操作失败时,说明发生了锁的竞争,这时当前线程会进行自旋获取锁.自旋是通过忙等待,在一个循环中不断获取锁

自旋锁膨胀为重量级锁:当自旋达到一定的阈值时,会导致锁膨胀,将轻量级锁膨胀为重量级锁

  1. 当前线程为加锁对象申请Monitor锁,将对象头中的markword指向Monitor地址,锁标识位改为10
  2. 当前线程进入EntryList中,处于阻塞状态
  3. 当持锁的线程退出同步代码块时,尝试用CAS操作将对象头中的markword恢复
  • CAS成功:成功释放锁
  • CAS失败:进入重量级锁的解锁过程,将Monitor的owner清空,并将EntryList中阻塞的线程唤醒来竞争锁

4.9. ReentrantLock

4.9.1. ReentrantLock与synchronized的对比

在Java多线程中,ReentrantLock是一个可重入锁,相比与synchronized有更多的灵活性

共同点:

  • 可重入:ReentrantLock和synchronized都是可重入锁,即对一个对象反复加锁

不同点:

  • 锁等待可终止:ReentrantLock允许一个线程在等待锁的过程中被其他线程打断,然后可以选择继续等待获取终止.synchronized获取锁的过程中是不可被打断的
  • 超时时间:ReentrantLock允许设置获取锁的超时时间,超过时间之后可以放弃获取这个锁.synchronized不能设置超时时间
  • 公平锁选择:ReentrantLock允许选择公平锁或者非公平锁.synchronized只有非公平锁
  • 在公平锁模式下,等待时间最长的线程将优先获取锁的访问权限,按先入先得的方式,防止饥饿现象出现,但是会降低并发度
  • 在非公平锁模式下,让等待中的线程竞争锁,具有随机性
  • 多个条件变量:ReentrantLock提供了Condition允许多个条件变量存在,也就是多个waitlist,可以让线程满足某个条件时等待,也可以让其他线程满足某个条件时唤醒.synchronized只有一个条件变量
  • synchronized是关键字级别,ReentrantLock是对象级别

4.9.2. API介绍

private Lock lock = new ReentrantLock()

  • lock.lock():获取锁,不可被打打断
  • lock.unlock():释放锁
  • lock.lockInterruptibly():可打断,获取锁:获取锁失败进入阻塞状态时,可以被其他线程的interrput方法打断
  • lock.trylock():尝试获得锁,返回值是boolean,true获取锁成功,false获取锁失败
  • lock.trylock(long n,TimeUnit.SECONDS):尝试获得锁,获取不到锁等待n单位时间,时间到了还没获取到锁就返回false,时间内获取到锁还是返回true,支持可打断
  • new ReentrantLock(true):设置公平锁,默认为false

Condition condition = lock.newCondition()//Condition可以创建多个也就是支持多个条变量

  • condition.await():await使用前需要获取锁,调用后释放锁,进入condition条件变量中等待,被唤醒(打断)后继续竞争lock锁,竞争成功后继续执行
  • condition.await(long timeout,TimeUnit unit):等待指定超时时间
  • condition.signal():从condition中随机唤醒一个等待中的线程
  • condition.signalAll():从condition中唤醒全部等待中的线程
  • condition.awaitUninterruptibly():等待不可被打断
try {
    // 获取锁 : 不可被打断
    lock.lock(); 
    // 在这里执行需要同步的代码块
} finally {
    // 释放锁
    reentrantLock.unlock(); 
}

image.gif

🎯 悲观锁总结:从原理到实战的完整指南 🔒➡️⚡

🔍 核心要点回顾

  1. 悲观锁哲学 🛡️
  • "宁可错杀,不可放过"的设计理念
  • 适合写多读少场景(支付/库存系统💰)
  1. synchronized进化史 🧬
  • JDK1.6前:重量级锁⛓️(微秒级延迟)
  • JDK1.6后:锁升级路径(无锁→偏向锁→轻量级锁→重量级锁)
  1. 关键对比 ⚖️
特性 synchronized ReentrantLock
条件变量 单个 多个
可中断
超时控制
公平锁

💡场景选择指南 🗺️

  • 简单场景 → synchronized(自动管理)
  • 复杂需求 → ReentrantLock(超时/公平锁)
  1. 性能监控工具 🔧
  • 使用JOL查看对象头:System.out.println(ClassLayout.parseInstance(obj).toPrintable())
  • Arthas监控锁竞争:monitor -c 5 com.example.MyClass methodName

🚀 进阶路线

  1. 下篇预告 📢
  • 无锁编程:CAS原理揭秘
  • Atomic类性能对比
  1. 思考挑战 💭
    当QPS超过10万时:
  • 偏向锁是否反而降低性能?
  • 如何选择自旋次数阈值?

📚 扩展学习

  1. 推荐书籍
  • 《Java并发编程实战》📖
  • 《并发编程的艺术》🎨
  1. 实战项目
  • 实现一个带性能统计的锁装饰器
  • 对比不同锁在秒杀场景的表现

🌟 一句话总结

"悲观锁用性能换取安全,而真正的艺术在于找到那个平衡点⚖️"

(欢迎在评论区分享你的锁优化经验🗨️,点赞收藏下次不迷路⭐️)

目录
相关文章
|
1月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
225 55
|
1月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
71 0
|
1月前
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
252 0
|
1月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
82 0
|
30天前
|
存储 关系型数据库 MySQL
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
|
1月前
|
Java 程序员 应用服务中间件
【高薪程序员必看】万字长文拆解Java并发编程!(2 2-2)
📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!💡 独家亮点抢先看:✅ 图解JVM内存模型(JMM)三大特性,看完再也不怕指令重排序✅ 手撕ReentrantLock源码,AQS队列同步器实现原理大揭秘✅ 全网最细线程状态转换图(附6种状态转换触发条件表)
53 0
|
1月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
51 0
|
1月前
|
安全 Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(2 2-1)
🔥【高薪程序员必看】万字长文拆解Java并发编程!面试官看了直呼内行,90%人不知道的线程安全骚操作!💻🚀《16个高频面试灵魂拷问+底层源码暴击》🔥👉戳这里看如何用1个月经验吊打3年程序员!📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!
40 0
|
1月前
|
网络协议 Java 大数据
【高薪程序员必看】万字长文拆解Java并发编程!(1)
📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!💡 独家亮点抢先看:✅ 图解JVM内存模型(JMM)三大特性,看完再也不怕指令重排序✅ 手撕ReentrantLock源码,AQS队列同步器实现原理大揭秘✅ 全网最细线程状态转换图(附6种状态转换触发条件表)
47 0
|
1月前
|
Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(3-2):并发共享问题的解决与分析
wait方法和notify方法都是Object类的方法:让当前获取锁的线程进入waiting状态,并进入waitlist队列:让当前获取锁的线程进入waiting状态,并进入waitlist队列,等待n秒后自动唤醒:在waitlist队列中挑一个线程唤醒:唤醒所有在waitlist队列中的线程它们都是之间协作的手段,只有拥有对象锁的线程才能调用这些方法,否则会出现IllegalMonitorStateException异常park方法和unpark方法是LockSupport类中的方法。
44 0
OSZAR »