Synchronized是Java中最基本且最常用的线程同步机制,它能够有效解决多线程环境下的共享资源竞争问题。本文将全面解析Synchronized的工作原理、使用方式、优化策略以及实际应用场景。
一、Synchronized基本概念与作用
Synchronized是Java语言原生支持的关键字,用于实现线程同步,保证多线程环境下对共享资源的安全访问。它的核心作用可以概括为三点:
- 保证线程互斥访问:同一时刻最多只有一个线程能执行被Synchronized修饰的方法或代码块,其他线程必须等待当前线程执行完毕后才能获取锁并执行
- 保证可见性:确保一个线程对共享变量的修改能够立即对其他线程可见。在退出Synchronized块时,JVM会将工作内存中的变量刷新回主内存;在进入时,会强制从主内存重新读取变量值
- 保证有序性:防止编译器和CPU对同步代码块内的指令进行重排序,确保代码按程序顺序执行
不使用同步机制的典型后果是数据不一致。例如两个线程同时执行count++操作,由于count++实际上包含"读取-修改-写入"三个步骤,在并发情况下可能导致最终结果小于预期值
二、Synchronized的四种使用方式
1. 修饰实例方法(对象锁)
arduino
public synchronized void method() {
// 方法体
}
这种方式锁住的是当前实例对象(this),同一个实例的多个同步方法会互斥,不同实例的同步方法不会相互干扰。例如银行账户操作:
arduino
public class BankAccount {
private double balance;
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
balance -= amount;
}
}
2. 修饰静态方法(类锁)
arduino
public static synchronized void method() {
// 方法体
}
这种方式锁住的是类对象(Class对象),所有实例共享同一把锁,即使不同实例调用静态同步方法也会互斥。例如:
arduino
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
3. 同步代码块(指定锁对象)
javascript
synchronized(obj) {
// 代码块
}
这种方式可以灵活指定锁对象,obj可以是任何对象实例。通常使用共享资源作为锁对象。例如:
typescript
public class TicketService {
private int tickets;
private final Object lock = new Object();
public void sellTicket() {
synchronized(lock) {
if(tickets > 0) {
tickets--;
}
}
}
}
4. 同步静态代码块(类锁)
javascript
synchronized(ClassName.class) {
// 代码块
}
这种方式同样锁住类对象,与静态同步方法效果相同。例如:
typescript
public class Logger {
public static void log(String message) {
synchronized(Logger.class) {
// 写入日志文件
}
}
}
三、Synchronized实现原理
1. 监视器锁(Monitor)机制
Synchronized的底层实现依赖于JVM的对象监视器锁(Monitor)机制。每个Java对象都与一个Monitor相关联,Monitor包含以下关键字段:
_owner
:记录持有锁的线程_recursions
:锁的重入次数_EntryList
:存放等待锁的阻塞线程队列_WaitSet
:存放调用wait()后等待的线程队列
当线程执行到同步代码块时:
- 执行
monitorenter
指令尝试获取Monitor所有权 - 获取成功则进入同步代码块执行,Monitor进入数+1
- 执行完毕后执行
monitorexit
指令,Monitor进入数-1 - 当进入数为0时完全释放锁,唤醒等待线程
对于同步方法,JVM通过方法访问标志ACC_SYNCHRONIZED
实现同步,原理与代码块同步类似
2. 锁升级优化过程
JDK1.6后对Synchronized进行了重大优化,引入了锁升级机制,根据竞争情况自动调整锁状态:
- 无锁状态:初始状态,没有线程竞争锁
- 偏向锁:当第一个线程访问同步块时,JVM会将对象头Mark Word标记为偏向该线程ID。后续该线程进入同步块时无需任何同步操作,只需检查线程ID是否匹配
- 轻量级锁 :当第二个线程尝试获取锁时,偏向锁升级为轻量级锁。线程通过CAS操作尝试获取锁,失败则短暂自旋等待,避免线程阻塞切换
- 重量级锁:当多个线程竞争激烈时,轻量级锁升级为重量级锁。竞争失败的线程会进入阻塞状态,依赖操作系统线程调度机制
锁升级是不可逆的,一旦升级为重量级锁就无法降级
3. 其他优化策略
- 锁消除:JVM检测到不可能存在共享数据竞争时,会自动消除同步锁
- 锁粗化:将多个连续的加锁/解锁操作合并为一个更大范围的锁操作,如将循环内的锁移到循环外
- 自适应自旋:根据以往自旋等待的成功率,动态调整自旋时间
四、Synchronized的特性与限制
1. 可重入性
Synchronized是可重入锁,同一线程在外层方法获取锁后,内层方法可以直接再次获取该锁。JVM通过记录锁的持有线程和进入次数实现这一特性。例如:
arduino
public class ReentrantDemo {
public synchronized void method1() {
method2(); // 可重入
}
public synchronized void method2() {
// ...
}
}
2. 不可中断性
一旦锁被其他线程获取,当前线程只能选择等待或阻塞,无法主动中断获取锁的过程。这与Lock类的可中断特性形成对比
3. 非公平锁
Synchronized是非公平锁,不保证等待时间最长的线程优先获取锁,任何尝试获取锁的线程都有机会立即获取锁
五、Synchronized的缺陷与替代方案
1. 主要缺陷
- 效率问题:在竞争激烈时,重量级锁会导致频繁的线程阻塞和唤醒,性能开销大
- 灵活性不足:无法设置超时、无法中断等待、无法实现读写分离等复杂同步需求
- 单条件限制:每个锁只能有一个等待条件(通过wait/notify),无法满足复杂条件判断
2. 替代方案
- Lock接口:提供了更灵活的加锁机制,支持尝试获取锁、可中断锁、超时锁等
- 读写锁(ReentrantReadWriteLock):实现读读共享、读写互斥、写写互斥,提高读多写少场景的性能
- CAS操作:基于乐观锁的无锁算法,如AtomicInteger等原子类
六、实战应用场景与最佳实践
1. 典型应用场景
- 单例模式实现:确保多线程环境下只创建一个实例
typescript
public 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;
}
}
- 线程安全的计数器:保证计数的原子性
arduino
public class SafeCounter {
private int count;
public synchronized void increment() {
count++;
}
}
- 资源池管理:如数据库连接池的获取和释放
csharp
public class ConnectionPool {
private List<Connection> pool = new ArrayList<>();
public synchronized Connection getConnection() {
if(pool.isEmpty()) return createConnection();
return pool.remove(pool.size()-1);
}
public synchronized void release(Connection conn) {
pool.add(conn);
}
}
2. 最佳实践
- 减小同步范围:尽量只同步必要的代码块,而非整个方法
- 分离读写锁:读多写少场景考虑使用读写锁替代
- 避免锁嵌套:防止死锁发生
- 使用私有锁对象:而非直接锁this或类对象,提高封装性
typescript
public class PrivateLock {
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// ...
}
}
}
七、Synchronized与volatile比较
特性 | Synchronized | volatile |
---|---|---|
原子性 | 保证代码块/方法的原子性 | 仅保证单次读/写的原子性 |
可见性 | 保证 | 保证 |
有序性 | 保证 | 有限保证 |
互斥性 | 支持 | 不支持 |
性能 | 较高开销 | 较低开销 |
使用复杂度 | 较高 | 较低 |
volatile适用于单一变量的可见性控制,而Synchronized适用于复杂操作的原子性控制
总结
Synchronized作为Java最基本的同步机制,虽然存在一些性能缺陷,但在JDK不断优化下仍然是大多数并发场景的首选方案。理解其底层原理和适用场景,能够帮助开发者编写出更高效、更安全的并发程序。对于更复杂的并发需求,可以考虑结合Lock、CAS等机制实现