news 2026/1/13 15:15:25

#深入理解Synchronized:Java并发编程的基石

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
#深入理解Synchronized:Java并发编程的基石

在Java并发编程中,线程安全是永恒的核心话题。当多个线程同时访问共享资源时,很容易出现数据不一致、脏数据等问题。而synchronized关键字作为Java内置的同步机制,是解决线程安全问题的基础手段。本文将从线程安全本质出发,逐步拆解synchronized的使用场景、底层实现原理、锁升级机制,并用生产者-消费者模型实战验证,帮助大家彻底掌握这一核心技术。

一、线程安全的核心:为什么需要Synchronized?

在深入synchronized之前,我们首先要明白:为什么多线程环境下会出现安全问题?

1.1 线程安全的定义

线程安全是指:多个线程并发访问某个Java对象时,无论操作系统如何调度线程、如何交替执行,该对象都能表现出一致的、正确的行为,最终结果与单线程执行完全相同。

1.2 线程不安全的根源:三个核心概念

要理解线程不安全,必须先掌握以下三个关键概念:

  • 临界区资源:可以被多个线程共享访问的资源(如共享变量、数据库连接、文件等)。
  • 临界区代码段:每个线程中访问临界资源的那段代码(比如自增运算、修改共享变量的逻辑)。
  • 竞态条件:多个线程在临界区代码段并发执行时,由于代码执行顺序不同,导致最终结果不确定的情况。

1.3 经典案例:自增运算的线程不安全

最典型的线程不安全场景就是自增运算(i++),它看似是一个原子操作,实则包含三个独立的步骤:

  1. 从主内存读取变量i的值到线程工作内存(内存取值);
  2. 在线程工作内存中对i进行加1操作(寄存器计算);
  3. 将计算后的结果写回主内存(存值到内存)。

当多个线程同时执行i++时,就可能出现"指令交错":

  • 线程A读取i=0,还未完成加1;
  • 线程B也读取i=0,执行加1后写回主内存,i=1
  • 线程A继续执行加1(此时工作内存中还是0),写回主内存,i=1

最终两次自增只得到了1,这就是竞态条件导致的线程不安全。而synchronized的核心作用,就是保证临界区代码段的原子性执行——同一时间只有一个线程能进入临界区,避免指令交错。

二、Synchronized的使用方式:同步块与同步方法

synchronized的使用非常灵活,主要分为同步块同步方法两大类,核心区别在于"锁对象"的不同。

2.1 同步块(Synchronized Block)

同步块是指用synchronized关键字包裹的代码块,语法格式:

synchronized(锁对象){// 临界区代码段}
  • 锁对象要求:必须是一个普通的Object对象(如new Object()、自定义对象实例),不能是基本数据类型(如int、long)。
  • 核心原理:线程进入同步块前,必须先获取"锁对象"的监视锁(内置锁);执行完代码块后,自动释放锁。
  • 优点:粒度更细,只锁定临界区代码,不影响其他代码的执行效率。

示例:用同步块解决自增线程安全问题

publicclassSafeCounter{privateintcount=0;// 锁对象(建议使用专门的锁对象,避免与其他用途冲突)privatefinalObjectlock=newObject();publicvoidincrement(){synchronized(lock){// 锁定临界区count++;}}publicintgetCount(){returncount;}}

2.2 同步方法(Synchronized Method)

同步方法是指在方法声明时添加synchronized关键字,语法格式:

// 普通同步方法publicsynchronizedvoidmethod(){// 临界区代码}// 静态同步方法publicstaticsynchronizedvoidstaticMethod(){// 临界区代码}

同步方法的锁对象是隐式指定的,分为两种情况:

  1. 普通同步方法:锁对象是当前对象(this),即调用该方法的对象实例。
  2. 静态同步方法:锁对象是当前类的Class对象(如SafeCounter.class),因为静态方法属于类,不属于某个实例。

注意事项

  • 普通同步方法和静态同步方法的锁对象不同,因此它们之间不会相互阻塞(比如一个线程执行普通同步方法,另一个线程执行静态同步方法,不会竞争锁)。
  • 同步方法的粒度较粗,会锁定整个方法体,如果方法中包含非临界区代码,会降低执行效率。

示例:同步方法实现自增安全

publicclassSafeCounter{privateintcount=0;// 普通同步方法,锁对象是thispublicsynchronizedvoidincrement(){count++;}// 静态同步方法,锁对象是SafeCounter.classpublicstaticsynchronizedvoidstaticIncrement(SafeCountercounter){counter.count++;}}

三、实战:用Synchronized实现生产者-消费者模型

生产者-消费者模型是并发编程中的经典场景,恰好能体现synchronized的同步能力,同时需要结合wait()notify()实现线程间通信。

3.1 模型核心需求

  1. 并发安全:生产者与生产者、消费者与消费者、生产者与消费者之间并发操作缓冲区,不能出现数据不一致(如重复生产、重复消费、数据丢失)。
  2. 边界控制:缓冲区满时生产者不能继续生产,缓冲区空时消费者不能继续消费。
  3. 资源优化:空闲线程(如缓冲区满的生产者、缓冲区空的消费者)应阻塞而非空循环,避免浪费CPU。

3.2 实现思路

  • 缓冲区:用队列Queue存储数据,作为临界区资源。
  • 同步机制:用synchronized锁定缓冲区,保证生产/消费操作的原子性。
  • 线程通信:用wait()(让线程阻塞并释放锁)和notifyAll()(唤醒所有阻塞线程)实现边界控制。

3.3 完整代码实现

importjava.util.LinkedList;importjava.util.Queue;// 数据缓冲区(临界区资源)classDataBuffer{privatefinalQueue<Integer>buffer=newLinkedList<>();privatefinalintMAX_CAPACITY=10;// 缓冲区最大容量// 生产者生产数据publicsynchronizedvoidproduce(intdata)throwsInterruptedException{// 缓冲区满时,生产者阻塞while(buffer.size()==MAX_CAPACITY){System.out.println("缓冲区满,生产者"+Thread.currentThread().getName()+"阻塞");wait();// 释放锁,进入阻塞状态}// 生产数据buffer.offer(data);System.out.println("生产者"+Thread.currentThread().getName()+"生产:"+data+",当前缓冲区大小:"+buffer.size());notifyAll();// 唤醒所有阻塞的线程(可能是消费者或其他生产者)}// 消费者消费数据publicsynchronizedintconsume()throwsInterruptedException{// 缓冲区空时,消费者阻塞while(buffer.isEmpty()){System.out.println("缓冲区空,消费者"+Thread.currentThread().getName()+"阻塞");wait();// 释放锁,进入阻塞状态}// 消费数据intdata=buffer.poll();System.out.println("消费者"+Thread.currentThread().getName()+"消费:"+data+",当前缓冲区大小:"+buffer.size());notifyAll();// 唤醒所有阻塞的线程returndata;}}// 生产者线程classProducerimplementsRunnable{privatefinalDataBufferbuffer;publicProducer(DataBufferbuffer){this.buffer=buffer;}@Overridepublicvoidrun(){for(inti=0;i<20;i++){// 生产20个数据try{buffer.produce(i);Thread.sleep(100);// 模拟生产耗时}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}}// 消费者线程classConsumerimplementsRunnable{privatefinalDataBufferbuffer;publicConsumer(DataBufferbuffer){this.buffer=buffer;}@Overridepublicvoidrun(){for(inti=0;i<20;i++){// 消费20个数据try{buffer.consume();Thread.sleep(200);// 模拟消费耗时}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}}

3.4 关键说明

  • 为什么用while而非if判断边界?wait()可能被"虚假唤醒"(即没有被notify()唤醒,而是系统自发唤醒),while会重新检查条件,确保线程只有在满足条件时才继续执行。
  • notifyAll()vsnotify()notify()只唤醒一个随机阻塞的线程,可能导致某些线程长期阻塞(如生产者一直唤醒生产者);notifyAll()唤醒所有阻塞线程,确保公平性,因此在生产者-消费者模型中更常用。
  • synchronizedwait()/notify()的关系wait()notify()必须在synchronized代码块/方法中调用,否则会抛出IllegalMonitorStateException。因为这两个方法需要操作对象的监视器(锁),必须先获取锁才能操作。

四、底层原理:Java对象结构与内置锁

要真正理解synchronized,必须深入其底层实现——它依赖于Java对象的内置锁(监视器锁,Monitor),而内置锁的实现又与Java对象结构密切相关。

4.1 Java对象的内存结构

4.1.1 核心部分:对象头(Object Header)

对象头是实现synchronized的关键,其中最重要的是Mark WordClass Pointer

  • Mark Word(标记字):占8字节(64位),用于存储对象的线程锁状态、哈希码、GC分代年龄等信息。其结构会随着锁状态的变化而变化(后面锁升级会详细讲)。
  • Class Pointer(类型指针):占8字节,指向方法区中该对象对应的Class元数据(如类名、方法、字段等),JVM通过它确认对象的类型。
  • Array Length(数组长度):仅数组对象有,占4字节,存储数组的长度。
4.1.2 对象体与对齐字节
  • 对象体:存储对象的成员变量值(如countname等),变量的类型和顺序会影响内存占用。
  • 对齐字节:JVM要求对象的总大小必须是8字节的整数倍(内存对齐),这样CPU访问内存时效率更高。如果对象头+对象体的大小不是8的倍数,就用对齐字节补齐。

4.2 内置锁(Monitor)的本质

synchronized的"锁"本质是Java对象的监视器(Monitor),每个Java对象在创建时都会伴随一个Monitor的创建(与对象生命周期一致)。

Monitor的结构可以简单理解为:

Monitor { Owner: null/线程ID // 持有锁的线程,初始为null EntryList: 线程队列 // 等待获取锁的线程(阻塞状态) WaitSet: 线程队列 // 调用wait()后阻塞的线程 RecursionCount: 0 // 重入计数器(记录线程重入锁的次数) }
  • 锁的获取:线程进入同步代码前,会尝试获取Monitor的Owner权限。如果Owner为null,当前线程成为Owner,RecursionCount+1;如果Owner是当前线程,RecursionCount+1(重入锁特性);如果Owner是其他线程,当前线程进入EntryList,变为BLOCKED状态。
  • 锁的释放:线程执行完同步代码或调用wait()后,RecursionCount-1。当RecursionCount为0时,Owner变为null,同时唤醒EntryList中的一个线程,让它尝试获取锁。

五、锁升级:从偏向锁到重量级锁的进化

在JDK1.6之前,synchronized的实现非常简单粗暴——无论是否有线程竞争,都直接使用重量级锁(依赖操作系统的互斥量Mutex)。但重量级锁有个致命问题:线程切换时需要从用户态切换到内核态,这个过程开销很大,导致并发效率极低。

为了解决这个问题,JDK1.6引入了锁升级机制:锁会根据线程竞争的激烈程度,从无锁→偏向锁→轻量级锁→重量级锁逐步升级,避免一开始就使用重量级锁带来的性能损耗。

5.1 锁的四种状态

锁的状态存储在对象的Mark Word中,通过lock(锁标志位)和biased_lock(偏向锁标志位)区分,四种状态的对比如下:

锁状态biased_lock(偏向标志)lock(锁标志位)Mark Word主要存储内容适用场景
无锁001哈希码、GC分代年龄无线程竞争
偏向锁101偏向线程ID、GC分代年龄单线程重复获取锁
轻量级锁000锁记录指针(指向线程栈帧)多线程轻度竞争(交替获取锁)
重量级锁010Monitor指针(指向监视器)多线程重度竞争(同时争抢锁)

5.2 锁升级的详细流程

5.2.1 1. 无锁状态
  • 场景:Java对象刚创建时,还没有任何线程尝试获取它的锁。
  • Mark Word结构:存储对象的哈希码(调用hashCode()后会计算并存储)、GC分代年龄,锁标志位01,偏向标志0。
5.2.2 2. 偏向锁(Biased Lock)
  • 核心目标:单线程重复获取锁时,避免频繁的CAS操作,提高效率。

  • 升级流程

    1. 当第一个线程尝试获取锁时,JVM检查Mark Word的偏向标志为0、锁标志位为01(无锁状态)。
    2. 线程通过CAS操作,将自己的线程ID写入Mark Word,同时将偏向标志设为1(变为偏向锁状态)。
    3. 后续该线程再次进入同步代码时,只需检查Mark Word中的线程ID是否为自己,无需再次CAS,直接获取锁(快速重入)。
  • 偏向锁的撤销
    当有其他线程尝试获取该锁时,偏向锁会被撤销,升级为轻量级锁或重量级锁。撤销的触发条件:

    1. 多个线程竞争偏向锁(第二个线程CAS修改Mark Word失败);
    2. 调用hashCode()方法(Mark Word需要存储哈希码,与偏向线程ID冲突)。

    撤销流程(会触发STW:Stop The World,所有用户线程暂停):

    1. JVM等待全局安全点(所有用户线程停止执行);
    2. 遍历所有线程的栈帧,检查是否有线程持有该锁的锁记录;
    3. 清空锁记录,将Mark Word恢复为无锁状态,清除偏向线程ID;
    4. 将锁升级为轻量级锁(少数场景直接升级为重量级锁);
    5. 唤醒被阻塞的线程,让它们竞争轻量级锁。
  • 注意:由于偏向锁的撤销会触发STW,对于高并发场景(多线程频繁竞争锁),偏向锁反而会降低性能。因此可以通过JVM参数-XX:-UseBiasedLocking关闭偏向锁(默认开启)。

5.2.3 3. 轻量级锁(Lightweight Lock)
  • 核心目标:多线程轻度竞争时,通过CAS自旋避免升级为重量级锁(自旋是用户态操作,开销远小于内核态切换)。
  • 升级流程
    1. 偏向锁撤销后,JVM会在抢锁线程的栈帧中创建一个锁记录(Lock Record),存储当前对象Mark Word的拷贝(称为Displaced Mark Word)。
    2. 抢锁线程通过CAS操作,尝试将对象Mark Word中的内容替换为"锁记录指针"(指向自己栈帧中的Lock Record)。
    3. 如果CAS成功,当前线程获取轻量级锁,执行同步代码;如果CAS失败,说明有其他线程也在争抢锁(竞争加剧),轻量级锁会膨胀为重量级锁。
5.2.4 4. 重量级锁(Heavyweight Lock)
  • 核心目标:多线程重度竞争时,通过操作系统的互斥量保证线程安全(牺牲效率换稳定性)。
  • 升级流程
    1. 轻量级锁CAS自旋失败后,JVM会将锁升级为重量级锁,此时对象Mark Word中的内容替换为"Monitor指针"(指向该对象对应的Monitor)。
    2. 后续争抢锁的线程会直接进入Monitor的EntryList,变为BLOCKED状态(释放CPU资源)。
    3. 持有锁的线程释放锁后,会唤醒EntryList中的一个线程,让它尝试获取Monitor的Owner权限。

5.3 锁升级的核心总结

  • 锁升级是不可逆的:一旦升级为重量级锁,就不会再降级为轻量级锁或偏向锁。
  • 核心优化思路:按需分配锁的开销——无竞争时用无锁,单线程竞争用偏向锁(低开销),轻度竞争用轻量级锁(自旋CAS),重度竞争用重量级锁(阻塞等待)。
  • 实际开发启示:高并发场景下,应尽量避免锁竞争(如使用局部变量、无锁编程),如果必须使用锁,也要控制锁的粒度(如用同步块而非同步方法),避免锁升级为重量级锁。

六、Synchronized的完整执行流程

结合前面的锁升级和Monitor原理,我们可以梳理出synchronized的完整执行流程:

  1. 线程进入同步代码前,首先检查锁对象的Mark Word

    • 如果biased_lock=1lock=01(可偏向状态),检查Mark Word中的线程ID是否为当前线程:
      • 是:直接进入同步代码(偏向锁重入);
      • 否:通过CAS尝试将线程ID写入Mark Word,CAS成功则进入同步代码,CAS失败则撤销偏向锁,升级为轻量级锁。
    • 如果biased_lock=0lock=00(轻量级锁状态):
      • 线程在栈帧中创建Lock Record,存储Mark Word拷贝;
      • 通过CAS尝试将Mark Word替换为Lock Record指针,CAS成功则获取锁,失败则自旋重试;
      • 自旋超过阈值(默认10次)或有更多线程竞争,轻量级锁膨胀为重量级锁。
    • 如果lock=10(重量级锁状态):
      • 线程进入Monitor的EntryList,变为BLOCKED状态,等待被唤醒。
  2. 线程执行同步代码:

    • 如果线程再次进入同步代码(重入),轻量级锁会增加Lock Record计数,重量级锁会增加RecursionCount。
  3. 线程释放锁:

    • 轻量级锁:删除Lock Record,通过CAS将Mark Word恢复为原始值;
    • 重量级锁:RecursionCount减1,当计数为0时,释放Monitor的Owner权限,唤醒EntryList中的线程;
    • 释放锁后,线程退出同步代码。

七、Synchronized的特性与线程间通信

7.1 Synchronized的三大核心特性

  • 原子性:临界区代码段的执行是不可中断的,同一时间只有一个线程能执行(由Monitor保证)。
  • 可见性:线程释放锁时,会将工作内存中的修改同步到主内存;线程获取锁时,会从主内存重新读取最新值(避免脏读)。
  • 有序性:禁止指令重排序(JVM会在同步代码前后添加内存屏障),保证代码执行顺序与编写顺序一致。

7.2 线程间通信的三种方式

多线程操作共享资源时,需要通过通信协调执行顺序,常见方式有:

  1. 等待-通知机制(wait/notify):基于synchronized实现,线程通过wait()释放锁并阻塞,通过notify()/notifyAll()唤醒阻塞线程(如生产者-消费者模型)。
  2. 共享内存:线程通过读写共享变量进行通信(如用volatile修饰共享变量,保证可见性)。
  3. 管道流:通过PipedInputStreamPipedOutputStream实现线程间的字节流通信(适用于线程间数据传输)。

其中,等待-通知机制是synchronized的配套通信方式,也是最常用的并发协调手段。

八、总结与面试重点

synchronized作为Java并发编程的基础,是面试中的高频考点。核心要点总结如下:

核心知识点

  1. 线程安全根源:竞态条件导致指令交错,synchronized通过锁定临界区保证原子性。
  2. 使用方式:同步块(锁对象自定义,粒度细)、同步方法(普通锁this,静态锁Class)。
  3. 底层原理:依赖Java对象的Monitor(内置锁),锁状态存储在Mark Word中。
  4. 锁升级:无锁→偏向锁→轻量级锁→重量级锁,按需优化性能。
  5. 核心特性:原子性、可见性、有序性,支持重入锁。

面试常问问题

  1. synchronized和volatile的区别?

    • volatile仅保证可见性和有序性,不保证原子性;synchronized保证原子性、可见性、有序性。
    • volatile用于变量级别的同步,synchronized用于代码块/方法级别的同步。
    • volatile不会阻塞线程,synchronized会导致线程阻塞。
  2. synchronized是公平锁还是非公平锁?

    • 非公平锁。唤醒线程时,会随机选择EntryList中的一个线程,不遵循"先到先得"(公平锁会增加开销)。
  3. synchronized的锁重入原理是什么?

    • 轻量级锁通过栈帧中的Lock Record计数实现,重量级锁通过Monitor的RecursionCount计数实现,线程再次获取锁时只需增加计数,无需重新竞争。
  4. 为什么偏向锁在高并发场景下建议关闭?

    • 高并发时,偏向锁会频繁被撤销,触发STW(Stop The World),反而降低性能。通过-XX:-UseBiasedLocking关闭后,直接从无锁升级为轻量级锁。
  5. 生产者-消费者模型用synchronized怎么实现?

    • 缓冲区作为临界区,用synchronized锁定;
    • 生产者用while(buffer满) wait(),消费者用while(buffer空) wait()
    • 生产/消费后调用notifyAll()唤醒线程。

通过本文的学习,相信大家已经对synchronized的使用、原理、优化有了全面的理解。在实际开发中,要根据并发场景选择合适的同步方式,同时结合锁升级机制优化性能;在面试中,要能清晰阐述底层原理和实战经验,展现自己的技术深度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/26 1:53:10

终极B站视频下载指南:一键批量保存你的最爱内容

你是否曾经遇到过这样的情况&#xff1a;看到一个精彩的B站视频想要收藏&#xff0c;却发现无法离线观看&#xff1f;或者想要批量保存自己喜欢的UP主系列视频&#xff0c;却苦于一个个下载太麻烦&#xff1f;现在&#xff0c;这些烦恼都将迎刃而解&#xff01; 【免费下载链接…

作者头像 李华
网站建设 2026/1/9 4:24:53

NVIDIA DALI数据预处理加速:8个深度优化实践方法

NVIDIA DALI数据预处理加速&#xff1a;8个深度优化实践方法 【免费下载链接】DALI NVIDIA/DALI: DALI 是一个用于数据预处理和增强的 Python 库&#xff0c;可以用于图像&#xff0c;视频和音频数据的处理和增强&#xff0c;支持多种数据格式和平台&#xff0c;如 Python&…

作者头像 李华
网站建设 2025/12/31 12:38:51

StringUtils终极选型指南

&#x1f3af; 前言&#xff1a;为何StringUtils的"战国时代"仍在继续&#xff1f; 在现代Java开发中&#xff0c;字符串处理如同空气般无处不在。每当新项目启动&#xff0c;开发者们总面临一个看似微小却影响深远的选择&#xff1a;用哪个StringUtils&#xff1f;…

作者头像 李华
网站建设 2025/12/15 8:00:33

万亿级AI新纪元:Kimi-K2-Base如何重塑大语言模型应用格局

在人工智能技术快速迭代的当下&#xff0c;Moonshot AI推出的Kimi-K2-Base模型正以前所未有的万亿参数规模&#xff0c;为全球开发者打开全新的技术视野。这款基于混合专家架构的基础预训练模型&#xff0c;不仅展现了卓越的技术性能&#xff0c;更为企业级应用提供了可靠的技术…

作者头像 李华
网站建设 2026/1/10 7:10:24

Kimi K2 Instruct:万亿参数MoE模型如何重塑企业智能代理应用

Kimi K2 Instruct&#xff1a;万亿参数MoE模型如何重塑企业智能代理应用 【免费下载链接】Kimi-K2-Instruct Kimi K2 is a state-of-the-art mixture-of-experts (MoE) language model with 32 billion activated parameters and 1 trillion total parameters. Trained with th…

作者头像 李华
网站建设 2025/12/15 7:55:24

百度网盘加速终极指南:完整解决方案深度解析

百度网盘加速终极指南&#xff1a;完整解决方案深度解析 【免费下载链接】baidupcs-web 项目地址: https://gitcode.com/gh_mirrors/ba/baidupcs-web 还在为百度网盘那令人抓狂的下载速度而烦恼吗&#xff1f;面对官方客户端的种种限制&#xff0c;其实你完全不必忍受。…

作者头像 李华