前言
Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值;一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS.
CAS优缺点
优点:
由于CAS是非阻塞的,可避免死锁,线程间的互相影响非常小。
没有锁竞争带来的系统开销,也没有线程间频繁调度的开销。
缺点(重点):
自旋循环时间长开销大
如果某个线程通过CAS方式操作某个变量不成功,长时间自旋,则会对CPU带来较大开销。
解决方式:限制自旋次数
ABA问题
假如线程T1取得在内存中offset处的值是A,那在赋值的过程中取到内存中的值还是A,此时我们能确定内存中的值A并没有被其他线程修改过吗?当然不行,我给你举一个例子
线程T1和T2一开始取得内存中的旧值都是A,但是线程T2运行的快,将A->B->A,所以T1进行CAS的时候依旧可以通过,误认为内存中的值重来没有被修改过,这实际上是不对的,这就是“ABA”问题。
解决方式:Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedRefernce来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查标志stamped是否为预期标志,如果全部一致,则继续。
原子性概念
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其它的线程干扰。
例如语句(a++)实际上包含了三个操作:
- 读取变量a的值
- 对a进行加1的操作
- 将计算后的值再赋值给变量a
像这三个操作就无法构成原子性操作。
原子类的工作原理-CAS机制
2.1 原子类概述
在java.util.concurrent.atomic包下定义了一些对“变量操作”的“原子类”,例:
java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
像这些原子类都可以保证对变量操作的:原子性、有序性、可见性。
2.2 AtomicInteger类示例
我们可以通过AtomicInteger类,来看看原子类是如何使用的:
1.线程类:
public class MyThread extends Thread {
public static volatile int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
//线程1:取出a的值a=0(被暂停)
a++;
//写回
}
System.out.println("修改完毕!");
}
}
2.测试类:
public class Demo {
public static void main(String[] args) throws InterruptedException {
//1.启动两个线程
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("获取a最终值:" + MyThread.a.get());
}
}
使用原子类对变量操作,无论程序运行多少次,其结果都是正确的!
2.3 原子类的工作原理
先来看一下调用过程
image.png
在Unsafe类中,调用了一个:compareAndSwapInt()方法,此方法的几个参数:
var1:传入的AtomicInteger对象
var2:AtommicInteger内部变量的偏移地址
var5:之前取出的AtomicInteger中的值;
var5 + var4:预期结果
此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用var1和var2先获取内存AtomicInteger的值,然后和传入的,之前获取的值var5做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为var5 + var4,否则就继续循环,再次获取AtomicInteger中的值,再进行比较并交换,直至成功交换为止。
compareAndSwapInt()方法是"线程安全"的。
我们假设两个线程交替运行的情况,看看它是怎样工作的:
初始AtomicInteger的值为0
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程A被暂停
线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
线程B成功将AtomicInteger中的值改为1
线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回false,继续循环。
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。
CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会导致CAS失败,而再次循环。
CAS用作原子操作
现在CPU内部已经执行原子的CAS操作。Java5以来,你可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能。
下面是一个使用AtomicBoolean类实现lock()方法的例子:
1. **public** **static** **class** MyLock {
2. **private** AtomicBoolean locked = **new** AtomicBoolean(**false**);
3. **public** **boolean** lock() {
4. **return** locked.compareAndSet(**false**, **true**);
5. }
6. }
locked变量不再是boolean类型而是AtomicBoolean。这个类中有一个compareAndSet()方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。
如果值被替换了,compareAndSet()返回true,否则,返回false。
使用Java5+提供的CAS特性而不是使用自己实现的的好处是Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性。这会使还有CAS的代码运行更快。
简单例子:
1. **public** **static** **void** main(String[] args) {
2.
3. AtomicInteger atomicInteger = **new** AtomicInteger(55);//55
4. **int** addAndGet = atomicInteger.addAndGet(23);//78
5. **boolean** compareAndSet = atomicInteger.compareAndSet(78, 43);//true
6. **int** i = atomicInteger.get();//43
7. }
CAS解决原子性问题
首先,我们设定一个场景,我们有一个数据,初始值为0,我们要将它使用多线程的形式从0累加到10,要求每个线程只能加一次,并且不能出现两个线程累加后的结果出现重复的情况(比如线程A和线程B同时将数据进行累加后变成8),这就不得不考虑原子性为题。
1.什么是原子性问题?
原子性问题指的是多线程并发的情况下,当我们要进行数据的读取与修改时,可能会因为线程同时读取数据,导致进行了重复修改的情况,但是我们希望每个线程进行读取修改操作是连贯的、原子性的,在A线程进行读取修改的时候,不允许其他线程进行读取修改,而一定要等到A线程完成修改后,其他线程再去进行读取修改。比如我们要将数字0累加到10,在多线程的情况下,可能会因为线程A在读完数字但是还没有进行修改的时候,线程B也来读了数字,导致线程A的原子性被打破,使得它们累加后得到的数据是一样的,这就破坏了原子性。
2.为什么用volatile关键字无法保证原子性和互斥性?
我们都知道volatile关键字修饰的变量是可以保证==内存可见性==的,能保证每次线程修改数据后,其他线程都能知道这个数据被修改了,但是当线程的操作不仅仅是读,变成两步:读+修改,由于volatile无法保证原子性和互斥性,那么就会造成这样的情况:线程A将数据更改为1,线程B和线程C都知道数据变成1了,但是他们是同时对数据1进行再修改,而我们要求每个线程累加后的结果要是不一样的,所以volatile关键字就无法满足我们设定的场景了。
将数据用volatile修饰,进行线程累加的代码:
package leo.casTest;
public class CasTest {
public static void main(String[] args) {
AtomicThread atomicThread = new AtomicThread();
// 完成从0加到10的任务需要10个线程
for (int i = 0; i <= 10; i++) {
Thread td = new Thread(atomicThread);
td.start();
}
}
}
// 每个线程的任务就是将数字+1
class AtomicThread implements Runnable {
// 我们需要修改的数据,从0累加到10
private int flagNumber = 0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flagNumber = flagNumber + 1;
System.out.println("线程: " + Thread.currentThread().getName() + "将数据变为: " + flagNumber);
}
}
结果:
image.png
3.什么是CAS算法?
CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问,它是一种无锁的非阻塞算法的实现。人话:CAS是靠硬件、底层代码支撑来解决原子性问题的。
4.CAS算法的过程原理
CAS有3个东西:
- 主存中的数据V;
- 线程拿到的数据A;
- 线程拟修改的新值B。
所以过程是这样的,主存里面有一个目前最新的数据假设为1,在我们的场景里,当线程想要对1进行累加的时候会做这么一件事:将线程内部缓存的数据(对应A)假设为1,与主存里面的数据1(对应V)进行比较,因为相等,所以线程就能直接将数据修改为新值2(对应B),这个比较和修改的过程是原子的,不允许其他线程介入。那么如果线程内部的数据缓存假设为1,主存里面的数据假设为2,他们不相等的时候,那么这个线程就不会完成更新,而是会将线程内部缓存的数据(A)变成主存的数据(V)2、并且将拟修改的新值B变成3后,再去和主存里面的值V判断(注意在这个过程中有可能又失败,因为可能其他线程又将主存里面的数据变成3了,就又会逼着线程循环刚才的操作)。
image.png
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。
5.CAS算法的有哪一些工具类呢?
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
- AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference
- AtomicIntegerArray 、AtomicLongArray
- AtomicMarkableReference
- AtomicReferenceArray
- AtomicStampedReference
他们的用法和非原子的是差不多的,会有对应的方法来代替原来的操作。
核心方法:boolean compareAndSet(expectedValue, updateValue)
6.使用CAS解决原子性问题
那么使用CAS的工具类来解决我们将数据从0顺序累加到10:
实现代码:
package leo.casTest;
import java.util.concurrent.atomic.AtomicInteger;
public class CasTest {
public static void main(String[] args) {
AtomicThread atomicThread = new AtomicThread();
// 完成从0加到10的任务需要10个线程
for (int i = 0; i <= 10; i++) {
Thread td = new Thread(atomicThread);
td.start();
}
}
}
// 每个线程的任务就是将数字+1
class AtomicThread implements Runnable {
// 我们需要修改的数据,从0累加到10,使用CAS工具类生成
private AtomicInteger flagNumber = new AtomicInteger();
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用方法进行累加
System.out.println("线程: " + Thread.currentThread().getName() + "将数据变为: " + flagNumber.getAndIncrement());
}
}
网友评论