【Java并发】乐观锁、悲观锁、CAS、版本号机制

前言

在现代计算机系统中,处理并发操作时,锁机制是至关重要的。本文将介绍乐观锁、悲观锁以及CAS(Compare and Swap)这三种常见的并发控制技术,帮助理解它们的原理和应用场景。


1.悲观锁

1.1 定义

悲观锁是一种在访问共享资源之前,首先对资源进行加锁的机制。它假设在任何时候都可能发生冲突,因此在开始操作之前就先锁定资源,防止其他线程访问。


1.2 特点

  • 阻塞式:如果一个线程持有锁,其他线程只能等待。
  • 简单直观:实现较为简单,容易理解和使用。
  • 性能问题:在高并发场景下,线程会因为等待锁而导致性能下降。

1.3 应用场景

悲观锁适用于写操作频繁的场景,如数据库事务处理。在这种情况下,通过加锁来保护数据的一致性和完整性是很重要的。

像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

public void synchronisedTask() {
	// 使用内置的同步机制,锁定当前对象(this)
    synchronized (this) {
        // 需要同步的操作,这些操作在同一时间只能由一个线程执行
    }
}
// 定义一个 ReentrantLock 对象,用于显式锁定
private Lock lock = new ReentrantLock();
lock.lock();	// 尝试获取锁
try {
   // 需要同步的操作,这些操作在同一时间只能由一个线程执行
} finally {
	// 确保在操作完成后释放锁,避免死锁
    lock.unlock();
}

悲观锁图解
悲观锁图解


2.乐观锁

2.1 定义

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


2.2 特点

  • 非阻塞:不需要等待锁,可以提高并发性。
  • 版本控制:通常通过版本号或时间戳来检测冲突。
  • 重试机制:如果发现冲突,线程会重试操作。

2.3 应用场景

乐观锁适合读操作多、写操作少的场景,比如某些在线应用或缓存系统。由于写操作相对少,冲突的概率低,因此可以利用乐观锁的优势,提高系统性能。

在 Java 中,有一些类和框架使用了乐观锁的思想,例如 java.util.concurrent 包下:

  • ConcurrentHashMap:在读取和更新时使用了乐观锁机制,允许多个线程并发访问而不阻塞。
  • AtomicReferenceAtomicInteger 等原子类:这些类利用 CAS(Compare-And-Swap)机制实现乐观锁,确保在更新值时只有在当前值与预期值相等时才进行修改。

乐观锁图解
乐观锁图解


3.CAS(Compare And Swap)

3.1 定义

CAS 的全称是 Compare And Swap(比较与交换),CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS图解:
CAS图解


3.2 代码示例

import java.util.concurrent.atomic.AtomicInteger;

public class CasCounter {
	// 提供了原子操作,保证在多线程环境中对其值的读取和更新是安全的。原子操作意味着不会被其他线程干扰,因此可以避免竞争条件。
    private AtomicInteger count = new AtomicInteger(0); 

    // 增加计数器的方法
    public void increment() {
        int currentValue;
        int newValue;

        while (true) {
            currentValue = count.get(); // 获取当前计数值
            newValue = currentValue + 1; // 计算新的计数值
            
            // 尝试将当前值更新为新值,只有当当前值未被其他线程修改时才会成功
            if (count.compareAndSet(currentValue, newValue)) {
                break; 
            }
        }
    }

    // 获取当前计数器的值
    public int getCount() {
        return count.get(); 
    }

    public static void main(String[] args) {
        CasCounter counter = new CasCounter();

        // 创建多个线程来增加计数器
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment(); // 每个线程增加1000次
                }
            });
            threads[i].start(); // 启动线程
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            try {
                thread.join(); // 等待线程结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final count: " + counter.getCount()); 
        
        // 结果:Final count: 10000
    }
}


3.3 存在的问题

  • ABA问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,但读取到赋值的这段时间内它的值可能被改为B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。
  • 循环开销时间大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

4.版本号机制

4.1 定义

版本号机制一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。


4.2 代码示例

-- 读取数据时获取版本号
SELECT name, version FROM users WHERE id = 1;

-- 尝试更新数据,更新时版本号也作为条件
UPDATE users
SET name = 'New Name', version = version + 1
WHERE id = 1 AND version = <original_version>;

总结

  • 悲观锁认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。
  • 乐观锁认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。
  • 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值