一文彻底搞懂CAS实现原理 & 深入到CPU指令
本文导读:
- 前言
- 如何保障线程安全
- CAS原理剖析
- CPU如何保证原子操作
- 解密CAS底层指令
- 小结
朋友,文章优先发布在公众号上,如果你愿意,可以扫右侧二维码支持一下下~,谢谢!
前言
日常编码过程中,基本不会直接用到 CAS 操作,都是通过一些JDK 封装好的并发工具类来使用的,在 java.util.concurrent 包下。
但是面试时 CAS 还是个高频考点,所以呀,你还不得不硬着头皮去死磕一下这块的技能点,总比一问三不知强吧?
一般都是先针对一些简单的并发知识问起,还有的面试官,比较直接:
面试官:Java并发工具类中的 CAS 机制讲一讲?
小东:额?大脑中问自己「啥是 CAS?」我听过的,容我想一想...
一分钟过去了...
小东:嘿嘿~,这块我看过的,记不大清楚了。
面试官:好的,今天先到这吧~
小东:在路上
当然 CAS 你若真不懂,你可以引导面试官到你擅长的技术点上,用你的其他技能亮点扳回一局。
接下来,我们通过一个示例代码来说:
// 类的成员变量 static int data = 0; // main方法内代码 IntStream.range(0, 2).forEach((i) -> { new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(0, 100).forEach(y -> { data++; }); }).start(); }); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(data); }
结合图示理解:
上述代码,问题很明显,data 是类中的成员变量,int 类型,即共享的资源。当多个线程同时
执行 data++
操作时,结果可能不等于 200,为了模拟出效果,线程中 sleep 了 20 毫秒,让线程就绪,代码运行多次,结果都不是 200 。
如何保障线程安全
示例代码执行结果表明了,多个线程同时操作共享变量导致了结果不准确,线程是不安全的。如何解决呢?
方案一:使用 synchronized 关键字
使用 synchronized 关键字,线程内使用同步代码块,由JVM自身的机制来保障线程的安全性。
synchronized 关键代码:
// 类中定义的Object锁对象 Object lock = new Object(); // synchronized 同步块 () 中使用 lock 对象锁定资源 IntStream.range(0, 100).forEach(y -> { synchronized (lock.getClass()) { data++; } });
方案二:使用 Lock 锁
高并发场景下,使用 Lock 锁要比使用 synchronized 关键字,在性能上得到极大的提高。
因为 Lock 底层是通过 AQS + CAS 机制来实现的。关于 AQS 机制可以参见往期文章 <<通过通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱>> 。CAS 机制会在文章中下面讲到。
使用 Lock 的关键代码:
// 类中定义成员变量 Lock lock = new ReentrantLock(); // 执行 lock() 方法加锁,执行 unlock() 方法解锁 IntStream.range(0, 100).forEach(y -> { lock.lock(); data++; lock.unlock(); });
结合图示理解:
方案三:使用 Atomic 原子类
除上面两种方案还有没有更为优雅的方案?synchronized 的使用在 JDK1.6 版本以后做了很多优化,如果并发量不大,相比 Lock 更为安全,性能也能接受,因其得益于 JVM 底层机制来保障,自动释放锁,无需硬编码方式释放锁。而使用 Lock 方式,一旦 unlock() 方法使用不规范,可能导致死锁。