Java 中的 synchronized 关键字
简介
在多线程编程中,同步是一个至关重要的概念。Java 中的 synchronized
关键字为我们提供了一种简单而有效的方式来控制多线程对共享资源的访问,确保在同一时刻只有一个线程可以访问特定的代码块或方法,从而避免数据竞争和其他并发问题。本文将深入探讨 synchronized
关键字的基础概念、使用方法、常见实践以及最佳实践。
目录
- 基础概念
- 使用方法
- 同步实例方法
- 同步静态方法
- 同步代码块
- 常见实践
- 解决线程安全问题
- 实现生产者 - 消费者模型
- 最佳实践
- 避免不必要的同步
- 减小同步块的范围
- 使用锁对象(Lock 接口)替代
- 小结
- 参考资料
基础概念
在 Java 中,每个对象都有一个内部锁(也称为监视器锁)。当一个线程访问被 synchronized
修饰的代码块或方法时,它首先需要获取对象的内部锁。如果锁已经被其他线程持有,那么当前线程将被阻塞,直到锁被释放。这种机制确保了同一时刻只有一个线程可以执行被同步的代码,从而保证了线程安全。
使用方法
同步实例方法
当一个实例方法被声明为 synchronized
时,调用该方法的对象的内部锁将被获取。也就是说,对于同一个对象,只有一个线程可以同时调用其 synchronized
实例方法。
public class SynchronizedInstanceMethodExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在上述代码中,increment
方法被声明为 synchronized
。这意味着,无论有多少个线程调用 increment
方法,同一时刻只有一个线程可以执行该方法中的代码,从而保证了 count
变量的线程安全。
同步静态方法
静态方法属于类,而不是对象实例。当一个静态方法被声明为 synchronized
时,获取的是该类的 Class 对象的内部锁。这意味着,对于整个类,同一时刻只有一个线程可以调用其 synchronized
静态方法。
public class SynchronizedStaticMethodExample {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
在这个例子中,increment
是一个静态同步方法。所有调用 SynchronizedStaticMethodExample.increment()
的线程都需要获取 SynchronizedStaticMethodExample.class
的锁,从而确保了静态变量 count
的线程安全。
同步代码块
同步代码块
允许我们精确地控制同步的范围,只对需要同步的代码部分进行加锁。它的语法如下:
synchronized (object) {
// 同步代码
}
其中,object
是用于获取锁的对象。通常,我们会使用当前对象(this
)或者一个专门用于同步的对象。
public class SynchronizedBlockExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
在上述代码中,increment
方法使用了一个同步代码块,通过 lock
对象来获取锁。这样,只有在进入同步代码块时才会获取锁,而不是整个方法都被同步,从而提高了代码的执行效率。
常见实践
解决线程安全问题
在多线程环境下,多个线程同时访问和修改共享资源可能会导致数据不一致的问题。synchronized
关键字可以有效地解决这个问题。
public class ThreadSafetyExample {
private int balance = 0;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
public int getBalance() {
return balance;
}
}
在这个银行账户的示例中,deposit
和 withdraw
方法都被声明为 synchronized
,确保了 balance
变量在多线程环境下的安全访问。
实现生产者 - 消费者模型
生产者 - 消费者模型是多线程编程中的经典问题。synchronized
关键字可以用于实现线程间的同步和通信。
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
private static final int MAX_SIZE = 5;
private Queue<Integer> queue = new LinkedList<>();
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.add(item);
System.out.println("Produced: " + item);
notify();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notify();
return item;
}
}
在这个示例中,produce
和 consume
方法都是同步的。wait
方法用于使当前线程等待,直到其他线程调用该对象的 notify
或 notifyAll
方法。notify
方法用于唤醒在此对象监视器上等待的单个线程。通过这种方式,实现了生产者和消费者之间的同步。
最佳实践
避免不必要的同步
过多的同步会导致性能下降,因为线程在获取锁时需要消耗一定的时间和资源。因此,应尽量避免对不需要同步的代码进行同步。
减小同步块的范围
尽量将同步块的范围减小到只包含需要同步的代码。这样可以减少线程等待锁的时间,提高并发性能。
使用锁对象(Lock 接口)替代
从 Java 5 开始,java.util.concurrent.locks
包中提供了 Lock
接口,它提供了比 synchronized
关键字更灵活和强大的锁控制功能。例如,ReentrantLock
类可以实现公平锁,而 synchronized
关键字是非公平的。
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
小结
Java
中的 synchronized
关键字是多线程编程中实现同步的重要工具。通过获取对象的内部锁,它可以确保同一时刻只有一个线程可以访问被同步的代码,从而避免数据竞争和其他并发问题。在使用 synchronized
关键字时,我们需要注意避免不必要的同步,减小同步块的范围,并根据具体需求选择合适的同步方式。同时,了解并合理使用 Lock
接口等更高级的同步机制,可以进一步提高程序的性能和并发处理能力。