悲观锁和乐观锁的内容总结

悲观锁和乐观锁

乐观锁和悲观锁是并发控制的两种不同策略,主要区别如下:

什么是悲观锁?

悲观锁
悲观锁的基本思想是假设并发访问会导致冲突,因此在访问共享资源之前会先加锁,以确保数据的一致性。悲观锁认为在整个数据处理过程中会发生并发冲突,因此对数据进行加锁,使得其他线程无法修改数据,直到当前线程完成操作。典型的悲观锁机制包括使用互斥锁(如synchronized关键字)或数据库中的行级锁。

什么是乐观锁?

乐观锁
**乐观锁的基本思想是假设并发访问不会导致冲突,因此在访问共享资源时不会加锁,而是在更新数据时进行检查,如果发现数据已被其他线程修改,则进行回滚或重新尝试。**乐观锁认为在整个数据处理过程中并发冲突的概率较低,因此不会立即对数据进行加锁。典型的乐观锁机制包括使用版本号或时间戳来判断数据是否被修改。

乐观锁和悲观锁的主要区别:

  • 加锁方式:悲观锁在访问共享资源之前会先加锁,而乐观锁在访问共享资源时不加锁
  • 冲突处理:悲观锁假设会发生冲突,因此加锁保证数据的一致性;乐观锁假设不会发生冲突,因此在更新数据时进行检查,发现冲突时进行回滚或重新尝试。
  • 并发性能:悲观锁在整个数据处理过程中都会持有锁,因此可能限制了并发性能;乐观锁在大部分情况下不会加锁,可以提高并发性能,但在发生冲突时需要进行回滚或重新尝试,可能会导致额外的开销。

选择悲观锁还是乐观锁取决于具体的应用场景和并发访问的特点。如果并发冲突较多,使用悲观锁可以确保数据的一致性;如果并发冲突较少,使用乐观锁可以提高并发性能。

悲观锁的实现方式

悲观锁的实现方式常见的有互斥锁(Mutex Lock):在访问共享资源之前,线程会先尝试获取互斥锁,如果锁已被其他线程占用,则当前线程会被阻塞,直到锁被释放。常见的互斥锁有 synchronized 关键字和 ReentrantLock 类。

互斥锁:用于保护共享资源,确保在任何时刻只有一个线程可以访问该资源。它的主要特点是互斥性,即同一时间只允许一个线程持有锁并执行关键代码块,其他线程需要等待锁的释放

使用synchronized实现互斥锁的示例代码:

public class MutexLockExample {
    private int count = 0; //共享变量,可能会被多个线程同时访问和修改
    private Object lock = new Object();

    public void increment() {
        //保证对count的安全访问,使用synchronized关键字对关键字代码块进行同步
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

increment()方法和getCount()方法中,我们通过synchronized (lock)语句来获取互斥锁。当一个线程进入synchronized块时,它将获得lock对象的锁,其他线程将被阻塞,直到该线程释放锁。

这样,当多个线程调用increment()方法时,每次只有一个线程可以执行该方法的代码块,保证了对count变量的互斥访问。同样地,当多个线程调用getCount()方法时,也只有一个线程可以执行该方法的代码块。

通过互斥锁的使用,我们确保了对共享变量的安全访问,避免了多线程环境下的竞态条件和数据不一致问题。

乐观锁的实现方式

乐观锁的实现方式主要有两种:版本号机制CAS(Compare and Swap)操作

  1. 版本号机制
    在数据表中添加一个版本号字段,每次更新数据时都对版本号进行更新。当读取数据时,同时读取版本号。在进行数据更新时,先比较当前数据的版本号与之前读取的版本号是否一致,如果一致,则执行更新操作,并增加版本号;如果不一致,则表示数据已被其他线程修改,需要进行冲突处理(如回滚或重新尝试)。

    示例代码:

    // 假设data是要更新的数据对象,version是版本号字段
    int version = data.getVersion();
    // 读取数据并版本号
    // ...
    // 更新数据
    data.setValue(newValue);
    data.setVersion(version + 1); // 增加版本号
    // 执行更新操作,检查版本号是否一致
    // ...
    

    版本号机制实现乐观锁的关键是通过版本号来判断数据是否被修改,从而决定是否执行更新操作

  2. CAS(Compare and Swap)操作
    **CAS是一种原子操作,用于实现乐观锁。它通过比较内存中的值与期望值是否一致,如果一致则进行更新,否则不做任何操作。**CAS操作通常使用底层硬件指令实现,可以保证原子性。在Java中,可以使用Atomic类提供的原子类来进行CAS操作。

    示例代码:

    AtomicInteger value = new AtomicInteger(0); // 原子整型变量
    int expect = value.get(); // 读取当前值
    int update = expect + 1; // 更新值
    while (!value.compareAndSet(expect, update)) {
        expect = value.get(); // 再次读取当前值
        update = expect + 1; // 更新值
    }
    

    **CAS操作适用于更新单个变量的情况,它不需要加锁,因此具有较高的并发性能。**如果CAS操作失败(即内存中的值与期望值不一致),则需要进行冲突处理(如回滚或重新尝试)。

乐观锁的实现方式可以根据具体的应用场景和技术选型进行选择。版本号机制适用于数据库等存储系统,CAS操作适用于多线程编程中的共享变量更新。

悲观锁会引发的问题

悲观锁的使用可能引发以下问题:

  1. 性能下降:悲观锁通常需要在访问数据之前获取锁,并在使用完数据后释放锁。导致其他线程必须等待锁的释放才能进行访问,从而导致整体性能降低。
  2. 死锁风险:如果在使用悲观锁时处理不当,可能会出现死锁情况。例如,一个线程持有锁A并等待锁B,而另一个线程持有锁B并等待锁A,这样就形成了死锁。死锁会导致线程无法继续执行,从而影响系统的正常运行。
  3. 并发性低:悲观锁在访问数据时需要锁定整个数据对象或数据表。这就限制了其他线程对数据的并发访问能力,从而降低了系统的并发性。
  4. 长时间占用资源:悲观锁在获取到锁之后会一直占用资源,直到任务完成或锁被释放。

乐观锁会引发的问题

乐观锁的使用可能引发以下问题:

  1. 冲突和重试:乐观锁不会主动加锁,如果在更新过程中发现数据已被修改,则需要进行冲突处理和重试操作。这可能导致额外的开销和延迟。
  2. 数据一致性问题:由于乐观锁不会主动加锁,如果多个线程同时修改同一数据,可能会导致其中一个线程的修改被覆盖或丢失,从而导致数据不一致的情况。例如ABA问题

多学一招:

ABA问题

ABA问题是在并发编程中可能出现的一种情况。它的名称来自于一个典型的示例,假设一个共享变量开始时的值为A,然后被线程1修改为B,最后又被线程1修改回A。在这种情况下,如果另一个线程2只关注变量的值是否发生变化,它可能无法察觉到变量值的中间修改过程,导致出现错误的结果。

ABA问题可能会影响使用CAS(比较并交换)操作的并发算法,其中线程会比较变量的当前值与期望值,并在相等时进行修改。在上述的例子中,线程2如果只关注变量的值是否与期望值A相等,那么它可能会错误地认为变量没有被修改,从而导致并发问题。

为了解决ABA问题,通常会引入版本号或标记,以便在比较并交换操作中除了比较值之外,还要求比较版本号或标记是否匹配。这样可以避免因为中间的修改导致的错误判断。

总之,ABA问题是在并发编程中,由于变量在修改过程中经历了一系列的变化,导致某些操作无法正确判断变量的状态。通过引入版本号或标记等机制,可以避免ABA问题的发生。

结语:若文章有错误,欢迎各位读者指正,并联系作者进行修改,感谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值