1. Synchronized 的基本概念
在多线程编程中,Synchronized 是一个至关重要的关键字,它用于控制对共享资源的访问。想象一下,当多个线程同时尝试修改同一个银行账户余额时,如果没有适当的同步机制,就会导致数据不一致的问题。Synchronized 就是 Java 提供的原生解决方案,它能够确保同一时刻只有一个线程可以执行特定的代码块或方法。
Synchronized 的实现原理是基于对象内部的一个称为"监视器锁"(monitor)的机制。每个 Java 对象都有一个与之关联的监视器,当线程进入 Synchronized 代码块时,它会尝试获取这个监视器的所有权。如果成功获取,线程就可以执行代码;如果失败,线程就会被阻塞,直到锁被释放。
2. Synchronized 的三种使用方式
2.1 实例方法同步
当 Synchronized 用于实例方法时,锁是当前对象实例(this)。这意味着同一时间只能有一个线程可以访问该对象的这个同步方法。其他线程如果尝试调用同一个对象实例的任何同步方法,都会被阻塞。
java复制public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
在实际项目中,这种方法适用于需要对对象状态进行原子性修改的场景。比如电商系统中的库存扣减,或者金融系统中的账户余额变更。
2.2 静态方法同步
当 Synchronized 用于静态方法时,锁是当前类的 Class 对象。因为静态方法属于类而非实例,所以这种同步会影响所有调用该方法的线程,无论它们操作的是哪个实例。
java复制public class Logger {
private static int logCount = 0;
public static synchronized void log(String message) {
logCount++;
System.out.println(message);
}
}
这种同步方式常用于需要跨实例共享状态的工具类,比如日志记录器、配置管理器等。需要注意的是,静态同步方法和实例同步方法使用的是不同的锁,所以它们之间不会互相阻塞。
2.3 代码块同步
Synchronized 也可以用于代码块,这时需要显式指定锁对象。这种方式提供了更细粒度的控制,可以选择只同步必要的代码部分,而不是整个方法。
java复制public class FineGrainedSync {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
// 非同步代码
synchronized(lock1) {
// 需要同步的代码
}
}
public void methodB() {
// 非同步代码
synchronized(lock2) {
// 需要同步的代码
}
}
}
在实际开发中,我经常使用这种细粒度锁来减少锁竞争,提高并发性能。特别是在处理复杂对象时,可以为不同属性使用不同的锁对象。
3. Synchronized 的底层实现原理
3.1 对象头与 Monitor
每个 Java 对象在内存中都有对象头,其中包含了与锁相关的信息。在 HotSpot 虚拟机中,对象头包含两部分:Mark Word 和 Klass Pointer。Mark Word 存储了对象的哈希码、GC 分代年龄和锁标志位等信息。
当 Synchronized 作用于方法或代码块时,JVM 会在编译后的字节码中插入 monitorenter 和 monitorexit 指令。这些指令会操作对象头中的锁标志位,实现锁的获取和释放。
3.2 锁升级过程
现代 JVM 实现了锁的优化机制,锁状态会随着竞争情况而变化:
- 无锁状态:初始状态,没有线程持有锁
- 偏向锁:当第一个线程获取锁时,会进入偏向模式
- 轻量级锁:当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁
- 重量级锁:当竞争激烈时,轻量级锁会升级为重量级锁
这种锁升级机制是为了在无竞争或低竞争情况下减少同步开销。在实际应用中,了解这一点有助于我们设计更高效的并发程序。
4. Synchronized 的最佳实践
4.1 锁的范围控制
锁的范围应该尽可能小,只包含真正需要同步的代码。这样可以减少线程等待时间,提高并发性能。我曾经在一个项目中看到有人对整个方法加锁,而实际上只有几行代码需要同步,这导致了严重的性能问题。
java复制// 不推荐的做法
public synchronized void process() {
// 大量不需要同步的代码
// ...
// 少量需要同步的代码
// ...
// 更多不需要同步的代码
}
// 推荐的做法
public void process() {
// 不需要同步的代码
// ...
synchronized(this) {
// 需要同步的代码
}
// 更多不需要同步的代码
}
4.2 避免死锁
死锁是指两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行的情况。为了避免死锁,我们应该:
- 按固定顺序获取多个锁
- 使用 tryLock() 设置超时时间
- 避免在持有锁时调用外部方法
我曾经遇到过一个典型的死锁场景:线程A持有锁1并尝试获取锁2,同时线程B持有锁2并尝试获取锁1。解决方法是统一规定必须先获取锁1再获取锁2。
4.3 性能考量
虽然 Synchronized 使用简单,但在高并发场景下可能会成为性能瓶颈。以下是一些优化建议:
- 使用更细粒度的锁(如 ConcurrentHashMap 的分段锁)
- 考虑使用读写锁(ReadWriteLock)替代互斥锁
- 对于读多写少的场景,考虑使用乐观锁
- 使用 volatile 配合 CAS 操作实现无锁编程
在实际项目中,我曾经通过将粗粒度锁拆分为多个细粒度锁,将系统吞吐量提高了3倍。
5. Synchronized 与其它同步机制的比较
5.1 Synchronized vs ReentrantLock
ReentrantLock 是 Java 5 引入的显式锁,与 Synchronized 相比有以下特点:
- 可中断:等待锁的线程可以被中断
- 公平性:可以选择公平或非公平获取锁
- 条件变量:支持多个条件队列
- 尝试获取锁:可以尝试获取锁而不阻塞
选择建议:在需要高级功能(如可中断、公平锁)时使用 ReentrantLock,否则优先使用更简单的 Synchronized。
5.2 Synchronized vs volatile
volatile 关键字保证了变量的可见性和有序性,但不保证原子性。它适用于:
- 单一变量的原子性读写
- 状态标志位
- 双重检查锁定模式
而 Synchronized 提供了更全面的同步保证,适用于复杂的复合操作。
5.3 Synchronized vs Atomic 类
Java 并发包中的 Atomic 类(如 AtomicInteger)使用 CAS(Compare-And-Swap) 操作实现无锁编程。它们适用于:
- 简单的计数器
- 标志位更新
- 单一变量的原子操作
对于更复杂的同步需求,仍然需要 Synchronized 或 Lock。
6. 常见问题与解决方案
6.1 锁粗化与锁消除
JVM 会进行一些锁优化,包括:
- 锁粗化:将连续的多个锁操作合并为一个更大的锁范围
- 锁消除:通过逃逸分析,移除不可能存在竞争的锁
了解这些优化有助于我们编写更高效的代码,但不应依赖这些优化而编写低效的同步代码。
6.2 虚假唤醒问题
在使用 wait()/notify() 时,可能会出现虚假唤醒(spurious wakeup)。为了防止这种情况,应该始终在循环中检查条件:
java复制synchronized(lock) {
while(!condition) {
lock.wait();
}
// 处理逻辑
}
6.3 锁的性能监控
在生产环境中,可以使用以下工具监控锁的使用情况:
- JConsole 或 VisualVM 查看线程状态
- Java Mission Control 进行详细的锁分析
- 日志记录锁等待时间
我曾经通过分析锁等待时间,发现了一个隐藏的性能瓶颈,优化后系统响应时间减少了40%。
7. 实际应用案例
7.1 单例模式实现
Synchronized 常用于实现线程安全的单例模式:
java复制public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种双重检查锁定模式既保证了线程安全,又避免了每次获取实例时的同步开销。
7.2 线程安全的缓存实现
下面是一个使用 Synchronized 实现的简单线程安全缓存:
java复制public class SimpleCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public synchronized void put(K key, V value) {
cache.put(key, value);
}
public synchronized V get(K key) {
return cache.get(key);
}
public synchronized boolean contains(K key) {
return cache.containsKey(key);
}
}
对于更高性能的需求,可以考虑使用 ConcurrentHashMap 替代 HashMap,或者使用读写锁优化读多写少的场景。
7.3 生产者-消费者模式
Synchronized 配合 wait()/notify() 可以实现经典的生产者-消费者模式:
java复制public class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
private final int maxSize;
public MessageQueue(int maxSize) {
this.maxSize = maxSize;
}
public synchronized void produce(String message) throws InterruptedException {
while (queue.size() == maxSize) {
wait();
}
queue.add(message);
notifyAll();
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
String message = queue.remove();
notifyAll();
return message;
}
}
在实际项目中,这种模式常用于解耦生产者和消费者,提高系统吞吐量。我曾经在一个日志处理系统中使用这种模式,将日志生成和日志处理分离,显著提高了系统性能。
