深入拆解 synchronized:从偏向锁到重量级锁的升级之旅与优化秘籍

简介: 本文深入剖析Java中synchronized的底层实现:详解偏向锁、轻量级锁到重量级锁的升级机制,结合对象头Mark Word结构、JVM锁优化(自旋、消除、粗化、逃逸分析),并附死锁排查实战,助你真正掌握并发同步原理。

在Java并发编程中,synchronized是最基础也最重要的同步工具之一。很多开发者只知道它能保证线程安全,却对其底层实现一知半解。本文将深入JVM底层,拆解synchronized的实现原理,详解从偏向锁、轻量级锁到重量级锁的完整升级流程,并介绍JVM为优化synchronized性能所做的努力。

一、synchronized的基本用法

synchronized可以保证在同一时刻,只有一个线程能执行特定的代码块或方法。它主要有三种使用方式:同步代码块、同步实例方法和同步静态方法。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
* synchronized基本用法示例
*
* @author ken
*/

@Slf4j
public class SynchronizedUsageDemo {

   private final Object lock = new Object();
   private static int staticCount = 0;
   private int instanceCount = 0;

   /**
    * 同步代码块:对象锁
    */

   public void syncBlock() {
       synchronized (lock) {
           instanceCount++;
           log.info("同步代码块执行,instanceCount: {}", instanceCount);
       }
   }

   /**
    * 同步实例方法:锁当前对象this
    */

   public synchronized void syncInstanceMethod() {
       instanceCount++;
       log.info("同步实例方法执行,instanceCount: {}", instanceCount);
   }

   /**
    * 同步静态方法:锁当前类的Class对象
    */

   public static synchronized void syncStaticMethod() {
       staticCount++;
       log.info("同步静态方法执行,staticCount: {}", staticCount);
   }

   public static void main(String[] args) {
       SynchronizedUsageDemo demo = new SynchronizedUsageDemo();
       new Thread(demo::syncBlock).start();
       new Thread(demo::syncInstanceMethod).start();
       new Thread(SynchronizedUsageDemo::syncStaticMethod).start();
       new Thread(SynchronizedUsageDemo::syncStaticMethod).start();
   }
}

二、Java对象头与Mark Word

要理解synchronized的底层实现,首先得了解Java对象在内存中的布局。Java对象由三部分组成:对象头、实例数据和对齐填充。其中对象头是实现锁的关键,它包含两部分信息:Mark Word和Klass Pointer。 Mark Word用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。在64位JVM中,Mark Word的长度是64bit,其结构会根据锁状态的不同而变化。

锁状态 Mark Word内容(64bit)
无锁 unused(25bit)| identity_hashcode(31bit)| unused(1bit)| age(4bit)| 0 | 01
偏向锁 thread(54bit)| epoch(2bit)| unused(1bit)| age(4bit)| 1 | 01
轻量级锁 ptr_to_lock_record(62bit)| 00
重量级锁 ptr_to_monitor(62bit)| 10
GC标记 empty(62bit)| 11

我们可以通过JOL(Java Object Layout)工具来查看对象头的实际布局。

<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.17</version>
</dependency>

package com.jam.demo;

import org.openjdk.jol.info.ClassLayout;

/**
* 查看对象头布局示例
*
* @author ken
*/

public class ObjectHeaderDemo {

   public static void main(String[] args) {
       Object obj = new Object();
       System.out.println(ClassLayout.parseInstance(obj).toPrintable());
   }
}

三、锁升级全流程

JDK 1.6之后,synchronized引入了锁升级机制,以减少获取锁和释放锁带来的性能消耗。锁会从无锁开始,随着竞争的加剧,逐渐升级为偏向锁、轻量级锁,最终升级为重量级锁。

3.1 偏向锁

当只有一个线程访问同步块时,JVM会将锁偏向于该线程,后续该线程再次访问时,无需进行任何同步操作,直接进入同步块。 偏向锁的获取流程:

  1. 线程访问同步块时,检查Mark Word中的线程ID是否为空。
  2. 如果为空,通过CAS操作将Mark Word中的线程ID设置为当前线程ID,此时进入偏向锁状态。
  3. 如果线程ID已存在,检查是否为当前线程ID,如果是,直接执行同步代码;如果不是,说明有其他线程竞争,需要撤销偏向锁。 偏向锁的撤销需要等到全局安全点(此时所有线程都停止执行字节码),然后检查原持有偏向锁的线程是否存活:
  • 如果原线程不存活,将对象头重置为无锁状态,然后重新偏向新线程。
  • 如果原线程存活,升级为轻量级锁。

3.2 轻量级锁

当偏向锁被撤销或有多个线程交替使用锁时,锁会升级为轻量级锁。轻量级锁使用CAS操作来尝试获取锁,避免了重量级锁的线程阻塞和唤醒开销。 轻量级锁的加锁流程:

  1. 线程在自己的栈帧中创建一个名为Lock Record的结构,用于存储Mark Word的副本。
  2. 将对象头中的Mark Word复制到Lock Record中。
  3. 通过CAS操作尝试将对象头的Mark Word替换为指向Lock Record的指针。
  4. 如果CAS成功,当前线程获得轻量级锁,执行同步代码。
  5. 如果CAS失败,检查是否是锁重入:如果是,将Lock Record的displaced_header设为null,作为重入的计数;如果不是,进行自适应自旋。
  6. 如果自旋成功,获得锁;如果自旋失败,升级为重量级锁。 轻量级锁的解锁流程:
  7. 通过CAS操作将Lock Record中的Mark Word复制回对象头。
  8. 如果CAS成功,解锁成功。
  9. 如果CAS失败,说明锁已经升级为重量级锁,需要释放锁并唤醒等待的线程。

3.3 重量级锁

当锁竞争激烈,轻量级锁的CAS操作和自旋都无法获取锁时,锁会升级为重量级锁。重量级锁基于ObjectMonitor实现,此时线程会被阻塞,等待锁释放后被唤醒。 ObjectMonitor是JVM内部的一个对象,包含以下关键字段:

  • _owner:指向持有锁的线程。
  • _EntryList:存储等待获取锁的线程。
  • _WaitSet:存储调用wait()方法后等待的线程。 重量级锁的加锁流程:
  1. 线程进入_EntryList,状态变为BLOCKED。
  2. 当_owner线程释放锁时,会从_EntryList中唤醒一个线程。
  3. 被唤醒的线程尝试获取锁,如果成功,成为新的_owner。 重量级锁的解锁流程:
  4. _owner线程释放锁,将_owner设为null。
  5. 从_EntryList中唤醒一个线程,让它尝试获取锁。

四、锁优化机制

JVM为了提高synchronized的性能,引入了多种锁优化机制,包括自适应自旋、锁消除、锁粗化和逃逸分析。

4.1 自适应自旋

轻量级锁加锁失败时,线程会进行自旋等待,而不是直接阻塞。自适应自旋意味着自旋的时间不是固定的,而是根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果前一次自旋成功获取了锁,JVM会认为这次自旋也很可能成功,从而延长自旋时间;如果前一次自旋失败,JVM会缩短自旋时间,甚至直接跳过自旋。

4.2 锁消除

锁消除是指JIT编译器在运行时,通过逃逸分析发现某个对象不会被其他线程访问,那么该对象的锁就可以被消除。因为不会有线程竞争,所以加锁和解锁操作是多余的。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
* 锁消除示例
*
* @author ken
*/

@Slf4j
public class LockEliminationDemo {

   /**
    * 方法中的StringBuffer是局部变量,不会逃逸,JIT会消除锁
    */

   public String concat(String a, String b, String c) {
       StringBuffer sb = new StringBuffer();
       sb.append(a);
       sb.append(b);
       sb.append(c);
       return sb.toString();
   }

   public static void main(String[] args) {
       LockEliminationDemo demo = new LockEliminationDemo();
       for (int i = 0; i < 100000; i++) {
           demo.concat("a", "b", "c");
       }
       log.info("执行完成");
   }
}

4.3 锁粗化

锁粗化是指将连续的加锁和解锁操作合并成一个更大的锁范围,减少加锁和解锁的次数。如果一个线程连续对同一个对象进行多次加锁和解锁,JIT会将这些操作合并,只进行一次加锁和解锁。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
* 锁粗化示例
*
* @author ken
*/

@Slf4j
public class LockCoarseningDemo {

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

   /**
    * 连续的加锁解锁,JIT会粗化锁范围
    */

   public void increment() {
       synchronized (lock) {
           count++;
       }
       synchronized (lock) {
           count++;
       }
       synchronized (lock) {
           count++;
       }
   }

   public static void main(String[] args) {
       LockCoarseningDemo demo = new LockCoarseningDemo();
       for (int i = 0; i < 100000; i++) {
           demo.increment();
       }
       log.info("count: {}", demo.count);
   }
}

4.4 逃逸分析

逃逸分析是JVM的一种分析技术,用于判断对象的作用域是否会逃逸出方法或线程。如果对象不会逃逸,JVM可以进行以下优化:

  • 栈上分配:将对象分配在栈上,而不是堆上,减少GC压力。
  • 标量替换:将对象分解成多个基本类型,直接分配在栈上。
  • 锁消除:如前面所述,消除不会逃逸对象的锁。

五、实战:死锁排查

在使用synchronized时,如果不注意加锁顺序,可能会导致死锁。死锁是指两个或多个线程互相等待对方释放锁,导致所有线程都无法继续执行。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
* 死锁示例
*
* @author ken
*/

@Slf4j
public class DeadlockDemo {

   private static final Object LOCK_A = new Object();
   private static final Object LOCK_B = new Object();

   public static void main(String[] args) {
       new Thread(() -> {
           synchronized (LOCK_A) {
               log.info("线程1获取到LOCK_A");
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   log.error("线程中断", e);
               }
               synchronized (LOCK_B) {
                   log.info("线程1获取到LOCK_B");
               }
           }
       }, "线程1").start();

       new Thread(() -> {
           synchronized (LOCK_B) {
               log.info("线程2获取到LOCK_B");
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   log.error("线程中断", e);
               }
               synchronized (LOCK_A) {
                   log.info("线程2获取到LOCK_A");
               }
           }
       }, "线程2").start();
   }
}

运行程序后,我们可以通过jstack工具排查死锁:

  1. 用jps命令找到进程ID。
  2. 用jstack <进程ID>查看线程堆栈,会看到明确的死锁提示。

synchronized的底层实现涉及对象头、Mark Word、锁升级和各种优化机制。理解这些原理,不仅能帮助我们写出更高效的并发代码,还能在遇到并发问题时快速定位和解决。

目录
相关文章
|
1月前
|
canal 缓存 NoSQL
数据库扛不住高并发?Redis缓存+双写一致性:给你的系统装上“涡轮增压”
数据库小学妹带你破解Redis缓存一致性难题!面对高并发,如何确保Redis与数据库数据同步?详解“先更库后删缓”“延时双删”“Binlog异步同步”等4大方案,直击雪崩、击穿、穿透三座大山,助你构建又快又稳的数据库架构.
|
2月前
|
SQL 存储 缓存
深入拆解 Java volatile:从内存屏障到无锁编程的实战指南
volatile是Java并发编程核心关键字,通过内存屏障保证共享变量的可见性与有序性,但不保证原子性。本文深入解析其原理、典型应用(如DCL单例、状态标记)及与synchronized、原子类的区别,助你正确高效使用。
193 12
|
2月前
|
安全 Java
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
本文深度解析Java并发核心基石——AbstractQueuedSynchronizer(AQS):详解其“volatile state + CLH虚拟队列”架构、独占/共享模式实现原理,并结合ReentrantLock、CountDownLatch、Semaphore源码,助你彻底掌握并发编程底层密钥。
250 10
|
11月前
|
存储 安全 Java
synchronized 锁升级
JDK 6 引入的 synchronized 锁升级机制,通过偏向锁、轻量级锁和重量级锁的动态切换,优化了多线程同步性能。该机制根据竞争情况逐步升级锁状态,减少线程阻塞和系统调用开销,从而提升并发效率。
511 0
|
3月前
|
Java 数据中心
Java 并发核心:CountDownLatch、CyclicBarrier、Semaphore 原理吃透 + 生产级实战
本文深度解析JUC三大核心并发工具:CountDownLatch(一次性等待)、CyclicBarrier(可重用屏障)和Semaphore(信号量限流)。从AQS原理、源码关键点、生产实战、易混淆对比到避坑指南,五维讲透多线程协作本质,助力高并发系统稳定落地。
417 1
|
2月前
|
存储 开发框架 架构师
软考系统架构师硬核通关笔记 - 计算机系统基础
本文专为软考系统架构师考生打造,直击计算机系统基础知识备考痛点:摒弃死记硬背,深度剖析CPU(运算器/控制器、CISC/RISC、GPU/DSP/FPGA)、存储体系(SRAM/DRAM/Cache映射与计算)、I/O控制(中断/DMA/通道)、总线接口及操作系统核心原理,并贯通分布式架构(CORBA/J2EE/DNA)与备考策略,强调场景理解与底层逻辑,助你高效通关。
259 5
|
29天前
|
存储 人工智能 自然语言处理
2026年阿里云新老用户最新优惠活动:云服务器活动、免费试用活动、AI产品活动参考
阿里云2026新老用户优惠活动涵盖三大板块。云服务器方面,轻量应用服务器低至38元/年,经济型e实例99元/年,u1实例199元/年,限时限量抢购。免费试用方面,新老用户可领最高200元试用点,AI产品免费提供7000万+大模型tokens及30+款产品体验,140+云产品最长12个月免费试用。AI产品方面,百炼Token Plan支持多模型切换、多档套餐;HappyHorse视频生成模型限时8折;OpenClaw一键部署低至9.9元起。此外还提供折扣券、学生无门槛券、算力补贴等多类优惠券,整体以"普惠基础设施+零成本体验+智能化赋能"策略,全面降低上云与用AI门槛。
2026年阿里云新老用户最新优惠活动:云服务器活动、免费试用活动、AI产品活动参考
|
3月前
|
存储 监控 Java
Java内存管理之道——从堆栈到GC的深度解析
对于Java开发者而言,内存管理是一道绕不开的关卡。与C/C++需要手动管理内存不同,Java引入了自动垃圾回收(GC)机制,极大降低了内存管理的复杂度。
184 3
|
2月前
|
存储 安全 Java
ConcurrentHashMap 深度解析:从 JDK7 到 JDK8 的演进与并发安全保障
本文详解 Java 中 `ConcurrentHashMap` 的演进:JDK7 采用分段锁(Segment)提升并发性;JDK8 重构为 CAS + synchronized + 红黑树,支持并发扩容与更优查询性能。对比差异,剖析线程安全机制与使用要点。
270 12
|
1月前
|
供应链 安全 前端开发
2026 年新型网络威胁演进与防御体系研究 —— 以两起典型攻击为例
本文剖析2026年ShinyHunters入侵Canvas与Play勒索软件利用CLFS零日漏洞两大典型事件,揭示供应链攻击、身份劫持、零日武器化、双重勒索等新威胁特征;提出以身份为中心、零信任为基座的五层防御体系,并提供可落地的令牌校验、提权检测、数据导出监控等代码实现,助力教育、金融等行业构建韧性安全防线。(239字)
397 8