在 Java 中,synchronized
是关键字,用于实现线程同步,确保多个线程对共享资源的访问是互斥的,从而避免数据不一致或竞态条件。以下是对 synchronized
的详细解析。
1. 为什么需要 synchronized
并发问题
当多个线程访问共享资源时,可能会导致以下问题:
- 数据竞争:多个线程同时修改同一变量,导致结果不确定。
- 竞态条件:线程之间执行的时序不一致,导致逻辑错误。
- 内存可见性问题:一个线程的修改对其他线程不可见。
解决方法
Java
提供了 synchronized
关键字,用于线程间同步,以确保:
- 同一时刻,只有一个线程能够执行同步代码块。
- 保证线程对共享资源的修改对其他线程可见。
2. synchronized
的用法
2.1 修饰实例方法
public synchronized void method() {
// 线程安全的代码
}
- 锁对象:调用该方法的对象实例 (
this
)。 - 同一时刻,只有一个线程可以调用该对象的同步方法。
- 示例:
public class Example { public synchronized void printNumbers() { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Example example = new Example(); Thread t1 = new Thread(() -> example.printNumbers(), "Thread-1"); Thread t2 = new Thread(() -> example.printNumbers(), "Thread-2"); t1.start(); t2.start(); }
- 输出结果(线程交替运行):
Thread-1: 1 Thread-1: 2 ... Thread-2: 1 Thread-2: 2
2.2 修饰静态方法
public static synchronized void staticMethod() {
// 线程安全的代码
}
- 锁对象:类的
Class
对象(Example.class
)。 - 同一时刻,只有一个线程可以调用该类的同步静态方法。
- 示例:
public class Example { public static synchronized void staticPrintNumbers() { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Thread t1 = new Thread(() -> Example.staticPrintNumbers(), "Thread-1"); Thread t2 = new Thread(() -> Example.staticPrintNumbers(), "Thread-2"); t1.start(); t2.start(); }
- 输出结果:
Thread-1: 1 Thread-1: 2 ... Thread-2: 1 Thread-2: 2
2.3 修饰代码块
public void method() {
synchronized (this) {
// 线程安全的代码
}
}
- 锁对象:
synchronized
括号中的对象(通常是this
或某个共享资源)。 - 可以同步部分代码,而不是整个方法,提高效率。
- 示例:
public class Example { public void printNumbers() { synchronized (this) { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
3. 锁对象的作用范围
-
锁定实例对象:
- 修饰实例方法或代码块时,锁定的是对象实例。
- 不同实例的同步方法可以同时执行。
-
锁定类对象:
- 修饰静态方法时,锁定的是类的
Class
对象。 - 所有线程必须争夺该类的唯一锁。
- 修饰静态方法时,锁定的是类的
4. 常见问题与注意事项
4.1 死锁
- 如果线程 A 持有锁 1,等待锁 2,而线程 B 持有锁 2,等待锁 1,可能会导致死锁。
- 示例:
public class DeadlockExample { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void method1() { synchronized (lock1) { System.out.println("Thread-1: Holding lock1..."); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("Thread-1: Holding lock2..."); } } } public void method2() { synchronized (lock2) { System.out.println("Thread-2: Holding lock2..."); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("Thread-2: Holding lock1..."); } } } public static void main(String[] args) { DeadlockExample example = new DeadlockExample(); Thread t1 = new Thread(() -> example.method1()); Thread t2 = new Thread(() -> example.method2()); t1.start(); t2.start(); } }
- 解决方法:避免嵌套锁或按固定顺序获取锁。
4.2 性能问题
synchronized
会阻塞线程,可能影响性能。- 替代方案:
- 使用
ReentrantLock
:更灵活的锁机制。 - 减少同步代码块的范围:仅同步关键代码。
- 使用
5. synchronized
和 ReentrantLock
的对比
特性 | synchronized | ReentrantLock |
---|---|---|
锁类型 | 隐式锁 | 显式锁 |
可重入性 | 支持 | 支持 |
超时机制 | 不支持 | 支持(tryLock 方法) |
公平性 | 不支持 | 支持(可构造公平锁) |
性能 | 比较低(依赖 JVM 优化) | 比较高 |
灵活性 | 较低,代码简单 | 高,需手动加锁解锁,代码复杂 |
6. 总结
-
用法:
synchronized
修饰方法或代码块,用于实现线程同步。- 锁定对象有实例锁(
this
)和类锁(Class
对象)。
-
特性:
- 线程安全。
- 简单易用,但可能影响性能。
-
注意事项:
- 避免死锁。
- 同步范围不宜过大,影响并发性能。
-
优化:
- 对复杂同步需求,使用
ReentrantLock
。 - 减少共享资源访问,尽量避免全局锁定。
- 对复杂同步需求,使用