在 Java 并发编程中,volatile、synchronized和Lock是最常用的三种同步机制。很多人能说出它们的区别,却说不清各自最适合用在什么地方。
这篇文章不用代码,只从“解决什么问题”的角度,帮你彻底理清它们的关系与运用场景。
一、先一句话概括各自角色
| 关键字/接口 | 一句话概括 | 本质 |
|---|---|---|
volatile | 保证变量修改的可见性和有序性,但不保证原子性 | JVM 轻量级同步机制 |
synchronized | 保证代码块的原子性、可见性、有序性,基于 monitor 机制 | 内置锁(重量级,可优化) |
Lock | 是synchronized的 API 版本,提供更灵活的控制(可中断、可超时、公平锁等) | 显式锁(JUC 提供) |
二、它们的关系是递进的
volatile是基础
它只解决了“一个线程写,多个线程读”的可见性问题。无法解决多个线程同时写(如count++)的原子性问题。synchronized是升级
用锁把代码块包起来,同一时刻只允许一个线程执行。但它较重,获取锁的线程必须阻塞等待,且不能中断、不能超时。Lock是增强
提供了synchronized没有的能力:可中断、可超时、公平锁、多个等待队列(Condition)。
功能强弱/灵活度:volatile<synchronized<Lock
使用复杂度:volatile<synchronized<Lock
性能(现代 JVM):volatile≈synchronized(优化后) ≈Lock(CAS 实现)
三、一句话概括它们各自负责什么
volatile:我改了你必须立刻看到 ——可见性synchronized:一次只让一个人进来,别人在外面等着 ——互斥 + 可见性Lock:像synchronized一样互斥,但我可以随时不排队、中途走人、叫号公平点 ——更灵活的互斥
四、volatile 用在哪儿?
核心场景:一个线程写,多个线程读的状态标志
volatile不保证原子性,不能用于count++。它适合表达“事情发生了 / 状态变了”,其他线程需要立刻感知。
典型运用:
开关控制(状态标志)
后台轮询线程需要一个running变量控制它停止。服务关闭时,主线程设置running = false,工作线程看到后立即退出循环,实现优雅停机。双重检查锁(DCL)实现单例
高并发下只创建一个对象实例。volatile阻止指令重排序,防止其他线程拿到未初始化完成的对象。读多写少的共享变量
配置项、系统参数,偶尔修改但大量读取。例如config.refreshInterval修改后,所有工作线程立刻用上新值。
五、synchronized 用在哪儿?
核心场景:大家都可能改,必须排队的原子性操作
volatile解决不了“同时写”的问题,需要synchronized把代码块变成原子操作。
典型运用:
计数器、累加器
统计接口调用次数、在线人数、库存扣减。increment()方法必须排队执行,否则多线程同时 +1 会丢失计数。懒汉式单例(整个方法或代码块加锁)
懒加载且保证只创建一个实例。直接锁住getInstance()方法,虽然效率低,但实现最简单。复合操作
先检查后执行(如if (map.containsKey(key))再map.get(key))。转账业务需要先判断余额再扣款,这两步必须一起锁住。操作非线程安全的集合
多线程并发操作HashMap、ArrayList。维护一个缓存 Map 时,增删改查都锁住对象,防止读的时候有人删导致ConcurrentModificationException。JVM 层面的唯一操作(wait/notify)
生产者-消费者模型中,wait()/notify()必须在synchronized块里调用。
六、Lock 用在哪儿?
核心场景:synchronized 功能不够用的地方
Lock提供了synchronized无法实现的精细控制:尝试加锁、超时加锁、可中断加锁。
典型运用:
尝试获取锁
拿不到锁就不想等,直接做别的事或返回错误。秒杀系统中,尝试获取锁,拿不到就直接告诉用户“太挤了,稍后再试”,而不是让用户卡死。可中断的锁
锁等待时间可能很长,用户想主动取消操作。例如用户在 GUI 界面点击“取消搜索”,需要打断正在等待数据库锁的线程。带超时的锁
避免某个线程异常导致其他线程无限等待。分布式任务调度中,抢锁的线程必须在 10 秒内完成任务并释放锁,否则锁自动失效。公平锁
严格按先来后到获取锁,防止饥饿。例如银行叫号系统(理想情况下)不允许插队。多个等待队列(Condition)
一个锁配合多个等待条件。有界阻塞队列中:队列空时,取元素线程等待“非空”信号;队列满时,存元素线程等待“未满”信号。一个锁配两个Condition,代码更清晰。
七、总结对比(运用场景)
| 机制 | 核心战场 | 典型应用 | 一句话场景 |
|---|---|---|---|
volatile | 状态标志 | 开关控制、DCL 单例 | 一个线程改,其他线程立刻看 |
synchronized | 原子操作 | 计数器、复合操作、wait/notify | 多个线程改,必须排队 |
Lock | 高级控制 | 尝试锁、超时锁、可中断锁 | synchronized不够灵活时 |
最后一句口诀帮你记住
开关用 volatile,简单排队用 synchronized,复杂控制(超时、中断、公平)用 Lock。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人理清 Java 并发中的这三个关键角色。