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. day02-实现01

    实现01 1.实现任务阶段1 编写mytomcat,该服务器能给浏览器返回"你好,我是服务器!"的简单信息. 根据之前的tomcat框架整体分析,我们将浏览器发送请求,tomcat ...

  2. 【云原生 · Kubernetes】部署zookeeper

    个人名片: 因为云计算成为了监控工程师‍ 个人博客:念舒_C.ying CSDN主页️:念舒_C.ying 部署zookeeper 1.1 zookeeper概述 1.2 ZooKeeper服务中操作 ...

  3. 解决Anaconda关联VSCode使用conda运行Python报错(无法将“conda”项识别为 cmdlet、函数、脚本文件或可运行程序)

    错误 刚安装好Anaconda之后创建好VS Code环境运行Python会报错,但是仍然是可以正常运行,强迫症想解决报错 PS C:\Users\Satan\Documents\Code\Pytho ...

  4. 我的Vue之旅 11 Vuex 实现购物车

    Vue CartView.vue script 数组的filter函数需要return显式返回布尔值,该方法得到一个新数组. 使用Vuex store的modules方式,注意读取状态的方式 this ...

  5. 视频超分之BasicVSR-阅读笔记

    1.介绍 对于视频超分提出了很多方法,EDVR中采用了多尺度可变形对齐模块和多个注意层进行对齐和定位并且从不同的帧聚合特征,在RBPN中,多个投影模块用于顺序聚合多个帧中的特征.这样的设计是有效的,但 ...

  6. go操作Kfaka

    目录 1. Kafka介绍 1.1.1. Kafka是什么 1.1.2. Kafka的特点 1.1.3. 常用的场景 1.1.4. Kafka中包含以下基础概念 1.1.5. 消息 1.1.6. 消息 ...

  7. python前言

    目录 一.typora软件以及markdown语法介绍 1.输入标题的两种方法 2.无序列表 3.有序列表 4.在typora里插入多行代码块 5.制作表格 6.表情包 7.链接 8.Typora查看 ...

  8. SQL 之 SELECT语句

    1.展示所有列语法 select * from table; #table表示表名 示例: select * from a 2.展示指定列语法 select column1, column2, ... ...

  9. 使用pip命令安装库时提示Could not build wheels for six, since package 'wheel' is not installed

    在使用pip命令安装库时提示Could not build wheels for six, since package 'wheel' is not installed 解决以上问题可用 pip in ...

  10. python内存机制

    内存机制 先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++,Java等语言一样,他们可以不用事 ...