同步锁(Synchronized Lock)是多线程编程中的一个关键概念,用来解决多个线程同时访问共享资源时可能引发的数据不一致问题。同步锁通过限制多个线程并发执行关键代码的能力,确保在同一时刻只有一个线程能够访问临界区(即共享资源的代码段),从而保证数据的一致性。
我们可以通过 Java 的 synchronized 关键字实现同步锁。
1. 为什么需要同步锁?
在多线程环境下,如果多个线程同时操作同一个共享资源(比如全局变量或对象),很可能会出现竞态条件(Race Condition)。即,当多个线程在没有适当同步的情况下并发执行时,某个线程的执行结果会依赖于其他线程的执行顺序,这种不确定性会导致数据不一致的情况。
举例说明:
假设我们有一个共享变量 counter,多个线程同时对它进行自增操作(counter++)。在没有同步锁的情况下,两个线程可能会同时读取到相同的 counter 值,然后都尝试对它进行加一操作,这样会导致有一次加一操作被丢失,最终的 counter 值比预期的要小。
2. 什么是同步锁?
同步锁的目的是控制多个线程对共享资源的并发访问。通过同步锁,只有一个线程可以进入临界区,其他线程必须等待该线程执行完毕并释放锁后,才能继续执行。
在 Java 中,synchronized 关键字用于加锁。它可以用来锁住代码块或整个方法,保证在同一时刻只有一个线程可以进入这个同步代码块或方法。
同步锁的核心概念:
- 锁(Lock):每个对象都隐式关联着一个锁。线程必须获得对象的锁才能执行被
synchronized保护的代码。获得锁的线程执行完同步代码后会释放锁,其他线程才能继续获得锁并执行。 - 同步代码块(Synchronized Block):被
synchronized修饰的代码就是同步代码块。只有一个线程能够进入该代码块,其他线程会被阻塞,直到该线程退出同步代码块并释放锁。
3. 如何使用 synchronized?
Java 中的 synchronized 可以用于方法或代码块,分为两种常见的用法:
a. 同步实例方法
将整个方法声明为同步方法。这样在调用这个方法时,线程必须获得当前对象的锁。
public class Counter {
private int count = 0;
// synchronized 修饰实例方法
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
- 当线程 A 调用
increment()方法时,它会锁定Counter对象,其他线程(比如线程 B)如果也试图调用increment(),就必须等待线程 A 执行完释放锁之后才能执行。 - 锁的粒度是整个方法,意味着整个方法都只能由一个线程执行。
b. 同步代码块
同步代码块允许我们锁定某个特定对象,而不是锁定整个方法。可以只同步一部分关键代码,来提高性能,因为这样可以让非关键代码并发执行。
public class Counter {
private int count = 0;
public void increment() {
// synchronized 锁住代码块,锁定当前对象(this)
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
- 这里
synchronized(this)意味着在当前对象上加锁。这样,只有一个线程能够进入synchronized代码块。 - 锁的粒度是代码块,而不是整个方法。
c. 同步静态方法
静态方法中的 synchronized 锁定的是类对象本身(Class 对象),而不是具体的实例。
public class Counter {
private static int count = 0;
// synchronized 修饰静态方法
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
- 锁的对象是类的
Class对象,因此所有访问该静态方法的线程都必须获得类级别的锁。 - 这意味着无论多少实例,都只有一个静态锁用于整个类的所有线程。
4. 如何选择锁对象?
a. 锁住当前对象 (this)
如果同步代码块只关心当前对象的状态,那么锁定 this 对象是常见的选择。
synchronized (this) {
// 临界区代码
}
b. 锁住类对象 (Class)
如果需要同步的是类级别的资源(如静态变量),应该锁定 Class 对象。
synchronized (Counter.class) {
// 临界区代码
}
c. 锁住某个特定对象
有时我们只想锁住某个特定对象,而不是整个类或实例。可以使用一个特定的对象作为锁。
public class Counter {
private Object lock = new Object();
public void increment() {
synchronized (lock) {
// 临界区代码
}
}
}
- 这种方式灵活性更强,可以将同步锁的粒度控制得更加精确,减少不必要的阻塞。
5. 线程安全 vs. 性能权衡
虽然同步锁可以保证线程安全,但也会带来性能问题。因为 synchronized 会导致线程在进入同步代码块时阻塞(锁竞争),这可能会降低系统性能,特别是在高并发场景下。
如何降低同步对性能的影响?
- 减少锁的范围:尽量只锁住需要同步的代码,而不是整个方法或大块代码。同步代码块能有效提高并发性能。
- 使用双重检查锁定(Double-Checked Locking):这在单例模式中比较常见,只有在第一次检查发现实例为空时才加锁,减少了不必要的同步开销。
- 使用更高效的并发工具:在 JDK 中,
java.util.concurrent包提供了一些高效的并发工具类,比如ReentrantLock、ReadWriteLock,这些可以在特定场景下替代synchronized,提供更灵活的并发控制。
6. 示例:线程不安全 vs. 线程安全
以下是一个简单的对比,展示没有同步和使用同步锁的区别:
线程不安全示例:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
如果多个线程同时调用 increment(),可能会导致竞态条件,最终 count 值不正确。
线程安全示例:
public class Counter {
private int count = 0;
// 使用 synchronized 保证线程安全
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
通过 synchronized 确保了 increment() 方法是线程安全的,每次只有一个线程能够执行它。
9万+

被折叠的 条评论
为什么被折叠?



