1. 深入理解synchronized关键字
在Java多线程编程中,synchronized关键字是最基础也是最常用的线程同步机制。作为一名有十年Java开发经验的工程师,我经常看到新手在使用synchronized时犯各种错误。今天我就来详细剖析这个关键字的原理和使用技巧。
synchronized主要解决两个核心问题:
- 原子性:确保同一时刻只有一个线程执行临界区代码
- 可见性:确保一个线程对共享变量的修改对其他线程立即可见
重要提示:synchronized的可见性保证经常被开发者忽视,但它实际上比volatile更强大,完全可以替代volatile的功能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. synchronized的三种使用方式
2.1 同步实例方法
这是最基本的用法,通过在方法声明前加synchronized关键字实现:
java复制public synchronized void method() {
// 临界区代码
}
这种方式的锁对象是当前实例(this)。我经常看到的一个典型错误是:
java复制public class Counter {
private static int count = 0;
public synchronized void increment() {
count++;
}
}
// 使用时
Counter c1 = new Counter();
Counter c2 = new Counter();
new Thread(() -> c1.increment()).start();
new Thread(() -> c2.increment()).start();
这个例子中虽然increment方法是同步的,但由于创建了两个Counter实例,实际上使用的是两把不同的锁,无法保证线程安全。正确的做法应该是:
- 使用同一个Counter实例
- 或者将increment方法改为静态同步方法
2.2 同步静态方法
静态同步方法的锁对象是类的Class对象:
java复制public static synchronized void method() {
// 临界区代码
}
这里有个重要的知识点:实例同步方法和静态同步方法使用的是不同的锁对象,因此它们之间不会互斥。我曾在项目中遇到过这样的问题:
java复制class BankAccount {
private static int total;
public static synchronized void addToTotal(int amount) {
total += amount;
}
public synchronized void transfer(BankAccount other, int amount) {
// 转账逻辑
addToTotal(amount); // 这里不会发生死锁
}
}
2.3 同步代码块
同步代码块是最灵活的同步方式,可以精确控制锁的范围:
java复制synchronized(lockObject) {
// 临界区代码
}
选择锁对象时有几个常见选项:
- this:当前实例对象
- Class对象:如MyClass.class
- 专门创建的锁对象:private final Object lock = new Object()
经验之谈:对于长时间执行的方法,应该尽量使用同步代码块而不是同步方法,只锁住真正需要同步的部分,这样可以显著提高并发性能。
3. synchronized的实现原理
3.1 对象头与Monitor
每个Java对象都有一个对象头,其中包含Mark Word用于存储锁信息。synchronized的实现依赖于对象头中的锁标志位和Monitor机制。
当线程进入synchronized块时:
- 尝试获取对象的Monitor
- 成功则持有锁,失败则进入同步队列等待
- 执行完同步代码后释放Monitor
3.2 锁升级过程
JDK1.6之后,synchronized实现了锁升级机制:
- 无锁状态
- 偏向锁:适用于只有一个线程访问的情况
- 轻量级锁:适用于线程交替执行,没有竞争的情况
- 重量级锁:真正的互斥锁,适用于高竞争场景
这个优化大幅减少了synchronized的性能开销。
4. 高级特性与使用技巧
4.1 可重入性
synchronized是可重入锁,这意味着:
- 线程可以重复获取已经持有的锁
- 防止了自身死锁的情况
java复制public class ReentrantDemo {
public synchronized void method1() {
method2(); // 可以调用另一个同步方法
}
public synchronized void method2() {
// ...
}
}
4.2 等待/通知机制
synchronized配合wait()/notify()可以实现线程间通信:
java复制public class TaskQueue {
private final Object lock = new Object();
private Queue<Task> queue = new LinkedList<>();
public void addTask(Task task) {
synchronized(lock) {
queue.add(task);
lock.notifyAll();
}
}
public Task getTask() throws InterruptedException {
synchronized(lock) {
while(queue.isEmpty()) {
lock.wait();
}
return queue.remove();
}
}
}
4.3 性能优化建议
- 减小同步范围:只同步必要的代码
- 降低锁粒度:使用多个锁保护不同的资源
- 避免锁嵌套:容易导致死锁
- 考虑使用并发容器:如ConcurrentHashMap
5. 常见问题与解决方案
5.1 死锁问题
死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
避免死锁的策略:
- 固定锁的获取顺序
- 使用tryLock()尝试获取锁
- 设置超时时间
5.2 锁竞争问题
高并发场景下锁竞争会导致性能下降,解决方案:
- 使用读写锁(ReentrantReadWriteLock)
- 考虑无锁编程(CAS)
- 使用分段锁
5.3 内存可见性问题
虽然synchronized保证了可见性,但在某些特殊场景下还是需要注意:
- 双重检查锁定问题
- 长期持有锁导致其他线程长时间不可见
6. 实际项目经验分享
在我参与的一个电商平台项目中,我们遇到了商品库存扣减的并发问题。最初我们是这样实现的:
java复制public class ProductService {
private Map<Long, Integer> stockMap = new HashMap<>();
public synchronized boolean reduceStock(long productId, int quantity) {
int stock = stockMap.getOrDefault(productId, 0);
if(stock < quantity) return false;
stockMap.put(productId, stock - quantity);
return true;
}
}
这个实现有几个问题:
- 锁粒度太粗,所有商品共用一个锁
- 同步方法导致性能瓶颈
优化后的版本:
java复制public class ProductService {
private ConcurrentHashMap<Long, Integer> stockMap = new ConcurrentHashMap<>();
private Map<Long, Object> locks = new ConcurrentHashMap<>();
public boolean reduceStock(long productId, int quantity) {
Object lock = locks.computeIfAbsent(productId, k -> new Object());
synchronized(lock) {
int stock = stockMap.getOrDefault(productId, 0);
if(stock < quantity) return false;
stockMap.put(productId, stock - quantity);
return true;
}
}
}
这个优化:
- 为每个商品创建单独的锁对象
- 使用ConcurrentHashMap提高并发性能
- 锁粒度细化到单个商品
在实际测试中,优化后的版本在100并发下性能提升了8倍。
