在多线程编程中,当我们有多个线程同时访问共享资源时,就会出现数据竞争的问题。想象一下,你和家人共享一个银行账户,如果你们同时在不同ATM机上取钱,系统该如何保证余额的正确性?
为了解决上述问题,synchronized出现了,它是Java中解决多线程并发问题最常用、最基础的同步机制,你也可以称呼它为锁机制。
一、synchronized的核心特性
1.1 原子性(Atomicity)
`synchronized`保证了被它修饰的代码块或方法在同一时刻只能被一个线程执行,从而确保了操作的原子性。
public class Counter {
private int count = 0;
// 没有同步 - 线程不安全
public void increment() {
count++; // 这实际上包含三个操作:读取、增加、写入
}
// 使用synchronized - 线程安全
public synchronized void safeIncrement() {
count++;
}
}
1.2 可见性(Visibility)
当一个线程释放锁时,JVM会强制将工作内存中的修改刷新到主内存中。当另一个线程获取锁时,JVM会强制它清空工作内存,从主内存重新加载变量。
1.3 有序性(Ordering)
`synchronized`限制了指令重排序,保证了在同步代码块内的指令不会与同步代码块外的指令重排序。
以上三种便是锁的三种主要机制。
二、synchronized的三种使用方式
2.1 同步实例方法
public class BankAccount {
private double balance;
// 同步实例方法 - 锁是当前实例对象(this)
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
public synchronized double getBalance() {
return balance;
}
}
使用场景:当多个线程操作同一个对象实例时。
2.2 同步静态方法
public class IdGenerator {
private static long id = 0;
// 同步静态方法 - 锁是当前类的Class对象
public static synchronized long generateId() {
return id++;
}
}
使用场景:当多个线程需要访问类的静态成员时。
2.3 同步代码块
public class FineGrainedSync {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private int counter1 = 0;
private int counter2 = 0;
public void increment1() {
synchronized (lock1) {
counter1++;
}
}
public void increment2() {
synchronized (lock2) {
counter2++;
}
}
}
优势:可以实现更细粒度的锁控制,减少锁竞争,提高并发性能。
三、synchronized的锁机制演进
3.1 JDK 1.6之前的重量级锁
早期版本中,synchronized直接对应操作系统的互斥锁(Mutex Lock),每次加锁和解锁都需要从用户态切换到内核态,性能开销很大。
3.2 锁升级过程(JDK 1.6+)
现代JVM中,synchronized锁会经历一个逐步升级的过程:
无锁状态
对象刚创建时的初始状态。
偏向锁(Biased Locking)
- 场景:只有一个线程访问同步块
- 原理:在对象头中记录偏向的线程ID,该线程再次访问时无需CAS操作
- 优点:几乎没有同步开销
// 大多数情况下,我们的同步方法其实只有一个线程访问
public class Logger {
private static final Logger instance = new Logger();
public static Logger getInstance() {
return instance;
}
// 对于单例模式,偏向锁非常高效
public synchronized void log(String message) {
System.out.println(message);
}
}
轻量级锁(Lightweight Locking)
- 场景:多个线程交替访问同步块,但没有真正同时竞争
- 原理:通过CAS操作在栈帧中创建锁记录(Lock Record)
- 优点:避免操作系统层面的线程阻塞
重量级锁(Heavyweight Locking)
- 场景:多个线程真正同时竞争锁
- 原理:使用操作系统的互斥量,竞争失败的线程进入阻塞状态
- 缺点:性能开销大,涉及用户态到内核态的切换
四、synchronized的注意事项
4.1 避免死锁
public class DeadlockDemo {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
synchronized (lockB) {
// 业务逻辑
}
}
}
public void method2() {
synchronized (lockB) { // 与method1的加锁顺序相反,可能导致死锁
synchronized (lockA) {
// 业务逻辑
}
}
}
}
解决方案:统一加锁顺序,或使用`tryLock`等机制。
4.2 性能考虑
// 不推荐的写法 - 锁粒度太粗
public synchronized void processData(List<String> data) {
for (String item : data) {
// 处理每个数据项,耗时操作
expensiveOperation(item);
}
}
// 推荐的写法 - 减小锁粒度
public void processDataBetter(List<String> data) {
List<String> results = new ArrayList<>();
for (String item : data) {
String result = expensiveOperation(item); // 无锁操作
synchronized (this) {
results.add(result); // 只在必要时加锁
}
}
return results;
}
关键要点:
1. 根据场景选择合适的同步方式:实例方法、静态方法或同步块
2. 尽量减小锁的粒度,提高并发性能
3. 了解锁升级过程,避免不必要的锁竞争
4. 注意死锁问题,保持一致的加锁顺序