常见的锁策略详细讲解(悲观锁 vs 乐观锁,轻量锁 vs 重量锁,自旋锁 vs 互斥锁,公平锁 vs 非公平锁,读写锁等)

文章目录

  • 悲观锁和乐观锁
    • Java中的悲观锁和乐观锁
    • 乐观锁常见的两种实现方式
    • 版本号机制
    • CAS(compare and swap) 算法
    • 乐观锁的缺点
  • 轻量级锁和重量级锁
  • 自旋锁 VS 互斥锁
  • 公平锁 VS 非公平锁
  • 读写锁
    • 读写锁的插队策略
    • 读写锁的升级策略
  • 可重入锁 VS 不可重入锁

悲观锁和乐观锁

所谓悲观和乐观

从人的精神层面来讲,悲观就是:在生活中,人思考问题时,总向着坏的方向思考,乐观就是:在思考问题时,总向着好的方向考

Java中的悲观锁和乐观锁

Java中的悲观锁和乐观锁是一种锁的思想,并不是一种具体的锁,所以以下的内容,都是在讲解锁的思想,请读者不要和具体的锁产生混乱;

  • 悲观锁:总是假设是最坏的情况,当一个线程获取数据时,都会有其他的线程同时来修改这个数据,所以,当一个线程拿数据时,总会去加锁,这样,当别的线程想要对这个数据进行操作时,就会阻塞等待,换句话讲,就是共享资源在一个时刻间只能一个线程获取,其他线程阻塞等待,直到这个线程释放锁之后,其他线程才能够在尝试获取资源,所以,悲观锁更适合于多个线程对同一个资源进行修改的情况
  • 乐观锁:总是假设是最好的情况,当一个线程获取数据的同时,认为其他的线程不会来对这个数据进行修改,所以,就没必要进行加锁,所以,正因为这个原因,乐观锁更适合于多个线程读取同一个资源的情况。

乐观锁常见的两种实现方式

1️⃣版本号机制
2️⃣CAS 算法

版本号机制

版本号机制是要引入一个版本号属性version,来记录数据被修改的次数,当数据被修改时,version+1,比如,线程A要更新数据的场景时,在获取这个这个数据的同时,会把version也获取到,当线程A对数据修改了以后,也会将version+1,然后,在提交这个更新后的数据时,如果刚才已经修改后的version值大于当前内存中的version值,更新数据,否则,重试更新操作,直到更新成功。

举个例子:假设有这样一种场景:当前,钱包中有100余额,线程A减了50,在线程A进行减50的过程中,线程B进行了减20的操作,请看下图:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

CAS(compare and swap) 算法

CAS 算法正如它的名字一样,比较和交换,它有三个操作数CAS(V,A,B),算法如下:

V是需要读写的内存值

A是要和V进行比较的值

B是要写入的新值

当 V 和 A相同时,CAS 算法就认为此时V没有被修改过,就会将 B 赋值给 V,否则,不会进行任何的操作,需要注意的是:这个比较和交换的操作,它是一个原子性的操作,也就是由一条 CPU 指令完成的,所以,CAS 算法也是一种无锁编程,即在不使用锁的情况下实现多线程之间的变量同步,在一般情况下,它是一个自旋的操作,也就是不断的重试。

乐观锁的缺点

1️⃣.ABA问题

ABA 问题是 CAS 操作中的一个大问题,如果一个变量 V 初次读取的时候是 A 值,在准备赋值的时候,检查到它仍然是 A 值,那我们就能说明这个值就没被其他线程改过吗?,答案是:不能,因为,在这段时间内,可能被其他线程改过了,但是又改了过来,那 CAS 操作就会认为它从来没有被修改过。

2️⃣.循环时间长,开销大

因为,在 CAS 操作下,它是一种自旋操作,以及在引入版本号的情况下,它也是一种循环重试的操作,如果长时间不成功,那么就会一直循环重试,进入一种“忙等”的状态,对 CPU 的开销比较大

本篇文章中有些内容借鉴于:https://zhuanlan.zhihu.com/p/40211594

轻量级锁和重量级锁

轻量级锁,锁的开销比较小;

重量级锁,锁的开销比较大;

轻量级锁和重量级锁也是和上面的乐观和悲观有关联的,因为,乐观锁做的工作比较少,所以就会比较轻量,而悲观锁,做的工作比较多,所以就会很重量。

它们只是站在了不同的角度来衡量的,一个是预测锁冲突的概率,一个是实际消耗的开销

所以乐观锁通常就是轻量级锁,悲观锁通常是重量级锁

自旋锁 VS 互斥锁

自旋锁 就属于轻量级锁的典型表现

互斥锁 就属于重量级锁的一种典型表现

对于互斥锁而言,当某一个线程获取锁后,其他线程再尝试获取锁时,就会进行阻塞等待,就暂时不参与 CPU 的调度,暂时就不参与 CPU 的运算了,直到锁释放以后,

互斥锁要借助系统 api 来实现,如果出现锁竞争,就会在内核中触发一系列的动作,比如,让线程进入阻塞状态,暂时不参与cpu的调度,直到锁被释放以后,才参与CPU的调度,这里就涉及了内核态和用户态切换操作,所以开销就比较大,就比较重量

自旋锁 往往是在纯用户态实现,比如使用一个while循环来不停的判定当前锁是否被释放,如果没释放,就继续循环,如果释放了,就获取倒锁,从而结束循环,它就不涉及到阻塞,会一直在CPU上运行,通过“忙等”的方式消耗cpu,换来更快的响应。

公平锁 VS 非公平锁

假设,现在有三个线程 A,B,C 轮流尝试获取同一把锁,此时,线程A获取到锁后,线程B 和 线程C依次阻塞等待,当线程A释放锁后,线程B获取锁,之后 线程C 再获取锁,这样按照“先来后到”的方式,来加锁,此时就是公平锁,反之,线程A释放锁喉,线程B 和 线程C 都有可能获取到锁,此时就是非公平锁

公平锁:按照“先来后到”的方式加锁,此时就是公平锁

非公平锁:不按照“先来后到”的方式,按照“抢占式”的方式,此时就是非公平锁。例如,synchronized 就是非公平锁

读写锁

在多线程下,进行读操作时,是不会产生线程安全问题的,在写操作时,非常容易出现线程安全问题,所以,就可以使用加锁产生互斥效果来解决线程安全问题,而在多个线程进行读操作时,既然不会产生线程安全问题,那么也就不用再进行互斥操作了因为只要涉及到互斥操作,就要阻塞等待,阻塞等待后,就不知道什么时候能够被唤醒了,而且,阻塞等待是内核态+用户态完成的,所以,效率就比较低,而为了提高效率,减少互斥就是一种重要的手段,所以直接并发读就可以了,只有在写操作时,进行互斥操作,所以,就有了读写锁策略。

读写锁的特性:

  • 读加锁 和 读加锁 之间不互斥
  • 写加锁 和 读加锁 之间互斥
  • 写加锁 和 写加锁 之间互斥

在 Java 标准库中,提供了 ReentrantReadWriteLock 类,该类是基于读写锁实现的;

在这个类中,又实现了两个内部类,分别表示 读锁 和 写锁:

  • ReentrantReadWriteLock.ReadLock 类表示读锁,这个类提供了 lock() 方法进行加锁 和 unlock() 方法进行解锁
  • ReentrantReadWriteLock.WriteLock 类表示写锁,,这个类提供了 lock() 方法进行加锁 和 unlock() 方法进行解锁

代码示例:

示例一:两个线程都进行读操作。执行结果:可以同时获取锁

public class Main {
    //创建 ReentrantReadWriteLock实例,用于创建读锁和写锁实例
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //创建读锁实例
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    //创建写锁实例
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    //创建线程池
    private static ExecutorService threadPool = Executors.newCachedThreadPool();

    //获取的读锁方法
    public static void read() {
        //线程获取到读锁
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁,执行完成");
        }
    }
    //获取写锁的方法
    public static void write() {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁,执行完成");
        }
    }
    public static void main(String[] args) {
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });
    }
}

结论:由结果可以看到,多个线程在获取读锁时不会产生阻塞等待

在这里插入图片描述

示例二:一个线程进行读操作,一个线程进行写操作。执行结果:一个可以获取到锁,一个阻塞

public class Main {
    //创建 ReentrantReadWriteLock实例,用于创建读锁和写锁实例
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //创建读锁实例
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    //创建写锁实例
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    //创建线程池
    private static ExecutorService threadPool = Executors.newCachedThreadPool();

    //获取的读锁方法
    public static void read() {
        //线程获取到读锁
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁,执行完成");
        }
    }
    //获取写锁的方法
    public static void write() {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁,执行完成");
        }
    }
    public static void main(String[] args) {
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                write();
            }
        });
    }
}

结果:可以看出,获取读锁后时,写锁无法进行加锁,必须等读锁释放后才可以获取写锁

在这里插入图片描述

示例三:两个线程都进行写操作。执行结果:一个可以获取到锁,一个阻塞

public class Main {
    //创建 ReentrantReadWriteLock实例,用于创建读锁和写锁实例
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //创建读锁实例
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    //创建写锁实例
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    //创建线程池
    private static ExecutorService threadPool = Executors.newCachedThreadPool();

    //获取的读锁方法
    public static void read() {
        //线程获取到读锁
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁,执行完成");
        }
    }
    //获取写锁的方法
    public static void write() {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
        try {
            //睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁,执行完成");
        }
    }
    public static void main(String[] args) {
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                write();
            }
        });
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                write();
            }
        });
    }
}

结论:由执行结果可以看出,无法同时获取写锁

在这里插入图片描述

读写锁的插队策略

插队策略:为了防止“线程饥饿”,读锁不能插队

举个例子:

假设在非公平的ReentrantReadWriteLock场景下:有4个线程,线程1 和 线程2 是同时读取,所以可以同时获取到锁,线程3 想要写入,此时就会阻塞等待(读加锁和写加锁互斥),进入等待队列,此时,线程4没有在队列中,但是,线程4想要进行读取操作,线程4能否有先有线程3执行呢?

针对上述场景,就有两种策略:

  • 策略一:允许线程4优先于线程3执行,因为,线程3是写锁,线程4是读锁,让线程4先读取,是不会对线程3的写操作有任何影响的,也可以提高一定的效率,但是,这个策略有一个弊端:如果在线程4之后又有 n 个线程也进行读操作,都进行插队的话,就会造成“线程饥饿”;
  • 策略二:不允许插队,就是,线程4的读操作必须放在线程3的写操作之后,放入队列中,排在线程3的后面,这样就能避免线程饥饿;

而事实上,ReentrantReadWriteLock 在非公平情况下,采用的是策略2,允许写锁插队,也允许读锁插队,但是,读锁插队的请提示,队列的第一个元素不能是想获取写锁的线程。

读写锁的升级策略

读锁 变成 写锁为升级策略

写锁 变成 读锁为降级策略

代码示例:

public class Main3{
    //创建 ReentrantReadWriteLock实例,用于创建读锁和写锁实例
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //创建读锁实例
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    //创建写锁实例
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    //创建线程池
    private static ExecutorService threadPool = Executors.newCachedThreadPool();

    //获取的读锁方法
    public static void read() {
            try {
                //线程获取到读锁
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
                System.out.println(Thread.currentThread().getName() + "尝试将读锁升级成写锁");
                writeLock.lock();//升级失败,不会执行到下面的代码
                System.out.println(Thread.currentThread().getName() + "读锁升级成写锁成功");
                //睡眠3秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //释放读锁
                readLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁,执行完成");
            }
    }
    //获取写锁的方法
    public static void write() {
            try {
                //线程获取到写锁
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
                System.out.println(Thread.currentThread().getName() + "尝试将写锁降级为读锁");
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "写锁降级成功");
                //睡眠3秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //释放读锁
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释锁,执行完成");
            }
    }

    public static void main(String[] args) {
        //读锁升级成写锁失败
/*       threadPool.submit(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });*/

       //写锁降级成读锁成功
       threadPool.submit(new Runnable() {
            @Override
            public void run() {
                write();
            }
        });
    }
}

在这里插入图片描述

在这里插入图片描述

ReentrantReadWriteLock 不支持升级为写锁是因为:为了避免死锁,如果多个线程同时进行升级的话,就会造成死锁,比如,假设线程A和线程B都是读锁,如果两个线程都想升级,那么,线程A升级时,就要等线程B释放了锁,而线程B想要升级时,就要等才能成A释放了锁,此时,就会互相等待,构成死锁。

使用场合:读写锁(ReentrantReadWriteLock)适合于读多写少的场合,可以提高并发效率

可重入锁 VS 不可重入锁

可重入锁:一个线程针对同一把锁连续加锁多次,如果不会死锁,就时可重入锁,反之就是不可重入锁。例如,synchronized就是可重入锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/286482.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android 13 - Media框架(29)- MediaCodec(四)

上一节我们了解了如何通过 onInputBufferAvailable 和 getInputBuffer 获取到 input buffer index,接下来我们一起学习上层如何拿到buffer并且向下写数据的。 1、获取 input Buffer 获取 MediaCodec 中的 buffer 有两种方式,一种是调用 getInputBuffers…

在vscode中创建任务编译module源文件

接昨天的文章 [创建并使用自己的C模块(Windows10MSVC)-CSDN博客],觉得每次编译转到命令行下paste命令过于麻烦,于是研究了一下在vscode中创建自动编译任务。 经过尝试,在task.json中增加如下代码: {"…

IDEA2023 最新版详细图文安装教程(安装+运行测试+汉化+背景图设置)

IDEA2023 最新版详细图文安装教程 名人说:工欲善其事,必先利其器。——《论语》 作者:Code_流苏(CSDN) o(‐^▽^‐)o很高兴你打开了这篇博客,跟着教程去一步步尝试安装吧。 目录 IDEA2023 最新版详细图文安…

SpringBoot整合Validator

前言 @Validation是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。 通过对DTO中实体类的约束,可以大大增加代码的简洁性。 错误的状态码 返回的响应码推荐使用400 bad request. 参数注解含义 实体类 /*** @author:…

索引语法SQL性能分析

创建 查看 删除 SQL执行频率 Com后七个下划线 慢查询日志 show profiles explain explain执行计划 各字段含义: 多表查询 根据主键或者唯一索引时会出现const const就已经是很棒的性能了,实际中 NULL几乎不会出现

信息安全管理与评估省赛经验总结

信息技能大赛 在比赛开始之前,一定要检查设配,认真审查注意事项;拿到题之后,把对应设备的基本配置完成,任何异常及时报告,这个时候可以把设备的线链接上配置好,登录清单上管理地址等查看是否能登…

二维码地址门牌系统技术服务:让您的生活更便捷,一码通行,安全无忧

文章目录 前言一、融合二维码技术与门牌的便捷服务二、手机开门便捷功能三、智能化安全保障四、智能化、便捷化的新型技术 前言 在数字化时代,二维码门牌系统技术应运而生,为了满足人们对安全、便捷生活的需求。这项技术将二维码与门牌结合,…

leetcode链表小练(1.反转链表2.链表的中间节点3.合并两个有序链表4.环形链表①5.环形链表②)详解 (୨୧• ᴗ •͈)◞︎ᶫᵒᵛᵉ ♡

目录 一.反转链表 思路一反转指针反向: 思路二头插法: 二.链表的中间节点: 三.合并两个有序数组: 思路一:从头开始,取两个链表中小的那个尾插到新链表。定义指针head,tail指向空,代表新链表的头结点。…

Hive/SparkSQL中UDF/UDTF/UDAF的含义、区别、有哪些函数

Hive官网:https://cwiki.apache.org/confluence/display/Hive/LanguageManualUDF#LanguageManualUDF-Built-inTable-GeneratingFunctions(UDTF) 1.UDF(User-Defined Function) 含义 即用户定义函数,UDF用于处理一行数据并返回一个标量值(单个值)&#x…

测试自动创建设备节点的功能

一. 简介 上一篇文章在 新设备驱动框架代码的基础上,添加了自动创建设备节点的代码。文章地址如下: 自动创建设备节点代码的实现-CSDN博客 本文对自动创建设备节点的功能进行测试。 二. 自动创建设备节点代码的测试 1. 编译驱动,并拷贝…

关于编程模式的总结与思考

淘宝创新业务的优化迭代是非常高频且迅速的,在这过程中要求技术也必须是快且稳的,而为了适应这种快速变化的节奏,我们在项目开发过程中采用了一些面向拓展以及敏捷开发的设计,本文旨在总结并思考其中一些通用的编程模式。 前言 静…

【Vue2+3入门到实战】(19)Vuex状态管理器通过辅助函数 - mapState获取 state中的数据代码实现 详细讲解

目录 一、通过辅助函数 - mapState获取 state中的数据1.第一步:导入mapState (mapState是vuex中的一个函数)2.第二步:采用数组形式引入state属性3.第三步:利用**展开运算符**将导出的状态映射给计算属性 二、开启严格模式及Vuex的单项数据流1…

2024年美赛数学建模ABCDEF题思路选题分析

文章目录 1 赛题思路2 美赛比赛日期和时间3 赛题类型4 美赛常见数模问题5 建模资料 1 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 2 美赛比赛日期和时间 比赛开始时间:北京时间2024年2月2日(周五&#xff…

MVCC 并发控制原理-源码解析(非常详细)

基础概念 并发事务带来的问题 1)脏读:一个事务读取到另一个事务更新但还未提交的数据,如果另一个事务出现回滚或者进一步更新,则会出现问题。 2)不可重复读:在一个事务中两次次读取同一个数据时&#xff0c…

Java实现短信发送业务

1、业务需求 发送短信功能是一个很普遍的需求,比如验证码,快递单号,通知信息一类。 而在Java中实现短信功能相对简单,只需要调用短信服务商提供的API。接下来以阿里云为例,介绍如何实现短信发送功能,其他短…

运算符的优先级(规矩是人定的)

运算符的优先级(规矩是人定的) 什么是经典?经典就是理论不随时间变迁而变化。《东方不败》中的很多台词让人时不时想起来振聋发聩。比如 很多事情不是自己想的那样,规矩是人定的。 舔狗和有思想 从小到大,我们都学过…

使用sdf文件+urdf文件模拟机器人示例(不用把urdf转sdf)

gazebo版本&#xff1a;harmonic&#xff1b; <launch> <group> <let name"robot_description" value"$(command xacro $(find-pkg-share gazebo_pkg)/urdf/total.xacro)"/> <node pkg"rviz2" exec"rviz2" name…

前端文件上传组件最全封装+删除+下载+预览

前言&#xff1a;使用的是若依的框架element uivue2封装的。如果有不对的地方欢迎指出。后台管理使用&#xff0c;文件需要上传。回显列表&#xff0c;详情也需要回显预览 // 开始封装组件&#xff1a;封装在 src/components/FileUpload/index.vue中 <template><div c…

如何使用Pyxamstore快速解析Xamarin AssemblyStore文件

关于Pyxamstore Pyxamstore是一款针对Xamarin AssemblyStore文件&#xff08;assemblies.blob&#xff09;的强大解析工具&#xff0c;该工具基于纯Python 2.7开发&#xff0c;支持从一个APK文件中解包并重封装assemblies.blob和assemblies.manifest Xamarin文件。 什么是ass…

YOLOv5算法进阶改进(10)— 更换主干网络之MobileViTv3 | 轻量化Backbone

前言:Hello大家好,我是小哥谈。MobileViTv3是一种改进的模型架构,用于图像分类任务。它是在MobileViTv1和MobileViTv2的基础上进行改进的,通过引入新的模块和优化网络结构来提高性能。本节课就给大家介绍一下如何在主干网络中引入MobileViTv3网络结构,希望大家学习之后能够…