java多线程-扩展知识三:乐观锁与悲观锁

1、悲观锁

        悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。

        悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
        Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。 

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

2、乐观锁

        乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题

         乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

        Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式CAS实现的。 

// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间(空间换时间)
LongAdder longAdder = new LongAdder();
// 自增
longAdder.increment();
// 获取结果
longAdder.sum();

3、使用选择

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。 

 4、乐观锁实现

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

4.1、CAS

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

        java多线程-扩展知识三:乐观锁与悲观锁_第1张图片

  • CAS操作包括三个操作数:内存位置(V)、预期原值(A)和新值(B)。
  • 当执行CAS操作时,只有当V的值等于A时,才会将V的值更新为B,否则不做任何操作。
  • CAS操作是原子性的,也就是说在同一时刻只能有一个线程执行CAS操作,因此CAS机制保证了数据的一致性。

 Java中提供了AtomicInteger、AtomicLong、AtomicReference等原子类来支持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));
    }
}

4.2、版本控制

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

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;
    }
}

5、补充

5.1、ConcurrentHashMap

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

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

5.2、LongAdder

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

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

你可能感兴趣的:(java基础进阶,java,Java多线程,乐观锁,悲观锁,乐观锁和悲观锁)