Java synchronized关键字从入门到原理解析

 更新时间:2026年02月24日 09:44:03   作者:百锦再@新空间创想科技  
这篇文章介绍Java synchronized关键字从入门到原理,本课程适合具备Java基础语法知识,初步了解多线程概念但尚未系统学习并发编程的开发者,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

适用对象

本课程适合具备Java基础语法知识,初步了解多线程概念但尚未系统学习并发编程的开发者。无论你是准备面试的求职者,还是希望提升并发编程能力的工程师,这门课程都将为你打下坚实基础。

学习目标

通过两个课时的系统学习,你将能够:

  • 掌握 synchronized的三种使用方式及其区别
  • 理解 synchronized解决线程安全问题的核心原理
  • 剖析 synchronized底层的Monitor机制和锁升级过程
  • 区分 synchronized与volatile、ReentrantLock的适用场景
  • 应对 面试中关于synchronized的高频问题

课程安排

  • 第一课时(约40分钟):基础概念、使用方式、特性详解
  • 第二课时(约50分钟):底层原理、锁升级机制、对比与实战

第一课时:synchronized基础与使用

1.1 从一个线程安全问题开始

让我们先看一个经典问题:两个线程同时对共享变量执行自增操作。

public class Counter {
    private int count = 0;
    public void increment() {
        count++; // 不是原子操作!
    }
    public int getCount() {
        return count;
    }
}
// 测试代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果: " + counter.getCount()); // 期望20000,实际小于20000
    }
}

运行结果:多次执行,结果总是小于20000,有时甚至相差甚远。

问题根源count++看似一行代码,但在JVM中分解为三条指令:

  1. 从主内存读取count到工作内存(读)
  2. 对count进行加1操作(改)
  3. 将结果写回主内存(写)

当两个线程同时执行这三步时,可能发生交替执行,导致最后写入的值覆盖了之前的计算结果——这就是典型的竞态条件

1.2 synchronized是什么?

synchronized是Java提供的一个关键字,译为“同步”。它可以保证在同一时刻,最多只有一个线程执行被它修饰的代码段,从而解决并发环境下的线程安全问题。

从专业角度说,synchronized实现了互斥访问,它保证了三个关键特性:

特性说明
原子性被保护的代码块要么全部执行,要么完全不执行,不会被打断
可见性一个线程修改共享变量后,其他线程能立即看到最新值
有序性禁止编译器和处理器对同步块内的代码进行指令重排序

1.3 初识synchronized的三种用法

synchronized的使用非常灵活,可以根据需要选择不同的加锁对象。下面我们通过三个典型场景来理解它们的区别。

1.3.1 修饰实例方法

public class SyncMethodDemo {
    private int count = 0;
    // synchronized修饰实例方法,锁是当前对象this
    public synchronized void increment() {
        count++;
    }
}

特点

  • 锁对象是调用该方法的实例对象(即this)
  • 同一个对象的多个synchronized实例方法,共用同一把锁
  • 不同对象的方法互不干扰

示意图

线程A ──> obj1.increment() ──> 获取obj1锁 ──> 执行
线程B ──> obj1.increment() ──> 等待obj1锁
线程C ──> obj2.increment() ──> 获取obj2锁 ──> 执行(与A不冲突)

1.3.2 修饰静态方法

public class SyncStaticDemo {
    private static int count = 0;
    // synchronized修饰静态方法,锁是当前类的Class对象
    public static synchronized void increment() {
        count++;
    }
}

特点

  • 锁对象是当前类的Class对象(如SyncStaticDemo.class)
  • 所有实例对象共享同一把类锁
  • 静态方法锁与实例方法锁互不干扰

思考题:如果一个类既有synchronized静态方法,又有synchronized实例方法,两个线程分别调用它们,会互斥吗?

  • 答案:不会。因为一个是类锁,一个是对象锁,两把不同的锁。

1.3.3 修饰代码块

public class SyncBlockDemo {
    private Object lock = new Object();
    private int count = 0;
    public void increment() {
        // 同步代码块,锁是指定的lock对象
        synchronized (lock) {
            count++;
        }
    }
    public void decrement() {
        // 也可以锁this
        synchronized (this) {
            count--;
        }
    }
}

特点

  • 锁对象可以任意指定,灵活性最高
  • 可以精确控制同步范围,提高并发性能
  • 常用锁对象:this、自定义锁对象、类.class

1.4 深入理解锁的范围

1.4.1 三种锁的对比表格

使用形式锁对象作用范围典型应用场景
修饰实例方法当前实例对象单个实例内保护实例变量
修饰静态方法当前类的Class对象所有实例间保护静态变量
修饰代码块指定的任意对象代码块内细粒度控制

1.4.2 常见面试题解析

问题1:synchronized修饰代码块可以给类加锁吗?
当然可以!只要在括号内传入类名.class即可。例如:

synchronized (SyncBlockDemo.class) {
    // 这是类锁
}

问题2:构造方法可以用synchronized修饰吗?
不可以!Java语法规定构造方法不能是同步的。实际上,构造方法本身就是线程安全的,因为JVM会保证在对象初始化完成前,其他线程无法访问该对象。

问题3:静态同步方法和非静态同步方法同时调用会互斥吗?
不会互斥。假设线程A调用实例对象的非静态同步方法,线程B同时调用该对象所属类的静态同步方法,它们持有的是两把不同的锁(对象锁 vs 类锁),因此可以并行执行。

1.5 synchronized的核心特性

1.5.1 可重入性

概念:同一个线程在持有锁的情况下,可以再次获取同一把锁,而不会被阻塞。

public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("进入methodA");
        methodB(); // 直接调用,不会死锁
    }
    public synchronized void methodB() {
        System.out.println("进入methodB");
    }
}

为什么需要可重入?
如果没有可重入性,线程在调用methodA获取锁后,调用methodB时发现自己还要申请同一把锁,就会形成死锁。这显然不合理。

实现原理:每个锁关联一个持有线程和一个计数器。线程第一次获取锁时,计数器置为1;同一线程再次获取时,计数器递增;释放一次,计数器递减。直到计数器归零,锁才真正释放。

1.5.2 可见性保证

synchronized不仅能保证原子性,还能保证内存可见性。JMM(Java内存模型)规定:

  • 线程释放锁时,会将工作内存中的共享变量刷新到主内存
  • 线程获取锁时,会清空工作内存,从主内存重新读取共享变量

这就确保了:一个线程修改的共享变量,在释放锁后,其他线程能立即看到最新值。

1.6 第一课时小结

  • synchronized是Java内置的同步关键字,解决原子性、可见性、有序性问题
  • 三种使用方式:实例方法(锁this)、静态方法(锁Class对象)、代码块(锁任意对象)
  • 核心特性:可重入性、可见性、互斥性
  • 不同锁对象决定了不同的同步范围

第二课时:synchronized原理与优化

2.1 从字节码看synchronized的本质

2.1.1 同步代码块的字节码

先看一段简单的同步代码块:

public class SyncCodeBlock {
    public void doSomething() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
}

使用javap -c -v反编译后,关键字节码如下:

 3: monitorenter      // 进入同步块,获取监视器锁
 4: getstatic         // 调用System.out
 7: ldc               // 加载字符串
 9: invokevirtual     // 调用println
12: aload_1
13: monitorexit       // 退出同步块,释放监视器锁
14: goto 22           // 正常结束跳转
17: astore_2          // 异常处理开始
18: aload_1
19: monitorexit       // 异常时也释放锁
20: aload_2
21: athrow            // 抛出异常
22: return

关键发现

  • 同步代码块使用monitorentermonitorexit指令
  • 有两个monitorexit:一个正常退出,一个异常退出,确保锁一定被释放
  • 这就是为什么synchronized即使抛出异常也不会死锁

2.1.2 同步方法的字节码

public synchronized void syncMethod() {
    System.out.println("hello");
}

反编译结果:

public synchronized void syncMethod();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 注意这个标志
  Code:
   0: getstatic     #2
   3: ldc           #3
   5: invokevirtual #4
   8: return

同步方法没有monitorenter/monitorexit指令,而是通过方法表flags中的ACC_SYNCHRONIZED标识。JVM根据这个标识判断是否需要获取锁。

结论:两种实现方式本质上都是获取对象的监视器锁(Monitor Lock),只是表现形式不同。

2.2 Monitor机制深度剖析

2.2.1 什么是Monitor?

Monitor(监视器)是操作系统中用于实现线程同步的机制。Java中的每个对象都与一个Monitor关联,这个关联关系存储在对象头的Mark Word中。

可以把Monitor理解为一个“接待室”,里面有三个关键区域:

                  ┌─────────────────────┐
                  │      Owner          │ ← 当前持有锁的线程
                  ├─────────────────────┤
                  │     EntryList       │ ← 等待获取锁的线程队列
                  ├─────────────────────┤
                  │     WaitSet         │ ← 调用了wait()的线程队列
                  └─────────────────────┘

2.2.2 Monitor的核心数据结构

在HotSpot虚拟机中,Monitor由C++的ObjectMonitor类实现,关键字段如下:

字段作用
_owner指向当前持有锁的线程
_EntryList等待获取锁的线程队列
_WaitSet调用了wait()的线程队列
_recursions记录锁的重入次数
_count记录线程获取锁的次数

2.2.3 锁获取和释放的完整流程

以两个线程T0和T1竞争锁为例:

步骤1:线程T0尝试获取锁

  • 根据对象头找到对应的ObjectMonitor
  • 检查_owner是否为null
  • 通过CAS操作将_owner设为T0,_count设为1
  • T0获取锁成功,进入同步块执行

步骤2:线程T1尝试获取锁

  • 此时_owner为T0,T1 CAS失败
  • T1会先自旋几次尝试(适应性自旋)
  • 如果T0很快释放锁,T1就成功获取
  • 如果T0未释放,T1进入_EntryList阻塞等待

步骤3:T0释放锁

  • 执行monitorexit_count减1
  • _count变为0,_owner设为null
  • 唤醒_EntryList中的线程(通常是队首线程)
  • T1被唤醒,重新竞争锁

2.3 锁升级:从偏向锁到重量级锁

早期Java的synchronized性能较差,被称为“重量级锁”。JDK 1.6之后引入了一系列优化,让synchronized的性能大幅提升。这就是著名的锁升级机制。

2.3.1 为什么需要锁升级?

想象一下这些场景:

  • 场景A:只有一个线程访问同步代码,根本不需要锁竞争
  • 场景B:两个线程交替访问,几乎没有同时竞争
  • 场景C:多个线程激烈竞争,需要操作系统级别的锁

如果用重量级锁统一处理所有场景,场景A和B会造成不必要的性能开销。锁升级就是让synchronized能够根据竞争激烈程度,动态调整锁的“重量”。

2.3.2 锁的四种状态

从低到高,锁有四种状态:

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

锁可以升级,但不能降级(除了一次GC清理)。

2.3.3 偏向锁(Biased Locking)

适用场景:只有一个线程反复获取同一把锁。

原理

  • 第一次获取锁时,通过CAS将线程ID记录到对象头
  • 之后该线程再来时,只需检查对象头中是否是自己ID
  • 如果是,直接进入,无需任何同步操作

示例代码

public class BiasedLockDemo {
    private static List<Integer> list = new Vector<>(); // Vector的方法都是同步的
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            list.add(i); // 同一线程反复获取锁
        }
    }
}

优点:几乎没有加锁开销,性能极高。
缺点:一旦有其他线程尝试竞争,偏向锁立即撤销并升级。

注意:从JDK 15开始,偏向锁被标记为废弃,未来可能移除。因为在实际应用中,它的性能提升有限,且维护成本高。

2.3.4 轻量级锁(Lightweight Locking)

适用场景:两个线程交替执行同步块,没有真正的竞争。

原理

  • 线程在自己的栈帧中创建Lock Record,存储对象头的拷贝
  • 通过CAS尝试将对象头指向Lock Record
  • 如果成功,获取轻量级锁
  • 如果失败,说明有竞争,升级为重量级锁

轻量级锁的解锁

  • 通过CAS将对象头恢复为原Mark Word
  • 如果恢复成功,解锁完成
  • 如果失败,说明已经升级为重量级锁,走重量级锁解锁流程

2.3.5 重量级锁(Heavyweight Locking)

适用场景:多个线程激烈竞争。

原理

  • 线程进入_EntryList队列阻塞
  • 依赖操作系统Mutex Lock实现
  • 涉及用户态和内核态切换,开销较大

2.3.6 锁升级流程总结

                   偏向锁
                     │
                     │ 有其他线程尝试获取
                     ↓
                 轻量级锁
                     │
                     │ 有真实竞争
                     ↓
                 重量级锁

2.4 synchronized与volatile的对比

特性synchronizedvolatile
原子性✅ 保证❌ 不保证
可见性✅ 保证✅ 保证
有序性✅ 保证✅ 保证(禁止重排序)
用法修饰方法或代码块修饰变量
性能较重(但有优化)极轻
适用场景复合操作单一变量状态标志

volatile适用示例

public class VolatileDemo {
    private volatile boolean flag = true; // 只作为状态标志
    public void stop() {
        flag = false; // 简单写操作
    }
    public void run() {
        while (flag) {
            // 执行任务
        }
    }
}

2.5 synchronized与ReentrantLock的对比

2.5.1 对比表格

对比维度synchronizedReentrantLock
实现方式JVM关键字Java API(基于AQS)
锁释放自动释放必须手动unlock
公平性非公平可设置公平/非公平
可中断不支持支持lockInterruptibly()
超时获取不支持支持tryLock(timeout)
尝试获取不支持支持tryLock()
条件变量一个等待集多个Condition
锁状态查询不支持支持查询持有线程等

2.5.2 如何选择?

优先使用synchronized的情况:

  • 同步逻辑简单,不需要高级功能
  • 希望代码简洁不易出错
  • JVM持续优化,性能已很好

选择ReentrantLock的情况:

  • 需要公平锁
  • 需要尝试获取锁或超时获取
  • 需要可中断的锁获取
  • 需要多个条件变量(如生产者-消费者模式)
  • 高竞争场景需要更精细控制

2.5.3 ReentrantLock使用示例

ReentrantLock lock = new ReentrantLock(true); // 公平锁
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 标准使用模式
lock.lock();
try {
    // 临界区代码
    while (条件不满足) {
        notEmpty.await(); // 等待
    }
    // 执行操作
    notFull.signal();
} finally {
    lock.unlock(); // 必须释放
}

2.6 实战:线程安全的单例模式

综合运用synchronized,实现几种经典的单例模式。

2.6.1 饿汉式(线程安全)

public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

2.6.2 懒汉式(同步方法版)

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

2.6.3 双重检查锁(DCL)

public class DCLSingleton {
    // volatile保证可见性和禁止重排序
    private static volatile DCLSingleton instance;
    private DCLSingleton() {}
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

为什么需要volatile?
instance = new DCLSingleton()不是原子操作,可能发生指令重排序。如果不加volatile,其他线程可能拿到一个未初始化完成的对象。这是DCL的关键细节。

2.7 第二课时小结

  • synchronized底层基于Monitor实现,字节码层面使用monitorenter/monitorexit或ACC_SYNCHRONIZED
  • 锁升级机制(偏向锁→轻量级锁→重量级锁)大幅提升了性能
  • synchronized保证原子性、可见性、有序性,是全面的同步工具
  • 与volatile相比,synchronized更重量但功能更全
  • 与ReentrantLock相比,synchronized简单易用,ReentrantLock功能更丰富
  • 实际开发中,根据场景选择合适的同步机制

课程总结

通过两个课时的学习,我们全面掌握了synchronized关键字:

  • 第一课时:从线程安全问题出发,学习了synchronized的三种使用方式、锁的范围、可重入性等基础概念
  • 第二课时:深入底层,剖析了Monitor机制、锁升级流程,并对比了volatile和ReentrantLock

synchronized作为Java最基础的同步工具,虽然简单,但背后的原理并不简单。理解它的实现机制,不仅能帮助我们写出更正确的并发程序,还能在面试中脱颖而出。

课后思考题

  1. 如果一个线程在同步块中抛出异常,锁会自动释放吗?为什么?
  2. 偏向锁在JDK 15中被废弃的原因是什么?谈谈你的理解。
  3. 如何用synchronized实现一个阻塞队列?
  4. 为什么说synchronized是“悲观锁”,而CAS是“乐观锁”?

到此这篇关于Java synchronized关键字详解:从入门到原理(两课时)的文章就介绍到这了,更多相关Java synchronized关键字内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java发送javax.mail邮件实例讲解

    java发送javax.mail邮件实例讲解

    这篇文章主要为大家介绍了java发送javax.mail邮件实例讲解,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-01-01
  • SpringBoot全局异常处理方式

    SpringBoot全局异常处理方式

    本文详细介绍了Spring Boot中异常处理的多种方案,包括基于请求转发、异常处理器、过滤器等方式,并对其特点、实现方式和适用场景进行了详细介绍,作者推荐了基于请求转发和异常处理器+请求转发补充的方案,认为它们能实现统一的全局异常处理,并能自定义统一响应信息格式
    2024-12-12
  • Java使用CountDownLatch实现统计任务耗时

    Java使用CountDownLatch实现统计任务耗时

    这篇文章主要为大家详细介绍了Java如何使用CountDownLatch实现统计任务耗时的功能,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-06-06
  • Spring Boot 快速搭建微服务框架详细教程

    Spring Boot 快速搭建微服务框架详细教程

    SpringBoot是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。本文重点给大家介绍Spring Boot 快速搭建微服务框架详细教程,需要的的朋友参考下吧
    2017-09-09
  • Java获取配置文件的值过程解析

    Java获取配置文件的值过程解析

    这篇文章主要介绍了java获取配置文件的值过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • MyBatis-Flex实现分页查询的示例代码

    MyBatis-Flex实现分页查询的示例代码

    在MyBatis-Flex中实现分页查询时,需要注意维护一个获取数据库总数的方法,详细介绍了UserService、UserServiceImpl类以及Mapper.xml配置,感兴趣的可以了解一下
    2024-10-10
  • Java双向链表按照顺序添加节点的方法实例

    Java双向链表按照顺序添加节点的方法实例

    这篇文章主要给大家介绍了关于Java双向链表按照顺序添加节点的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • SpringBoot 整合 Netty 多端口监听的操作方法

    SpringBoot 整合 Netty 多端口监听的操作方法

    Netty提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架,这篇文章主要介绍了SpringBoot 整和 Netty 并监听多端口,需要的朋友可以参考下
    2023-10-10
  • SpringBoot图文并茂讲解登录拦截器

    SpringBoot图文并茂讲解登录拦截器

    其实spring boot拦截器的配置方式和springMVC差不多,只有一些小的改变需要注意下就ok了,下面这篇文章主要给大家介绍了关于如何在Springboot实现登陆拦截器功能的相关资料,需要的朋友可以参考下
    2022-06-06
  • SpringBoot使用Editor.md构建Markdown富文本编辑器示例

    SpringBoot使用Editor.md构建Markdown富文本编辑器示例

    这篇文章主要介绍了SpringBoot使用Editor.md构建Markdown富文本编辑器示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03

最新评论