一、synchronized 的特性
1. 原子性
-
确保被保护的代码块或方法在同一时刻只能被一个线程执行
-
防止多线程并发访问导致的数据不一致问题
假设:
电影院有 100张票
3个售票窗口 同时 售票
每个窗口都尝试卖出这100张票
无同步的情况(竞态条件)
javapublic class CinemaTicketProblem { public static void main(String[] args) { TicketSystem ticketSystem = new TicketSystem(); // 创建3个售票窗口(线程) Thread window1 = new Thread(ticketSystem, "窗口1"); Thread window2 = new Thread(ticketSystem, "窗口2"); Thread window3 = new Thread(ticketSystem, "窗口3"); System.out.println("=== 电影院开始售票(无同步版) ==="); window1.start(); window2.start(); window3.start(); } } class TicketSystem implements Runnable { private int tickets = 100; @Override public void run() { while (tickets > 0) { try { Thread.sleep(50); // 模拟售票耗时 } catch (InterruptedException e) { e.printStackTrace(); } // 非原子操作:检查票数 → 卖票 → 减库存 System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets + " 号票"); tickets--; } System.out.println(Thread.currentThread().getName() + " 结束售票"); } }问题 :当tickets=1时,三个窗口可能同时检查到
tickets > 0为真,然后都卖出了"第1张票",导致:
卖了3次"第1张票"
实际多卖了票(卖出了101张或更多)
使用synchronized保证原子性
javapublic class CinemaTicketSolution { public static void main(String[] args) throws InterruptedException { TicketSystemSync ticketSystem = new TicketSystemSync(); Thread window1 = new Thread(ticketSystem, "窗口1"); Thread window2 = new Thread(ticketSystem, "窗口2"); Thread window3 = new Thread(ticketSystem, "窗口3"); System.out.println("=== 电影院开始售票(同步版) ==="); window1.start(); window2.start(); window3.start(); // 等待所有售票结束 window1.join(); window2.join(); window3.join(); System.out.println("=== 售票结束,最终余票:" + ticketSystem.getRemainingTickets() + " ==="); } } class TicketSystemSync implements Runnable { private int tickets = 100; // synchronized保证整个售票过程的原子性 @Override public void run() { while (true) { if (!sellTicket()) { break; // 票已售完 } } System.out.println(Thread.currentThread().getName() + " 结束售票"); } // 关键:售票方法用synchronized修饰 private synchronized boolean sellTicket() { if (tickets > 0) { try { Thread.sleep(50); // 模拟售票耗时 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets + " 号票"); tickets--; return true; } return false; } public synchronized int getRemainingTickets() { return tickets; } }
2. 可见性
-
线程解锁前,必须将共享变量的最新值刷新到主内存
-
线程加锁时,将清空工作内存中共享变量的值,需要从主内存重新读取
-
遵循 happens-before 原则中的监视器锁规则
例一:
办公室项目进度板
理想情况(有可见性)
早上9:00:项目经理更新白板:"项目deadline提前到明天"
↓ 立即广播通知
所有团队成员瞬间看到最新deadline
↓ 大家调整工作计划
javapublic class ProjectBoard { // volatile保证可见性 private volatile String deadline = "下周五"; public void updateDeadline(String newDeadline) { deadline = newDeadline; // 修改立即对所有线程可见 System.out.println("项目经理更新deadline为:" + newDeadline); } public void checkDeadline(String staffName) { System.out.println(staffName + " 看到deadline:" + deadline); } }例二:
火车站售票大屏(无可见性)
场景描述
火车站后台系统:剩余票数
tickets = 1显示大屏(线程A):读取票数显示
10个售票窗口(线程B-K):同时卖最后一张票
javapublic class TicketSystemProblem { // 没有volatile,修改可能不及时可见 private int remainingTickets = 1; private boolean soldOut = false; public void sellTicket(String windowName) { if (!soldOut) { // 窗口1:卖票成功,剩余0 remainingTickets--; soldOut = true; try { Thread.sleep(100); } // 模拟处理时间 System.out.println(windowName + " 卖出最后一张票"); } else { System.out.println(windowName + ":票已售完"); } } public void displayScreen() { // 大屏线程一直显示剩余票数 while (true) { // 可能一直显示旧值1,看不到已售出 System.out.println("大屏显示:剩余 " + remainingTickets + " 张票"); try { Thread.sleep(50); } catch (Exception e) {} } } }可能发生的场景:
时间线:
10:00:00 - 窗口1:卖出最后一张票,remainingTickets=0
10:00:01 - 大屏显示:剩余1张票 ← 看不到更新!
10:00:02 - 旅客A看到大屏显示有票,排队30分钟
10:00:03 - 窗口2:检查soldOut还是false(看不到更新)
10:00:04 - 窗口2:试图卖票,系统出错!
10:00:32 - 旅客A排到窗口:票已售完,愤怒投诉
3. 有序性
-
通过互斥执行确保代码有序性
-
禁止指令重排序优化(as-if-serial 语义在单线程内依然有效)
做菜的错误顺序
正常做菜顺序
public void cookRamen() {
烧水(); // 第一步
下面条(); // 第二步(必须等水开)
加调料(); // 第三步
出锅(); // 第四步
}
处理器"优化"后的重排序
// 编译器/CPU觉得这样更高效:
public void cookRamen() {
加调料(); // 先做这个(因为烧水要等)
烧水(); //
出锅(); // ← 面条还没下!
下面条(); // 顺序错了!
}
结果:端出一碗开水+干调料,面条还在手里。
二、synchronized 的使用方式
1. 同步代码块
java
// 锁对象可以是任意对象
synchronized(lockObject) {
// 需要同步的代码
}
2. 同步实例方法
java
public synchronized void method() {
// 锁的是当前实例对象 this
}
3. 同步静态方法
java
public static synchronized void staticMethod() {
// 锁的是当前类的 Class 对象
}
三、synchronized 的锁机制
1. 锁的存储位置
-
锁信息存储在 Java 对象头中的 Mark Word 中
-
包括锁状态标志位、指向锁记录的指针等信息
2. 锁升级过程(JDK 1.6 优化)
无锁状态
- 初始状态,没有线程竞争
偏向锁
-
适用场景:只有一个线程访问同步块
-
原理:当线程访问同步块时,通过 CAS 操作将线程 ID 记录到 Mark Word
-
优点:加锁解锁不需要额外的 CAS 操作
轻量级锁
-
适用场景:多个线程交替执行,没有实际竞争
-
原理:
-
在当前线程的栈帧中创建锁记录(Lock Record)
-
将对象头的 Mark Word 复制到锁记录中
-
使用 CAS 尝试将对象头指向锁记录
-
-
加锁失败会升级为重量级锁
重量级锁
-
适用场景:多个线程竞争激烈
-
原理:通过操作系统互斥量(mutex)实现,线程会进入阻塞状态
-
涉及用户态到内核态的切换,性能开销较大
3. 锁消除
- JIT 编译器在运行时,如果检测到不可能存在共享数据竞争,会消除锁
4. 锁粗化
-
将多个连续的加锁、解锁操作合并为一次范围更大的锁操作
-
减少不必要的锁竞争开销
5. 锁的释放
-
正常执行完成同步代码块
-
抛出异常跳出同步代码块
-
调用锁对象的 wait() 方法(会释放锁并进入等待池)
四、synchronized 的优缺点
优点
-
使用简单,语义清晰
-
JVM 原生支持,自动管理锁的获取和释放
-
锁升级机制提高了性能
缺点
-
不够灵活,获取和释放锁的方式单一
-
不可中断,等待锁的线程会一直阻塞
-
无法实现公平锁
-
读写场景下效率不如 ReentrantReadWriteLock
五、注意事项
-
避免锁的嵌套:可能导致死锁
-
锁对象的选择:
-
通常使用 private final 对象
-
不要使用 String 常量、基本类型包装类等作为锁
-
-
同步范围:尽量缩小同步代码块的范围,提高并发性能
-
避免在构造方法中使用 synchronized:可能导致 this 引用逸出