一、为什么需要线程同步?
在多线程编程中,当多个线程同时访问共享资源时,若缺乏有效的同步机制,极易引发数据不一致问题。例如,两个线程同时对一个计数器进行递增操作,最终结果可能比预期值小;银行转账场景中,两个线程同时读取账户余额并修改,可能导致资金异常流失。这类问题统称为竞态条件(Race Condition),其本质是线程执行顺序的不确定性导致程序行为不可预测。
二、synchronized的核心作用
synchronized是Java提供的内置同步机制,通过互斥锁实现线程安全。其核心功能包括:
- 原子性操作:确保被修饰的代码块或方法在同一时间仅由一个线程执行,其他线程必须等待锁释放后才能进入。
- 可见性保证:锁释放时,所有修改的变量会强制刷新到主内存,避免线程间的缓存不一致问题。
- 有序性约束:通过happens-before规则,禁止编译器和处理器对同步代码块内的指令进行重排序优化。
三、synchronized的三种使用方式
1. 同步实例方法
public class Counter {private int count = 0;public synchronized void increment() {count++;}}
- 锁对象:当前实例对象(
this) - 特点:方法调用时自动获取锁,执行完毕后自动释放锁
- 适用场景:需要保护实例变量的线程安全
2. 同步静态方法
public class Utils {private static int sharedValue = 0;public static synchronized void modify() {sharedValue++;}}
- 锁对象:当前类的Class对象(
Utils.class) - 特点:作用于整个类,所有实例共享同一把锁
- 适用场景:需要保护静态变量的线程安全
3. 同步代码块
public class BankAccount {private double balance;private final Object lock = new Object(); // 专用锁对象public void withdraw(double amount) {synchronized(lock) {if (balance >= amount) {balance -= amount;}}}}
- 锁对象:任意Java对象(推荐使用专用私有对象)
- 特点:更细粒度的控制,可减少锁的竞争范围
- 适用场景:需要精确控制同步范围或复用锁对象时
四、synchronized的底层实现
Java 5之后,synchronized经历了重大优化,其底层实现分为两个阶段:
-
Java对象头:每个Java对象在内存中包含一个对象头,其中Mark Word存储锁状态信息:
- 无锁状态:记录哈希码和分代年龄
- 轻量级锁:指向线程栈帧的指针
- 重量级锁:指向互斥量(Monitor)的指针
-
Monitor机制:
- 每个对象关联一个Monitor对象,由OS内核管理
- 线程竞争锁时,未获取到的线程会被挂起,进入阻塞状态
- Java 6引入自适应自旋、锁消除等优化,显著减少线程阻塞开销
五、synchronized的常见误区与最佳实践
误区1:同步范围过大
// 错误示例:同步范围过大导致性能下降public synchronized void processAllData() {loadData(); // I/O操作,不应同步synchronized(this) { // 嵌套同步无意义processData();}}
优化建议:仅同步必要的代码块,将I/O操作移出同步范围
误区2:锁对象选择不当
// 错误示例:使用公共对象作为锁public class Service {public static final Object LOCK = new Object(); // 危险!可能被外部代码锁定public void doSomething() {synchronized(LOCK) { ... }}}
优化建议:使用私有专用对象作为锁,避免外部代码干扰
误区3:忽略锁的释放
// 错误示例:异常导致锁未释放public void criticalOperation() {synchronized(lock) {if (errorCondition) {throw new RuntimeException(); // 锁未释放!}}}
优化建议:使用try-finally确保锁释放,或使用Java 7的try-with-resources模式
六、synchronized与ReentrantLock的选择
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | 隐式获取 | 显式调用lock() |
| 公平锁支持 | 不支持 | 支持 |
| 锁超时 | 不支持 | 支持tryLock(timeout) |
| 中断响应 | 不支持 | 支持lockInterruptibly() |
| 适用场景 | 简单同步需求 | 复杂并发控制需求 |
选择建议:
- 优先使用
synchronized:代码更简洁,JVM会自动优化 - 选择
ReentrantLock:需要公平锁、可中断锁或超时机制时
七、性能优化技巧
- 减少同步范围:仅保护必要的临界区代码
- 降低锁粒度:使用分段锁或Concurrent集合
- 避免嵌套同步:防止死锁风险
- 使用读写锁:读多写少场景使用
ReentrantReadWriteLock - 监控锁竞争:通过JMX或日志记录锁等待时间
八、实战案例:线程安全计数器
public class ThreadSafeCounter {private volatile int count = 0; // 保证可见性private final Object lock = new Object();public void increment() {synchronized(lock) {count++;}}public int getCount() {synchronized(lock) { // 保证原子性读取return count;}}}
关键点:
- 使用
volatile保证可见性 - 同步方法保证原子性
- 专用锁对象减少竞争
九、总结与展望
synchronized作为Java并发编程的基础设施,其设计哲学体现了”简单即美”的原则。虽然现代Java开发中出现了更多高级并发工具(如Atomic类、CompletableFuture等),但理解synchronized的原理仍是掌握并发编程的关键。对于初学者,建议从同步代码块开始实践,逐步理解锁的获取/释放机制,最终达到能够根据场景选择合适同步方案的水平。
未来并发编程的发展方向包括:
- 更轻量级的同步原语(如
VarHandle) - 无锁数据结构(CAS操作实现)
- 函数式编程与并发结合(如
Stream并行处理)
掌握synchronized不仅是学习并发编程的起点,更是理解Java内存模型和线程协作机制的重要基石。建议读者结合JConsole等工具观察锁竞争情况,通过实际项目积累同步方案的设计经验。