同步锁(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()
方法是线程安全的,每次只有一个线程能够执行它。