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

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问题!

目录
相关文章
|
3天前
|
存储 缓存 安全
深度剖析Java HashMap:源码分析、线程安全与最佳实践
深度剖析Java HashMap:源码分析、线程安全与最佳实践
|
4天前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
13 0
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
|
3天前
|
算法 安全 Java
深入解析Java多线程:源码级别的分析与实践
深入解析Java多线程:源码级别的分析与实践
|
4天前
|
Java 程序员 调度
深入浅出Java多线程编程
Java作为一门成熟的编程语言,在多线程编程方面提供了丰富的支持。本文将通过浅显易懂的语言和实例,带领读者了解Java多线程的基本概念、创建方法以及常见同步工具的使用,旨在帮助初学者快速入门并掌握Java多线程编程的基础知识。
4 0
|
4天前
|
Java
java中获取当前执行线程的名称
这篇文章介绍了两种在Java中获取当前执行线程名称的方法:使用`Thread`类的`getName`方法直接获取本线程的名称,以及使用`Thread.currentThread()`方法获取当前执行对象的引用再调用`getName`方法。
|
3月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
64 2
|
3月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
39 1
|
10天前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
47 6
|
10天前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
51 5
|
10天前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
42 3