1. 为什么需要synchronized?
在Java多线程环境中,当多个线程同时访问共享资源时,会出现竞态条件(Race Condition)。举个实际例子:假设我们有个银行账户类,其中包含余额属性和取款方法:
java复制class BankAccount {
private int balance = 1000;
public void withdraw(int amount) {
if (balance >= amount) {
try {
Thread.sleep(100); // 模拟处理延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
}
}
}
如果两个线程同时调用withdraw(600),理论上应该只有一个能成功。但由于没有同步控制,两个线程可能都通过余额检查,最终导致余额变为-200。这就是典型的线程安全问题。
关键点:线程安全问题发生的三个必要条件——共享资源、多线程环境、非原子操作。synchronized能同时解决这三个问题。
2. synchronized的四种使用方式
2.1 实例方法同步
直接在方法声明中添加synchronized关键字:
java复制public synchronized void withdraw(int amount) {
// 方法体
}
这种方式的锁对象是当前实例(this),同一个实例的多个同步方法会互斥。比如:
- 线程A调用account1的同步方法X
- 线程B调用account1的同步方法Y
- 线程C调用account2的同步方法X
其中线程A和B会互斥(因为锁对象相同),但线程C不受影响(不同锁对象)。
2.2 静态方法同步
java复制public static synchronized void staticMethod() {
// 方法体
}
静态同步方法的锁对象是类的Class对象(如BankAccount.class),所有调用该静态方法的线程都会互斥,无论有多少个实例。
2.3 同步代码块(实例锁)
java复制public void method() {
// 非同步代码
synchronized(this) {
// 同步代码块
}
}
这种方式比同步整个方法更灵活,可以缩小同步范围提升性能。锁对象也可以是任意实例对象。
2.4 同步代码块(类锁)
java复制public void method() {
synchronized(BankAccount.class) {
// 同步代码块
}
}
使用类的Class对象作为锁,效果等同于静态同步方法。
3. synchronized的底层实现原理
3.1 对象头与Monitor机制
每个Java对象在内存中分为三部分:
- 对象头(Mark Word + 类型指针)
- 实例数据
- 对齐填充
其中Mark Word(32位JVM中占4字节)存储了对象的hashCode、分代年龄和锁标志位。当对象被synchronized锁定时,Mark Word会指向一个Monitor对象。
Monitor的主要结构:
- _owner:记录持有锁的线程
- _EntryList:等待获取锁的线程队列
- _WaitSet:调用wait()后进入等待状态的线程队列
3.2 字节码层面分析
编译后的同步方法会多出monitorenter和monitorexit指令:
code复制public void synchronizedMethod();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入同步块
4: aload_1
5: monitorexit // 正常退出
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常退出
12: aload_2
13: athrow
14: return
可以看到编译器会自动添加异常处理,确保锁一定会被释放。
3.3 锁升级过程
JDK1.6后synchronized进行了优化,锁状态会随着竞争情况升级:
- 无锁状态:初始状态
- 偏向锁:第一个线程访问时,通过CAS将线程ID记录到Mark Word
- 轻量级锁:当有第二个线程尝试获取锁时,升级为自旋锁
- 重量级锁:自旋超过一定次数(默认10次)后,升级为操作系统层面的互斥锁
这个升级过程是不可逆的,目的是减少锁操作的开销。
4. 高级特性与使用技巧
4.1 可重入性
synchronized具有可重入性,即同一个线程可以重复获取已经持有的锁:
java复制public class ReentrantDemo {
public synchronized void method1() {
method2(); // 可以调用另一个同步方法
}
public synchronized void method2() {
// ...
}
}
每次获取锁时,锁计数器+1;释放时计数器-1;计数器归零时完全释放锁。
4.2 等待/通知机制
配合wait()/notify()实现线程间通信:
java复制public class WaitNotifyDemo {
private final Object lock = new Object();
private boolean condition = false;
public void waitForCondition() throws InterruptedException {
synchronized(lock) {
while(!condition) {
lock.wait(); // 释放锁并等待
}
// 条件满足后的处理
}
}
public void setCondition() {
synchronized(lock) {
condition = true;
lock.notifyAll(); // 唤醒等待线程
}
}
}
重要规则:wait()/notify()必须在同步块内调用,且锁对象必须与调用对象一致。
4.3 死锁预防
典型的死锁场景:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
预防措施:
- 固定锁的获取顺序(如都先获取lockA)
- 使用tryLock()设置超时
- 通过工具检测死锁(如jstack)
4.4 性能优化建议
- 减小同步范围:同步代码块优于同步方法
- 降低锁粒度:如ConcurrentHashMap的分段锁
- 读写分离:读多写少场景使用ReadWriteLock
- 避免锁嵌套:容易导致死锁和性能下降
- 考虑使用java.util.concurrent中的高级工具类
5. 常见面试问题解析
5.1 synchronized和ReentrantLock的区别
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现机制 | JVM层面实现 | JDK代码实现 |
| 锁获取方式 | 自动获取和释放 | 需要手动lock()/unlock() |
| 可中断性 | 不可中断 | 支持lockInterruptibly() |
| 公平锁 | 非公平 | 可配置公平/非公平 |
| 条件变量 | 一个锁对应一个等待队列 | 一个锁可对应多个Condition |
| 性能 | JDK6后优化,性能接近 | 高竞争下表现更好 |
5.2 volatile和synchronized的区别
volatile保证可见性和有序性,但不保证原子性。适合用于状态标志位:
java复制private volatile boolean running = true;
public void stop() {
running = false; // 所有线程立即可见
}
而synchronized能保证操作的原子性,适合复合操作。
5.3 双重检查锁定问题
错误的单例实现:
java复制class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 可能发生指令重排序
}
}
}
return instance;
}
}
正确实现(JDK5+):
java复制class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile防止了指令重排序,保证对象初始化完全完成后再赋值。
6. 实战案例分析
6.1 线程安全的计数器
java复制public class SafeCounter {
private int count;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
}
}
public int get() {
synchronized(lock) {
return count;
}
}
}
优化方案:对于这种简单操作,可以使用AtomicInteger替代。
6.2 生产者-消费者模型
java复制public class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
private final int maxSize;
private final Object lock = new Object();
public MessageQueue(int maxSize) {
this.maxSize = maxSize;
}
public void put(String message) throws InterruptedException {
synchronized(lock) {
while(queue.size() == maxSize) {
lock.wait();
}
queue.add(message);
lock.notifyAll();
}
}
public String take() throws InterruptedException {
synchronized(lock) {
while(queue.isEmpty()) {
lock.wait();
}
String message = queue.remove();
lock.notifyAll();
return message;
}
}
}
关键点:
- 使用while而不是if检查条件(防止虚假唤醒)
- 操作后调用notifyAll()(避免信号丢失)
- 同步整个操作过程(保证原子性)
6.3 性能敏感场景的优化
对于高并发场景,可以考虑更细粒度的锁策略:
java复制public class StripedCounter {
private final int stripes;
private final AtomicLong[] counts;
public StripedCounter(int stripes) {
this.stripes = stripes;
this.counts = new AtomicLong[stripes];
for (int i = 0; i < stripes; i++) {
counts[i] = new AtomicLong();
}
}
public void increment() {
int index = Thread.currentThread().hashCode() % stripes;
counts[index].incrementAndGet();
}
public long getTotal() {
long sum = 0;
for (AtomicLong counter : counts) {
sum += counter.get();
}
return sum;
}
}
这种分段锁设计将竞争分散到多个计数器上,显著提升并发性能。
