热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面试官问我Java并发/多线程CAS原理分析,我这样回答给了我30koffer

目录什么是CAS并发安全问题举一个典型的例子i++如何解决?底层原理CAS需要注意的问题使用限制ABA问题概念解决方案高竞争下的开销问

目录

  • 什么是CAS

  • 并发安全问题

    • 举一个典型的例子i++

    • 如何解决?

    • 底层原理


  • CAS需要注意的问题

    • 使用限制

    • ABA 问题

      • 概念

        • 解决方案



    • 高竞争下的开销问题



什么是CAS

CAS 即 compare and swap,比较并交换。


CAS是一种原子操作,同时 CAS 使用乐观锁机制。

J.U.C中的很多功能都是建立在 CAS 之上,各种原子类,其底层都用 CAS来实现原子操作。用来解决并发时的安全问题。

另外本人整理收藏了20年多家公司面试知识点整理 ,以及各种Java核心知识点免费分享给大家,想要资料的话请点(点击此处)来免费获取!

并发安全问题

举一个典型的例子i++

public class AddTest {
public volatile int i;
public void add() {
i++;
}
}

通过javap -c AddTest可以看到add 方法的字节码指令:

public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return

i++被拆分成了多个指令:


  1. 执行getfield拿到原始内存值;

  2. 执行iadd进行加 1 操作;

  3. 执行putfield写把累加后的值写回内存。

假设一种情况:


  • 线程 1 执行到iadd时,由于还没有执行putfield,这时候并不会刷新主内存区中的值。

  • 此时线程 2 进入开始运行,刚刚将主内存区的值拷贝到私有内存区。

  • 线程 1正好执行putfield,更新主内存区的值,那么此时线程 2 的副本就是旧的了。错误就出现了。


如何解决?

最简单的,在 add 方法加上 synchronized 。

public class AddTest {
public volatile int i;
public synchronized void add() {
i++;
}
}

虽然简单,并且解决了问题,但是性能表现并不好。

最优的解法应该是使用JDK自带的CAS方案,如上例子,使用AtomicInteger

public class AddIntTest {
public AtomicInteger i;
public void add() {
i.getAndIncrement();
}
}

底层原理

CAS 的原理并不复杂:


  • 三个参数,一个当前内存值 V、预期值 A、更新值 B

  • 当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true

  • 否则什么都不做,并返回 false

拿 AtomicInteger 类分析,先来看看源码:

我这里的环境是Java11,如果是Java8这里一些内部的一些命名有些许不同。

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersiOnUID= 6214790243416807050L;
/*
* This class intended to be implemented using VarHandles, but there
* are unresolved cyclic startup dependencies.
*/
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
//...
}

Unsafe 类,该类对一般开发而言,少有用到。

Unsafe 类底层是用 C/C++ 实现的,所以它的方式都是被 native 关键字修饰过的。

它可以提供硬件级别的原子操作,如获取某个属性在内存中的位置、修改对象的字段值。

关键点:


  • AtomicInteger 类存储的值在 value 字段中,而value字段被volatile


  • 在静态代码块中,并且获取了 Unsafe 实例,获取了 value 字段在内存中的偏移量 VALUE


接下回到刚刚的例子:

如上,getAndIncrement() 方法底层利用 CAS 技术保证了并发安全。

public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}

getAndAddInt() 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}

v 通过 getIntVolatile(o, offset)方法获取,其目的是获取 o 在 offset 偏移量的值,其中 o 就是 AtomicInteger 类存储的值,即value, offset 内存偏移量的值,即 VALUE

重点weakCompareAndSetInt 就是实现 CAS 的核心方法


  • 如果 o 和 v相等,就证明没有其他线程改变过这个变量,那么就把 v 值更新为 v + delta,其中 delta 是更新的增量值。

  • 反之 CAS 就一直采用自旋的方式继续进行操作,这一步也是一个原子操作。

分析:


  • 设定 AtomicInteger 的原始值为 A,线程 1 和线程 2 各自持有一份副本,值都是 A。



  1. 线程 1 通过getIntVolatile(o, offset)拿到 value 值 A,这时线程 1 被挂起。

  2. 线程 2 也通过getIntVolatile(o, offset)方法获取到 value 值 A,并执行weakCompareAndSetInt方法比较内存值也为 A,成功修改内存值为 B。

  3. 这时线程 1 恢复执行weakCompareAndSetInt方法比较,发现自己手里的值 A 和内存的值 B 不一致,说明该值已经被其它线程提前修改过了。

  4. 线程 1 重新执行getIntVolatile(o, offset)再次获取 value 值,因为变量 value 被 volatile 修饰,具有可见性,线程A继续执行weakCompareAndSetInt进行比较替换,直到成功


CAS需要注意的问题

使用限制

CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受限。

但是CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS无能为力。

原子性也不一定能保证线程安全,如在Java中需要与volatile配合来保证线程安全。

ABA 问题


概念

CAS 有一个问题,举例子如下:


  • 线程 1 从内存位置 V 取出 A

  • 这时候线程 2 也从内存位置 V 取出 A

  • 此时线程 1 处于挂起状态,线程 2 将位置 V 的值改成 B,最后再改成 A

  • 这时候线程 1 再执行,发现位置 V 的值没有变化,符合期望继续执行。

此时虽然线程 1还是成功了,但是这并不符合我们真实的期望,等于线程 2狸猫换太子线程 1耍了。

这就是所谓的ABA问题

解决方案

引入原子引用,带版本号的原子操作。

把我们的每一次操作都带上一个版本号,这样就可以避免ABA问题的发生。既乐观锁的思想。


  • 内存中的值每发生一次变化,版本号都更新。


  • 在进行CAS操作时,比较内存中的值的同时,也会比较版本号,只有当二者都没有变化时,才能执行成功。


  • Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。



高竞争下的开销问题



  • 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。


  • 针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。


  • 更重要的是避免在高竞争环境下使用乐观锁。


另外本人整理收藏了20年多家公司面试知识点整理 ,以及各种Java核心知识点免费分享给大家,想要资料的话请点(点击此处)来免费获取!


推荐阅读
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • ListView简单使用
    先上效果:主要实现了Listview的绑定和点击事件。项目资源结构如下:先创建一个动物类,用来装载数据:Animal类如下:packagecom.example.simplelis ... [详细]
  • 本文将详细探讨 Java 中提供的不可变集合(如 `Collections.unmodifiableXXX`)和同步集合(如 `Collections.synchronizedXXX`)的实现原理及使用方法,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • 软件工程课堂测试2
    要做一个简单的保存网页界面,首先用jsp写出保存界面,本次界面比较简单,首先是三个提示语,后面是三个输入框,然 ... [详细]
  • 本文详细探讨了Java中的ClassLoader类加载器的工作原理,包括其如何将class文件加载至JVM中,以及JVM启动时的动态加载策略。文章还介绍了JVM内置的三种类加载器及其工作方式,并解释了类加载器的继承关系和双亲委托机制。 ... [详细]
  • 在寻找轻量级Ruby Web框架的过程中,您可能会遇到Sinatra和Ramaze。两者都以简洁、轻便著称,但它们之间存在一些关键区别。本文将探讨这些差异,并提供详细的分析,帮助您做出最佳选择。 ... [详细]
  • 近期我们开发了一款包含天气预报功能的万年历应用,为了满足这一需求,团队花费数日时间精心打造并测试了一个稳定可靠的天气API接口,现正式对外开放。 ... [详细]
  • 优化Flask应用的并发处理:解决Mysql连接过多问题
    本文探讨了在Flask应用中通过优化后端架构来应对高并发请求,特别是针对Mysql 'too many connections' 错误的解决方案。我们将介绍如何利用Redis缓存、Gunicorn多进程和Celery异步任务队列来提升系统的性能和稳定性。 ... [详细]
  • Spring Boot 中静态资源映射详解
    本文深入探讨了 Spring Boot 如何简化 Web 应用中的静态资源管理,包括默认的静态资源映射规则、WebJars 的使用以及静态首页的处理方法。通过本文,您将了解如何高效地管理和引用静态资源。 ... [详细]
  • 本文详细介绍了如何在 Android 中使用值动画(ValueAnimator)来动态调整 ImageView 的高度,并探讨了相关的关键属性和方法,包括图片填充后的高度、原始图片高度、动画变化因子以及布局重置等。 ... [详细]
  • 本文深入探讨了UNIX/Linux系统中的进程间通信(IPC)机制,包括消息传递、同步和共享内存等。详细介绍了管道(Pipe)、有名管道(FIFO)、Posix和System V消息队列、互斥锁与条件变量、读写锁、信号量以及共享内存的使用方法和应用场景。 ... [详细]
  • KMP算法是处理字符串匹配的一种高效算法它首先用O(m)的时间对模板进行预处理,然后用O(n)的时间完成匹配。从渐进的意义上说,这样时间复 ... [详细]
  • 深入浅出TensorFlow数据读写机制
    本文详细介绍TensorFlow中的数据读写操作,包括TFRecord文件的创建与读取,以及数据集(dataset)的相关概念和使用方法。 ... [详细]
author-avatar
手机用户2602916737
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有