线程基础知识11-CAS+自旋锁
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
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);
}
}
在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+自旋锁的更多相关文章
- java线程基础知识----线程与锁
我们上一章已经谈到java线程的基础知识,我们学习了Thread的基础知识,今天我们开始学习java线程和锁. 1. 首先我们应该了解一下Object类的一些性质以其方法,首先我们知道Object类的 ...
- 我们常说的 CAS 自旋锁是什么
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作. 它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值, ...
- 并发编程--CAS自旋锁
在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系 ...
- Java__线程---基础知识全面实战---坦克大战系列为例
今天想将自己去年自己编写的坦克大战的代码与大家分享一下,主要面向学习过java但对java运用并不是很熟悉的同学,该编程代码基本上涉及了java基础知识的各个方面,大家可以通过练习该程序对自己的jav ...
- java线程基础知识----线程基础知识
不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...
- 线程基础知识01-Thread类,Runnable接口
常见面试题:创建一个线程的常用方法有哪些?Thread创建线程和Runnable创建线程有什么区别? 答案通常集中在,继承类和实现接口的差别上面: 如果深入问一些问题:1.要执行的任务写在run()方 ...
- Java线程基础知识(状态、共享与协作)
1.基础概念 CPU核心数和线程数的关系 核心数:线程数=1:1 ;使用了超线程技术后---> 1:2 CPU时间片轮转机制 又称RR调度,会导致上下文切换 什么是进程和线程 进程:程序运行资源 ...
- java并发编程(一)----线程基础知识
在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线 ...
- Java 线程基础知识
前言 什么是线程?线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成.另外,线 ...
- Windows核心编程 第六章 线程基础知识 (上)
第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...
随机推荐
- windows查看端口和杀掉端口
//执行下面命令 netstat --help 获取netstat的所有命令参数 //例如查看8080端口占用 netstat -ano | findstr 8080 //查看该端口是什么 taskl ...
- kubernetes笔记-1-基础环境部署
一.环境信息: 操作系统:ubuntu 18.04 server amd64 docker:docker 19.03.ce kubernetes:v1.19 IP地址 主机名 角色 172.29. ...
- Velero 系列文章(一):基础
概述 Velero 是一个开源工具,可以安全地备份和还原,执行灾难恢复以及迁移 Kubernetes 集群资源和持久卷. 灾难恢复 Velero 可以在基础架构丢失,数据损坏和/或服务中断的情况下,减 ...
- 详解redis网络IO模型
前言 "redis是单线程的" 这句话我们耳熟能详.但它有一定的前提,redis整个服务不可能只用到一个线程完成所有工作,它还有持久化.key过期删除.集群管理等其它模块,redi ...
- 玩转 Go 生态|Hertz WebSocket 扩展简析
WebSocket 是一种可以在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层.WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在 W ...
- SQLMap入门——判断文本中的请求是否存在注入
从文件中加载HTTP请求,SQLMap可以从一个文本文件中获取HTTP请求,这样就可以不设置其他参数(如cookie.POST数据等),txt文件中的内容为Web数据包 文本文件如图(请求数据可以通过 ...
- 手把手教你一套完善且高效的k8s离线部署方案
作者:郝建伟 背景 面对更多项目现场交付,偶而会遇到客户环境不具备公网条件,完全内网部署,这就需要有一套完善且高效的离线部署方案. 系统资源 编号 主机名称 IP 资源类型 CPU 内存 磁盘 01 ...
- JavaScript:操作符:比较运算符及其隐式转换数据类型
不等关系 即大于>:大于等于>=:小于<:小于等于<= 当比较的两个变量,有非数字时,会隐式转换为数字再比较,转换情况同算术运算符: 当两个变量均为字符串时,不会进行转换,而是 ...
- RSA中用到的推导,笔记持续更新
1.同余式组求p和q 已知条件: 推导过程: 根据上述已知条件,以及同余式性质,我们可以得到如下: c1e2 = (2p + 3q)e1*e2 mod N c2e1 = (5p + 7q)e1*e2 ...
- Python 文件操作(IO 技术)
目录 Python 文件操作(IO 技术) 文本文件和二进制文件 文件操作相关模块概述 建文件对象 open() 文本文件的写入 write()/writelines()写入数据 close()关闭文 ...