当前位置: 首页 > news >正文

JAVAEE--5.多线程之常见的锁策略

常见的锁策略

        注意:接下来讲解的锁策略不仅仅是局限于JAVA,任何和"锁"相关的话题,都可能会涉及到以下内容,这些特性主要是给锁的实现者来参考的  

        乐观锁 VS 悲观锁(预测锁冲突的概率是否高)

       悲观锁:

        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会阻塞直到他拿到锁

        简而言之:        假设出现锁冲突的概率很大 => 接下来围绕加锁要做很多工作

               

        乐观锁:

        假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做

        简而言之:        假设出现锁冲突的概率很小

        Synchronized 初始使用乐观锁策略,同时会偷偷统计所冲突的次数,如果发现锁冲突的次数达到了一定程度,也就是当发现锁竞争比较频繁的时候,就会自动切换成悲观锁的策略

        重量级锁 VS 轻量级锁(效果和悲观乐观是重叠的,站在的角度不一样)

        重量级锁:

        加锁的开销比较大,要做更多的工作 [往往,悲观锁的时候,会做的重]

        轻量级锁:

        加锁的开销较小,要做更少的工作 [往往,乐观锁的时候,会做的轻]

        synchronized也是自适应的

      挂起等待锁 VS 自旋锁

        挂起等待锁

        挂起等待锁就是属于 悲观锁/重量级锁 的一种典型实现

        挂起等待锁相当于"让出了CPU资源",CPU就可以用来做别的事情了,过了一段时间之后,再来拿锁,适合"悲观锁"这样的场景,锁竞争十分激烈,预测拿到锁的概率不大,不妨把CPU让出来,充分的做其他的事情


自旋锁

        自旋锁就属于 乐观锁/轻量级锁 的一种典型实现

        自旋锁,忙等的过程中,不会释放cpu资源,不停的检测锁是否被释放,一旦锁被释放了就立即有机会能够获取到锁了

        

        synchronized 是自适应的

        轻量级锁就是基于自旋的方式实现的

        (JVM 内部,用户态代码实现的)

        重量级锁 就是基于 挂起等待的方式实现的

        (调用操作系统api,在内核中实现的)

        

      公平锁 VS 非公平锁        

        公平锁

        遵守"先来后到",B比C先来的,当A释放锁之后,B就能先于C获取到锁

        非公平锁

        不遵守"先到后到",B和C都有可能获取到锁

        synchronized属于非公平锁,当N个线程竞争同一把锁,其中一个线程先拿到了锁,后续该线程释放锁之后,剩下的N-1个线程就是要重新竞争,谁拿到锁就都不一定了

        如果需要使用公平锁,就需要做额外的操作(比如引入队列,记录每个线程加锁的顺序)

        可重入锁 VS 不可重入锁

        死锁问题

        如果一个线程,针对一把锁连续加锁两次,就可能出现死锁

        如果把锁生定位"可重入"(reentrant),就可以避免死锁了

        

        操作:

        1).记录当前是哪个线程持有了这把锁

        2).在加锁的时候判定,申请当前锁的线程,是否就是锁的持有者线程

        3).计数器,记录加锁的次数,从而确定合适真正的释放锁

        

        synchronized关键字锁都是可重入的

        读写锁

      

        所谓读写锁,把"加锁操作"分成两种情况

        读加锁                                   如果多个线程同时读这个变量,没有线程安全问题

        写加锁                                但是,一个线程读/一个线程写 或者两个线程都写就会产生这种问题

      

        读写锁,提供了两种加锁的api

        加读锁                                  如果两个线程都是按照读方法加锁,此时不会产生锁冲突               

        加写锁                                       如果两个线程都是加写锁,此时会产生锁冲突

        解锁的api是一样的                如果一个线程是读锁,一个线程是写锁,也会产生锁冲突

        读写锁 本身也是系统的内置的锁

        读写锁就是把读操作和写操作区分对待,Java标准库提供了ReentrantReadWriteLock类,实现了读写锁

        ReentrantReadWriteLock.ReadLock类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁

        ReentrantReadWriteLock.WriteLock类标识一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁

                (这两个放入finally中,确保能够执行到)

        synchronized并非是读写锁 

        读写锁特别适合于"频繁读,不频繁写"的场景中

synchronized的加锁过程(锁升级)

synchronized 

1)乐观悲观,自适应

2)重量轻量,自适应

3)自旋挂起等待,自适应

4)非公平锁

5)可重入锁

6)不是读写锁

当代码执行到synchronized 代码块中,jvm 大概做哪些事情

锁升级的过程

刚开始使用synchronized加锁,首先锁会处于"偏向锁"的状态 

当遇到线程之间的锁竞争,升级到"轻量级锁"

进一步的统计竞争出现的频次,达到一定程度之后,升级到"重量级锁"

synchronized 加锁的时候,会经历  无锁 => 偏向锁  => 轻量级锁 => 重量级锁

偏向锁

        偏向锁不是真的加锁(真的加锁,开销可能会比较大),偏向锁只是做了标记(标记的过程,非常的轻量高效)

偏向锁  => 轻量级别      :出现竞争

轻量级锁  => 重量级锁   :竞争激烈

        上述锁升级的过程,主要是为了能够让synchronized这个锁很好的适应不同的场景,就可以降低程序员的负担

        对于当前JVM的实现来说,上述锁升级的过程,属于"不可逆"

锁消除(编译器的优化策略)

        编译器会对你写的synchronized代码做出判定,判定这个地方是否确实需要加锁

        如果这里没有必要加锁的,就能够自动把synchronized干掉

        例如:Vector,StringBuffer 自带synchronized

        

        锁消除虽然存在,但是写代码的时候,不能无脑加锁

锁粗化(编译器的优化策略)

锁的粒度:粗和细(和代码量有关,并且是实际执行的)

代码越少,粒度就越细,代码越多,粒度就越大

一段代码中,有多次加锁解锁操作,编译器和JVM会自动进行锁的粗化

细粒度代码每一次加锁都会有可能涉及到阻塞等待

频繁的申请释放锁可能会影响到性能,每一次申请锁都有一定的开销的

锁的粗化就是把多个细粒度的锁变成一个粗粒度的锁

CAS(Compare and Swap)

 比较内存中的数据和cpu寄存器中的数据,如果相同,就交换(内存和另一个寄存器中的数据)

        一个内存的数据,和两个寄存器中的数据进行操作(寄存器1和寄存器2)

        比较内存中的值和寄存器1中的值是否相等

        如果相等,则交换内存的数据和寄存器2的数据

        如果不相等就不交换,返回false

         以下代码为伪代码,仅为了方便理解

        

        此处的代码并

不是原子的,真是的CAS是一个原子的硬件指令完成的

        当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他的线程,其他的线程只会收到操作失败的信号

        CAS可以理解为是乐观锁的一种实现方式

        CAS具体的使用场景

        1)基于CAS实现"原子类"

        int/long 在进行++操作的时候,并不是原子的

        基于CAS实现原子类,对int/long等这些类型进行封装,从而可以原子的完成++,--等操作

        原子类在 Java 标准库中,也有现成的实现

        

    public static void main(String[] args) throws InterruptedException {AtomicInteger atomicInteger =new AtomicInteger(0);Thread thread1=new Thread(()->{for(int i=0;i<5000;i++){atomicInteger.incrementAndGet();}});Thread thread2=new Thread(()->{for(int i=0;i<5000;i++){atomicInteger.incrementAndGet();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(atomicInteger.get());}

        

结果

incrementAndGet()=++i;

getAndIncrement()=i++;

decrementAndGet()=--i;

getAndDecrement()=i--;

        

源码中的native修饰的方法是"本地方法",这个方法的实现是在JVM内部,通过C++代码实现的

Unsafe不太安全(这里的代码偏底层的代码,需要有一定的操作系统和硬件的理解才能正确的使用这里的代码,一般不建议直接使用Unsafe)

原子类的伪代码如下:

    private  int value;public int getAndIncrement(){int oldValue =value;while(CAS(value,oldValue,oldValue+1)!=true){oldValue=value;}return oldValue;

循环的判断条件里对比value和oldValue是否相同,相同就讲oldValue+1的值给value

也意味着没有线程穿插到两个代码之间,此时就可以安全的修改变量的内容

如果不相同意味着上方的赋值和此处的CAS之间有其他的线程穿插执行,其他线程修改了Value的值

这个循环条件能感知到是否有其它线程进行修改,如果有修改则CAS返回false,再次循环

2)实现自旋锁

        基于CAS实现更灵活的锁(自旋锁)

        

    public class  SpinLock{private Thread Oner=null;public void Lock(){//查看锁是否被线程持有,如果锁被别的线程持有就自旋等待,如果没有被别线程持有,则尝试加锁while(!CAS(this.Oner , null ,Thread.currentThread())){//被别的线程持有,则自旋等待}}public void unlock(){this.Oner=null;}}

CAS存在的问题---ABA问题

ABA问题是什么 ?

例如:

        CAS(value,oldValue,swapValue)

        如果value的值和oldValue值一样,则,将swapValue的值赋给value

        但是这期间,value的值也许会发生变化,线程无法区分这个值是否始终是value或者是经历了一个变化过程

        通常不会发生异常,但是不能排除一些的特殊情况

异常的过程:银行存款有100,有两个线程A,B都读取到了100,并且都计划将100改为50,线程A先改了,在B执行前,有人给存款又打了50,所以B读取到的余额是100,所以B又执行了一次扣款操作

        

        解决方案:要给修改的值引入版本号(或者时间戳),读取数值是否相同的时候,也要读取版本号是否相同,如果版本号也相同,此时真正的执行修改操作,并且版本号+1,如果当前版本高于获取到的版本号,就操作失败(认为数据已经被修改过了)

http://www.dtcms.com/a/313412.html

相关文章:

  • AI Competitor Intelligence Agent Team
  • 【openlayers框架学习】七:绘制线要素以及点击画线功能
  • 力扣热题100----------141.环形链表
  • 基于BiLSTM+CRF实现NER
  • 【机器人】VLN-R1 微调 | 增强训练 | 连续导航
  • Web3合约ABI,合约地址生成部署调用及创建,连接钱包,基础交易流程
  • ARPO:让LLM智能体更高效探索
  • 【Linux网络编程基础--socket地址API】
  • 多 4G 通讯模组共存时的干扰问题深度解析与解决方案
  • leecode-每日一题-2106. 摘水果
  • vmfusion启动centos6.10 一直卡到call 169.254.169.254
  • 全面解析 BGE Embedding 模型:训练方式、模型系列与实战用法
  • Redis——常用指令汇总指南(三)(哈希类型)
  • 编写xsync集群分发脚本(保姆级别)
  • Redis 数据同步机制
  • 【Linux】Makefile Cmake—基操
  • [特殊字符]字节Get!免费进楼攻略速存[特殊字符]
  • LWIP从FreeRTOS到uC/OS-III的适配性改动
  • linux 扩展未分配的磁盘空间到home下
  • SQL157 更新记录(一)
  • 代码随想录算法训练营第五十八天|动态规划part8
  • 成功解决ImportError: DLL load failed while importing _multiarray_umath: 找不到指定的模块。
  • 深度学习中的模型知识蒸馏
  • 深度学习中卷积与互相关
  • 记录使用ruoyi-flowable开发部署中出现的问题以及解决方法
  • FastAPI-Vue3-Admin 一款Python 全栈融合的高可用中后台快速开发平台方案
  • golang 函数选项模式
  • 数据结构(概念及链表)
  • 【数据结构】队列的顺序存储与链式存储(C语言版)
  • 基于深度学习的医学图像分析:使用变分自编码器(VAE)实现医学图像生成