什么是CAS?
Posted Dream_it_possible!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是CAS?相关的知识,希望对你有一定的参考价值。
目录
CAS是CompareAndSet的缩写,它是一组操作的整合,CAS将当前值与期望的值做比较,如果当前值与期望值相等,那么就将新值给更新进去作为当前值,否则不会更新新值给当前值。CAS是Java在多线程并发场景下提供的一种非阻塞的机制,具有原子性,是一种避免加锁的也能实现线程安全的,它的实现是在JVM层面上的。
为什么会有CAS?
举一个生活中最常见的例子,小刚要给小红转账100块钱,小刚的账户有1000块钱,那么小刚转账给小红的这个动作可以分解为2个步骤: 1) 小刚的账户余额减100元。2) 将900元赋值给小刚的账户余额。这2个动作要么全部执行成功,要么全部执行失败,因此小刚转账的动作需要具备原子性。
我们可以把小刚账户的钱当做一个整型的变量放入到AtomicInteger里,AtomicInteger提供了很多具有原子性的方法,那么小刚转账的操作使用getAndAdd(-100)就能够保证原子性,底层实现是CAS,CAS的出现就能解决类似的问题。
刚提到有用AtomicInteger保证整型变量的原子性, 那这个到底是个什么玩意呢,接着看AtomicInteger。
AtomicInteger是什么?
AtomicInteger在atomic包下的一个保证整型变量执行加减等操作原子性的类,atomic在java.util.concurrent包下,java.util.concurrent简称JUC,该包下还有AtomicBoolean,AtomicLong,AtomicReference用法类似,下面只看AtomicInteger的用法和原理。
AtomicInteger类里几个核心属性:
unsafe: 提供CAS操作,并且提供类字段的在内存中的地址偏移量获取方法。
valueOffset: 共享属性在类AtomicInteger对象的内存所在的地址偏移量。
value: 由volatile关键字修饰,标记AtomicInteger当前的值,保证共享变量在多线程中的可见性。
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;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue)
value = initialValue;
...
这些属性是AtomicInteger实现原子操作的核心和关键,其中unsafe类是sun公司提供的在JVM层面上能对共享的变量进行原子操作的类,包含了很多native修饰的方法以及CAS操作的方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
AtomicInteger的compareAndSet()方法的底层实现就是compareAndSwapInt
public final boolean compareAndSet(int expect, int update)
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
unsafe的实现都是在JVM层面上的,如果想了解更多,那么需要看汇编层面上的代码。
CAS用法演示
i+=1是一个非原子的操作,但如果你用addAndGet(1)方法执行,就能保证i+1的原子性。
package com.example.jucdemo;
import java.util.concurrent.atomic.AtomicInteger;
public class BankDemo
private static AtomicInteger wallet = new AtomicInteger(1000);
private static int money = 1000;
public static void main(String[] args)
for (int i = 0; i < 100; i++)
new Thread(new safeSubTask()).start();
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("余额:" + wallet.get());
for (int j = 0; j < 100; j++)
new Thread(new subTask()).start();
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("余额:" + money);
static class safeSubTask implements Runnable
@Override
public void run()
wallet.addAndGet(-1);
System.out.println(Thread.currentThread().getName() + "花了1块钱!");
static class subTask implements Runnable
@Override
public void run()
money -= 1;
System.out.println(Thread.currentThread().getName() + "花了1块钱!");
打印结果如下:
由打印可知,AtomicInteger能提供对变量操作的原子性,而没用AtomicInteger出现了异常的结果!
ABA问题
虽然说CAS能保证线程安全,但是存在着有线程将变量从A更新到B,然后从B又更新到A的问题,貌似该线程并没有用过共享变量,因此会存在潜在的风险,例如场景:
给定一个链表,如果链表中的某个节点没有修改,那么进行CAS操作。 假如此时的线程1 拿到了该链表里的头节点的值A, 线程1将该值更新为了B,然后线程2再次拿到头节点,将头节点的值从B更新为A,那么线程3来的时候就懵了,他会觉得这个头节点没有被动过,那么应该需要进行CAS操作。
这样问题就来了,其实头节点已经被2个线程修改过,但是线程3仍然认为头节点没有被修改,因此CAS有潜在的ABA问题,这种场景很少见,问题也比较刁钻,有没有解决方法呢?
ABA解决方案
解决ABA的问题,主要有2种方案,一种是用AtomicStampedReferemce, 另外一个是AtomicMarkableReference。AtomicStampedReference内部维护了一个整型值stamp,类似于版本号的作用,修改了那么表示已经变更, AtomicMarkableReference内部则是维护了一个布尔值mark, 如果mark被更新了那么表示值已经被更改,那么就不会进行CAS操作。
CAS应用实战
基于CAS手写一把互斥锁
Unsafe不能采用Unsafe.getUnsafe()的方式获取,否则的话会报securityException。我们可以从Unsafe里定义的unsafe属性获取到Unsafe对象。
package cas;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeFactory
public static Unsafe getUnsafe()
try
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
catch (NoSuchFieldException | IllegalAccessException e)
e.printStackTrace();
return null;
实现CAS操作,需要定义共享变量state在内存中的地址偏移量offset。
private static Unsafe unsafe = UnsafeFactory.getUnsafe();
private static Long offset;
private volatile int state;
static
try
offset = unsafe.objectFieldOffset
(CasLock.class.getDeclaredField("state"));
catch (Exception ex)
throw new Error(ex);
有了offset后,那么就能通过unsafe获取到state变量在内存中的地址,也就是说能进行CAS操作, 举个例子,Unsafe类里的compareAndSwapInt方法,比较并设置新的整型值给state:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
下面是参数解释:
- var1: 表示当前类的对象。
- var2: 类对象所在内存的地址偏移量,指向state属性。
- var3: 当前值,必须为整型。
- var4: 新值,必须为整型。
加锁:
/**
* 加锁, 互斥锁,在JVM层面上实现
*/
public boolean lock()
return unsafe.compareAndSwapInt(this, offset, 0, 1);
解锁:
/**
* 释放锁
*/
public boolean release()
return unsafe.compareAndSwapInt(this, offset, 1, 0);
CASLock完整代码:
package cas;
import sun.misc.Unsafe;
public class CasLock
private static Unsafe unsafe = UnsafeFactory.getUnsafe();
private static Long offset;
private volatile int state;
static
try
offset = unsafe.objectFieldOffset
(CasLock.class.getDeclaredField("state"));
catch (Exception ex)
throw new Error(ex);
public CasLock()
this.state = 0;
public CasLock(int state)
this.state = state;
public boolean cas()
return unsafe.compareAndSwapInt(this, offset, 0, 1);
/**
* 加锁, 互斥锁,在JVM层面上实现
*/
public boolean lock()
return unsafe.compareAndSwapInt(this, offset, 0, 1);
/**
* 释放锁
*/
public boolean release()
return unsafe.compareAndSwapInt(this, offset, 1, 0);
用刚实现的CASLock替换掉ReentrantLock实现同步:
import cas.CasLock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* 卖面包
*/
public class ReentrantLockDemo
private static ReentrantLock reentrantLock = new ReentrantLock();
/**
* 使用caslock
*/
private static CasLock casLock = new CasLock();
private static int num = 5;
private static volatile int money = 100;
static int threadNums = 10;
public static void main(String[] args)
for (int i = 0; i < threadNums; i++)
Thread t = new Thread(new CurrentTask());
t.start();
// try
// t.join();
// catch (InterruptedException e)
// e.printStackTrace();
//
try
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("面包售卖结束!");
// Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
// Thread-1获取到一个面包,剩余数量:3,剩余钱数:80
// Thread-0充值:20 RMB,剩余钱数:90
// Thread-2获取到一个面包,剩余数量:2,剩余钱数:70
// Thread-3获取到一个面包,剩余数量:1,剩余钱数:100
// Thread-1充值:20 RMB,剩余钱数:110
// Thread-4获取到一个面包,剩余数量:0,剩余钱数:130
// Thread-4充值:20 RMB,剩余钱数:150
// Thread-3充值:20 RMB,剩余钱数:130
// Thread-2充值:20 RMB,剩余钱数:120
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// 最后剩余钱数:150
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// Process finished with exit code 0
static class SubTask implements Runnable
@Override
public void run()
if (num <= 0)
System.out.println("对不起,面包卖完了!");
return;
money -= 10;
System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
// 充值20元
money += 20;
System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
static class CurrentTask implements Runnable
@Override
public void run()
if (casLock.lock())
// reentrantLock.lock();
if (num <= 0)
System.out.println("对不起,面包卖完了!");
// reentrantLock.unlock();
return;
money -= 10;
System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
// 充值20元
money += 20;
System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
// reentrantLock.unlock();
casLock.release();
打印结果:
Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
Thread-0充值:20 RMB,剩余钱数:110
Thread-3获取到一个面包,剩余数量:3,剩余钱数:100
Thread-3充值:20 RMB,剩余钱数:120
Thread-5获取到一个面包,剩余数量:2,剩余钱数:110
Thread-5充值:20 RMB,剩余钱数:130
Thread-9获取到一个面包,剩余数量:1,剩余钱数:120
Thread-9充值:20 RMB,剩余钱数:140
面包售卖结束!
从打印结果发现面包并没有卖完, 因为是互斥的有的线程会出现争夺失败的情况,继续在此基础上完善,添加一个自旋和volatile开关。
import cas.CasLock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* 卖面包
*/
public class ReentrantLockDemo
private static ReentrantLock reentrantLock = new ReentrantLock();
/**
* 使用caslock
*/
private static CasLock casLock = new CasLock();
private static int num = 5;
private static volatile int money = 100;
static int threadNums = 10;
private static volatile boolean sellout = false;
public static void main(String[] args)
for (int i = 0; i < threadNums; i++)
Thread t = new Thread(new CurrentTask());
t.start();
// try
// t.join();
// catch (InterruptedException e)
// e.printStackTrace();
//
try
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("面包售卖结束!");
// Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
// Thread-1获取到一个面包,剩余数量:3,剩余钱数:80
// Thread-0充值:20 RMB,剩余钱数:90
// Thread-2获取到一个面包,剩余数量:2,剩余钱数:70
// Thread-3获取到一个面包,剩余数量:1,剩余钱数:100
// Thread-1充值:20 RMB,剩余钱数:110
// Thread-4获取到一个面包,剩余数量:0,剩余钱数:130
// Thread-4充值:20 RMB,剩余钱数:150
// Thread-3充值:20 RMB,剩余钱数:130
// Thread-2充值:20 RMB,剩余钱数:120
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// 最后剩余钱数:150
// 对不起,面包卖完了!
// 对不起,面包卖完了!
// Process finished with exit code 0
static class SubTask implements Runnable
@Override
public void run()
if (num <= 0)
System.out.println("对不起,面包卖完了!");
return;
money -= 10;
System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
// 充值20元
money += 20;
System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
static class CurrentTask implements Runnable
@Override
public void run()
while (!sellout)
if (casLock.lock())
// reentrantLock.lock();
if (num <= 0)
System.out.println("对不起,面包卖完了!");
// reentrantLock.unlock();
sellout = true;
return;
money -= 10;
System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
// 充值20元
money += 20;
System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
// reentrantLock.unlock();
casLock.release();
打印结果:
Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
Thread-0充值:20 RMB,剩余钱数:110
Thread-2获取到一个面包,剩余数量:3,剩余钱数:100
Thread-2充值:20 RMB,剩余钱数:120
Thread-1获取到一个面包,剩余数量:2,剩余钱数:110
Thread-1充值:20 RMB,剩余钱数:130
Thread-0获取到一个面包,剩余数量:1,剩余钱数:120
Thread-0充值:20 RMB,剩余钱数:140
Thread-2获取到一个面包,剩余数量:0,剩余钱数:130
Thread-2充值:20 RMB,剩余钱数:150
对不起,面包卖完了!
面包售卖结束!Process finished with exit code 0
自旋能继续尝试获取CASLock,通过添加 sellout变量实现结束售卖的行为。
以上是关于什么是CAS?的主要内容,如果未能解决你的问题,请参考以下文章
Java -- 每日一问:AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?