1.CAS是什么(CompareAndSet)

  CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。

2.CAS的使用场景(原子类)

  我们先看看下面的例子

package com.atguigu.springcloud.test;

import org.omg.CORBA.Current;

/**
* @Classname Demo
* @Description TODO
* @Date 2021/4/25 0025 下午 3:25
* @Created by jcc
*/
public class Demo { int i = 0; public static void main(String[] args) {
Demo d = new Demo();
new Thread(()->{
while (d.i < 100){
int j = d.i;
System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i);
} },"aa").start();; new Thread(()->{
while (d.i < 100){
int j = d.i;
System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i);
}
},"bb").start(); }
}

//一次的输出结果
bb-j-0-i-1
bb-j-2-i-3
aa-j-0-i-2
aa-j-4-i-5
aa-j-5-i-6
aa-j-6-i-7
aa-j-7-i-8
........

我们期望输出的结果i和j相差为1

看这个的输出结果的第三行 aa-j-0-i-2    输出的j的值是0,i的值是2

说明aa线程最开始获取到的i的值是0,而在++i的操作时,i已经被bb线程变为1了,所以++i的输出结果是2

如果我们想要aa线程和bb线程的j的值和++i的值必须是相差为1,也就是说,一个线程在对i进行操作的过程中不能被另外一个线程干扰

方法1,加锁 

package com.atguigu.springcloud.test;

import org.omg.CORBA.Current;

/**
* @Classname Demo
* @Description TODO
* @Date 2021/4/25 0025 下午 3:25
* @Created by jcc
*/
public class Demo { int i = 0; public static void main(String[] args) {
Demo d = new Demo(); new Thread(()->{
synchronized (Demo.class){
while (d.i < 100){
int j = d.i;
System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i);
}
} },"aa").start();; new Thread(()->{
synchronized (Demo.class){
while (d.i < 100){
int j = d.i;
System.out.println(Thread.currentThread().getName() + "-j-" + j + "-i-" + ++d.i);
}
} },"bb").start(); } }

方法2,使用原子类

package com.atguigu.springcloud.test;

import org.omg.CORBA.Current;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; /**
* @Classname Demo
* @Description TODO
* @Date 2021/4/25 0025 下午 3:25
* @Created by jcc
*/
public class Demo1 { AtomicInteger in = new AtomicInteger(); //默认值是0 public static void main(String[] args) {
Demo1 d = new Demo1();
new Thread(()->{
while (d.in.get() < 100){
int j = d.in.get();
int next = j + 1;
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!d.in.compareAndSet(j,next)){
System.out.println(Thread.currentThread().getName() + "-false -j-" + j + "-i-" + d.in.get());
j = d.in.get();
next = j + 1;
}
System.out.println(Thread.currentThread().getName() + "true -j-" + j + "-i-" + d.in.get());
}
},"aa").start();; new Thread(()->{ while (d.in.get() < 100){ int j = d.in.get();
int next = j + 1;
try {
TimeUnit.MILLISECONDS.sleep(110);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!d.in.compareAndSet(j,next)){
System.out.println(Thread.currentThread().getName() + "-false -j-" + j + "-i-" + d.in.get());
j = d.in.get();
next = j + 1;
}
System.out.println(Thread.currentThread().getName() + "-true -j-" + j + "-i-" + d.in.get());
} },"bb").start(); } }
这里面最关键的代码 

 while (!d.in.compareAndSet(j,next)){
System.out.println("false -j-" + j + "-i-" + d.in.get());
j = d.in.get();
next = j + 1;
}
 

  compareAndSet这个方法的意思是 把j的值和主线程中in的值进行对比,如果一致,则in的值变为next返回true,否则不进行的操作,返回false,这里就是用到了CAS。而外面加上while,则是采用了自旋锁的思想。当j的值和in在主内存中的值不一致时,重新把j的值赋值为主内存中in的值,再调用compareAndSet方法,知道j和in的值一致

  看下面部分输出结果

aatrue  -j-0-i-1
bb-false  -j-0-i-1
bb-true  -j-1-i-2
aa-false  -j-1-i-2
aatrue  -j-2-i-3
......

  看着可输出结果

      aatrue  -j-0-i-1        aa线程获取in的值是0赋值给j,compareAndSet比较j和in的值一样,可以,把next的值赋值给in

      bb-false  -j-0-i-1      aa线程获取in的值是0赋值给j,compareAndSet比较j和in的值不一样,此时in已经变为1了,所以不把next的值赋值给in。

      bb-true  -j-1-i-2       把in的值1重新赋值给j,i+1赋值给next,再去调用compareAndSet,此时j=1,in的值也为1,可以,把next的值赋值给in

3.关于compareAndSet方法详解

3.1 源码

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
} private volatile int value; //AtomicInteger 的变量value被volatile修饰,所有线程可见 /**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}

3.2 compareAndSwapInt

  在compareAndSet方法中,调用的是unsafe类的compareAndSwapInt(this, valueOffset, expect, update)方法

    参数1:this,要操作的对象

    参数2:valueOffset,要操作的对象属性地址的偏移量,因为Unsafe就是根据内存偏移地址获取数据的

    参数3:expect期待值

    参数4:expect,要修改的新值

  compareAndSwapInt方法是底层的方法,就是用来比较内存中的值和期望的值是否一致,如果一致,把update的值赋值给它,返回true,否则不赋值,返回false

4 Unsafe类

4.1 简介

  Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

  调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

    

5 自旋锁

  尝试获取锁的线程不会阻塞,而是采用循环的方式去尝试获取锁, 好处是减少上下文的切换时间,坏处是循环会占用CPU

   下面是一个自旋锁的实现

package com.atguigu.springcloud.test;

import org.omg.CORBA.Current;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; /**
* @Classname ZiXuanSuoDemo
* @Description TODO
* @Date 2021/4/25 0025 下午 3:02
* @Created by jcc
*/
public class ZiXuanSuoDemo { //自旋锁:尝试获取锁的线程不会阻塞,而是采用循环的方式去尝试获取锁,
// 好处是减少上下文的切换时间,坏处是循环会占用CPU //1.原子类-Thread
AtomicReference<Thread> atomicReference = new AtomicReference<>(); void myLock(){
Thread thread = Thread.currentThread();
while (!atomicReference.compareAndSet(null,thread)){ }
System.out.println(thread.getName() + "成功获取锁");
} void unLock(){
Thread thread = Thread.currentThread();
atomicReference.set(null);
System.out.println(thread.getName() + "成功释放锁");
} public static void main(String[] args) {
ZiXuanSuoDemo de = new ZiXuanSuoDemo();
new Thread(()->{
System.out.println("AA进来了");
de.myLock();//去获取锁
//成功获取锁后,执行下面的操作
try {
System.out.println("Aa执行操作");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
de.unLock();
},"Aa").start(); new Thread(()->{
System.out.println("Bb进来了");
de.myLock(); //去获取锁
//成功获取锁后,执行下面的操作
try {
System.out.println("Bb执行操作");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
de.unLock();
},"Bb").start(); } }

6 CAS中的A-B-A问题

6.1 简介

    CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
  比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,
  然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
       尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

6.2 解决方案-版本号 

  AtomicStampedReference有两个参数:值和版本号

  compareAndSet方法传入四个参数,期待值,新值,期待版本号,新版本号,期待值和期待版本号都对才会更新新值和新版本号

public class LockTest7 {

   static AtomicStampedReference<Integer> at = new AtomicStampedReference(100,1);

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } int stamp = at.getStamp();
at.compareAndSet(100,101,stamp,stamp + 1); int stamp2 = at.getStamp();
at.compareAndSet(101,100,stamp2,stamp2 + 1); }).start(); new Thread(()->{
int stamp = at.getStamp();
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
boolean b = at.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("操作是否成功" + b);
}).start(); }
}

执行结果

操作是否成功false

7 CAS中的线程安全是依靠什么解决的

从这个流程上来看是存在一个问题的:当当前线程判断值相等进去准备赋值的时候,这个值整好被其它线程改变了,那么还是存在问题。

所以需要保证这两个操作的原子性,而原子类也解决了这个问题

 

public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

发现它是native方法,是在c++里面实现判断和赋值操作的原子性的。

它是通过下面这条指令实现的

线程基础知识11-CAS+自旋锁的更多相关文章

  1. java线程基础知识----线程与锁

    我们上一章已经谈到java线程的基础知识,我们学习了Thread的基础知识,今天我们开始学习java线程和锁. 1. 首先我们应该了解一下Object类的一些性质以其方法,首先我们知道Object类的 ...

  2. 我们常说的 CAS 自旋锁是什么

    CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作. 它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值, ...

  3. 并发编程--CAS自旋锁

    在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系 ...

  4. Java__线程---基础知识全面实战---坦克大战系列为例

    今天想将自己去年自己编写的坦克大战的代码与大家分享一下,主要面向学习过java但对java运用并不是很熟悉的同学,该编程代码基本上涉及了java基础知识的各个方面,大家可以通过练习该程序对自己的jav ...

  5. java线程基础知识----线程基础知识

    不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...

  6. 线程基础知识01-Thread类,Runnable接口

    常见面试题:创建一个线程的常用方法有哪些?Thread创建线程和Runnable创建线程有什么区别? 答案通常集中在,继承类和实现接口的差别上面: 如果深入问一些问题:1.要执行的任务写在run()方 ...

  7. Java线程基础知识(状态、共享与协作)

    1.基础概念 CPU核心数和线程数的关系 核心数:线程数=1:1 ;使用了超线程技术后---> 1:2 CPU时间片轮转机制 又称RR调度,会导致上下文切换 什么是进程和线程 进程:程序运行资源 ...

  8. java并发编程(一)----线程基础知识

    在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线 ...

  9. Java 线程基础知识

    前言 什么是线程?线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成.另外,线 ...

  10. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

随机推荐

  1. c#使用Bitmap绘图的时候,内存增大问题

    最近碰到一个问题,就是使用Biamap绘图的时候,为了防止闪烁,使用了双缓存绘制的方式,但是会碰到内存急剧增加的情况,而且在XP的工控机和Win10的机器上运行结果不一样,在Win10 上运行的时候, ...

  2. golang 简书

    https://www.jianshu.com/p/548adff0d10d Go 入门指南 https://github.com/wuxiaoxiaoshen/go-example-for-live ...

  3. lightdm开机无法自启问题

    简述 由于我学习了 systemctl disable 服务 这条命令,然后开始皮,把 lightdm 自启动关了,然后开不开了 解决办法:重置 lightdm 服务配置 sudo dpkg-reco ...

  4. 轻松玩转sed

    sed处理文本方法 1.文本或管道输入 2.读入一行到模式控件 3.sed命令处理 4.输出到屏幕 所以 sed是一个流处理编辑器 sed一次处理一行内容 sed不改变文件内容(可以通过重定向改变文件 ...

  5. day16 异常处理生成器

    day16 异常处理生成器 今日内容概要 异常处理 异常处理实战应用 生成器对象 生成器对象实现range方法 生成器表达式 今日内容详细 一.异常处理 1.异常常见类型 SyntaxError语法错 ...

  6. nginx-1.22.0版本安装

    nginx运行状态查看 查看80端口占用情况:netstat -tunlp | grep 80 # 查看进程是否运行ps -A | grep nginx # 强制关闭nginxpkill nginx ...

  7. Kafka教程(一)基础入门:基本概念、安装部署、运维监控、命令行使用

    Kafka教程(一)基础入门   1.基本概念   背景   领英->Apache   分布式.消息发布订阅系统   角色   存储系统   消息系统   流处理平台-Kafka Streami ...

  8. error: expected ‘)’ before ‘PRIx64’

    打印uint64时编译报错 printf("prefix:0x%"PRIx64"\n",ipv6Prefix); 解决办法:添加头文件 #include < ...

  9. 万字长文解析Scaled YOLOv4模型(YOLO变体模型)

    一,Scaled YOLOv4 摘要 1,介绍 2,相关工作 2.1,模型缩放 3,模型缩放原则 3.1,模型缩放的常规原则 3.2,为低端设备缩放的tiny模型 3.3,为高端设备缩放的Large模 ...

  10. [深度学习] CCPD车牌数据集介绍

    CCPD是一个大型的.多样化的.经过仔细标注的中国城市车牌开源数据集.CCPD数据集主要分为CCPD2019数据集和CCPD2020(CCPD-Green)数据集.CCPD2019数据集车牌类型仅有普 ...