在 Java 编程中,悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)是两种常用的并发控制机制,用于管理对共享资源的访问。它们在不同的场景下具有各自的优势和适用范围。
一、悲观锁的定义及实现方法
悲观锁的思想是假设在并发环境下,其他线程会频繁地修改共享资源,因此在访问共享资源之前,悲观锁会先获取锁,以防止其他线程的并发修改。这种方式虽然保证了数据的一致性,但在高并发情况下,可能会导致大量的线程等待锁,降低系统的并发性能。
在 Java 中,悲观锁通常通过同步代码块(synchronized)或重入锁(ReentrantLock)来实现。
-
同步代码块(synchronized)
- 同步代码块是 Java 中最基本的同步机制,它使用
synchronized
关键字来修饰一段代码块,使得在同一时刻,只有一个线程能够进入该代码块执行。 - 以下是一个使用同步代码块实现悲观锁的示例:
public class PessimisticLockExample { private final Object lock = new Object();
public void performAction() { synchronized (lock) { // 在此处执行需要同步的代码 System.out.println("Thread " + Thread.currentThread().getName() + " is in critical section."); } } }
- 在上述代码中,`lock` 是一个共享的对象,多个线程通过同步代码块 `synchronized (lock)` 来获取锁。只有获取到锁的线程才能进入同步代码块执行,其他线程则会被阻塞,直到锁被释放。
- 同步代码块是 Java 中最基本的同步机制,它使用
-
重入锁(ReentrantLock)
- 重入锁是 Java 中的一种高级同步机制,它提供了与同步代码块类似的功能,但更加灵活和强大。
- 重入锁实现了
Lock
接口,并提供了与synchronized
相似的同步机制,例如lock()
和unlock()
方法。 - 以下是一个使用重入锁实现悲观锁的示例:
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExampleWithReentrantLock { private final ReentrantLock lock = new ReentrantLock();
public void performAction() {
lock.lock();
try {
// 在此处执行需要同步的代码
System.out.println("Thread " + Thread.currentThread().getName() + " is in critical section.");
} finally {
lock.unlock();
}
}
}
- 在上述代码中,`lock` 是一个重入锁对象,通过调用 `lock()` 方法获取锁,在 `try` 块中执行需要同步的代码,最后在 `finally` 块中调用 `unlock()` 方法释放锁。即使在执行同步代码块的过程中抛出异常,也能确保锁被正确释放。
**二、乐观锁的定义及实现方法**
乐观锁的思想是假设在并发环境下,其他线程不会频繁地修改共享资源,因此在访问共享资源之前,乐观锁不会获取锁,而是在更新共享资源时,通过比较版本号或使用 CAS(Compare and Swap)操作来确保数据的一致性。如果在更新过程中发现数据已经被其他线程修改,则重试更新操作,直到更新成功。这种方式虽然在高并发情况下能够提高系统的并发性能,但需要额外的开销来实现版本号管理或 CAS 操作。
在 Java 中,乐观锁通常通过原子类(Atomic类)或版本号机制来实现。
1. **原子类(Atomic类)**
- Java 中的原子类提供了一组原子操作,例如原子更新字段、原子更新数组元素等。这些原子操作是线程安全的,能够在不使用锁的情况下实现对共享资源的更新。
- 以下是一个使用原子类实现乐观锁的示例:
```java
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
int expectedValue = counter.get();
while (!counter.compareAndSet(expectedValue, expectedValue + 1)) {
expectedValue = counter.get();
}
}
public int getValue() {
return counter.get();
}
}
- 在上述代码中,`counter` 是一个原子整数,通过调用 `compareAndSet()` 方法来实现乐观锁。`compareAndSet()` 方法会先获取当前的值,然后尝试将其更新为指定的值,如果当前值没有被其他线程修改,则更新成功,否则继续重试。
-
版本号机制
- 版本号机制是一种常见的乐观锁实现方式,它通过在共享资源中添加一个版本号字段,在更新共享资源时,先获取当前的版本号,然后在更新时将版本号加 1。如果在更新过程中发现版本号已经被其他线程修改,则说明数据已经被其他线程修改,需要重新获取最新的数据并更新。
- 以下是一个使用版本号机制实现乐观锁的示例:
public class OptimisticLockExampleWithVersion { private int data; private int version;
public synchronized int getData() { return data; }
public synchronized void updateData(int newData) { int currentVersion = version; // 模拟数据更新的延迟 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (version == currentVersion) { data = newData; version++; System.out.println("Thread " + Thread.currentThread().getName() + " updated data."); } else { System.out.println("Data has been modified by other thread."); } } }
- 在上述代码中,`data` 是共享的数据,`version` 是版本号字段。`updateData()` 方法在更新数据之前,先获取当前的版本号,然后在更新时将版本号加 1。如果在更新过程中发现版本号已经被其他线程修改,则说明数据已经被其他线程修改,需要重新获取最新的数据并更新。
三、总结
悲观锁和乐观锁是两种不同的并发控制机制,它们在不同的场景下具有各自的优势和适用范围。
悲观锁适用于对数据一致性要求较高的场景,例如银行转账等。在这种情况下,悲观锁能够保证在同一时刻只有一个线程能够访问共享资源,从而避免数据的不一致性。
乐观锁适用于对并发性能要求较高的场景,例如缓存更新等。在这种情况下,乐观锁能够在不获取锁的情况下实现对共享资源的更新,从而提高系统的并发性能。
在实际应用中,需要根据具体的业务需求和并发情况来选择合适的锁机制。如果对数据一致性要求较高,可以选择悲观锁;如果对并发性能要求较高,可以选择乐观锁。同时,也可以结合使用悲观锁和乐观锁,以充分发挥它们的优势。