开发者社区> bug郭> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

java多线程常见锁策略CAS机制(1)

简介: java多线程常见锁策略CAS机制(1)
+关注继续查看

本节要点

了解常见锁策略

了解synchronized使用的锁策略

理解CAS实现逻辑

了解CAS出现的ABA问题,并解决

synchronized锁的原理

常见锁策略

我们已经知道锁在我们的并发编程十分重要.那我们就需要了解,这些锁实现的策略!都有那些策略,便于我们更加深刻的理解锁!


下面介绍的几组锁策略,每一组里面都是相异的,每组策略之间又有相互关联的!


乐观锁 vs 悲观锁

这是程序员处理锁冲突的态度(原因),通过自己的预期而实现咋样的锁

就好比疫情:

乐观的人觉得过段时间就好了,就不会囤太多物资

悲观的人觉得紧张,就屯好多物资,做了好多工作


乐观锁


程序员在设计锁的时候,预期锁冲突概率很低


做的工作更少,付出的成本低,更高效


悲观锁


预期锁冲突概率很高


做的工作多,付出的成本高,更低效


互斥锁vs读写锁

互斥锁


只有多个线程对同一个对象加锁才会导致互斥


互斥锁就是普通的锁,只有加锁和解锁


读写锁


可以对读操作加锁(不存在互斥关系,可以多线程读)

对写操作加锁(只能进行写操作)

读写操作加锁(读时不能写,写时不能读)


轻量级锁vs重量级锁

轻量级锁


做的事情更少,开销比较小


重量级锁


做的事情更多,开销比较大


这里的轻量级锁和重量级锁和上面的悲观锁和乐观锁有所重叠

一个是设计锁的态度(原因),一个是处理锁冲突的结果!

通常情况下一般可以认为乐观锁一般都是轻量级锁,悲观锁都是重量级锁!但是不绝对!!!


是如何实现轻量和重量呢?

其实我们基于纯用户态实现的锁就是认为是轻量级锁,开销小,程序员可控!

如果是基于内核的一些功能实现(比如调用了操作系统内核的mutex接口)的锁就认为是重量级锁(操作系统的锁会在内核做好多事情,比如让线程等待…)


自旋锁vs挂起等待锁

这是上述轻量级锁和重量级锁的典型实现


自旋锁


往往通过纯用户态代码实现,较轻


挂起等待锁


通过内核的一些机制实现,往往较重


公平锁vs非公平锁

公平锁


多个线程等待一把锁时,遵循先来后到原则


非公平锁


多个线程等待一把锁时,每个线程拿到锁的机会均等!


这里就有人有疑惑了,咋的,机会均等还不公平了?

但是你换个场景想想,如果你在等待办理业务,先来不就应该先办业务嘛,就好比排队,你先来排在前面!所以这才公平嘛!!!


可重入锁vs不可重入锁

可重入锁


可重入锁就是对一个对象多次加锁时不会造成死锁


不可重入锁


一个对象多次加锁时会造成死锁!


synchronized使用的锁策略

我们了解了上述的多组锁策略,我们来分析一下,synchronized用了那些锁策略!


自适应锁,即是乐观锁又是悲观锁

不是读写锁只是普通的互斥锁

既是一个轻量级锁又是重量级锁(根据锁竞争程度自适应)

轻量级锁的部分基于自旋锁实现,重量级锁基于挂起等待宿实现

非公平锁(锁拿到的机会均等)

可重入锁(加锁多次,不会导致死锁)

CAS

什么是cas?


CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 – 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。


可能有点抽象,我们看下面案例!


我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。


比较 A 与 V 是否相等。(比较)

如果比较相等,将 B 写入 V。(交换)

返回操作是否成功。

image.png

可能看到这里你还是很懵!


boolean CAS(address, expectValue, swapValue) {
  if (&address == expectedValue) {
        &address = swapValue;
     return true;
    }
  return false;
}

我们看这个伪代码!

image.png

通俗点讲,就是CAS解决了多线程中多条指令进行的赋值问题!

我们之前已经了解过当我们需要对一个值进行++在cpu中其实要执行3条指令!

先拿到值放在寄存器,将寄存器中的值更改,然后在放回内存!

image.png

而我们知道在多线程执行写操作时,就会导致线程不安全问题!

因为++操作并不是原子性的!


而这里的CAS做的就是将多条指令封装成一条指令,达到原子性的效果!避免线程不安全问题!


我们的cpu提供了一个单独的指令cas来执行上诉代码!!!


CAS使用

CAS可以做什么呢?


基于CAS能够实现"原子类"

java标准库中给我们提供了一组原子类,就是将常用类(int long array …)进行了封装,可以基于CAS进行修改,并且线程安全!

//基于CAS多线程对一个数实现自加
import java.util.concurrent.atomic.AtomicInteger;
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        //原子类
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000 ; i++) {
                //这个方法相当于 ++num
                atomicInteger.incrementAndGet();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000 ; i++) {
                //这个方法相当于 ++num
                atomicInteger.incrementAndGet();
            }
        });
        t1.start();//启动线程
        t2.start();
        t1.join();
        t2.join();//等待2个线程执行结束
        System.out.println("多线程自加结果:"+atomicInteger.get());
    }
}

image.png

可以看到我们基于CAS多线程进行一个数的更改并不用加锁也能保证线程安全!!!


我们来学习一下java原子类中的一些方法!


原子类在java.util.concurrent.atomic包下!

image.png

构造方法,可以给初值!

image.png

实现+=操作

image.png

自加自减!

image.png

我们来看一下这里自加的实现逻辑!


//伪代码
class AtomicInteger {
    private int value;
    public int getAndIncrement() {
    int oldValue = value;
    while ( CAS(value, oldValue, oldValue+1) != true) {
     oldValue = value;
    }
    return oldValue;
    }
}

我们分析上述伪代码!

image.png


image.png


基于CAS能够实现"自旋锁"

//伪代码
public class SpinLock {
    private Thread owner = null;
    public void lock(){
  // 通过 CAS 看当前锁是否被某个线程持有.
  // 如果这个锁已经被别的线程持有, 那么就自旋等待.
  // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
  while(!CAS(this.owner, null, Thread.currentThread())){
  }
    }
    public void unlock (){
  this.owner = null;
    }
}

image.png

可以看到自旋锁的实现和原子类的实现类似!

我们来分析一下!


如果已经有线程持有了该锁对象,那么while循环就会一直自旋,直到该锁被释放,该线程才可以拿到该锁!


if(this.owner==null) 为真,Thread.currentThread当前线程就可以拿到该锁!否者自旋等待锁!


CAS的ABA面试问题

我们知道CAS的实现是通过对比当前CPU中的值和内存中的值是否相等,如果相等就采取计然后进行交换!


我们是否想过另外一种情况,就是该内存中的值改变了多次,又改回了原来那个值,显然这时内存中的值虽然和cpu中值相等,但是该值已经进行了多次改变,并没有保证此次CAS的原子性!


举个例子:

当有两个线程t1 和t2 这两个对象对同一块内存空间采取CAS修改操作!


我们已经知道CAS的原子性,t1和t2都能执行完成!

image.png

而如果这时有第3个线程在t1的load和t2的CAS之间将该值又更改回去,那么就出现bug了


我们假设一种现实场景:


某一天你去ATM取款

你的余额为1000元

然后你取款500元

你不小心多点了一次取款,但是你没有察觉到!

然后第一次你取款成功了,正常情况你第二次肯定无法取款成功!

因为我们知道CAS会比较寄存器和内存中的值!而此时的余额已经不是1000了,该CAS指令就无法成功执行!

但是如果在你执行第二个取款操作之前,你的朋友刚好给你转账500元!这样CAS在比较时发现相等就会再次执行取款操作,你取500居然取出了1000,余额还有500,这就是一个BUG!


我们用图来描述一下上述情况!

image.png



我们如何解决这个问题呢?


我们可以引入一个版本号

记录每次更改内存的次数,如果更改一次,版本号就加1,且版本号只能递增!!!

进行比较时只需要比较版本号即可,如果版本号相等就可以进行交换!


我们再进行上述的CAS就不会产生bug了!

image.png


当我们引入版本号时,每次只要比较版本号的值是否相等就可以判断内存中的值是否已经修改过,很好的解决了CAS中的ABA问题!


我们也可以用时间戳代替版本号,达到的效果一样,也可解决ABA问题!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
JAVA 多线程——实现创建线程的五种写法
JAVA 多线程——实现创建线程的五种写法
12 0
Java多线程(三)、线程的通信、wait(),notify(),notifyAll()、生产者/消费者问题、创建线程的方式三:实现Callable接口、创建线程的方式四:使用线程池
Java多线程(三)、线程的通信、wait(),notify(),notifyAll()、生产者/消费者问题、创建线程的方式三:实现Callable接口、创建线程的方式四:使用线程池
24 0
数据结构与算法 | 用Java语言实现顺序表真的不难
数据结构与算法 | 用Java语言实现顺序表真的不难
30 0
JAVA一个接口多个实现逐个调用
JAVA一个接口多个实现逐个调用
11 0
教你用纯Java实现一个即时通讯系统(附源码)
教你用纯Java实现一个即时通讯系统(附源码)
56 0
Java:SpringBoot集成JWT实现token验证
Java:SpringBoot集成JWT实现token验证
29 0
Java:CGLib动态代理实现原始类的扩展
Java:CGLib动态代理实现原始类的扩展
10 0
Java:java-jwt实现JsonWebToken
Java:java-jwt实现JsonWebToken
14 0
Java:Springboot整合PageHelper实现分页
Java:Springboot整合PageHelper实现分页
17 0
计算一组数字中最小的若干个数字(Java数组和栈实现)
本文目录 1. 问题 2. 思路 2.1 思路1 2.2 思路2 2.3 思路3 3. 实现
44 0
+关注
bug郭
卷java中!
82
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载