likes
comments
collection
share

🔥🔥🔥你知道常见的锁策略有哪些吗?

作者站长头像
站长
· 阅读数 23

一、 什么是锁策略

  锁策略无关乎编程语言,它是多线程或多进程编程中,用于管理共享资源访问的一种策略或机制。

  举个例,锁策略就好比是在家里共用的一台电视机,多个人想看电视。为了不让大家同时按遥控器或者频道,需要一个规则。这个规则可以是:

  1. 排队拿遥控器:只有一个人可以拿到遥控器,其他人必须等待,直到拿到遥控器的人放下它。
  2. 只有一个人可以换频道:如果有人正在换频道,其他人必须等待,直到换频道的人完成操作。
  3. 等待一段时间后轮流:每个人最多可以看一段时间电视,然后必须把遥控器交给下一个人。

  就像上述规则一样,锁策略可以限制同时访问资源的数量或规定谁可以访问资源以确保程序的正确性和稳定性。

二、乐观锁 与 悲观锁

(一)悲观锁

  顾名思义,悲观锁是一种悲观的思想,它假设在多个线程或进程之间会发生竞争和冲突,因此在访问共享资源之前,先锁住资源,以防止其他线程访问。就像一个保安,他认为人们总是不守规矩,所以只允许一个人进入房间。

(二)乐观锁

  乐观锁是一种乐观的思想,它假设在多个线程或进程之间的竞争和冲突比较少,因此允许多个线程同时访问共享资源,但在更新共享资源时,会先检查是否发生了冲突。

  那如何检查是否发生了冲突呢?这里使用版本号或时间戳等标记来追踪数据的变化。

  假设我们需要多线程修改“用户账户余额”:当前余额为100,引入一个版本号 version, 初始值为 1。并且我们规定“提交版本必须大于记录当前版本才能执行更新余额”。

  1. 线程 A 准备将其读出,线程 B 也读出。

🔥🔥🔥你知道常见的锁策略有哪些吗?

  1. 线程 A 从其帐户余额中扣除 50,线程 B 从其帐户余额中扣除 20。

🔥🔥🔥你知道常见的锁策略有哪些吗?

  1. 线程 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50)写回到主存中。

🔥🔥🔥你知道常见的锁策略有哪些吗?

  1. 线程 B 尝试向内存中提交数据,但是对比版本发现,线程 B 提交的数据版本号为 2 ,主存的当前版本也为 2 ,不满足“提交版本必须大于记录当前版本才能执行更新“的规则,就认为这次操作失败。

🔥🔥🔥你知道常见的锁策略有哪些吗?

三、读写锁

  读写锁允许多个线程同时读取共享资源,但在写操作时只允许一个线程独占访问。

读写锁分为两种基本类型:

  1. 读锁:也称为共享锁,多个线程可以同时获取读锁,允许并发读取共享资源,因为读操作通常不会修改数据,所以多个线程可以同时执行读操作,提高了性能。
  2. 写锁:也称为排他锁,只允许一个线程获取写锁,写锁会独占资源,防止其他线程同时读或写,确保数据的一致性和完整性。

  在 Java 标准库中提供了 ReentrantReadWriteLock 类, 实现了读写锁。

public class Main {

    //读写锁
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private static int sharedData = 0;

    public static void main(String[] args) {
        // 创建多个读线程和写线程
        Thread writer1 = new Thread(() -> writeData());
        Thread reader1 = new Thread(() -> readData());
        Thread reader2 = new Thread(() -> readData());
        writer1.start();
        reader1.start();
        reader2.start();
    }

    public static void readData() {
        readLock.lock(); // 获取读锁
        try {
            System.out.println(Thread.currentThread().getName() + " is reading: " + sharedData);
        } finally {
            readLock.unlock(); // 释放读锁
        }
    }

    public static void writeData() {
        writeLock.lock(); // 获取写锁
        try {
            // 模拟写操作
            sharedData++;
            System.out.println(Thread.currentThread().getName() + " is writing: " + sharedData);
        } finally {
            writeLock.unlock(); // 释放写锁
        }
    }
}

四、自旋锁

  自旋锁的特点是,当一个线程尝试获取锁时,如果发现锁已被其他线程占用,它不会立即进入阻塞状态,而是会反复尝试(自旋)一段时间,等待锁被释放。只有当一定的条件满足或者自旋次数达到上限时,线程才会真正阻塞等待或执行其他操作。

  • 优点:没有放弃 CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。
  • 缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源。

五、重量级锁 与 轻量级锁

(一)重量级锁

  重量级锁与轻量级锁是针对于加锁解锁的开销来看的。

  重量级锁是一种传统的锁机制,它基于操作系统的原语(例如互斥锁)实现,通常需要较多的系统资源和处理器时间来管理。当一个线程获取了重量级锁时,其他线程会被阻塞,直到锁被释放。这种锁的竞争开销相对较高,因为它需要切换线程的状态,涉及内核态和用户态之间的切换,以及线程的上下文切换。

对于内核态和用户态:

用户态:就像是你在计算机上运行普通软件时的状态,比如打开浏览器、编辑文档、玩游戏等。在这种状态下,你只能使用计算机的一部分资源,而且你不能直接控制计算机的底层硬件,就像你不能改变电脑的内核。

内核态:则是操作系统的工作状态,就像操作系统是计算机的"老板"一样。在内核态下,操作系统有最高的权限,可以管理计算机的所有资源,执行重要的任务,比如控制硬件、响应网络请求、和进程等等。

  内核态和用户态之间的切换就像你需要请求老板做一些特殊的事情。当你需要访问计算机的某些特权功能时,比如读取文件或者连接网络,你必须请求操作系统(老板)的帮助。操作系统会在内核态下执行这些特殊任务,然后返回结果给你(用户态)。

(二)轻量级锁

  轻量级锁是为了减少锁的竞争和提高性能而设计的锁机制,通常用于低竞争情况。当线程尝试获取轻量级锁时,它会首先尝试自旋(忙等待)一段时间,而不会立即将线程置于阻塞状态。只有在自旋一定次数后仍然无法获取锁时,线程才会升级为重量级锁。上面的自旋锁是一种轻量级锁的实现方式。

六、公平锁 与 非公平锁

  现在有一个问题:假设有三个线程 A、B、C。 A 先尝试获取锁,获取成功。然后 B 再尝试获取锁,获取失败,阻塞等待;然后 C 也尝试获取锁,C 也获取失败,也阻塞等待。那么当线程 A 释放锁的时候, 会发生啥呢?

  • 公平锁: 遵守 “先来后到”。B 比 C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。

  • 非公平锁: 不遵守 “先来后到”。B 和 C 都有可能获取到锁。

  公平锁严格按照线程请求锁的顺序来分配锁资源。也就是说,当多个线程尝试获取锁时,会根据它们发出请求的顺序来决定哪个线程首先获得锁。

  非公平锁不考虑线程请求锁的顺序,而是允许任何线程在任何时间尝试获取锁。

七、可重入锁 与 不可重入锁

可重入锁:可重入锁允许同一个线程多次获取同一个锁。也就是说,如果一个线程已经获取了某个可重入锁,它可以再次获取该锁,而不会被阻塞。

不可重入锁:不可重入锁不允许同一个线程多次获取同一个锁。如果一个线程已经持有该锁,如果它再次尝试获取该锁时会被阻塞,可能会造成死锁。