大家好,我是栗子为
又和大家见面了,今天咱们还是接着上次没聊完的CAS继续说,这一部分看完,相信大家会对CAS有更深入的了解,在面试中也能游刃有余
废话不多说,咱们来看看今天的内容
01
—
CAS之原子引用
我们经常使用的原子类例如AtomicInteger
、AtomicBoolean
、AtomicLong
不能满足我们真正业务中的需求,例如我想对一个引用类型做原子引用,例如AtomicPeople
,那么就需要用到AtomicReference
举个🌰
@Getter
@ToString
@AllArgsConstructor
class ChestNuts {
String name;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<ChestNuts> atomicReference = new AtomicReference<>();
ChestNuts nut1 = new ChestNuts("栗子为", 18); // 别管,真的18
ChestNuts nut2 = new ChestNuts("花栗鼠小K", 98); // 猜猜
atomicReference.set(nut1);
System.out.println(atomicReference.compareAndSet(nut1, nut2) + "t" + atomicReference.get());
System.out.println(atomicReference.compareAndSet(nut1, nut2) + "t" + atomicReference.get());
}
}
结果如下
true ChestNuts(name=花栗鼠小K, age=98)
false ChestNuts(name=花栗鼠小K, age=98)
可以看到,对于引用类型,也可以利用compareAndSet
方法对其进行比较替换,当第二次调用时,由于当前变量不是nut1了,所以compareAndSet返回FALSE
02
—
CAS与自旋锁
什么是自旋锁?
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,自旋是指不断循环尝试去获取锁,这个过程不会阻塞,减少线程上下文切换的消耗,缺点是循环会消耗CPU
自旋锁小Demo
通过CAS操作完成自旋锁,假设线程A先持有锁5秒钟,此时线程B进来发现当前有线程持有锁,所以自旋等待,直到当前线程释放锁后,线程B抢到锁
SpinLockDemo.java
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
static long count = 1;
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "线程正在上锁...");
while (!atomicReference.compareAndSet(null, thread)) {
count++;
}
System.out.println("在尝试了" + count + "次后," + Thread.currentThread().getName() + "线程终于上锁成功!");
}
public void unLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().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();
try {
TimeUnit.MILLISECONDS.sleep(500); // 保证线程A先于线程B启动
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "B").start();
}
}
结果如下
A线程正在上锁...
在尝试了1次后,A线程终于上锁成功!
B线程正在上锁...
A线程解锁成功...
在尝试了693182812次后,B线程终于上锁成功!
B线程解锁成功...
上面我们都是看到CAS解决了多线程操作数据不一致问题,那么CAS有什么缺点呢?
03
—
CAS缺点
-
循环时间长开销很大 -
会出现ABA问题
什么是ABA问题?
线程1从内存位置V中取出A,此时线程2也从内存中取出A,线程2对其操作,先将值变为B,然后又变为A,此时线程1开始CAS操作,发现内存中的值仍为A,满足条件,完成修改操作
尽管线程1操作成功,但是不代表这个过程不存在问题
那么解决这个问题就需要引入版本号、戳记流水
单线程
举个🌰
@Getter
@ToString
@AllArgsConstructor
class Book {
int id;
String name;
}
public class AtomicStampedDemo {
public static void main(String[] args) {
Book java = new Book(1, "深入理解Java虚拟机");
Book mysql = new Book(2, "数据库原理");
AtomicStampedReference<Book> reference = new AtomicStampedReference<>(java, 1);
System.out.println(reference.getReference() + "t" + reference.getStamp());
boolean result;
result = reference.compareAndSet(java, mysql, reference.getStamp(), reference.getStamp() + 1);
System.out.println(reference.getReference() + "t" + reference.getStamp());
result = reference.compareAndSet(mysql, java, reference.getStamp(), reference.getStamp() + 1);
System.out.println(reference.getReference() + "t" + reference.getStamp());
}
}
结果如下
Book(id=1, name=深入理解Java虚拟机) 1
Book(id=2, name=数据库原理) 2
Book(id=1, name=深入理解Java虚拟机) 3
可以看到,利用AtomicStampedReference
类的compareAndSet
方法可以对变量加上版本号,用来避免ABA问题,即使经过两次修改后,内存位置的值仍为”深入理解Java虚拟机”,但由于版本号不一致,也会阻止其进行修改
多线程
举个🌰
public class ABADemo {
static AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + "第一次拿到的版本号为:" + stamp);
try {
// 保证另一个线程和我拿到同样的版本号
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
reference.compareAndSet(100, 101, reference.getStamp(), reference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第二次拿到的版本号为:" + reference.getStamp());
reference.compareAndSet(101, 100, reference.getStamp(), reference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第三次拿到的版本号为:" + reference.getStamp());
}, "t1").start();
new Thread(() -> {
// 这个表示我和线程t1具有相同的初始stamp
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + "第一次拿到的版本号为:" + stamp);
try {
// 保证另一个线程完成ABA
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = reference.compareAndSet(100, 999, stamp, stamp + 1);
System.out.println(result + "t" + reference.getReference() + "t" + reference.getStamp());
}, "t2").start();
}
}
线程t1做了ABA操作,同时reference的stamp值不断增加,当线程t2进行比较时,发现版本号和自己的初始值不同,所以拒绝了此次修改
04
—
总结
以上就是Java中CAS的全部内容啦,相信大家看完不只是会答一句“Compare And Swap”,在面试中就算被问到能否手写一个自旋锁或者手写一个ABA问题,相信大家都能轻松拿捏,关于这块的内容,小为就和大家分享到这
关注六只栗子,面试不迷路,我们下次再见~
作者 栗子为
编辑 一口栗子
原文始发于微信公众号(六只栗子):深入理解Java中的CAS(二)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88454.html