目录
1、CAS是什么
1.1 CAS基本知识
Compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用的一种技术。它包含三个操作数——内存位置、预期原值、更新值
1.2 CAS基本思想
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
重试的行为称为——自旋!
过程解析:
1、线程A、B 同时读取到了主内存的共享变量number(5)并复制到各自线程工作内存中成为共享变量副本number'(5)
2、线程A、B同时计算出结果number''(6)
3、到写回主内存中时只能由一个先执行(这里为什么先不用纠结,后面会说——靠的是CPU原语级别的锁,硬件来保证原子性)
4、线程A执行,线程B挂起;线程A进行共享变量number'(5)与主内存number(5)比较,发现一致修改内存值中的number=6
5、线程B执行时,进行比较线程B中的number'(5)与主内存中的number(6)对比发现不一致,然后需要自旋重新走一遍线程B的方法
2、对原子类中使用的CAS进行分析
2.1 CAS demo 代码
多线程情况下使用原子类可以保证线程安全
public class CasDemo {
public static void main(String[] args){
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t"+atomicInteger.get());
}
}
//true 2020
//false 2020
public class CasDemo {
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 10 ; i++){
new Thread(() -> {
for(int j = 1 ; j <= 500; j++){
atomicInteger.incrementAndGet();
}
},String.valueOf(i)).start();
}
// 等待所有线程执行完毕, 这里还可以使用CountDownLatch
Thread.sleep(2000L);
System.out.println(atomicInteger.get());
}
}
// 5000
上面代码的优化
public class CasDemo {
static AtomicInteger atomicInteger = new AtomicInteger(0);
/** 线程数 */
static Integer THREAD_NUM = 10;
/** 线程控制 */
static CountDownLatch downLatch = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < THREAD_NUM ; i++){
new Thread(() -> {
try{
for(int j = 1 ; j <= 500; j++){
atomicInteger.incrementAndGet();
}
}finally {
downLatch.countDown();
}
},String.valueOf(i)).start();
}
// 等待所有线程执行完毕
downLatch.await();
System.out.println(atomicInteger.get());
}
}
// 5000
2.2 源码分析
不管是调用了什compareAndSet 还是 incrementAndGet() 等方法;底层都调用的是compareAndSwapXxx
//compareAndSet
//发现它调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//compareAndSwapInt
//发现它调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
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);
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
3、原子类
3.1 有那些属性(用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();
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;//保证变量修改后多线程之间的可见性
}
1、unsafe:保证数据的原子性
2、valueOffset:该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据
3、value:保存的数据 使用volatile修饰,保证可见性
3.2 Unsafe类解析
CAS这个理念 ,落地就是Unsafe类
它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门 ,基于该类可以直接操作特定内存的数据 。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 。
OpenJDK源码中查看Unsafe.java 源码地址:Unsage.java位置
1、do-while 体现了自旋的思想
2、如果compareAndSwapInt 为true设置成后取反为false退出循环;假如返回的是false表示设置失败,取反true继续循环
以下内容可以看一看,不需要懂!!!!
compareAndSwapInt方法被native关键字修饰,所以是一个本地方法,该方法可以从unsafe.cpp中看到,源码地址:unsafe.cpp位置
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv* env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 先想办法拿到变量value在内存中的地址,根据偏移量valueOffset,计算 value 的地址
jint* addr = (jint* ) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是要交换的值,e是要比较的值
//cas成功,返回期望值e,等于e,此方法返回true;
//cas失败,返回内存中的value值,不等于e,此方法返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
//-------------核心(Atomic::cmpxchg(x, addr, e)) == e;
//JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用cmpxchg指令比较并更新变量值(原子性)
// 调用 Atomic 中的函数 cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
//根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}
不同的操作系统下会调用不同的compxchg重载函数,例如win10
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
//判断是否是多核CPU
int mp = os::is_MP();
__asm {
//三个move指令表示的是将后面的值移动到前面的寄存器上
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
//CPU原语级别,CPU触发
LOCK_IF_MP(mp)
//比较并交换指令
//cmpxchg: 即“比较并交换”指令
//dword: 全称是 double word 表示两个字,一共四个字节
//ptr: 全称是 pointer,与前面的 dword 连起来使用,表明访问的内存单元是一个双字单元
//将 eax 寄存器中的值(compare_value)与 [edx] 双字内存单元中的值进行对比,
//如果相同,则将 ecx 寄存器中的值(exchange_value)存入 [edx] 内存单元中
cmpxchg dword ptr [edx], ecx
}
}
总结(需背):
1、unsafe类提供了硬件级别的原子性操作,实现方式是基于硬件平台的汇编指令,在intel的CPIU中,使用的汇编指令是cmpxchg指令
2、核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设置为新的N(swap)。如果不相等自旋再来。
4、使用原子类自定义原子引用
案例:
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zs = new User("张三",18);
User ls = new User("李四",25);
// 将类型丢入泛型即可
AtomicReference<User> atomicReferenceUser = new AtomicReference<>();
// 将这个原子类设置为张三
atomicReferenceUser.set(zs);
// 张三换位李四
System.out.println(atomicReferenceUser.compareAndSet(zs,ls)+"\t"+atomicReferenceUser.get().toString());
// true User(userName=李四, age=25)
System.out.println(atomicReferenceUser.compareAndSet(zs,ls)+"\t"+atomicReferenceUser.get().toString());
// false User(userName=李四, age=25)
}
}
@Data
@AllArgsConstructor
class User {
String userName;
int age;
}
5、利用CAS思想,实现自旋锁SpinLockDemo
题目:实现一个自旋锁
自旋锁好处:循环比较获取没有类似wait的阻塞。
通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
public class SpinLockDemo {
AtomicReference<Thread> lock = new AtomicReference();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 争抢锁");
// 用这个循环实现自旋;如果是空的表示没有线程持有该锁,那把当前thread放进去
while (!lock.compareAndSet(null,thread)){
}
System.out.println(thread.getName() + " 持有锁");
}
public void unlock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " 准备释放锁");
// 把当前线程踢出去,置为null
boolean compareAndSet = lock.compareAndSet(thread, null);
System.out.println(compareAndSet ? thread.getName() + " 释放锁成功" : thread.getName() + " 释放锁失败");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.unlock();
},"A").start();
//暂停一会儿线程,保证A线程先于B线程启动并完成
try { TimeUnit.MILLISECONDS.sleep( 500); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
spinLockDemo.lock();//B -----come in B只是尝试去抢锁,但是一直在自旋。
spinLockDemo.unlock();//A结束后 B立马抢到锁,然后马上结束了
},"B").start();
}
}
//A 争抢锁
//A 持有锁
//B 争抢锁
//A 准备释放锁
//A 释放锁成功
//B 持有锁
//B 准备释放锁
//B 释放锁成功
6、CAS缺点
6.1 循环时间长CPU开销会变大
如果CAS一直失败,会一直尝试,可以回给CPU带来很大的开销。
6.2 ABA问题
6.2.1 什么是ABA问题
线程A先将内存中的数据取出,这个时候线程B也将内存中的数据拿出;线程A做了两步操作,第一步将内存的数据改成了其他的数据,第二步操作又将数据改会了一开始内存的值;这个时候线程B再来比较的时候发现自己的值与预期值一致,就进行了改变!
这个事情可以用现实情况去比喻:
1、先挪用公款的钱去理财,然后等公司要钱的时候,再将钱换回去!这样看似没有什么问题,但是其实已经犯了罪;所以银行每一笔钱的来往都有流水。
2、隔壁老王案例
6.2.2 解决:加版本号、流水
AtomicStampedReference版本号 (注意区分前面的Class AtomicReference)
// 相关API
AtomicStampedReference(V initialRef, int initialStamp)
创建一个新的 AtomicStampedReference与给定的初始值。
public boolean weakCompareAndSet(V expectedReference,//旧值
V newReference,//新值
int expectedStamp,//旧版本号
int newStamp)//新版本号
以原子方式设置该引用和邮票给定的更新值的值,如果当前的参考是==至预期的参考,并且当前标志等于预期标志。
May fail spuriously and does not provide ordering guarantees ,所以只是很少适合替代compareAndSet 。
参数
expectedReference - 参考的预期值
newReference - 参考的新值
expectedStamp - 邮票的预期值
newStamp - 邮票的新值
结果
true如果成功
//基本情况
@NoArgsConstructor
@AllArgsConstructor
@Data
class Book{
private int id;
private String bookName;
}
public class AtomicStampedDemo {
public static void main(String[] args) {
Book javaBook = new Book(1, "javaBook");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
System.out.println(stampedReference.getReference()+"\t"+stampedReference.getReference());
Book mysqlBook = new Book(2, "mysqlBook");
boolean b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
}
}
//Book(id=1, bookName=javaBook) Book(id=1, bookName=javaBook)
//true Book(id=2, bookName=mysqlBook) 2
//ABA复现(单线程情况下)
```java
public class AtomicStampedDemo {
public static void main(String[] args) {
Book javaBook = new Book(1, "javaBook");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
System.out.println(stampedReference.getReference()+"\t"+stampedReference.getReference());
Book mysqlBook = new Book(2, "mysqlBook");
boolean b;
b= stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
b= stampedReference.compareAndSet(mysqlBook,javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
}
}
//Book(id=1, bookName=javaBook) Book(id=1, bookName=javaBook) --------
//true Book(id=2, bookName=mysqlBook) 2
//true Book(id=1, bookName=javaBook) 3 --------虽然1.3行内容是一样的,但是版本号不一样
//ABA复现(多线程情况下)
public class ABADemo{
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
public static void main(String[] args){
new Thread(() -> {
atomicInteger.compareAndSet(100,101);
atomicInteger.compareAndSet(101,100);//这里 中间就有人动过了,虽然值是不变的,假如不检查版本号,CAS就直接能成功了
},"t1").start();
new Thread(() -> {
//暂停一会儿线程
try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };
System.out.println(atomicInteger.compareAndSet(100, 2022)+"\t"+atomicInteger.get());
},"t2").start();
//-------------------- true-2022
//暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("============以下是ABA问题的解决=============================");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1-----------初始获得一样的版本号
//暂停500毫秒,保证t4线程初始化拿到的版本号和我一样,
try { TimeUnit.MILLISECONDS.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); }
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();//记录一开始的版本号,并且写死
System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1------------初始获得一样的版本号
//暂停1秒钟线程,等待上面的t3线程,发生了ABA问题
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);//这个还是初始的版本号,但是实际上版本号被T3修改了,所以肯定会失败
System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
},"t4").start();
}
}
//t3 首次版本号:1
//t4 首次版本号:1
//t3 2次版本号:2
//t3 3次版本号:3
//false 100 3 -----因为版本号实际上已经被修改了