01_本文章前提要求和说明
一些大厂的面试题
某蚁某呗一面 :
- Java 容器有哪些 ? 哪些是同步容器, 哪些是并发容器 ?
- ArrayList 和 LinkedList 的插入和访问的时间复杂度 ?
- java 反射原理 , 注解原理 ?
- 新生代分为几个区 ? 使用什么算法进行垃圾回收 ? 为什么使用这个算法 ?
- HashMap 在什么情况下会扩容 , 或者有哪些操作会导致扩容 ?
- HashMap push 方法的执行过程 ?
- HashMap 检测到 hash 冲突后 , 将元素插入在链表的末尾还是开头 ?
- 1.8 还采用了红黑树 , 讲讲红黑树的特性 , 为什么人家一定要用红黑树而不是 AVL、B 树之类的 ?
- https 和 http 区别 , 有没有用过其他安全传输手段 ?
- 线程池的工作原理 , 几个重要参数 , 然后给了具体几个参数分析线程池会怎么做 , 最后问阻塞队列的作用是什么 ?
- linux 怎么查看系统负载情况 ?
- 请详细描述 springmvc 处理请求全流程 ?spring 一个 bean 装配的过程 ?
- 讲一讲 AtomicInteger, 为什么要用 CAS 而不是 synchronized?
某团一面 :
- 最近做的比较熟悉的项目是哪个 , 画一下项目技术架构图。
- JVM 老年代和新生代的比例 ?
- YGC 和 FGC 发生的具体场景 ?
- jstack,jmap,jutil 分别的意义 ? 如何线上排查 JVM 的相关问题 ?
- 线程池的构造类的方法的 5 个参数的具体意义 ?
- 单机上一个线程池正在处理服务如果忽然断电怎么办 ( 正在处理和阻塞队列里的请求怎么处理)?
- 使用无界阻塞队列会出现什么问题 ? 接口如何处理重复请求 ?
某度一面 :
- 介绍一下集合框架 ?
- hashmap hastable 底层实现什么区别 ?hashtable 和 concurrenthashtable 呢 ?
- hashmap 和 treemap 什么区别 ? 低层数据结构是什么 ?
- 线程池用过吗都有什么参数 ? 底层如何实现的 ?
- sychnized 和 Lock 什么区别 ?sychnize 什么情况情况是对象锁 ? 什么时候是全局锁为什么 ?
- ThreadLocal 是什么底层如何实现 ? 写一个例子呗 ?
- volitile 的工作原理 ?
- cas 知道吗如何实现的 ?
- 请用至少四种写法写一个单例模式 ?
- 请介绍一下 JVM 内存模型 ? 用过什么垃圾回收器都说说呗线上发送频繁 full gc 如何处理 ?CPU 使用率过高怎么办 ? 如何定位问题 ? 如何解决说一下解决思路和处理方法
- 知道字节码吗 ? 字节码都有哪些 ?Integer x =5,int y =5, 比较 x =y 都经过哪些步骤 ? 讲讲类加载机制呗都有哪些类加载器 , 这些类加载器都加载哪些文件 ?
- 手写一下类加载 Demo
- 知道 osgi 吗 ? 他是如何实现的 ?
- 请问你做过哪些 JVM 优化 ? 使用什么方法达到什么效果 ?
- classforName(“java.lang.String”)和 String classgetClassLoader() LoadClass(“java.lang.String”)什么区别啊 ?
某条
- HashMap 如果一直 put 元素会怎么样 ? hashcode 全都相同如何 ?
- ApplicationContext 的初始化过程 ?
- GC 用什么收集器 ? 收集的过程如何 ? 哪些部分可以作为 GC Root?
- Volatile 关键字 , 指令重排序有什么意义 ?synchronied, 怎么用 ?
- Redis 数据结构有哪些 ? 如何实现 sorted set?
- 并发包里的原子类有哪些 , 怎么实现 ?
- MvSql 索引是什么数据结构 ? B tree 有什么特点 ? 优点是什么 ?
- 慢查询怎么优化 ?
- 项目: cache, 各部分职责 , 有哪些优化点
某东金融面试
- Dubbo 超时重试 ;Dubbo 超时时间设置
- 如何保障请求执行顺序
- 分布式事务与分布式锁(扣款不要出现负数)
- 分布式 Session 设置
- 执行某操作 , 前 50 次成功 , 第 51 次失败 a 全部回滚 b 前 50 次提交第 51 次抛异常 ,ab 场景分别如何设计 Spring (传播特性)
- Zookeeper 有却些作用
- JVM 内存模型
- 数据库垂直和水平拆分
- MyBatis 如何分页; 如何设置缓存;MySQL 分页
某蚁金服二面
- 自我介绍、工作经历、技术栈
- 项目中你学到了什么技术 ?(把三项目具体描述了很久)
- 微服务划分的粒度
- 微服务的高可用怎么保证的 ?
- 常用的负载均衡 , 该怎么用 , 你能说下吗 ?
- 网关能够为后端服务带来哪些好处 ?
- Spring Bean 的生命周期
- HashSet 是不是线程安全的 ? 为什么不是线程安全的 ?
- Java 中有哪些线程安全的 Map?
- Concurrenthashmap 是怎么做到线程安全的 ?
- HashTable 你了解过吗 ?
- 如何保证线程安全问题 ?
- synchronized、lock
- volatile 的原子性问题 ? 为什么 i ++ 这种不支持原子性﹖从计算机原理的设计来讲下不能保证原子性的原因
- happens before 原理
- cas 操作
- lock 和 synchronized 的区别 ?
- 公平锁和非公平锁
- Java 读写锁
- 读写锁设计主要解决什么问题 ?
02_volatile 是什么
volatile 是 JVM 提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排 ( 保证有序性 )
03_JMM 内存模型之可见性
JMM(Java 内存模型 Java Memory Model, 简称 JMM) 本身是一种抽象的概念并不真实存在 , 它描述的是一组规则或规范 , 通过这组规范定义了程序中各个变量 ( 包括实例字段 , 静态字段和构成数组对象的元素 ) 的访问方式。
JMM 关于同步的规定 :
- 线程解锁前 , 必须把共享变量的值刷新回主内存
- 线程加锁前 , 必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于 JVM 运行程序的实体是线程 , 而每个线程创建时 JVM 都会为其创建一个工作内存 ( 有些地方称为栈空间 ), 工作内存是每个线程的私有数据区域 , 而 Java 内存模型中规定所有变量都存储在主内存 , 主内存是共享内存区域 , 所有线程都可以访问 , 但线程对变量的操作 ( 读取赋值等 ) 必须在工作内存中进行 , 首先要将变量从主内存拷贝的自己的工作内存空间 , 然后对变量进行操作 , 操作完成后再将变量写回主内存 , 不能直接操作主内存中的变量 , 各个线程中的工作内存中存储着主内存中的变量副本拷贝 , 因此不同的线程间无法访问对方的工作内存 , 线程间的通信 ( 传值 ) 必须通过主内存来完成 , 其简要访问过程如下图 :
可见性
通过前面对 JMM 的介绍 , 我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程 AAA 修改了共享变量 X 的值但还未写回主内存时 , 另外一个线程 BBB 又对主内存中同一个共享变量 X 进行操作 , 但此时 A 线程工作内存中共享变量 x 对线程 B 来说并不可见 , 这种工作内存与主内存同步延迟现象就造成了可见性问题
04_可见性的代码验证说明
import java.util.concurrent.TimeUnit; /** * 假设是主物理内存 */ class MyData {//volatile int number = 0; int number = 0; public void addTo60() {this.number = 60;} } /** * 验证 volatile 的可见性 * 1. 假设 int number = 0, number 变量之前没有添加 volatile 关键字修饰 */ public class VolatileDemo {public static void main(String args []) {// 资源类 MyData myData = new MyData(); // AAA 线程 实现了 Runnable 接口的 ,lambda 表达式 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); // 线程睡眠 3 秒 , 假设在进行运算 try {TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace(); } // 修改 number 的值 myData.addTo60(); // 输出修改后的值 System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number); }, "AAA").start(); // main 线程就一直在这里等待循环 , 直到 number 的值不等于零 while(myData.number == 0) {} // 按道理这个值是不可能打印出来的 , 因为主线程运行的时候 ,number 的值为 0 , 所以一直在循环 // 如果能输出这句话 , 说明 AAA 线程在睡眠 3 秒后 , 更新的 number 的值 , 重新写入到主内存 , 并被 main 线程感知到了 System.out.println(Thread.currentThread().getName() + "\t mission is over"); } }
由于没有 volatile 修饰 MyData 类的成员变量 number,main 线程将会卡在 while(myData.number == 0) {}, 不能正常结束。若想正确结束 , 用 volatile 修饰 MyData 类的成员变量 number 吧。
volatile 类比
没有 volatile 修饰变量效果 , 相当于 A 同学拷贝了老师同一课件 ,A 同学对课件进一步的总结归纳 , 形成自己的课件 , 这就与老师的课件不同了。
有 volatile 修饰变量效果 , 相当于 A 同学拷贝了老师同一课件 ,A 同学对课件进一步的总结归纳 , 形成自己的课件 , 并且与老师分享 , 老师认可 A 同学修改后的课件 , 并用它来作下一届的课件。
05_volatile 不保证原子性
原子性指的是什么意思 ?
不可分割 , 完整性 , 也即某个线程正在做某个具体业务时 , 中间不可以被加塞或者被分割。需要整体完整要么同时成功 , 要么同时失败。
volatile 不保证原子性案例演示 :
class MyData2 {/** * volatile 修饰的关键字 , 是为了增加 主线程和线程之间的可见性 , 只要有一个线程修改了内存中的值 , 其它线程也能马上感知 */ volatile int number = 0; public void addPlusPlus() {number ++;} } public class VolatileAtomicityDemo {public static void main(String[] args) {MyData2 myData = new MyData2(); // 创建 10 个线程 , 线程里面进行 1000 次循环 for (int i = 0; i < 20; i++) {new Thread(() -> {// 里面 for (int j = 0; j < 1000; j++) {myData.addPlusPlus(); } }, String.valueOf(i)).start();} // 需要等待上面 20 个线程都计算完成后 , 在用 main 线程取得最终的结果值 // 这里判断线程数是否大于 2 , 为什么是 2 ? 因为默认是有两个线程的 , 一个 main 线程 , 一个 gc 线程 while(Thread.activeCount() > 2) {// yield 表示不执行 Thread.yield(); } // 查看最终的值 // 假设 volatile 保证原子性 , 那么输出的值应该为 : 20 * 1000 = 20000 System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number); } }
最后的结果总是小于 20000。
06_volatile 不保证原子性理论解释
number++ 在多线程下是非线程安全的。
我们可以将代码编译成字节码 , 可看出 number++ 被编译成 3 条指令。
假设我们没有加 synchronized 那么第一步就可能存在着 , 三个线程同时通过 getfield 命令 , 拿到主存中的 n 值 , 然后三个线程 , 各自在自己的工作内存中进行加 1 操作 , 但他们并发进行 iadd 命令的时候 , 因为只能一个进行写 , 所以其它操作会被挂起 , 假设 1 线程 , 先进行了写操作 , 在写完后 ,volatile 的可见性 , 应该需要告诉其它两个线程 , 主内存的值已经被修改了 , 但是因为太快了 , 其它两个线程 , 陆续执行 iadd 命令 , 进行写入操作 , 这就造成了其他线程没有接受到主内存 n 的改变 , 从而覆盖了原来的值 , 出现写丢失 , 这样也就让最终的结果少于 20000。
07_volatile 不保证原子性问题解决
可加 synchronized 解决 , 但它是重量级同步机制 , 性能上有所顾虑。
如何不加 synchronized 解决 number++ 在多线程下是非线程安全的问题 ? 使用 AtomicInteger。
import java.util.concurrent.atomic.AtomicInteger; class MyData2 {/** * volatile 修饰的关键字 , 是为了增加 主线程和线程之间的可见性 , 只要有一个线程修改了内存中的值 , 其它线程也能马上感知 */ volatile int number = 0; AtomicInteger number2 = new AtomicInteger(); public void addPlusPlus() { number ++;} public void addPlusPlus2() { number2.getAndIncrement(); } } public class VolatileAtomicityDemo {public static void main(String[] args) {MyData2 myData = new MyData2(); // 创建 10 个线程 , 线程里面进行 1000 次循环 for (int i = 0; i < 20; i++) {new Thread(() -> {// 里面 for (int j = 0; j < 1000; j++) {myData.addPlusPlus(); myData.addPlusPlus2();} }, String.valueOf(i)).start();} // 需要等待上面 20 个线程都计算完成后 , 在用 main 线程取得最终的结果值 // 这里判断线程数是否大于 2 , 为什么是 2 ? 因为默认是有两个线程的 , 一个 main 线程 , 一个 gc 线程 while(Thread.activeCount() > 2) {// yield 表示不执行 Thread.yield(); } // 查看最终的值 // 假设 volatile 保证原子性 , 那么输出的值应该为 : 20 * 1000 = 20000 System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number); System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2); } }
输出结果为 :
main finally number value: 18766 main finally number2 value: 20000
08_volatile 指令重排案例 1
计算机在执行程序时 , 为了提高性能 , 编译器和处理器的常常会对指令做重排 , 一般分以下 3 种 :
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的 数据依赖性
多线程环境中线程交替执行 , 由于编译器优化重排的存在 , 两个线程中使用的变量能否保证一致性是无法确定的 , 结果无法预测。
重排案例
public void mySort{int x = 11;// 语句 1 int y = 12;// 语句 2 × = × + 5;// 语句 3 y = x * x;// 语句 4}
可重排序列 :
- 1234
- 2134
- 1324
问题 : 请问语句 4 可以重排后变成第一个条吗 ? 答 : 不能。
重排案例 2
int a,b,x,y = 0
线程 1 |
线程 2 |
x = a; |
y = b; |
b = 1; |
a = 2; |
x = 0; y = 0 |
如果编译器对这段程序代码执行重排优化后 , 可能出现下列情况 :
线程 1 |
线程 2 |
b = 1; |
a = 2; |
x = a; |
y = b; |
x = 2; y = 1 |
这也就说明在多线程环境下 , 由于编译器优化重排的存在 , 两个线程中使用的变量能否保证一致性是无法确定的。
09_volatile 指令重排案例 2
观察以下程序 :
public class ReSortSeqDemo{int a = 0; boolean flag = false; public void method01(){a = 1;// 语句 1 flag = true;// 语句 2} public void method02(){ if(flag){a = a + 5; // 语句 3} System.out.println("retValue: " + a);// 可能是 6 或 1 或 5 或 0 } }
多线程环境中线程交替执行 method01()和 method02(), 由于编译器优化重排的存在 , 两个线程中使用的变量能否保证一致性是无法确定的 , 结果无法预测。
禁止指令重排小总结
volatile 实现 禁止指令重排优化, 从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念 , 内存屏障(Memory Barrier) 又称内存栅栏 , 是一个 CPU 指令 , 它的作用有两个:
- 保证特定操作的执行顺序 ,
- 保证某些变量的内存可见性 ( 利用该特性实现 volatile 的内存可见性 )。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU, 不管什么指令都不能和这条 Memory Barrier 指令重排序 , 也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种 CPU 的缓存数据 , 因此任何 CPU 上的线程都能读取到这些数据的最新版本。
对 volatile 变量进行写操作时 , 会在写操作后加入一条 store 屏障指令 , 将工作内存中的共享变量值刷新回到主内存。
文章可能太长 , 需要完整的私信博主 OK
对 Volatile 变量进行读操作时 , 会在读操作前加入一条 load 屏障指令 , 从主内存中读取共享变量。
线性安全性获得保证
- 工作内存与主内存同步延迟现象导致的可见性问题 – 可以使用 synchronized 或 volatile 关键字解决 , 它们都可以使一个线程修改后的变量立即对其他线程可见。
- 对于指令重排导致的可见性问题和有序性问题 – 可以利用 volatile 关键字解决 , 因为 volatile 的另外一个作用就是禁止重排序优化。
10_单例模式在多线程环境下可能存在安全问题
懒汉单例模式
public class SingletonDemo {private static SingletonDemo instance = null; private SingletonDemo () {System.out.println(Thread.currentThread().getName() + "\t 我是构造方法 SingletonDemo"); } public static SingletonDemo getInstance() { if(instance == null) {instance = new SingletonDemo(); } return instance; } public static void main(String[] args) {// 这里的 == 是比较内存地址 System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); } }
输出结果 :
main 我是构造方法 singletonDemo true true true true
但是 , 在多线程环境运行上述代码 , 能保证单例吗 ?
public class SingletonDemo {private static SingletonDemo instance = null; private SingletonDemo () {System.out.println(Thread.currentThread().getName() + "\t 我是构造方法 SingletonDemo"); } public static SingletonDemo getInstance() { if(instance == null) {instance = new SingletonDemo(); } return instance; } public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {SingletonDemo.getInstance(); }, String.valueOf(i)).start();} } }
输出结果 :
4 我是构造方法 SingletonDemo 2 我是构造方法 SingletonDemo 5 我是构造方法 SingletonDemo 6 我是构造方法 SingletonDemo 0 我是构造方法 SingletonDemo 3 我是构造方法 SingletonDemo 1 我是构造方法 SingletonDemo
显然不能保证单例。
解决方法之一 : 用 synchronized 修饰方法 getInstance(), 但它属重量级同步机制 , 使用时慎重。
public synchronized static SingletonDemo getInstance() { if(instance == null) {instance = new SingletonDemo(); } return instance; }
11_单例模式 volatile 分析
解决方法之二 :DCL(Double Check Lock 双端检锁机制 )
public class SingletonDemo{private SingletonDemo(){} private volatile static SingletonDemo instance = null; public static SingletonDemo getInstance() {if(instance == null) {synchronized(SingletonDemo.class){if(instance == null){instance = new SingletonDemo(); } } } return instance; } }
DCL 中 volatile 解析
原因在于某一个线程执行到第一次检测 , 读取到的 instance 不为 null 时 ,instance 的引用对象 可能没有完成初始化。instance = new SingletonDemo(); 可以分为以下 3 步完成(伪代码):
memory = allocate(); //1. 分配对象内存空间 instance(memory); //2. 初始化对象 instance = memory; //3. 设置 instance 指向刚分配的内存地址 , 此时 instance != null
步骤 2 和步骤 3 不存在数据依赖关系 , 而且无论重排前还是重排后程序的执行结果在单线程中并没有改变 , 因此这种重排优化是允许的。
memory = allocate(); //1. 分配对象内存空间 instance = memory;//3. 设置 instance 指向刚分配的内存地址 , 此时 instance! =null, 但是对象还没有初始化完成! instance(memory);//2. 初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程), 但并不会关心多线程间的语义一致性。
所以当一条线程访问 instance 不为 null 时 , 由于 instance 实例未必已初始化完成 , 也就造成了线程安全问题。
12_CAS 是什么
Compare And Set
示例程序
public class CASDemo{public static void main(string[] args){AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . .. System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get()); System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get()); } }
输出结果为
true 2019 false 2019
13_CAS 底层原理 - 上
Cas 底层原理 ? 如果知道 , 谈谈你对 UnSafe 的理解
atomiclnteger.getAndIncrement(); 源码
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(); private static final long valueOffset; static {try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) {throw new Error(ex); } } private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) {value = initialValue;} /** * Creates a new AtomicInteger with initial value {@code 0}. */ public AtomicInteger() {} ... /** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } ... }
nSafe
1 Unsafe
是 CAS 的核心类 , 由于 Java 方法无法直接访问底层系统 , 需要通过本地 (native) 方法来访问 ,Unsafe 相当于一个后门 , 基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中 , 其内部方法操作可以像 C 的指针一样直接操作内存 , 因为 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法。
注意 Unsafe 类中的所有方法都是 native 修饰的 , 也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应任务。
2 变量 valueOffset, 表示该变量值在内存中的偏移地址 , 因为 Unsafe 就是根据内存偏移地址获取数据的。
3 变量 value 用 volatile 修饰 , 保证了多线程之间的内存可见性。
CAS 是什么
CAS 的全称为 Compare-And-Swap, 它是一条 CPU 并发原语。
它的功能是判断内存某个位置的值是否为预期值 , 如果是则更改为新的值 , 这个过程是原子的。
CAS 并发原语体现在 JAVA 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法 ,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能 , 通过它实现了原子操作。再次强调 , 由于 CAS 是一种系统原语 , 原语属于操作系统用语范畴 , 是由若干条指令组成的 , 用于完成某个功能的一个过程 , 并且原语的执行必须是连续的 , 在执行过程中不允许被中断 , 也就是说 CAS 是一条 CPU 的原子指令 , 不会造成所谓的数据不一致问题。( 原子性 )
14_CAS 底层原理 - 下
继续上一节
UnSafe.getAndAddInt()源码解释 :
- var1 AtomicInteger 对象本身。
- var2 该对象值得引用地址。
- var4 需要变动的数量。
- var5 是用过 var1,var2 找出的主内存中真实的值。
- 用该对象当前的值与 var5 比较 :
- 如果相同 , 更新 var5+var4 并且返回 true,
- 如果不同 , 继续取值然后再比较 , 直到更新完成。
假设 线程 A 和 线程 B 两个线程同时执行 getAndAddInt 操作 ( 分别跑在不同 CPU 上) :
- Atomiclnteger 里面的 value 原始值为 3 , 即主内存中 Atomiclnteger 的 value 为 3 , 根据 JMM 模型 , 线程 A 和线程 B 各自持有一份值为 3 的 value 的副本分别到各自的工作内存。
- 线程 A 通过 getIntVolatile(var1, var2)拿到 value 值 3 , 这时线程 A 被挂起。
- 线程 B 也通过 getintVolatile(var1, var2)方法获取到 value 值 3 , 此时刚好线程 B 没有被挂起并执行 compareAndSwapInt 方法比较内存值也为 3 , 成功修改内存值为 4 , 线程 B 打完收工 , 一切 OK。
- 这时线程 A 恢复 , 执行 compareAndSwapInt 方法比较 , 发现自己手里的值数字 3 和主内存的值数字 4 不一致 , 说明该值己经被其它线程抢先一步修改过了 , 那 A 线程本次修改失败 , 只能重新读取重新来一遍了。
- 线程 A 重新获取 value 值 , 因为变量 value 被 volatile 修饰 , 所以其它线程对它的修改 , 线程 A 总是能够看到 , 线程 A 继续执行 compareAndSwaplnt 进行比较替换 , 直到成功。
底层汇编
Unsafe 类中的 compareAndSwapInt, 是一个本地方法 , 该方法的实现位于 unsafe.cpp 中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x) UnsafeWrapper("Unsafe_CompareAndSwaplnt"); oop p = JNlHandles::resolve(obj); jint* addr = (jint *)index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e))== e; UNSAFE_END // 先想办法拿到变量 value 在内存中的地址。// 通过 Atomic::cmpxchg 实现比较替换 , 其中参数 x 是即将更新的值 , 参数 e 是原内存的值。
小结
CAS 指令
CAS 有 3 个操作数 , 内存值 V , 旧的预期值 A , 要修改的更新值 B。
当且仅当预期值 A 和内存值 V 相同时 , 将内存值 V 修改为 B , 否则什么都不做。
15_CAS 缺点
循环时间长开销很大
// ursafe.getAndAddInt public final int getAndAddInt(Object var1, long var2, int var4){int var5; do { var5 = this.getIntVolatile(var1, var2); }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4)); return var5; }
我们可以看到 getAndAddInt 方法执行时 , 有个 do while, 如果 CAS 失败 , 会一直进行尝试。如果 CAS 长时间一直不成功 , 可能会给 CPU 带来很大的开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时 , 我们可以使用循环 CAS 的方式来保证原子操作 , 但是 , 对多个共享变量操作时 , 循环 CAS 就无法保证操作的原子性 , 这个时候就可以用锁来保证原子性。
引出来 ABA 问题
16_ABA 问题
ABA 问题怎么产生的
CAS 会导致“ABA 问题”。
CAS 算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换 , 那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A , 这时候另一个线程 two 也从内存中取出 A , 并且线程 two 进行了一些操作将值变成了 B, 然后线程 two 又将 V 位置的数据变成 A , 这时候线程 one 进行 CAS 操作发现内存中仍然是 A , 然后线程 one 操作成功。
尽管线程 one 的 CAS 操作成功 , 但是不代表这个过程就是没有问题的。
17_AtomicReference 原子引用
import java.util.concurrent.atomic.AtomicReference; class User{String userName; int age; public User(String userName, int age) {this.userName = userName; this.age = age;} @Override public String toString() { return String.format("User [userName=%s, age=%s]", userName, age); } } public class AtomicReferenceDemo {public static void main(String[] args){User z3 = new User( "z3",22); User li4 = new User("li4" ,25); AtomicReference<User> atomicReference = new AtomicReference<>(); atomicReference.set(z3); System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString()); } }
输出结果
true User [userName=li4, age=25] false User [userName=li4, age=25]
18_AtomicStampedReference 版本号原子引用
原子引用 + 新增一种机制 , 那就是修改版本号 ( 类似时间戳 ), 它用来解决 ABA 问题。
19_ABA 问题的解决
ABA 问题程序演示及解决方法演示 :
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo {/** * 普通的原子引用包装类 */ static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); // 传递两个值 , 一个是初始值 , 一个是初始版本号 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) {System.out.println("============ 以下是 ABA 问题的产生 =========="); new Thread(() -> { // 把 100 改成 101 然后在改成 100, 也就是 ABA atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); }, "t1").start(); new Thread(() -> {try { // 睡眠一秒 , 保证 t1 线程 , 完成了 ABA 操作 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); } // 把 100 改成 101 然后在改成 100, 也就是 ABA System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get()); }, "t2").start(); / try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) {e.printStackTrace(); } / System.out.println("============ 以下是 ABA 问题的解决 =========="); new Thread(() -> { // 获取版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t 第一次版本号 " + stamp); // 暂停 t3 一秒钟 try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); } // 传入 4 个值 , 期望值 , 更新值 , 期望版本号 , 更新版本号 atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t 第二次版本号 " + atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); System.out.println(Thread.currentThread().getName() + "\t 第三次版本号 " + atomicStampedReference.getStamp()); }, "t3").start(); new Thread(() -> {// 获取版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t 第一次版本号 " + stamp); // 暂停 t4 3 秒钟 , 保证 t3 线程也进行一次 ABA 问题 try {TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName() + "\t 修改成功否 :" + result + "\t 当前最新实际版本号 :" + atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值 " + atomicStampedReference.getReference()); }, "t4").start();} }
输出结果
============ 以下是 ABA 问题的产生 ========== true 2019 ============ 以下是 ABA 问题的解决 ========== t3 第一次版本号 1 t4 第一次版本号 1 t3 第二次版本号 2 t3 第三次版本号 3 t4 修改成功否 :false 当前最新实际版本号 :3 t4 当前实际最新值 100
20_集合类不安全之并发修改异常
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.Vector; public class ArrayListNotSafeDemo {public static void main(String[] args) {List<String> list = new ArrayList<>(); //List<String> list = new Vector<>(); //List<String> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 30; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); }, String.valueOf(i)).start();} } }
上述程序会抛
java.util.ConcurrentModificationException
解决方法之一 :Vector
解决方法之二 :
Collections.synchronizedList()
本文限于篇幅 , 故而只展示部分的面试内容 , 完整的面试学习文档小编已经帮你整理好了 , 需要的朋友点赞 + 关注私信我 (777) 免费领取 Java 知识与技巧、课件 , 源码 , 安装包等等等还有大厂面试学习资料哦 !
所有的面试题目都不是一成不变的 , 特别是像一线大厂 , 上面的面试题只是给大家一个借鉴作用 , 最主要的是给自己增加知识的储备 , 有备无患。最后给大家分享 Spring 系列的学习笔记和面试题 , 包含 spring 面试题、spring cloud 面试题、spring boot 面试题、spring 教程笔记、spring boot 教程笔记、最新阿里巴巴开发手册 (63 页 PDF 总结 )、2022 年 Java 面试手册。一共整理了 1184 页 PDF 文档。私信博主 (777) 领取 , 祝大家更上一层楼 !!!
原文链接:https://blog.csdn.net/m0_68850571/article/details/124192704