乐观锁和悲观锁自我理解

一、何为悲观锁、乐观锁

1、悲观锁

顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

2、乐观锁

反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

二、悲观锁和乐观锁应用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

三、实现方式

3.1悲观锁的实现方式:

1悲观锁在数据库中的常见实现方式:

  • 行锁:对特定的数据行进行加锁,确保其他事务不能修改或读取这行数据。
  • 表锁:对整个表加锁,阻止其他事务对表中任何数据进行操作。

举例:

假设有一个库存管理系统,当用户购买商品时,需要减少库存。使用悲观锁的方式如下:

-- 1. 获取商品库存时加锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;

-- 2. 减少库存
UPDATE products SET stock = stock - 1 WHERE id = 1;

在这里,FOR UPDATE 语句会加锁,确保在当前事务提交之前,其他事务无法对这个商品的库存进行操作。这种方式适用于高并发的场景下,确保数据一致。

2java中的悲观锁实现

在Java编程中,悲观锁的概念也可以通过多线程并发控制来实现。通常是通过SynchronizedLock机制来确保线程间的互斥操作,从而达到悲观锁的效果。

  1. synchronized 关键字synchronized是Java中最常用的实现悲观锁的方式,确保同一时间只有一个线程能够进入同步代码块或方法,其他线程必须等待锁的释放。

    示例

    public class PessimisticLockExample {
    
        private int stock = 100;
    
        // 使用synchronized保证线程安全
        public synchronized void decreaseStock() {
            if (stock > 0) {
                stock--;
            }
        }
    }
    

    在这个例子中,decreaseStock方法被synchronized关键字修饰,保证同一时刻只有一个线程可以减少库存,其他线程必须等待当前线程释放锁后才能操作。

  2. Lock接口(如ReentrantLock)Lock接口提供了比synchronized更灵活的锁定机制。你可以显式地获取和释放锁,适合处理复杂的同步需求。

    示例

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class PessimisticLockExample {
    
        private int stock = 100;
        private final Lock lock = new ReentrantLock();
    
        // 使用显式锁
        public void decreaseStock() {
            lock.lock();  // 获取锁
            try {
                if (stock > 0) {
                    stock--;
                }
            } finally {
                lock.unlock();  // 释放锁
            }
        }
    }
    

    ReentrantLock提供了显式的锁管理方式,允许程序员在代码中控制锁的获取和释放,避免了synchronized在复杂并发场景下的一些局限性。

3悲观锁的优缺点

优点:
  1. 数据一致性好:悲观锁通过加锁保证了同一时间只有一个线程能够操作数据,避免了多个线程同时修改数据引起的冲突。
  2. 简单可靠:实现简单,逻辑清晰,适合并发冲突较多的场景,不容易出现复杂的并发问题。
  3. 性能开销大:由于悲观锁会阻塞其他线程的操作,可能导致较长的等待时间,影响系统的并发性能,特别是在高并发环境中。
缺点:
  1. 死锁风险:如果多个线程同时等待不同的锁,并互相依赖,可能会导致死锁,系统会进入僵局。
  2. 降低并发性:悲观锁会降低并发访问的效率,尤其在冲突较少的情况下,悲观锁会显得过于保守。

3.2乐观锁的实现方式:

1版本号机制:

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

如下面的sql语句:

SELECT stock, version FROM products WHERE id = 1;
UPDATE products SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 10;

2.CAS算法:

CAS算法有三个操作数:

  • 内存地址(V):表示需要操作的变量的内存地址。
  • 旧的预期值(A):线程期望的变量当前值,也称为比较值。
  • 新值(B):如果比较成功,变量将被更新为的新值。

操作流程

  1. 比较变量当前的值(V)与期望值(A)是否相等。
  2. 如果相等,则将变量的值更新为新值(B)。
  3. 如果不相等,则说明该变量已经被其他线程修改过,操作失败,不更新值。

通过这种方式,CAS实现了一个“乐观锁”的机制,不需要在操作前加锁。只有在变量被其他线程修改过时,当前线程的修改才会失败,导致CAS操作需要重试。

2. 1CAS的工作原理示例

设想一个简单的多线程场景,某个变量的初始值为 V = 10。现在两个线程分别想要将该变量修改为不同的值。

线程 1:期望值 A = 10,新值 B = 11
线程 2:期望值 A = 10,新值 B = 12

线程 1 执行

  1. 比较当前值 V = 10 和期望值 A = 10 是否相等。
  2. 相等,则将 V 更新为 11,操作成功。

线程 2 执行

  1. 比较当前值 V = 11 和期望值 A = 10 是否相等。

  2. 不相等,操作失败,线程 2 需要重新读取当前值 V 并重新进行CAS操

2.2 CAS的优缺点
优点:
  1. 无锁操作:CAS属于无锁操作,不需要像悲观锁那样阻塞其他线程的访问,因此能够提升系统的并发性,减少线程的等待时间。
  2. 原子性:CAS由硬件提供原子指令支持,保证了在多核处理器下的原子操作,使其能够在多线程环境中保证数据的一致性。
  3. 高性能:相比传统的加锁机制,CAS具有更高的性能,特别是在高并发的场景下表现优异。
缺点
  1. ABA问题
    • 由于CAS只比较值是否相等,但不关心期间的变化过程,可能导致ABA问题。ABA问题的本质是,一个变量被修改为A,然后又被修改回A,CAS无法检测到这种情况。
    • 解决方案:可以通过引入版本号的方式来解决ABA问题,每次变量被修改时,版本号加1,保证每次操作时即使值相同,版本号也不同。
  2. 自旋等待
    • CAS操作通常是乐观的,如果操作失败,线程不会阻塞,而是会不断重试(自旋),直到成功为止。在高并发、竞争激烈的场景中,可能会出现大量的重试操作,从而导致性能下降。
  3. 只能保证单个变量的原子操作
    • CAS算法只能保证对一个变量的原子性操作,如果涉及到多个变量时,就需要通过其他方式进行同步。解决办法:使用带版本号的CAS操作。例如每次操作时不仅检查变量的值,还检查其版本号,只有当值和版本号都符合期望时,才允许更新。
2.3CAS的应用场景
  1. Java中的原子类:Java的java.util.concurrent.atomic包中的类(如AtomicIntegerAtomicLongAtomicReference)内部实现就是基于CAS算法,来确保在多线程环境下对变量的安全更新。
  2. 操作系统中的线程调度:在某些操作系统中,CAS用于实现无锁队列和无锁栈,保证线程调度时的高效性。
  3. 数据库中的乐观锁实现:乐观锁机制也经常通过CAS操作来实现,避免数据在并发情况下的修改冲突。

使用时间戳实现乐观锁是一种常见的并发控制技术,特别适用于需要高并发访问的场景。时间戳作为一种冲突检测机制,可以判断在某个操作期间,数据是否被其他线程或事务修改过,从而确保数据的一致性。

3.时间戳实现乐观锁

  1. 读取数据和时间戳

    • 每次读取数据时,数据库表中会存储一个时间戳字段,该字段记录了数据的最后修改时间。
    • 读取数据时,将时间戳一并读取并保存下来。
  2. 更新数据时进行冲突检测

    • 当准备更新数据时,首先检查当前数据的时间戳是否和读取时一致。
    • 如果一致,表示数据没有被其他线程或事务修改过,允许进行更新操作。
    • 如果不一致,说明数据在这期间已经被其他线程修改,更新失败,可以选择回滚或重试。
3.1实现步骤
  1. 数据库表结构

    • 在数据库表中增加一个timestamp字段(通常用DATETIMETIMESTAMPBIGINT类型来记录最后修改时间)。
    CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), 
    stock INT, last_modified TIMESTAMP );

  2. 读取数据和时间戳

    • 读取数据时,连同时间戳一起读取。比如读取商品信息和最后修改时间。
    SELECT stock, last_modified FROM products WHERE id = 1;

    例如,假设读取到的 last_modified 时间戳为 2024-10-01 12:00:00

  3. 更新操作时检查时间戳

    • 更新数据时,先检查时间戳是否与读取时的相同。如果相同,表示数据未被修改,执行更新;如果不同,表示数据已经被修改,操作失败。
    UPDATE products SET stock = stock - 1, last_modified = NOW() WHERE id = 1 
    AND last_modified = '2024-10-01 12:00:00';

    在这个例子中,只有当last_modified的值仍然是2024-10-01 12:00:00时,更新才会成功。如果该字段已被其他线程修改(即last_modified不同),则说明数据在操作之间已经发生了变化,更新操作将不会执行。

  4. 处理更新失败的情况

    • 如果更新操作因时间戳不一致而失败,可以选择:
      • 重试:重新读取数据和时间戳,再次尝试更新。
      • 回滚:放弃本次操作,或提示用户数据已被修改。
3.2使用时间戳实现乐观锁的示例

假设一个库存管理系统中,两个用户几乎同时购买同一件商品,它们的请求操作如下:

  • 用户 A 在读取商品库存时,last_modified时间戳为 2024-10-01 12:00:00
  • 用户 B 也读取到了同样的库存和时间戳。

接下来,用户 A 执行了购买操作,库存减少并成功更新,更新后的last_modified时间戳变为 2024-10-01 12:01:00

UPDATE products SET stock = stock - 1, last_modified = NOW() 
WHERE id = 1 AND last_modified = '2024-10-01 12:00:00';

之后,用户 B 也尝试更新库存,但由于last_modified已经变成了 2024-10-01 12:01:00,它的更新操作会失败,因为时间戳不匹配:

UPDATE products SET stock = stock - 1, last_modified = NOW() 
WHERE id = 1 AND last_modified = '2024-10-01 12:00:00'; -- 更新失败
3.3时间戳乐观锁的优缺点
优点:
  1. 实现简单:通过时间戳可以轻松判断数据是否被修改,代码逻辑简洁明了。
  2. 无锁操作:不需要锁定数据,避免了传统悲观锁带来的性能开销,提升并发性能。
  3. 避免死锁:由于不需要加锁,避免了死锁的风险。
缺点:
  1. 冲突处理复杂:在高并发下,更新失败的几率较高,可能导致频繁重试或失败,增加了处理逻辑的复杂度。
  2. 数据一致性不完美:如果有大量写操作,可能会导致频繁的时间戳不一致,尤其在分布式系统中,时间戳的同步可能成为一个问题。
  3. 只能保证单表单字段的冲突检测:时间戳机制适合单个字段的更新,如果多个字段或者多个表同时更新,处理的复杂度会增加。

四. 对比与适用场景

特性悲观锁乐观锁
加锁机制直接加锁,阻塞其他操作不加锁,通过版本控制检查冲突
性能影响并发性能低,可能导致锁竞争并发性能高,适合读多写少的场景
适用场景写操作多、冲突频繁读操作多、冲突较少
并发性并发性差,可能出现死锁并发性好,可能需要多次重试
实现复杂度实现简单,依赖数据库的锁机制实现复杂,需要额外的冲突检查机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值