乐观锁和悲观锁

一、基本概念

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

二、实现方式(含实例)

在说明实现方式之前,需要明确:乐观锁和悲观锁是两种思想,它们的使用是非常广泛的,不局限于某种编程语言或数据库。

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制,下面详细介绍。

2.1 CAS(Compare And Swap)

CAS操作包括了3个操作数:

  • 需要读写的内存位置(V)
  • 进行比较的预期值(A)
  • 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?

答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

下面以Java中的自增操作(i++)为例,看一下悲观锁和CAS分别是如何保证线程安全的。我们知道,在Java中自增操作不是原子操作,它实际上包含三个独立的操作:

(1)读取i值;(2)加1;(3)将新值写回i

因此,如果并发执行自增操作,可能导致计算结果的不准确。在下面的代码示例中:value1没有进行任何线程安全方面的保护,value2使用了乐观锁(CAS),value3使用了悲观锁(synchronized)。运行程序,使用1000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于1000,而value1的值常常小于1000。

public class Test {

	private static int value1 = 0;  //线程不安全

	private static AtomicInteger value2 = new AtomicInteger(0);  //乐观锁

	private static int value3 = 0; //悲观锁

	private static synchronized void increateValue3(){
		value3++;
	}

	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						throw new RuntimeException(e);
					}
					value1++;
					value2.getAndIncrement();
					increateValue3();
				}
			}).start();
		}
		Thread.sleep(1000);
		System.out.println("线程不安全:" + value1);
		System.out.println("乐观锁(AtomicInteger):" + value2);
		System.out.println("悲观锁(synchronized):" + value3);
	}

}

首先来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。

下面看一下AtomicInteger的源码,了解下它的自增操作getAndIncrement()是如何实现的(源码以Java8为例)。

package java.util.concurrent.atomic;
 
import sun.misc.Unsafe;
 
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
 
/**
 * 原子类
 */
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
 
    // Unsafe可以执行低级别、不安全操作的方法,比如直接访问系统内存资源、自主管理内存资源等
    // 使用Unsafe,可以实现内存操作、CAS、内存屏障...在AtomicInteger中,主要用来进行CAS操作
    private static final Unsafe unsafe = Unsafe.getUnsafe();
 
    // value就是AtomicInteger保存的值,因为加了volatile关键字,所以在并发操作时,多线程能感知数据发生变化
    private volatile int value;
 
    // 该字段用来保存内存偏移地址,会在下面的静态代码块初始化
    private static final long valueOffset;
 
    static {
        try {
            // valueOffset为字段value的内存偏移地址(相对于atomicInteger对象基地址的偏移)
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }
 
    /**
     * 创建AtomicInteger对象,并设置初始值
     *
     * @param initialValue 初始值
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
 
    /**
     * 创建AtomicInteger对象,初始值为0
     */
    public AtomicInteger() {
    }
 
    /**
     * 获取value.
     *
     * @return 当前的value
     */
    public final int get() {
        return value;
    }
 
    /**
     * 立即修改或者设置value
     * set操作能够保证可见性,避免指令重排
     *
     * @param newValue 设置的新值
     */
    public final void set(int newValue) {
        value = newValue;
    }
 
    /**
     * 不会立即修改或者设置值(但是最终会)
     * lazySet不能保证可见性,可能会发生指令重排,但是性能比set高
     *
     * @param newValue 要设置的值
     * @since 1.6
     */
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
 
    /**
     * 设置新值,并且返回旧值(原子操作)
     *
     * @param newValue 新值
     * @return 旧值
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
 
    /**
     * 原子操作,CAS,当value和expect相等时,才将value修改为update
     *
     * @param expect 期望value的值
     * @param update 要修改的值
     * @return true:value和expect相等,且完成修改;false:value和expect不相等
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 
    /**
     * 和compareAndSet相同功能,都是使用CAS原子操作,但是无法保证多个线程CAS的有序性
     *
     * @param expect 期望的value值
     * @param update 更改后的值
     * @return 操作是否成功
     */
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 
    /**
     * 将value加1,然后返回旧值,原子操作
     *
     * @return 旧值
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
 
    /**
     * 将value减1,然后返回旧值,原子操作
     *
     * @return 旧值
     */
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
 
    /**
     * 对value增加指定值,然后返回旧值,原子操作
     *
     * @param delta 要加的值
     * @return 旧值
     */
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
 
    /**
     * 将value加1,并返回新值,原子操作
     *
     * @return value加1后的值
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
 
    /**
     * 将value减1,并返回新值,原子操作
     *
     * @return value减1后的新值
     */
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
 
    /**
     * 对value增加值,然后返回新值,原子操作
     *
     * @param delta 新增的值
     * @return value新增后的值
     */
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
 
    /**
     * 传入Function,对value进行操作(同样使用CAS保证原子性),会一直重试直到成功才中断,然后返回旧值
     *
     * @param updateFunction a side-effect-free function
     * @return 旧值
     * @since 1.8
     */
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }
 
    /**
     * 传入Function,对value进行操作(同样使用CAS保证原子性),会一直重试直到成功才中断,然后返回新值
     *
     * @param updateFunction a side-effect-free function
     * @return the updated value
     * @since 1.8
     */
    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
            // 一直重试,直到CAS操作成功
        } while (!compareAndSet(prev, next));
        return next;
    }
 
    /**
     * Atomically updates the current value with the results of
     * applying the given function to the current and given values,
     * returning the previous value. The function should be
     * side-effect-free, since it may be re-applied when attempted
     * updates fail due to contention among threads.  The function
     * is applied with the current value as its first argument,
     * and the given update as the second argument.
     *
     * @param x                   the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @return the previous value
     * @since 1.8
     */
    public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }
 
    /**
     * Atomically updates the current value with the results of
     * applying the given function to the current and given values,
     * returning the updated value. The function should be
     * side-effect-free, since it may be re-applied when attempted
     * updates fail due to contention among threads.  The function
     * is applied with the current value as its first argument,
     * and the given update as the second argument.
     *
     * @param x                   the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @return the updated value
     * @since 1.8
     */
    public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }
 
    public String toString() {
        return Integer.toString(get());
    }
 
    public int intValue() {
        return get();
    }
 
    public long longValue() {
        return (long) get();
    }
 
    public float floatValue() {
        return (float) get();
    }
 
    public double doubleValue() {
        return (double) get();
    }
}

源码分析说明如下:

  1. getAndIncrement()实现的自增操作是自旋CAS操作:在循环中进行compareAndSet,如果执行成功则退出,否则一直执行。
  2. 其中compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。
  3. Unsafe又是何许人也呢?Unsafe是用来帮助Java访问操作系统底层资源的类(如可以分配内存、释放内存),通过Unsafe,Java具有了底层操作能力,可以提升运行效率;强大的底层资源操作能力也带来了安全隐患(类的名字Unsafe也在提醒我们这一点),因此正常情况下用户无法使用。AtomicInteger在这里使用了Unsafe提供的CAS功能。
  4. valueOffset可以理解为value在内存中的偏移量,对应了CAS三个操作数(V/A/B)中的V;偏移量的获得也是通过Unsafe实现的。
  5. value域的volatile修饰符:Java并发编程要保证线程安全,需要保证原子性、可视性和有序性;CAS操作可以保证原子性,而volatile可以保证可视性和一定程度的有序性;在AtomicInteger中,volatile和CAS一起保证了线程安全性。关于volatile作用原理的说明涉及到Java内存模型(JMM),这里不详细展开。

说完了AtomicInteger,再说synchronized。synchronized通过对代码块加锁来保证线程安全:在同一时刻,只能有一个线程可以执行代码块中的代码。synchronized是一个重量级的操作,不仅是因为加锁需要消耗额外的资源,还因为线程状态的切换会涉及操作系统核心态和用户态的转换;不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现已经越来越好。

2.2 版本号机制

除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

为了更好地理解乐观锁/悲观锁的具体实现,我们可以使用一个简单的银行账户余额更新作为例子。我们假设有一个名为 accounts 的表,其中包括字段 account_id(账户标识)、balance(账户余额)和 version(版本号,用于乐观锁)。

乐观锁实现

SQL 表结构

CREATE TABLE accounts (
	account_id INT AUTO_INCREMENT PRIMARY KEY,
	balance DECIMAL(10, 2),
	version INT DEFAULT 1 ); 

更新操作
在进行更新操作时,检查 version 字段确保数据未被修改过,然后进行更新,并将 version 字段的值增加1。

UPDATE accounts
SET balance = balance + 1000,  -- 假设我们要增加1000元
    version = version + 1
WHERE account_id = 1 AND version = @CurrentVersion;

@CurrentVersion 是从应用程序传入的,基于最初查询得到的版本号
如果 version 不匹配,即另一个事务已经更新了记录,这个更新操作将不会改变任何行,应用程序可以据此知道更新失败,可能需要重新尝试或通知用户。

悲观锁实现

在使用悲观锁时,可以利用数据库提供的锁机制(如行锁),确保在当前事务完成之前,其他事务不能修改被锁定的数据。

SQL 表结构

-- 使用与乐观锁相同的表结构
CREATE TABLE accounts (
    account_id INT AUTO_INCREMENT PRIMARY KEY,
    balance DECIMAL(10, 2)
);

更新操作
在查询时使用 FOR UPDATE 语句来锁定数据行。这会在当前事务完成之前阻止其他事务修改这些行。

START TRANSACTION;

SELECT balance
FROM accounts
WHERE account_id = 66
FOR UPDATE;  -- 锁定账户66

-- 执行业务逻辑,比如增加余额1000
UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 1;

COMMIT;

在这个例子中,SELECT ... FOR UPDATE 语句将阻止其他任何尝试更新或读取(在需要相同锁的情况下)account_id 为 66 的行的事务,直到当前事务提交或回滚。

三、优缺点和适用场景

乐观锁和悲观锁并没有优劣之分,它们有各自适合的场景;下面从两个方面进行说明。

3.1 功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

3.2 竞争激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

  • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

乐观锁和悲观锁各有用武之地,具体使用哪种锁机制取决于应用场景和并发级别。乐观锁适用于写操作较少的场景,悲观锁则适用于高冲突环境,尤其是在多用户频繁写入同一数据的场合。

四、知识点补充

  4.1 乐观锁加锁吗?

  • 乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了,AtomicInteger便是一个例子。

 4.2 CAS有哪些缺点?

ABA问题

假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

  1. 线程1读取内存中数据为A;
  2. 线程2将该数据修改为B;
  3. 线程2将该数据修改为A;
  4. 线程1对数据进行CAS操作

在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了,这就是ABA问题。在某些场景下,ABA会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

高竞争下的开销问题

在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

功能限制

CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:

(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;

(2)当涉及到多个变量(内存值)时,CAS也无能为力。

除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

### Java乐观锁悲观锁的概念 #### 悲观锁 悲观锁假设最坏的情况,在整个数据处理过程中都持有独占锁。这种方式能够有效防止其他线程修改同一份数据,但是也带来了资源占用的问题。在Java中,`synchronized`关键字以及`Lock`接口下的实现类均属于悲观锁范畴[^1]。 ```java // synchronized 关键字示例 public class Counter { private int count; public synchronized void increment() { this.count++; } } ``` 对于更灵活的锁定机制,可以使用`ReentrantLock`: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class CounterWithLock { private final Lock lock = new ReentrantLock(); private int count; public void increment() { lock.lock(); // 加锁 try { this.count++; } finally { lock.unlock(); // 解锁 } } } ``` #### 乐观锁 乐观锁则采取了一种更加宽松的态度来对待并发访问控制问题。它不会主动加锁阻止其他线程的操作,而是允许所有线程自由读取并尝试更新共享变量;当提交更改时会检测是否有其他线程已经改变了该值,如果有,则放弃当前操作或执行特定逻辑(如重试)。这种策略特别适合那些写入频率较低的数据结构。 版本号校验是最常见的乐观锁实现方法之一。通过给定对象增加一个version字段,在每次更新前先验证此字段是否发生变化从而判断是否存在竞争条件。 ```sql UPDATE table_name SET value = newValue, version = version + 1 WHERE id = givenId AND version = expectedVersion; ``` 如果上述SQL语句影响到0行记录,则说明有其他事务抢先完成了对该条目的修改,此时可以根据业务需求决定如何响应——比如抛出异常告知调用方失败了,或是自动重新获取最新状态再做一次尝试。 而在Java环境中,可以通过CAS(Compare And Swap)原子指令配合`AtomicInteger`, `AtomicReference`等工具类轻松达成相同效果: ```java import java.util.concurrent.atomic.AtomicInteger; class OptimisticCounter { AtomicInteger counter = new AtomicInteger(); boolean update(int oldValue, int newValue){ return counter.compareAndSet(oldValue,newValue); } } ``` ### 区别与适用场景 - **性能**: 由于乐观锁减少了不必要的等待时间,所以在大多数情况下其表现优于悲观锁,尤其是在争用不激烈的情形下[^3]。 - **冲突处理**: 如前所述,悲观锁全程保持锁定状态故无需担心冲突解决;相反地,采用乐观模式意味着开发者需自行设计应对潜在冲突的方法. - **应用场景的选择**: - 当应用程序中的写操作频繁且容易引发竞态条件时,应该优先考虑使用悲观锁以确保一致性; - 对于读多写少的工作负载而言,乐观锁往往是一个更好的选择,因为这有助于提高整体吞吐量并减少死锁风险[^4].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值