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

多线程—锁策略

上篇文章:

多线程—应用案例https://blog.csdn.net/sniper_fandc/article/details/146456564?fromshare=blogdetail&sharetype=blogdetail&sharerId=146456564&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

目录

1 乐观锁VS悲观锁

2 互斥锁VS读写锁

3 重量级锁VS轻量级锁

4 自旋锁VS挂起等待锁

5 公平锁VS非公平锁

6 可重入锁VS不可重入锁

7 CAS

7.1 CAS的概念与理解

7.2 CAS应用

7.2.1 实现原子类

7.2.2 实现自旋锁

7.3 CAS的ABA问题


1 乐观锁VS悲观锁

        乐观锁:认为发生并发冲突的概率不大,只有提交数据修改时才会检测是否发生并发冲突,发生并发就返回错误信息给用户处理。往往是纯用户态执行。

        悲观锁:认为发生并发冲突的概率很大,每次取数据时都会加锁。往往是需要进入内核态对当前线程挂起等待。

        synchronized是一个自适应锁,即初始是乐观锁策略,当锁竞争激烈时就切换为悲观锁。

2 互斥锁VS读写锁

        互斥锁:任意两个加锁操作互斥。

        读写锁:细化加锁操作,分为加读锁和加写锁。读锁与读锁不互斥(线程安全),读锁与写锁互斥(线程不安全),写锁与写锁互斥(线程不安全)。读写锁常用于“频繁读,不频繁写”的场景。

        Java 标准库提供了ReentrantReadWriteLock类实现了读写锁,使用lock/unlock方法进行加锁/解锁。ReentrantReadWriteLock.ReadLoc类表示读锁,ReentrantReadWriteLock.WriteLock类表示一个写锁。

        synchronized是互斥锁,不是读写锁

3 重量级锁VS轻量级锁

        从CPU到Java代码,各级向上提供的API是不同的,而ReentrantLock和synchronized是对操作系统API的封装,并在内部做了许多其他工作:

        重量级锁:加锁使用mutex互斥锁,涉及到用户态和内核态切换,容易引起线程调度,开销大。

        轻量级锁:加锁尽可能不使用mutex互斥锁,尽量在用户态完成加锁工作,开销小。

synchronized是一个自适应锁,即初始是轻量级锁策略,当锁竞争激烈时就切换为重量级锁。

4 自旋锁VS挂起等待锁

        这组锁是轻量级锁和重量级锁的具体实现。

        自旋锁:当线程获取锁失败,不会被阻塞,会立即再次尝试获取锁,直到获取锁成功。因此自旋锁更轻量(尽力避免线程调度,在用户态完成工作),自旋锁是轻量级锁,也是乐观锁。但是缺点就是如果锁一直不释放,就会消耗大量CPU资源。伪代码:

while (抢锁(lock) == 失败) {}

        挂起等待锁:当前程获取锁是被,会被挂起等待,放弃CPU资源。因此挂起等待锁更重量(挂起等待涉及到线程调度,用户态和内核态的切换),挂起等待锁是重量级锁,也是悲观锁。但是缺点就是锁被释放时,线程无法第一时间获取到锁。

        synchronized采用轻量级锁策略时,内部是自旋锁的实现;采用重量级锁策略时,内部是挂起等待锁的实现。

5 公平锁VS非公平锁

        公平锁:符合“先来后到”原则,即线程A持有锁,B比C先到,A释放锁后B会先得到锁。

        非公平锁:符合“机会平等”原则,即线程A释放锁后,无论BC谁先到,都会一起竞争锁。

        操作系统内部的挂起等待锁就是非公平锁,要想实现公平锁,必须使用额外的数据结构来实现(比如队列),synchronized也是非公平锁。

6 可重入锁VS不可重入锁

        可重入锁一个线程可以重复对同一个锁对象加锁。比如某一个线程先进入一段synchronized 代码块对一个锁对象加锁,但是在这个代码块中还需要对该锁对象加锁,按照互斥的特性,这个锁对象未被释放时线程无法再次加锁,出现线程阻塞等待自己未释放的锁(自己等自己,死锁),对应代码如下:

class Counter {

    public int count = 0;

    synchronized void increase1() {

        count++;

   }

    synchronized void increase2() {

        increase1();

   }

}

        这段代码可以正常运行,因为synchronized是可重入锁。在可重入锁内部包含“线程持有者”和“计数器”两个属性,线程持有者指明锁持有的线程,同一个线程重复加锁时,判断持有的线程还是自己,就同意加锁,计数器+1;计数器的值减为0时,锁才被释放。Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类。

        不可重入锁:一个线程不能重复对同一个锁对象加锁。Linux提供的mutex互斥锁是不可重入锁。

7 CAS

7.1 CAS的概念与理解

        CAS是CPU提供的原子性的硬件指令,全称是Compare And Swap,比较并交换,可以

        具体而言:假设某个变量存在寄存器A、寄存器B和内存共3个版本,B存的是最新修改后的值(newValue),A存的是旧值(oldValue),内存存的是value,CAS(value,oldValue,newValue)要做的事是比较value与oldValue,如果相等,就把newValue写入value的值;如果不相等,就什么都不做。

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

        如何理解CAS:当value与oldValue相等,说明在此时多线程环境下,内存数据和寄存器的副本保持了数据一致性,此时没有线程安全问题,直接执行写操作即可。而当value与oldValue不相等,说明此时寄存器的副本和内存数据没有保存数据一致性,发生了多个线程同时读写数据的情况,且该线程的寄存器中的数据已经不是最新的了,此时如果再执行就会导致线程不安全,因此就不执行写操作(也就是什么都不做)。因此CAS也是乐观锁的一种实现方式。

7.2 CAS应用

7.2.1 实现原子类

        在java.util.concurrent.atomic包中的类都是原子类,基于CAS实现,比如AtomicInteger类,使用AtomicInteger类代替Integer修饰变量,就会保证读写操作的原子性,进而避免使用重量级锁(通过阻塞来引起大开销的操作)。

        AtomicInteger有getAndIncrement()和incrementAndGet()方法,分别对应i++和++i操作,但是i++和++i操作不是原子性的,而getAndIncrement()和incrementAndGet()方法使用CAS机制保证了原子性。内部实现的伪代码:

public int getAndIncrement() {

        int oldValue = value;

        while ( CAS(value, oldValue, oldValue+1) != true) {

            oldValue = value;

       }

        return oldValue;

   }

        举一个例子理解CAS保证i++操作原子性的例子,假设变量i初始值为0,两个线程并发执行一次对i的自增操作,执行序列依次是线程1先执行int oldValue = value(操作1),线程2再执行int oldValue = value(操作2),然后线程1执行while(操作3),最后线程2执行while(操作4),流程如下:

        操作1和操作2两个线程都会读取value=0的值到自己的工作内存(即oldValue=0);操作3线程1判断value和oldValue相同,因此执行交换,value=oldValue+1,同时退出循环,此时value=1;操作4线程2判断value和oldValue不相同,因此不做任何操作,此时while条件成立,执行循环体一次,因此执行oldValue = value,此时线程2的oldValue值和value相同,再次执行条件判断CAS,会执行交换value=oldValue+1,因此value=2。即两次多线程并发的自增操作并没有引起线程不安全问题。

7.2.2 实现自旋锁

public class SpinLock {

    private Thread owner = null;

    public void lock(){

        while(!CAS(this.owner, null, Thread.currentThread())){

       }

   }

    public void unlock (){

        this.owner = null;

   }

}

        自旋锁使用CAS实现和原子类类似,owner表示当前锁的持有线程是谁,null表示锁没有任何线程持有。通过 CAS 指令判断当前锁是否被某个线程持有,如果这个锁已经被别的线程持有(this.owner==null),那么就继续一次循环。如果这个锁没有被别的线程持有,那么就把owner设为当前尝试加锁的线程(owner=Thread.currentThread())。

7.3 CAS的ABA问题

        CAS的ABA问题是指:多线程环境下,一个线程读取到变量的值为A,在还未执行修改操作前,由于并发执行的CPU调度,导致其他线程修改变量的值由A变为B再变回A,导致原线程无法判断这个变量的值一直是A还是经过变化后是A,从而容易引发一些问题。

        比如银行取钱:用户希望取100,但是不小心连续按了两个取100的按键,启动两个线程并发执行取钱程序,第一个线程先成功取100,第二个线程后判断余额比原来读取的少了100,因此不执行交换操作,也就是取钱失败,最终账户只扣了100。这属于正常情况,符合用户期望。但是如果在第二个线程比较前有其他线程进行了转账业务,转入100,此时第二个线程比较余额就会和原来读取的一样,因此就会执行交换操作,余额又扣了100,因此这就引发了bug。

        如何解决:引入版本号,每一个数据都有对应的版本号,每一次修改版本号只会增加,CAS进行比较时如果发现数据一致,就比较版本号,只有版本号和数据都一样是才进行交换操作,否则就不进行任何操作(操作失败)。

        Java提供AtomicStampedReference<E>类来包装某个类,进行版本管理工作。

下篇文章:

相关文章:

  • 去中心化金融
  • 漏洞挖掘---锐明Crocus系统Service.do接口任意文件读取
  • 《数字图像处理》第三章 3.8 基于模糊技术的图像强度变换与空间滤波学习笔记
  • 微积分小白入门:第二章 数列与极限——从困惑到顿悟的奇妙之旅
  • Vue 3中的Provide与Inject
  • stm32f103c8t6使用pwm(DMA)驱动24个ws2812b灯驱动
  • 【探寻C++之旅】第十章:map和set(STL续)
  • 看 MySQL InnoDB 和 BoltDB 的事务实现
  • 3.Excel:快速分析
  • 深入理解现代C++在IT行业中的核心地位与应用实践
  • 在 Windows 上安装 PowerShell 的多种方法与完整指南
  • 【设计模式】策略模式(Strategy Pattern)详解
  • 群体智能优化算法-沙丁鱼群优化算法(Salp Swarm Algorithm (SSA,含Matlab源代码)
  • C# Modbus TCP/IP学习记录
  • 【Unity网络编程知识】使用Socket实现简单UDP通讯
  • 算法 之 矩阵的对角线问题
  • Spring AI Alibaba 工具(Function Calling)使用
  • 2025.3.25总结
  • Java动态代理的使用和安全问题
  • WPS二次开发系列:以自动播放模式打开PPT文档
  • 简要说明网站建设的步骤/公司网站建设哪个好
  • asp网站首页模板/抖音seo推广
  • doku做网站/安卓手机优化软件哪个好
  • 沈阳大型网站制作公司/手游推广个人合作平台
  • 男人女人做羞羞事网站/网络营销师怎么考
  • 汕头手机模板建站/semseo