java常见面试考点

往期文章推荐:
  java常见面试考点(二十):Elasticsearch 和 solr 的区别
  java常见面试考点(二十一):单点登录
  java常见面试考点(二十二):购物车实现
  java常见面试考点(二十三):消息队列
  java常见面试考点(二十四):强引用,软引用,虚引用,弱引用


【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权);

本博客的内容来自于:java常见面试考点(二十五):CAS是什么;

学习、合作与交流联系q384660495;

本博客的内容仅供学习与参考,并非营利;

文章目录

  • java常见面试考点
  • 一、CAS是什么
  • 二、CAS的基本原理
  • 三、AtomicInteger源码解析
    • 1、AtomicInteger的使用
    • 2、AtomicInteger的源代码
    • 3、unsafe类
  • 四、CAS优缺点
  • 五、Java 8对CAS机制的优化
  • 六、参考资料

一、CAS是什么

CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题,本文将深入的介绍CAS的原理。

synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

二、CAS的基本原理

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。


2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

三、AtomicInteger源码解析

1、AtomicInteger的使用

  在没有AtomicInteger之前,对于一个Integer的线程安全操作,是需要使用同步锁来实现的,当然现在也可以通过ReentrantLock来实现,但是最好最方便的实现方式是采用AtomicInteger。

package com.collection.test;import java.util.concurrent.atomic.AtomicInteger;/*** 原子类的测试*/
public class AtomicTest {private static AtomicInteger atomicInteger = new AtomicInteger();//获取当前值public static void getCurrentValue(){System.out.println(atomicInteger.get());//-->0}//设置value值public static void setValue(){atomicInteger.set(12);//直接用12覆盖旧值System.out.println(atomicInteger.get());//-->12}//根据方法名称getAndSet就知道先get,则最后返回的就是旧值,如果get在后,就是返回新值public static void getAndSet(){System.out.println(atomicInteger.getAndSet(15));//-->12}public static void getAndIncrement(){System.out.println(atomicInteger.getAndIncrement());//-->15}public static void getAndDecrement(){System.out.println(atomicInteger.getAndDecrement());//-->16}public static void getAndAdd(){System.out.println(atomicInteger.getAndAdd(10));//-->15}public static void incrementAndGet(){System.out.println(atomicInteger.incrementAndGet());//-->26}public static void decrementAndGet(){System.out.println(atomicInteger.decrementAndGet());//-->25}public static void addAndGet(){System.out.println(atomicInteger.addAndGet(20));//-->45}public static void main(String[] args) {AtomicTest test = new AtomicTest();test.getCurrentValue();test.setValue();//返回旧值系列test.getAndSet();test.getAndIncrement();test.getAndDecrement();test.getAndAdd();//返回新值系列test.incrementAndGet();test.decrementAndGet();test.addAndGet();}
}

2、AtomicInteger的源代码

  private volatile int value;// 初始化值/*** 创建一个AtomicInteger,初始值value为initialValue*/public AtomicInteger(int initialValue) {value = initialValue;}/*** 创建一个AtomicInteger,初始值value为0*/public AtomicInteger() {}/*** 返回value*/public final int get() {return value;}/*** 为value设值(基于value),而其他操作是基于旧值<--get()*/public final void set(int newValue) {value = newValue;}public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}/*** 基于CAS为旧值设定新值,采用无限循环,直到设置成功为止* * @return 返回旧值*/public final int getAndSet(int newValue) {for (;;) {int current = get();// 获取当前值(旧值)if (compareAndSet(current, newValue))// CAS新值替代旧值return current;// 返回旧值}}/*** 当前值+1,采用无限循环,直到+1成功为止* @return the previous value 返回旧值*/public final int getAndIncrement() {for (;;) {int current = get();//获取当前值int next = current + 1;//当前值+1if (compareAndSet(current, next))//基于CAS赋值return current;}}/*** 当前值-1,采用无限循环,直到-1成功为止 * @return the previous value 返回旧值*/public final int getAndDecrement() {for (;;) {int current = get();int next = current - 1;if (compareAndSet(current, next))return current;}}/*** 当前值+delta,采用无限循环,直到+delta成功为止 * @return the previous value  返回旧值*/public final int getAndAdd(int delta) {for (;;) {int current = get();int next = current + delta;if (compareAndSet(current, next))return current;}}/*** 当前值+1, 采用无限循环,直到+1成功为止* @return the updated value 返回新值*/public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;//返回新值}}/*** 当前值-1, 采用无限循环,直到-1成功为止 * @return the updated value 返回新值*/public final int decrementAndGet() {for (;;) {int current = get();int next = current - 1;if (compareAndSet(current, next))return next;//返回新值}}/*** 当前值+delta,采用无限循环,直到+delta成功为止  * @return the updated value 返回新值*/public final int addAndGet(int delta) {for (;;) {int current = get();int next = current + delta;if (compareAndSet(current, next))return next;//返回新值}}/*** 获取当前值*/public int intValue() {return get();}

注意:

  • value是volatile的,具体可以参考我的这篇文章深入浅出JVM系列(四):Java内存模型(JMM)
  • 单步操作:例如set()是直接对value进行操作的,不需要CAS,因为单步操作就是原子操作。
  • 多步操作:例如getAndSet(int newValue)是两步操作–>先获取值,在设置值,所以需要原子化,这里采用CAS实现。
  • 对于方法是返回旧值还是新值,直接看方法是以get开头(返回旧值)还是get结尾(返回新值)就好
  • CAS:比较CPU内存上的值是不是当前值current,如果是就换成新值update,如果不是,说明获取值之后到设置值之前,该值已经被别人先一步设置过了,此时如果自己再设置值的话,需要在别人修改后的值的基础上去操作,否则就会覆盖别人的修改,所以这个时候会直接返回false,再进行无限循环,重新获取当前值,然后再基于CAS进行加减操作。
  • 如果还是不懂CAS,类比数据库的乐观锁。

3、unsafe类

重点关注这个方法

 public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}


如图所示,这里的代码用到了自旋锁的相关知识,其实就是不断获取内存地址的最新值,然后利用CAS判断比较。

上面源码分析时,提到最后调用了compareAndSwapInt方法,接着继续深入探讨该方法,该方法在Unsafe中对应的源码如下。

可以看到调用了“Atomic::cmpxchg”方法,保证了CAS操作是一个原子操作,保证了CAS同时具有volatile读和volatile写的内存语义。

CAS并发原语体现在sun.misc.Unsafe类中的各个方法,调用Unsafe类的CASfangfa ,JVM会帮我们事先CAS汇编指令,这是一种完全依赖于硬件的功能,它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,这是若干个指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

四、CAS优缺点

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。
  2. 只能保证一个共享变量的原子操作。
  3. ABA问题。
  • 循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

  • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  • 什么是ABA问题?ABA问题怎么解决?

    CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

    但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

    如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

五、Java 8对CAS机制的优化

大量的线程同时并发修改一个AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起CAS操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环。

在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。

Java 8新推出一个类:LongAdder,它尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能.


在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,比如刚开始累加成了base = 5。

接着如果发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。

这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!

这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!

而且他内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。

最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。

假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。

但是,如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操作。

六、参考资料

第十一章 AtomicInteger源码解析
面试必问的CAS,你懂了吗?

java常见面试考点(二十五):CAS是什么相关推荐

  1. JAVA常见算法题(二十五)

    /*** Java实现中文数字转换为阿拉伯数字* * * @author WQ**/ public class Demo26 {public static void main(String[] arg ...

  2. java常见面试考点(十一):git与svn区别

    java常见面试考点 往期文章推荐:   java常见面试考点(六):深入理解String类型   java常见面试考点(七):递归与迭代   java常见面试考点(八):成员变量与局部变量   ja ...

  3. java面试(二十五)--(1)redis为什么读写速率快性能好(2)说说web.xml文件中可以配置哪些内容(3)和的区别(4)扑克牌顺子

    1. redis为什么读写速率快性能好? 1.Redis将数据存储在内存上,避免了频繁的IO操作 2.Redis其本身采用字典的数据结构,时间复杂度为O(1),且其采用渐进式的扩容手段 3.Redis ...

  4. 【Java学习笔记之二十五】初步认知Java内部类

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人对内部类也只是略知一二). 第一次见面 内部类我们从外面看是非常容易理解的,无非就是在 ...

  5. 【精选】JAVA算法题(二十五)

    好长时间没有写博客了,之前因为期末考试耽误了一段时间,回家又玩了几天,然后又赶来上海入职,所以就把博客这事给忘了,哈哈,懒惰啊. 一.最长回文字符串 题目: /*** 给定一个包含大写字母和小写字母的 ...

  6. Java真的不难(二十五)Stream流

    Stream流 上篇文章讲了Java 8 的一个新特性:Lambda表达式,在业务中若能熟练的使用,可以节省很多代码量,看着也整洁很多.那么这篇文章将介绍另一个新特性:Stream流,不要看错哈!!! ...

  7. B站韩顺平java学习笔记(二十五)-- 正则表达式章节

    目录 一  正则表达式入门 1  极速体验正则表达式威力 二  为什么要学正则表达式 三  正则表达式基本介绍 1  介绍 2  正则表达式底层实现

  8. 【Java学习笔记之二十六】深入理解Java匿名内部类

    在[Java学习笔记之二十五]初步认知Java内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节问题,所以就衍生出这篇博客.在这篇博客中你可以了解到匿名内部类的使用.匿名内部类要注意 ...

  9. JAVA之旅(三十五)——完结篇,终于把JAVA写完了,真感概呐!

    JAVA之旅(三十五)--完结篇,终于把JAVA写完了,真感概呐! 这篇博文只是用来水经验的,写这个系列是因为我自己的java本身也不是特别好,所以重温了一下,但是手比较痒于是就写出了这三十多篇博客了 ...

最新文章

  1. 脑电数据分析工具汇总
  2. Spring MVC 数据验证——validate注解方式
  3. java 链接为分布式 hbase,hbase学习记录(一):hbase伪分布式安装
  4. java定时执行一段代码
  5. 计算机四级软件工程知识点,计算机四级考试题库及搜题软件,送一份备考指南给大家!...
  6. 洛谷——P1680 奇怪的分组
  7. Java 208 道面试题:第一模块答案
  8. java nutch 爬虫_Java分布式爬虫Nutch教程——导入Nutch工程,执行完整爬取
  9. java jdom_java中JDOM的基本使用方法
  10. 墨天轮“高可用架构”干货文档分享(含Oracle、MySQL、PG资料124篇)
  11. 二叉树的深度遍历和广度遍历
  12. java怎么遵循ws规范,WS-BPEL语言基础
  13. switch-case案例*
  14. 订单管理html页面,订单管理.html
  15. 微信网页授权获取用户昵称中文乱码
  16. 怎么删除电脑上的另一个用户名?删除电脑上多余的用户名
  17. 小米note刷android8.1,小米note安卓8.1.0刷机包
  18. ps的钢笔工具抠图方法
  19. 远征日服·信喵之野望 按键精灵脚本6.高级自动抽吉
  20. Proguard 常用规则

热门文章

  1. 鸿蒙系统安卓模拟器,华为鸿蒙OS再次爆出缺陷!被误判为安卓模拟器:不属于手机系统设备...
  2. H5下载视频到andriod/ios相册中
  3. Eigen库矩阵的求逆函数无法在CUDA代码使用
  4. [转贴] 男人必看的故事
  5. 史上最全的dB分贝单位合集: dB,dBFS, dB FS, dBTP, dB TP, dBO, dBov, dBu/dBv, dBV, dBm/dBmW, dBW,...
  6. 适合装u盘的linux版本,U盘安装linux(针对各个版本测试成功)
  7. JAVA基础(一)-面试篇
  8. 下载付费音乐(贝塔的随手日记)
  9. [转]win7.lnk文件打开方式修复
  10. PREV-6翻硬币(Java)