JUC面试题一 (2)

简介: JUC面试题一

1.4.2、单例volatile 分析

DCL 问题分析:

  1. DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
  2. 原因:可能出现某一个线程执行到第一次检测,读取到的instance不为null时,但是instance的引用对象可能没有完成初始化。原因如下:
  3. 实例化代码 instance=new SingletonDemo(); 可以分为以下3步完成(伪代码)
memory=allocate();  //1.分配对象内存空间
instance(memory)  //2.初始化对象
instance=memory;  //3.设置instance指向刚分配的内存地址,此时instance!=null
123
  1. 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory=allocate();  //1.分配对象内存空间
instance=memory;  //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
123
  1. 指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
  2. 就比如说我们需要使用 instance 对象中的一个对象 heygo ,但是由于 instance 并未初始化完成,此时 heygo == null ,访问 instance.heygo 将抛出空指针异常

单例模式正确写法:

加上 volatile ,禁止指令重排

private static volatile SingletonDemo singletonDemo=null;
1

2、CAS 算法

CAS你知道吗?

2.1、CAS 概述

CAS:compare and set(比较并交换)

代码示例

  • 代码
    public class CASDemo { public static void main(String[] args) { /* CAS是什么? ==>compareAndSet 比较并交换 */ AtomicInteger atomicInteger = new AtomicInteger(5); // 期望值与上次相同,修改成功 System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data : " + atomicInteger.get()); // 期望值与上次不同,修改失败 System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data : " + atomicInteger.get()); } } 12345678910111213141516171819
  • 程序运行结果
    true current data : 2019 false current data : 2019 12

分析CAS:就拿 JMM 模型来说

  1. 现在有两个线程:线程 A 和线程 B ,同时操作主内存中的变量 i
  2. 线程 A 将变量 i 的副本拷贝回自己线程的工作内存,先记录变量 i 当前的值,记录为期望值
  3. 线程 A 修改值后,将 i 的值写回主内存前,先判断一下当前主内存的值是否与期望值相等,相等我才写回,不相等证明别的线程(线程 B)改过了,如果强行写,将出现写覆盖

2.2、CAS 原理

2.2.1、Unsafe 类

CAS底层原理?如果知道,谈谈你对Unsafe的理解

一句话总结:自旋锁 + Unsafe 类

AtomicInteger 类的底层源码

  • getAndIncrement() 方法
    public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 123
  • 分析参数含义
  1. this:当前对象
  2. valueOffset:内存偏移量(内存地址)
  3. 为什么AtomicInteger能解决i++多线程下不安全的问题,靠的是底层的Unsafe类
  • AtomicInteger 类中维护了一个 Unsafe 实例,和一个 volatile 修饰的 value 值
    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;
  • 123456789101112131415

Unsafe 类

  1. Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据
  2. Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。
  3. 注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应在务
  4. 变量valueOffset,表示该量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
  5. 变量value用volatile修饰,保证了多线程之间的内存可见性
    public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 123

2.2.2、CAS 是什么

CAS 到底是个什么玩意儿?

  1. CAS的全称为Compare-And-Swap,它是一条CPU并发原语
  2. 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  3. CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
  4. 再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

AtomicInteger 类 CAS 算法分析

  • 通过 AtomicInteger 类调用 getAndIncrement() 方法
    public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 123
  • atomicInteger.getAndIncrement() 方法调用 unsafe.getAndAddInt() 方法
  • this.getIntVolatile(var1,var2) 方法获取var1这个对象在var2地址上的值
  • this.compareAndSwapInt(var1, var2, var5, var5 + var4) 方法判断 var5 变量是否与期望值相同:
  • 如果 var5 与内存中的期望值相同,证明没有其他线程改过,则执行 +var 操作
  • 如果 var5 与内存中的期望值不同,证明没有其他线程改过 var2 地址处的值,然后再重新获取 var2 地址处的值,重复 compare and set 操作
  • public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
  • } 12345678
  • 总结:getAndIncrement()方法底层调用的是Unsafe类的getAndAddInt()方法,底层是CAS思想

atomicInteger.getAndIncrement() 方法详解

  • AtomicInteger 类的 getAndIncrement() 方法
    public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 123
  • Unsafe 类的 getAndAddInt() 方法
    public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
  • } 12345678

流程分析:

  1. var1:Atomiclnteger对象本身。
  2. var2:该对象值得引用地址。
  3. var4:需要变动的数量。
  4. var5:使用var1 var2找出的主内存中真实的值。
  5. 用该对象当前的值与var5比较:
  • 如果相同,更新var5 + var4并且返回true,
  • 如果不同,继续取值然后再比较,直到更新完成。

举例说明:

  1. 假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
  2. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本,分别拷贝到各自的工作内存
  3. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  4. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  5. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  6. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

底层汇编指令

  1. Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中
  2. Atomic:cmpxchg 指令:但凡带 Atomic 汇编指令都是不会被其他线程打断

image.png

CAS 简单小总结

CAS(CompareAndSwap)

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止

CAS应用

  1. CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
  2. 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

2.3、CAS 缺点

1、循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}
12345678

2、只能保证一个共享变量的原子操作

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

3、引出来ABA问题?

2.4、面试题

为什么用 CAS 而不用synchronized?

以下是我的理解

  1. 使用 synchronized 虽然能保证操作的原子性,但是将操作变成了串行操作,大大降低了程序的并发性
  2. 如果使用 synchronized 没有抢到同步锁,那么线程将处于阻塞状态,等待 CPU 的下一次调度
  3. CAS 使用 Unsafe 类 + 自旋锁实现操作的原子性,Unsafe 类中使用 do while 循环实现 compare and set ,多个线程可以同时操作,大大提高了程序的并发性,并且不存在让线程等待的问题

3、ABA 问题

原子类AtomicInteger的ABA问题?原子更新引用知道吗?

3.1、ABA 问题的产生

面试坑爹套路

CAS —> UnSafe —> CAS底层思想 —> ABA —> 原子引用更新 —> 如何规避ABA问题

ABA问题是怎样产生的?

CAS会导致 ABA 问题

  1. CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
  2. 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
  3. 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
  4. 一句话总结:狸猫换太子

3.2、原子引用

原子引用代码示例

  • 代码:使用 AtomicReference 原子引用类封装我们自定义的 User 类/**
  • @ClassName AtomicReferenceDemo
  • @Description TODO
  • @Author Heygo
  • @Date 2020/8/7 18:45
  • @Version 1.0 */ public class AtomicReferenceDemo {
    public static void main(String[] args) { AtomicReference atomicReference = new AtomicReference<>();
User z3 = new User("z3", 23);
 User l4 = new User("l4", 24);
 User w5 = new User("w5", 25);
 atomicReference.set(z3);
 System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
 System.out.println(atomicReference.compareAndSet(z3, w5) + "\t" + atomicReference.get().toString());
  • }
  • }
    class User {
String userName;
int age;
public User(String userName, int age) {
    this.userName = userName;
    this.age = age;
}
@Override
public String toString() {
    return "User{" +
            "userName='" + userName + '\'' +
            ", age=" + age +
            '}';
}
  • } 123456789101112131415161718192021222324252627282930313233343536373839404142
  • 程序运行结果
    true User{userName='l4', age=24} false User{userName='l4', age=24} 12

3.3、版本号原子引用

解决ABA问题:理解原子引用 + 新增一种机制,那就是修改版本号(类似时间戳)

  • 代码:使用带版本号的原子类 AtomicStampedReference 解决 ABA 问题/**
  • @ClassName ABADemo
  • @Description TODO
  • @Author Heygo
  • @Date 2020/8/7 21:08
  • @Version 1.0 */ public class ABADemo { // 初始值为 100 static AtomicReference atomicReference = new AtomicReference<>(100); // 初始值为 100 ,初始版本号为 1 static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);
    public static void main(String[] args) {
System.out.println("======ABA问题的产生======");
 new Thread(() -> {
     atomicReference.compareAndSet(100, 101);
     atomicReference.compareAndSet(101, 100);
 }, "t1").start();
 new Thread(() -> {
     // 暂停1秒钟线程2,保证上面t1线程完成一次ABA操作
     try {
         TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
 }, "t2").start();
 // 保证上面的操作执行完成
 while (Thread.activeCount() > 2) {
     Thread.yield();
 }
 System.out.println("======以下是ABA问题的解决=====");
 new Thread(() -> {
     System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + atomicStampedReference.getStamp());
     // 暂停1秒钟t3线程
     try {
         TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
     System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
     atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
     System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
 }, "t3").start();
 new Thread(() -> {
     int stamp = atomicStampedReference.getStamp();
     System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
     // 暂停3秒钟t4线程,保证上面t3线程完成一次ABA操作
     try {
         TimeUnit.SECONDS.sleep(3);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, atomicStampedReference.getStamp() + 1);
     System.out.println(Thread.currentThread().getName() + "\t修改成功否: " + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
     System.out.println(Thread.currentThread().getName() + "\t当前实际值:" + atomicStampedReference.getReference());
 }, "t4").start();
  • } }
  • 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  • 程序运行结果
    ======ABA问题的产生====== true 2019 ======以下是ABA问题的解决===== t3 第1次版本号:1 t4 第1次版本号:1 t3 第2次版本号:2 t3 第3次版本号:3 t4 修改成功否: false 当前最新实际版本号:3 t4 当前实际值:100 123456789

关于 AtomicStampedReference 的一些说明

  • AtomicStampedReference 的构造器
  • initialRef:初始值
  • initialStamp:初始版本号
  • public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } 123
  • compareAndSet() 方法
  • expectedReference:期望值
  • newReference:新值
  • expectedStamp:期望版本号
  • newStamp:新的版本号
    public boolean compareAndSet(V   expectedReference, V   newReference, int expectedStamp, int newStamp) { Pair current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } 123456789101112


目录
相关文章
|
12月前
|
设计模式 监控 安全
JUC第一讲:Java并发知识体系详解 + 面试题汇总(P6熟练 P7精通)
JUC第一讲:Java并发知识体系详解 + 面试题汇总(P6熟练 P7精通)
1636 0
|
资源调度
JUC并发编程之同步器(Semaphore、CountDownLatch、CyclicBarrier、Exchanger、CompletableFuture)附带相关面试题
1.Semaphore(资源调度) 2.CountDownLatch(子线程优先) 3.CyclicBarrier(栅栏) 4.Exchanger(公共交换区) 5.CompletableFuture(异步编程)
143 0
|
19天前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
Java 程序员 容器
【多线程面试题二十四】、 说说你对JUC的了解
这篇文章介绍了Java并发包java.util.concurrent(简称JUC),它是JSR 166规范的实现,提供了并发编程所需的基础组件,包括原子更新类、锁与条件变量、线程池、阻塞队列、并发容器和同步器等多种工具。
|
5月前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
61 0
|
5月前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
57 0
|
5月前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
63 0
|
12月前
|
安全 算法 Java
JUC第十五讲:JUC集合 - 面试 ConcurrentHashMap 看这篇就够了
JUC第十五讲:JUC集合 - 面试 ConcurrentHashMap 看这篇就够了
|
SQL 安全 Java
Java并发编程面试题——JUC专题
Java并发编程面试题——JUC专题
390 0
|
存储 安全 Java
JUC并发编程(JUC核心类、TimeUnit类、原子操作类、CASAQS)附带相关面试题
1.JUC并发编程的核心类,2.TimeUnit(时间单元),3.原子操作类,4.CAS 、AQS机制
58 0
下一篇
无影云桌面