乐观锁和悲观锁的详细介绍

在多线程编程中,锁(Lock)是为了保证数据的正确性和线程安全性而使用的一种机制。常见的锁有悲观锁(Pessimistic Lock)乐观锁(Optimistic Lock)。它们的主要区别在于处理并发冲突的方式:悲观锁假设会有冲突,而乐观锁则假设不会有冲突,直到发现冲突才进行处理。

悲观锁(Pessimistic Lock)

定义

悲观锁假设在多线程环境中会发生冲突,因此在访问共享资源时会采用锁机制来防止其他线程的干扰。悲观锁通常会锁住资源,直到操作完成,其他线程不能同时访问该资源。

特点

  • 悲观的锁:始终假设会发生冲突,因此在访问数据时对资源加锁,直到操作完成。
  • 阻塞性:当一个线程获取锁时,其他线程必须等待锁释放才能继续执行。
  • 使用场景:适用于事务较长或冲突较为频繁的情况,确保数据的安全性。

常见实现

  1. 数据库悲观锁:使用 SELECT FOR UPDATE 语句来锁定数据库记录,防止其他事务修改相同记录。
  2. Java中的同步锁:使用 synchronizedReentrantLock 来锁定代码块或方法。

具体示例(基于 Java 和数据库):

  1. Java 中的悲观锁:
java

public class PessimisticLockExample {
    private static final Object lock = new Object();
    
    public void updateBalance() {
        synchronized (lock) {
            // 模拟数据库操作,更新余额
            System.out.println(Thread.currentThread().getName() + " is updating balance");
            // 执行某些操作,可能是数据库更新操作等
            try {
                Thread.sleep(1000); // 模拟操作的延迟
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() + " finished updating balance");
        }
    }

    public static void main(String[] args) {
        PessimisticLockExample example = new PessimisticLockExample();
        
        // 创建两个线程,尝试同时更新余额
        Thread t1 = new Thread(example::updateBalance);
        Thread t2 = new Thread(example::updateBalance);
        
        t1.start();
        t2.start();
    }
}

在这个例子中,synchronized 关键字用于在 updateBalance 方法上加锁。无论有多少个线程调用该方法,只有一个线程能够获取到锁,其他线程必须等待。

  1. 数据库中的悲观锁:
sql

-- 假设表名为 account,包含 balance 字段
BEGIN;

-- 锁定所需的记录
SELECT * FROM account WHERE account_id = 1 FOR UPDATE;

-- 更新余额
UPDATE account SET balance = balance - 100 WHERE account_id = 1;

COMMIT;

在这个数据库例子中,SELECT ... FOR UPDATE 语句会在读取记录的同时对该记录加锁,确保没有其他事务在此期间修改这条记录,避免并发修改的问题。

乐观锁(Optimistic Lock)

定义

乐观锁假设在多线程环境中不会发生冲突,因此不对资源加锁,而是通过版本控制、时间戳等机制,在提交修改时检查数据是否被其他线程修改。如果在提交时发现数据已被修改,乐观锁就会触发冲突,通常会重新尝试或通知用户。

特点

  • 乐观的锁:假设不会发生冲突,因此不会对资源加锁,直到提交时检查数据是否被修改。
  • 非阻塞性:线程不需要等待其他线程释放锁,而是直接进行操作,操作完成后检查是否发生冲突。
  • 使用场景:适用于数据冲突较少的场景,能够提高系统的并发性能。

常见实现

  1. 数据库乐观锁:使用版本号或时间戳字段来检测数据是否被修改。
  2. Java中的乐观锁:使用 CAS(Compare And Swap) 操作来实现乐观锁。

具体示例(基于 Java 和数据库):

  1. Java 中的乐观锁(CAS)
java

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private AtomicInteger balance = new AtomicInteger(1000);

    public void updateBalance() {
        int expectedValue = balance.get();
        int newValue = expectedValue - 100;
        
        // CAS 操作,尝试修改 balance
        if (balance.compareAndSet(expectedValue, newValue)) {
            System.out.println(Thread.currentThread().getName() + " successfully updated balance to " + newValue);
        } else {
            System.out.println(Thread.currentThread().getName() + " failed to update balance, retrying...");
            updateBalance();  // 如果 CAS 失败,可以选择重试
        }
    }

    public static void main(String[] args) {
        OptimisticLockExample example = new OptimisticLockExample();

        // 创建多个线程,模拟并发更新余额
        Thread t1 = new Thread(example::updateBalance);
        Thread t2 = new Thread(example::updateBalance);

        t1.start();
        t2.start();
    }
}

在这个例子中,AtomicInteger 提供了原子操作,通过 compareAndSet 方法来实现乐观锁。这个方法会将 balance 的值与预期值进行比较,如果相同则更新余额,否则返回 false,表示数据已被修改。

  1. 数据库中的乐观锁(使用版本号)
sql

-- 假设表结构包含 version 字段
-- 更新操作,首先检查版本号,确保没有其他事务修改该记录

BEGIN;

-- 获取当前记录的版本号
SELECT balance, version FROM account WHERE account_id = 1;

-- 假设获取的版本号为 1,余额为 1000
UPDATE account 
SET balance = balance - 100, version = version + 1
WHERE account_id = 1 AND version = 1;

-- 如果受影响的记录数为 0,说明版本号不匹配,数据已经被修改
COMMIT;

在数据库中,通常使用版本号来实现乐观锁。在更新操作时,除了更新数据之外,还要检查当前记录的版本号是否与之前读取的版本号一致。如果不一致,说明其他线程已经修改了数据,这时当前操作会失败,通常需要重新尝试或者报告冲突。

乐观锁与悲观锁的对比

锁机制

假设会发生冲突,对共享资源加锁,直到操作完成

假设不会发生冲突,操作完成后再检查是否发生冲突

性能

因为加锁导致等待,性能较差,尤其在冲突多的情况下

不加锁,性能较好,尤其在冲突少的情况下

适用场景

高冲突场景,或者事务需要操作较长时间的数据

低冲突场景,尤其是读多写少的情况

阻塞性

阻塞,其他线程必须等待锁释放

非阻塞,操作完成后检查数据是否被修改

实现方式

使用数据库的 SELECT FOR UPDATE或 Java 的 synchronized

使用版本号、时间戳,或者 CAS(Compare And Swap)

总结

  • 悲观锁 适用于冲突较为频繁的场景,保证了数据的一致性和正确性,但可能会导致较低的性能。
  • 乐观锁 适用于冲突较少的场景,能提高系统的并发性,但需要在提交时进行冲突检测,增加了重试的可能性。
Java碎碎念 文章被收录于专栏

来一杯咖啡,聊聊Java的碎碎念呀

全部评论

相关推荐

02-19 22:38
门头沟学院 Java
2025/2/14一面项目亮点(面试官明白之后就没问了)说 JAVA 里面的那个反射,它的原理是什么?它能够反射哪一些类别的内容呢?  JAVA 里面的多线程有哪些实现方式呢?  线程池有几个拒绝策略,分别是什么呢?  注解注解里面它有四种标准原注解,你了解吗?那你知道注解吗?注解它是做什么用的呢? Java对象,我想实现序列化和反序列化,可以继承什么接口? 反系列化就另外一个接口。Java垃圾回收机制,你能够讲一讲吗?垃圾概念,垃圾回收算法。我再问几个计算机基础相关的问题,比如说https,它为什么是安全的呢? 它其实涉及到对称加密,也涉及到非对称加密两种加密算法,它都涉及到。为什么呢? 好的 TCP 有两个概念是跟相关的哈,一个是流量控制,一个是拥塞控制,这两个概念你能够分别简单的描述一下。它(拥塞控制)有大概的具体过程吗?你们有没有记得它这个算法大概涉及到什么? 那流量控制呢? 什么是哈希表,然后怎么解决哈希冲突? 好的 b 树跟 b +树有什么区别?  进程间的通讯方法有哪一些呢?  管道有什么缺点吗?  线程锁多线程同步的时候,我们用的锁锁。你知道的有哪些锁? 自旋锁能描述下吗?数据库的话,我们会建索引,是这个建索引的目的是什么?数据库的索引,它的底层数据结构一般有哪一些呢? 事务四个隔离级别?快排的大概原理是什么? 设计模式,你了解过哪一些呢? 观察者模式?整型数组现在有个整型数组,我想找出里面重复次数最多的值,可以怎么样来实现呢? (第一种方法,hashmap,第二种方法,排序,然后找)性格特点?补充一道题:tcp三次握手,四次挥手?算法:无重复最长子串。反问2025/2/17二面忘了录音了~问的不是八股,而是底层的东西算法:给你1-100数,返回一个100的数字,含有这100个数,要求随机顺序。#牛客AI配图神器##面经#
查看28道真题和解析
点赞 评论 收藏
分享
评论
3
2
分享

创作者周榜

更多
牛客网
牛客企业服务