首页 > 编程笔记 > Java笔记 阅读:13

Java乐观锁和悲观锁的用法(附带实例)

乐观锁与悲观锁不是指具体的某个锁,是处理数据库并发控制的两种策略,它们看待并发同步的角度不同,因此它们对共享数据的访问产生冲突的处理方式也不同。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有发生修改,也会认为发生了修改。因此对于同一个数据的并发操作,它悲观地认为,不使用锁的并发操作一定会出问题。悲观锁在 Java 中的使用可以利用多种锁实现。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断尝试更新数据。它乐观地认为,不使用锁的并发操作是不会出问题的。

从上面的描述我们可以看出,悲观锁适用于写入操作非常多的场景,乐观锁适用于读取操作非常多的场景,不使用锁会带来大量的性能提升。

乐观锁在 Java 中的使用是无锁编程,常常采用 CAS 操作,典型的示例就是原子类,通过 CAS 自旋实现原子性的更新操作。

Java悲观锁

悲观锁适用于写入操作非常多、冲突频繁的场景。当认为多个线程尝试同时修改同一数据的可能性很高时,使用悲观锁可以避免冲突,因为在修改数据之前必须获取锁。

悲观锁的实现假定会有冲突发生,并在数据被读取之前就获取锁。这意味着,只要锁定了数据,其他线程就无法读取或写入,直到锁被释放。这种锁定可以是行级锁、表级锁或数据库级锁,具体取决于数据库系统的实现。

在 Java 中,悲观锁可以通过 synchronized 关键字或 java.util.concurrent.locks.Lock 接口的实现类(例如 ReentrantLock)来实现。

【实例 1】使用 synchronized 关键字实现悲观锁:
public class Counter {
    private int value;
    public synchronized int increment() {
        return value++;
    }
    public synchronized int getValue() {
        return value;
    }
}
在上述示例中,increment() 和 getValue() 方法都是同步的,这意味着同时只能有一个线程执行这两个方法中的一个。尝试执行这些方法的其他线程会被阻塞,直到当前线程释放锁。

【实例 2】使用 ReentrantLock 实现悲观锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int value;
    public int increment() {
        lock.lock();
        try {
            return value++;
        } finally {
            lock.unlock();
        }
    }
    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}
在上述示例中,我们使用 ReentrantLock 实现悲观锁。increment() 和 getValue() 方法在执行前都必须获取锁,并在执行后释放锁。

Java乐观锁

乐观锁适用于读取操作非常多、写入操作少的场景,这是因为乐观锁允许多个事务几乎同时完成读取操作,只有在提交更改时,才会检查是否有冲突。如果冲突(即数据被其他事务修改)很罕见,那么乐观锁会有很好的性能表现,因为不需要频繁地阻塞线程。

乐观锁的实现通常依靠数据版本控制。每次读取数据时,程序都会获得数据的版本号。在提交更新时,会检查数据的当前版本号是否与先前获得的版本号相同。如果相同,更新将会执行,并将版本号增加。如果不同,说明在两次读取之间有其他事务修改了数据,更新将不会执行,而且在这种情况下,通常需要重新读取数据,并尝试更新操作。

在软件层面,乐观锁可以通过在数据库中使用版本号字段或时间戳字段实现。在编程语言中,可以使用原子操作(如 Java 中的 CAS 操作)实现乐观锁。

【实例 3】使用版本号实现乐观锁。假设我们有一个数据库表,其中包含一个版本字段(version)。当更新记录时,我们会检查版本号是否未被其他线程更改。
UPDATE table_name
SET column1 = value1, version = version + 1
WHERE primary_key = some_value AND version = current_version;
在应用代码中,我们会首先读取记录,获取 current_version,然后执行上面的更新。如果 version 字段被其他线程更新,这个 UPDATE 操作将不会修改任何记录,这时可以选择重试或放弃操作。

【实例 4】使用 CAS 操作实现乐观锁。Java 的 java.util.concurrent.atomic 包提供了原子类,如 AtomicInteger,它们可以用来实现乐观锁。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
    private AtomicInteger value = new AtomicInteger();
    public int increment() {
        int currentValue;
        int newValue;
        do {
            currentValue = value.get();
            newValue = currentValue + 1;
        } while (!value.compareAndSet(currentValue, newValue));
        return newValue;
    }
    public int getValue() {
        return value.get();
    }
}
在 increment() 方法中,我们尝试循环使用 compareAndSet() 方法来更新 value。如果在此期间 value 没有被其他线程更改,compareAndSet() 将成功返回 true;否则我们将进行重试,直到成功为止。

总结

乐观锁和悲观锁的选择依赖于应用的数据访问模式和冲突发生的频率。乐观锁在并发冲突较少时性能更好,适用于读取操作非常多、写入操作少的场景,因为它减少了锁定的开销。但是,如果冲突非常频繁,乐观锁需要多次重试,可能会导致性能下降。

相反,悲观锁在冲突频繁的环境中更为有效,适用于写入操作频繁的场景,因为它通过限制并发访问来防止冲突。然而,它可能导致较大的性能开销,因为在使用它的情况下,任何时候只有一个线程能够处理锁定的数据。

乐观锁和悲观锁都有自己的应用场景和实现原理,我们需要根据实际的应用场景和需求选择最合适的锁策略。

相关文章