核心概念:
- 原子性:一组操作不可分割,要么全部执行完,要么不执行。
- 可见性:一个线程修改共享变量,其他线程立刻感知。
- 有序性:禁止 JVM/CPU 指令重排序。
一、按并发策略:悲观锁 vs 乐观锁【思想层面】
1. 悲观锁
思想:默认一定会发生并发冲突,访问资源先上锁,锁住资源,别人不许碰。 只要操作共享资源,先拿到锁,其他线程阻塞等待。
Java 中代表实现:
- synchronized
- ReentrantLock、ReentrantReadWriteLock写锁
数据库代表: select … for update 行级悲观锁。
代码示例
ReentrantLock(悲观锁)
ReentrantLock lock = new ReentrantLock();
lock.lock(); //上来直接加锁,假设会有竞争
try{
count++;
}finally {
lock.unlock();
}
synchronized 内置锁
synchronized锁的是对象,不是代码。
⚠️重点:锁对象必须是同一个,才能互斥。
三种使用方式
示例 1:修饰实例方法
public class SyncDemo {
private int count = 0;
//锁:this对象
public synchronized void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncDemo demo = new SyncDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + demo.count); //输出20000,线程安全
}
}
原理:t1、t2 操作同一个demo对象,add 方法上加synchronized,锁 this,同一时刻只能一个线程进入 add。
❌坑:new 两个不同对象,锁失效!
SyncDemo d1 = new SyncDemo();
SyncDemo d2 = new SyncDemo();
//两个对象,两把不同的锁,互不干扰,并发会出现count小于20000
示例 2:synchronized 代码块(推荐,粒度可控)
⚠️:只锁需要保护的临界区,不要锁整个大方法,减少锁持有时间,提升并发性能。
//抢票例子
public class TicketRightDemo {
private int count = 10;
public void saleTicket() {
while (true) {
//锁对象 this,两个线程使用同一个对象锁
synchronized (this) {
// 在锁内部做库存判断
if (count <= 0) {
break; //库存为0,退出循环
}
System.out.println(Thread.currentThread().getName() + "抢到票:" + count);
count–;
}
}
}
public static void main(String[] args) {
TicketRightDemo task = new TicketRightDemo();
new Thread(task::saleTicket, "窗口A").start();
new Thread(task::saleTicket, "窗口B").start();
}
}
输出示例:
窗口A抢到票:10
窗口A抢到票:9
窗口B抢到票:8
窗口A抢到票:7
窗口B抢到票:6
窗口A抢到票:5
窗口B抢到票:4
窗口A抢到票:3
窗口B抢到票:2
窗口A抢到票:1
库存不会出现负数,不会超卖。
正确写法:判断 + 修改共享变量全部放入同步块
示例 3:synchronized 修饰 static 静态方法
锁对象是SyncDemo.class类对象,所有实例共用一把锁。
public static synchronized void staticAdd(){
count++;
}
等价:
public static void staticAdd(){
synchronized (SyncDemo.class){
count++;
}
}
特点:
- 冲突多的时候性能稳定;竞争激烈线程会阻塞,带来线程切换开销。
2. 乐观锁
思想:默认不会发生冲突,不上锁;操作完成时检查是否发生冲突;冲突则重试。 不加真正互斥锁,依靠版本号 / CAS 做冲突检测。
Java 代表:AtomicXXX 原子类(CAS 实现) 数据库代表:版本号机制乐观锁
示例 1:AtomicInteger CAS 乐观锁
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); //CAS,不加锁,冲突自动重试
示例 2:数据库乐观锁伪代码
— version版本号,更新时校验版本
update goods set stock = stock‑1, version = version+1
where id=100 and version = #{oldVersion};
如果返回影响行数 0,代表已经被其他线程修改,发生冲突,业务重试。
乐观锁优缺点:
✅没有阻塞,没有线程切换开销;适合冲突少场景。
❌冲突多的时候大量自旋重试,CPU 暴涨;只能保证单个变量原子,不能保护一段业务逻辑。
重点区分:
悲观锁:先上锁,再干活;
乐观锁:先干活,最后校验冲突,失败重试。
二、按资源访问方式:独占锁 (排他 / 互斥锁) VS 共享锁
1. 独占锁(排他锁、互斥锁)
同一时间只允许一个线程持有锁;其他线程全部阻塞。
实现:
- 独占:强调资源被一个线程占有
- 排他:排斥其他线程进来
- 互斥:线程之间互相排斥
代码 ReentrantLock,独占锁示例
ReentrantLock lock = new ReentrantLock();
//同一时刻只能一个线程获取
lock.lock();
try{
//临界区
}finally {
lock.unlock();
}
2. 共享锁
多个线程可以同时获取锁,并发读;
典型:读写锁的读锁。 ReentrantReadWriteLock.ReadLock 读锁。
规则:
- 读锁是共享锁:读‑读可以并发
- 读锁和写锁互斥;写锁和写锁互斥
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//多个线程可以同时拿到读锁,并发读取
readLock.lock();
try{
int val = data;
}finally {
readLock.unlock();
}
读写锁 ReentrantReadWriteLock(读共享,写独占)
| 读线程 + 读线程 | ✅ 共享,并发执行 |
| 读线程 + 写线程 | ❌互斥,写等待读释放 / 读等待写释放 |
| 写线程 + 写线程 | ❌互斥 |
缺陷:大量读线程持续占读锁,写线程一直拿不到写锁 → 写饥饿。
三、按获取锁排队策略:公平锁 VS 非公平锁
只针对 AQS 实现的锁(ReentrantLock);synchronized 只有非公平锁。
1. 公平锁
线程按照请求锁的先后顺序排队;先来先得,不允许插队。
//true 开启公平锁
ReentrantLock fairLock = new ReentrantLock(true);
- 优点:不会出现线程饥饿,所有线程都有机会拿到锁。
- 缺点:线程切换多,性能较低。
2. 非公平锁(ReentrantLock 默认,synchronized 也是)
新线程上来直接尝试抢锁,可以插队,不一定排队。
//false 默认非公平锁
ReentrantLock lock = new ReentrantLock(false);
- 优点:减少线程挂起唤醒,性能高。
- 缺点:有可能老线程一直抢不到锁,线程饥饿。
⚠️synchronized 只有非公平锁,没有公平锁选项。
代码对比
//公平锁
ReentrantLock lock = new ReentrantLock(true);
//非公平锁(默认)
ReentrantLock lock = new ReentrantLock();
更加详细例子
开 5 个线程,每个线程抢锁多次,打印线程名称,观察输出顺序。
import java.util.concurrent.locks.ReentrantLock;
public class FairVsUnFairLockDemo {
// true=公平锁;false=非公平锁(默认)
private static final ReentrantLock lock = new ReentrantLock(true);
// private static final ReentrantLock lock = new ReentrantLock(false);
public static void main(String[] args) {
// 创建5个线程
for (int i = 1; i <= 5; i++) {
final int threadNo = i;
new Thread(() -> {
// 每个线程抢锁5次
for (int j = 0; j < 5; j++) {
lock.lock();
try {
System.out.printf("【%s】拿到锁,执行任务%n", Thread.currentThread().getName());
//模拟业务耗时,短暂持有锁
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "线程‑" + threadNo).start();
}
}
}
✅打开公平锁 new ReentrantLock(true) 现象
输出大致顺序:线程会交替按顺序执行,不会一个线程连续多次霸占锁。
【线程‑1】拿到锁,执行任务
【线程‑2】拿到锁,执行任务
【线程‑3】拿到锁,执行任务
【线程‑4】拿到锁,执行任务
【线程‑5】拿到锁,执行任务
【线程‑1】拿到锁,执行任务
【线程‑2】拿到锁,执行任务
……
公平锁 AQS 内部维护等待队列;锁释放之后,优先唤醒队列头部等待最久的线程;新到来线程不会尝试插队抢锁,直接进队列排队。
✅打开非公平锁 new ReentrantLock(false)(默认)现象
经常出现:同一个线程连续多次拿到锁(插队)。
【线程‑1】拿到锁,执行任务
【线程‑1】拿到锁,执行任务
【线程‑1】拿到锁,执行任务
【线程‑2】拿到锁,执行任务
【线程‑3】拿到锁,执行任务
【线程‑3】拿到锁,执行任务
……
为什么连续拿到?
线程 1 释放锁之后,还没等队列里沉睡的线程被唤醒,线程 1 自己又立刻去抢锁,CAS 抢占成功,直接再次拿到锁,不用进等待队列。 队列里其他线程还在休眠,还没被唤醒,就被新的抢占抢走了机会,这就是插队。
完整过程拆解(非公平锁):
多次循环,就看到日志里:线程 1 连续多次拿到锁。
这就是非公平锁的插队机制;
好处:避免唤醒其他线程的开销,性能高;
坏处:队列老线程反复抢不到锁,发生线程饥饿。
四、其他常见锁概念(附加分类)
1. 可重入锁
同一个线程,可以重复获取同一把锁,不会自己卡死自己。 synchronized、ReentrantLock都是可重入锁。
代码示例:
ReentrantLock lock = new ReentrantLock();
public void m1(){
lock.lock();
try{
m2(); //同一个线程再次获取同一把锁,重入
}finally { lock.unlock(); }
}
public void m2(){
lock.lock();
try{
System.out.println("重入成功");
}finally { lock.unlock(); }
}
lock 几次,就要 unlock 几次。
2. 自旋锁
获取锁失败,不把线程挂起,循环重试抢锁,不进入阻塞。
- 优点:避免内核态线程切换,速度快。
- 缺点:占用 CPU,如果锁持有时间长,CPU 空转浪费。
应用:
自旋锁一般不会单独使用,作为其他锁内部实现机制。
3. 偏向锁 / 轻量级锁 / 重量级锁
仅属于 synchronized 锁升级,JVM 内部实现,开发者不能手动使用。
锁只能升级,不能降级。
4. 分段锁(思想,不是具体锁对象)
代表:ConcurrentHashMap JDK7。 把大容器拆分成多个小分段,每个分段一把锁,减小锁粒度,提升并发。 JDK8 之后放弃分段锁,改为 CAS+synchronized。
各类锁归属关系总表(面试非常重要)
| synchronized | 悲观锁 | 独占锁 | 只能非公平 | ✅可重入 |
| ReentrantLock | 悲观锁 | 独占锁 | 可配置公平 / 非公平 | ✅可重入 |
| ReentrantReadWriteLock‑写锁 | 悲观锁 | 独占锁 | 可配置公平 / 非公平 | ✅可重入 |
| ReentrantReadWriteLock‑读锁 | 悲观锁 | 共享锁 | 可配置公平 / 非公平 | ✅可重入 |
| AtomicInteger(CAS) | 乐观锁 | 无锁 | 无 | 不涉及 |
⚠️:乐观锁没有 “独占 / 共享” 概念,乐观锁不加锁。
面试高频问答
Q1:独占锁、排他锁、互斥锁是什么关系?
三者大部分场景是同一个东西。同一时刻只能一个线程访问资源。
Q2:悲观锁和乐观锁区别?
悲观锁假设冲突一定会发生,操作前先上锁;乐观锁假设冲突很少,不加锁,最后校验冲突,冲突重试。 synchronized、ReentrantLock 是悲观锁;Atomic、数据库 version 版本号是乐观锁。
Q3:读写锁读锁是共享锁,那它属于悲观还是乐观?
悲观锁,读锁还是会上锁,只是允许多线程同时读;写的时候会阻塞。不是乐观锁。
Q4:公平锁非公平锁,synchronized 可以设置公平锁吗?
不行,synchronized 只有非公平锁;只有 ReentrantLock 支持公平锁。
Q5:乐观锁一定比悲观锁快吗?
不是。竞争激烈的时候乐观锁大量自旋重试,CPU 飙升,性能不如悲观锁。冲突少乐观锁性能好。
Q6:什么是写饥饿?
读写锁大量读线程一直占有读锁,写线程永远拿不到写锁。StampedLock 乐观读可以缓解。

