Java乐观锁的实现原理和典型案例

Java乐观锁的实现原理

什么是乐观锁?

在并发编程中,多个线程同时对同一资源进行操作时,需要使用锁来保证数据的一致性。

乐观锁与悲观锁是两种不同的锁机制。

悲观锁会在整个操作期间占用资源的独占性,以保证数据的一致性,而乐观锁则是基于版本号或时间戳的机制,在操作前做一个乐观的估计,如果操作成功,则版本号加1,如果失败,则重试。因为乐观锁不需要在整个操作期间占用资源的独占性,所以可以提高并发性。

Java中乐观锁的实现方式

Java中的乐观锁主要有两种实现方式:CAS(Compare and Swap)和版本号控制。

CAS

CAS是实现乐观锁的核心算法,它通过比较内存中的值是否和预期的值相等来判断是否存在冲突。如果存在,则返回失败;如果不存在,则执行更新操作。

CAS 它包含了 3 个参数:V(需要更新的变量)、E(预期值)和 N(最新值)。只有当需要更新的变量等于预期值时,需要更新的变量才会被设置为最新值,如果更新值和预期值不同,则说明已经有其它线程更新了需要更新的变量,此时当前线程不做操作,返回 V 的真实值。

Java中提供了AtomicInteger、AtomicLong、AtomicReference等原子类来支持CAS操作。

下面是一个简单的CAS示例:

代码

public class Counter { private AtomicInteger value = new AtomicInteger(0);
    public void increment() { int expect;
        int update;
        do { expect = value.get();
            update = expect + 1;
        } while (!value.compareAndSet(expect, update));
    }
}

在这个示例中,increment()方法会不断地执行CAS操作,直到更新成功为止。

版本号控制

版本号控制是乐观锁的另一种实现方式。

每当一个线程要修改数据时,都会先读取当前的版本号或时间戳,并将其保存下来。线程完成修改后,会再次读取当前的版本号或时间戳,如果发现已经变化,则说明有其他线程对数据进行了修改,此时需要回滚并重试。

下面是一个简单的版本号控制示例:

代码

public class Counter { private int value = 0;
    private int version = 0;
    public void increment() { int currentVersion;
        int currentValue;
        do { currentVersion = version;
            currentValue = value;
            currentValue++;
        } while (currentVersion != version);
        value = currentValue;
        version = currentVersion + 1;
    }
}

在这个示例中,increment()方法会不断地执行循环,直到当前的版本号与保存的版本号相同为止。如果版本号不同,则说明有其他线程修改了数据,此时需要回滚并重试。

Java中乐观锁的案例

ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它使用分离锁(Segment)来保证线程安全。每个Segment都是一个独立的哈希表,每个操作只锁定相关的Segment,因此可以支持更高的并发性。

ConcurrentHashMap使用了一种基于CAS的技术来实现乐观锁,它通过比较当前的value和预期的value是否相等来判断是否存在冲突。如果存在,则返回失败;如果不存在,则执行更新操作。

LongAdder

在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。

LongAdder 的原理就是降低操作共享变量的并发数,也就是将对单一共享变量的操作压力分散到多个变量值上,将竞争的每个写线程的 value 值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的 value 值进行 CAS 操作,最后在读取值的时候会将原子操作的共享变量与各个分散在数组的 value 值相加,返回一个近似准确的数值。

数据库并发控制

在日常开发中,使用乐观锁最常见的场景就是数据库的更新操作了。

为了保证操作数据库的原子性,我们常常会为每一条数据定义一个版本号,并在更新前获取到它,到了更新数据库的时候,还要判断下已经获取的版本号是否被更新过,如果没有,则执行该操作。

总结

在Java中,并发编程的时候可以使用Synchronized 和 Lock 实现的同步锁机制,但是两种同步锁都属于悲观锁。

悲观锁在高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销,这个时候乐观锁就是我们可以考虑的一种方案。

当然CAS 乐观锁在平常使用时比较受限,它只能保证单个变量操作的原子性,当涉及到多个变量时,CAS 就无能为力了。

好了,以上就是今天分享的全部内容,enjoy~ 欢迎关注我的微信公众号【AI黑板报】

本文由博客一文多发平台 OpenWrite 发布!