乐观锁和悲观锁的区别

本文探讨了乐观锁和悲观锁在并发控制中的应用,比较了两者的工作原理,以及乐观锁(如版本号控制、CAS和时间戳)和悲观锁(共享锁、读写锁、计数信号量和排他锁)的具体实现。同时,介绍了Java中的synchronized和ReentrantLock这两种互斥锁的特性与优势。
摘要由CSDN通过智能技术生成

乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是并发控制中常用的两种策略,用于处理多个并发操作对共享资源的访问冲突。它们的区别如下:

  1. 假设:

    • 乐观锁:乐观锁假设在大多数情况下,对共享资源的并发访问不会产生冲突,因此允许多个操作同时进行,只在提交操作时检查是否发生冲突。
    • 悲观锁:悲观锁假设对共享资源的并发访问会产生冲突,因此在访问资源之前,会先获取锁以阻止其他操作对资源的访问。

乐观锁的实现方式:

版本号控制:

为共享资源引入一个版本号或时间戳,每个操作在读取资源时获取当前版本号,执行操作后在提交时比较版本号是否一致,如果一致则提交成功,否则表示发生冲突需要进行处理。

CAS(Compare and Swap):

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存中共享变量的副本值,也叫预期值:A
  • 需要将共享变量更新到的最新值:B

主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同。

如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。

可以看CAS机制实现的锁是自旋锁,如果线程一直无法获取到锁,则一直自旋,不会阻塞

CAS缺点


ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。

循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销

基于时间戳的乐观锁:

每个操作在执行时记录一个时间戳,提交时检查其他操作是否在此期间修改了资源,如果没有则提交成功,否则发生冲突需要进行处理。

悲观锁实现方式

悲观锁主要分为:

  • 共享锁【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

共享锁

1. 读写锁(ReadWriteLock):
读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作。读操作之间不会互斥,可以并发执行,提高了读操作的并发性能。写操作需要互斥,即在进行写操作时,其他线程无法进行读取或写入操作。Java 提供了 ReentrantReadWriteLock 类来实现读写锁

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedResource {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private int data;

    public int readData() {
        lock.readLock().lock();
        try {
            // 读取数据
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void writeData(int newData) {
        lock.writeLock().lock();
        try {
            // 写入数据
            data = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

在上述例子中,SharedResource 类使用了 ReentrantReadWriteLock 来实现读写锁。readData() 方法获取读锁并读取数据,writeData() 方法获取写锁并写入数据。多个线程可以同时调用 readData() 方法进行并发读取,但只有一个线程可以持有写锁并执行 writeData() 方法。

2.计数信号量(Counting Semaphore):
计数信号量是一种用于控制同时访问共享资源的线程数量的机制。它维护一个可配置的计数器,线程在访问资源之前需要获取信号量,并将计数器减一。当计数器为0时,其他线程需要等待信号量的释放才能访问资源。Java 提供了 Semaphore 类来实现计数信号量。

import java.util.concurrent.Semaphore;

public class SharedResource {
    private final Semaphore semaphore = new Semaphore(3); // 允许同时访问的线程数量为3
    private int data;

    public void accessResource() throws InterruptedException {
        semaphore.acquire(); // 获取信号量
        try {
            // 访问共享资源
            data++;
        } finally {
            semaphore.release(); // 释放信号量
        }
    }
}

排他锁

  1. synchronized:

    synchronized是Java中的关键字,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性,Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。

    它修饰的对象有以下几种:
    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
    2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
    4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

这篇博客写的非常好,包括了不同情况的例子

Java多线程——synchronized使用详解_synchronized用法-CSDN博客

ReentrantLock:

ReentrantLock(可重入锁)是Java中提供的一种高级互斥锁,它提供了与synchronized相似的功能,但具有更多的灵活性和扩展性。下面是对ReentrantLock的详细介绍:

  1. 可重入性:
    ReentrantLock是可重入的,即同一个线程可以多次获得同一个锁,而不会发生死锁。这个特性类似于synchronized关键字,允许线程在持有锁的情况下再次获取相同的锁。可重入性使得ReentrantLock可以实现更复杂的同步语义。

  2. 公平性:
    ReentrantLock可以选择是公平锁(Fair Lock)还是非公平锁(Nonfair Lock)。公平锁会按照线程请求锁的顺序进行获取,而非公平锁则允许插队,可能导致某些线程始终无法获取到锁。默认情况下,ReentrantLock是非公平锁,但可以通过构造函数参数进行设置。

  3. 条件变量:
    ReentrantLock提供了条件变量(Condition),可以使用newCondition()方法创建一个条件变量。条件变量可以用于线程的等待和通知机制,通过await()方法使线程等待,signal()signalAll()方法唤醒等待的线程。与synchronized相比,ReentrantLock的条件变量提供了更多的灵活性,可以创建多个条件变量来实现复杂的线程间通信。

  4. 扩展性:
    ReentrantLock提供了一些额外的特性,如可中断、可轮询、超时等待等。通过使用tryLock()方法,线程可以尝试获取锁,如果锁不可用则立即返回,避免长时间阻塞。此外,ReentrantLock还提供了lockInterruptibly()方法,允许线程在等待锁的过程中响应中断。

具体可看

ReentrantLock详解_reentrantlock lock-CSDN博客

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值