2019尚硅谷互联网大厂高频重点面试题(第2季)2 :JVM7种垃圾回收,volatile,可见性,指令重排,原子引用,ABA问题,UnSafe和CAS

新生代 和 垃圾回收算法

蚂蚁:4. 新生代分为几个区?使用什么算法进行垃圾回收?

为什么使用这个算法?

新生代分为

  • 伊甸园区 Eden Space

  • 幸存区0区

  • 幸存区1区

  • 复制算法(Copying)

  • 标记清除(Mark-Sweep)

  • 标记压缩(Mark-Compact)

    • 标记清除压缩( Mark-Sweep-Compact)
  • 引用计数法(JVM不用)

JVM 有七种垃圾收集器

  • Java 虚拟机
Serial 收集器

垃圾收集器的原始实现

  • 垃圾收集器的原始实现
  • 适用于能够承受短暂停顿的应用程序
    • 会停止应用程序(通常称为“stop the world”事件)
  • 占用内存空间比较小
Parallel 收集器
  • 也使用“stop the world”方法
  • Parallel 收集器运行时有多个线程执行垃圾收集操作
  • 这是 JVM 中的默认垃圾收集器,也被称为 吞吐量收集器
  • 以通过使用各种合适的 JVM 参数进行调优,
    • 例如吞吐量、暂停时间、线程数和内存占用
Concurrent Mark Sweep(CMS)收集器
  • 这个收集器在 Java8 已过时,并在 Java14 中被移除。

  • 应用程序将暂停两次

G1 收集器
  • G1 垃圾收集器旨在替代 GMS。
  • 具备并行、并发以及增量压缩,且暂停时间较短。
Epsilon 收集器

是在 Java11 中引入的,是一个 no-op (无操作)收集器。它不做任何实际的内存回收,只负责管理内存分配。

epsilon
英
/ˈepsɪlɒn;
n.
希腊语字母表的第五个字母;小的正数;(一系列项目、分类中的)第五

lion
英
/ˈlaɪən/
n.
狮子;名人;勇猛的人;社交场合的名流
Shenandoah 收集器

Shenandoah 是在 JDK12 中引入的,是一种 CPU 密集型垃圾收集器。

  • 它会进行内存压缩,立即删除无用对象并释放操作系统的空间。
Shen an do ah 
ZGC 收集器

JDK11 引入,在 JDK12 改进。在 JDK15,ZGC 和 Shenandoah 都被移出了实验阶段。

  • 低延迟需要和大量堆空间使用而设计

  • 允许当垃圾回收器运行时 Java 应用程序继续运行。

  • ZGC(Z Garbage Collector) 是一款性能比 G1 更加优秀的垃圾收集器。

  • JDK 16 发布后,GC 暂停时间已经缩小到 1 ms 以内。毫秒。

  • 内存多重映射

  • 把不同的虚拟内存地址映射到同一个物理内存地址上

  • 染色指针

  • 读屏障

如何线上排查JVM的相关问题?

美团4:题

  • 调优
//使用-Xmx10m 调节。最大分配内存。默认为1/4
//使用-Xms5m调节。初始化内存分配大小。默认1/64

-Xms1024m -Xmx1024m -XX:+PrintGCDetails //打印GC垃圾回收
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError //oom DUMP 堆dump在 内存溢出错误
    //Print GC Details 和 Heap Dump On OutOfMemoryError
  • 日志使用:Jprofiler
    • J pro file r

JUC多线程 高并发

1.请谈谈你对volatile的理解

volatile 3作用

1.volatile是Java虚拟机提供的轻量级的同步机制

  • 乞丐版的 synchronized

  • 1.1.保证可见性:

    • JMM 中,线程的工作内存 改变值后,并写回给 主内存后,要及时通知 其他线程。
      • 及时通知的情况,叫做: JMM中 第一个重要的特性:可见性。
      • 其他线程,要知道 自己拿着的值,已经不是最新的值了,要重新获取。
  • 1.2.不保证原子性

  • 1.3.禁止指令重排

2.JMM你谈谈

3.你在哪些地方用到过volatile?

volatile可见性 代码演示

class MyData {
    //只要有一个线程改了,其他线程,马上收到通知。
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }
}

/**
 * 1.1假如 int number = 0; , number变量之前根本没有添加volatile关键字修饰,没有可见性
 */
public class VolatileDemo {
    public static void main(String[] args) {

        MyData m = new MyData();

        new Thread(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println(tName + "come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            m.addTo60();
            System.out.println(tName + "进行了更新" + m.number);
        }, "AAA").start();

        while (m.number == 0) {
            // main线程就一直再这里等待循环,直到number值不再等于零。
        }

        System.out.println(Thread.currentThread().getName() + "任务已经完成" + m.number);

//        AAAcome in
//        AAA进行了更新60
//        main任务已经完成60
    }
}

通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,
这种工作内存与主内存同步延迟现象就造成了可见性问题

volatile不保证原子性

验证volatile不保证原子性

2.1原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。

  • 结果 不为 2万。1万9左右
class MyData {
    //只要有一个线程改了,其他线程,马上收到通知。
    volatile int number = 0;

    public void addPlusPlus() {

        number++;
    }
}
        MyData m = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    m.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }
        
        //默认有:main 和 gc 线程
        while (Thread.activeCount() > 2) {
            //礼让
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + m.number);
i++ 源码
javap -c #查看字节码
javap -verbose
verbose
英
/vɜːˈbəʊs/
adj.
冗长的;啰嗦的
  • 先javac 编译 .java 文件,生成字节码 .class文件,然后再查看源码
Compiled from "MyData2.java"
class com.example.demo.jvm.MyData2 {
  volatile int n;

  com.example.demo.jvm.MyData2();
    Code:
       0: aload_0 

       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field n:I
       9: return

  public void add();
    Code:
       0: aload_0 //从局部变量0中装载引用类型值。初始化0的变量。
       1: dup  //复制栈顶部一个字长内容

       //n++,有4行代码构成。有3个指令。
       2: getfield      #2                  // Field n:I代表int型。获得初始值,目前是0
       5: iconst_1   //读出
       6: iadd   //底层+1
       7: putfield      #2                  // Field n:I。写回主内存
           
      10: return
          
n++被拆分成了3个指令:
    执行getfield拿到原始n;
    执行iadd进行加1操作; 这是在各自的 工作内存+1
	执行putfield写把累加后的值写回。
        
        //3个线程,在自己线程的工作空间,都取到了 0
        //操作后,都加为1,加了volatile,改变后,写回给主内存。
        
        //线程1,写回时(putfield) 被挂起了。
        //线程2:写回1 成功。通知其他线程
        //还未通知,线程1(非常快 被唤醒了) 写回成功。丢失更新(写覆盖)。
        //原因:线程1没有拿到最新值,就去 putfield
解决原子性
  1. 加 synchronized
  2. AtomicInteger
    1. 底层用了 CAS,unsafe类,自旋锁
    2. addAndGet(int delta) 加在获得。++i 多值
    3. getAndadd (int delta) 获得在加价。i++ 多值
    4. decrementAngGet () --i
      1. getAndDecrement () i–
    5. incrementAndGet ()。++i
      1. getAndIncrement ()。i++。先获得,得到了后 在 +
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }

指令重排

  • 你做题的顺序 和 卷子上题的顺序,不一致。

先行发生原则,happen before

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

源代码
编译器优化的重排
指令并行的重排
内存系统的重排
最终执行的指令

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性

  • 先 初始化 父类,

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

案例1
    public void mySort() {
        int x = 11;
        int y = 12;

        x = x + 5;
        y = x * x; //有数据依赖,所以 这条一定最后执行。
        //1234
        //2134
        //1324
    }
案例2
int a,b,x,y = 0;

线程1
x = a;
b = 1;

线程2
y = b;
a = 2;

// 先执行每个,线程的第一行。
x = a;
y = b;
// 结果为:x=0,y=0

//由于没有依赖,每个线程都会指令重排。
b = 1;
a = 2;
x = a; //x为2
y = b; //y为1
案例3
public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;

    public void method01() {
        a = 1;
        flag = true;
    }

    public void method02() {
        if (flag) {
            a = a + 5;
            System.out.println("a的值为:" + a);
        }
    }

    //第一种情况:先执行 方法1,在执行方法2。a的值为6
    //第二种情况:先执行:flag = true;,方法2执行完毕。a=5;
}

在重复一遍:

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

总结

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

先了解一个概念,**内存屏障(Memory Barrier)**又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 一是保证特定操作的执行顺序,
  • 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

对Volatile变量进行写操作时,
会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存

对Volatile变量进行读操作时,
会在读操作前加入一条load屏障指令,从主内存中读取共享变量

  • 就是设置一个屏障,防止屏障 上面的代码 和 下面的代码重排序。
volatile 线程安全性 获得保证

工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

在那些地方用到过 volatile

  1. 单例模式DCL代码
  2. 读写锁,手写一个缓存
  3. CAS,juc 包底层

单例模式volatile分析

DCL单例

DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。

  • 就是 虽然对象不为null,但是:也不能使用(还未完成初始化)
  • 一个座位,现在分配过去,一个小时后,这个插班生过来。
    • 座位已经不为空了,人还没到。
instance = new SingletonDemo();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);//2.初始化对象。1个小时内:配好网线,插座,等着新同学。

instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null
//1个小时后,张三来了。
//1个小时后,人们的目光看到这,看到他了。名副其实。

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

memory = allocate(); //1.分配对象内存空间。1小时后,张三要过来。
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =ull,但是对象还没有初始化完成!。 有这个内存地址,但是没有真人在这。
//对象还没有初始化完成,人们就看过来了。有名无实。

//某些线程,一判断,它不为空了,过来取对象。取到地址不为空,但地址上的内容为空。


instance(memory);//2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成也就造成了线程安全问题。

public class Singleton {
    private Singleton() {
        System.out.println(Thread.currentThread().getName() + "构造器执行");
    }

    //进制指令重排
    private static volatile Singleton instance = null;

    //DCL Double check lock 双端检索机制。双重检查。
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        //不加:volatile,可能存在,地址不为空,地址上的内容为空。
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Singleton.getInstance();
            }, String.valueOf(i)).start();
        }

//        1构造器执行
//        4构造器执行
    }
}

AQS

AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器

JMM

要求保证,这三个 特性:

  • 2.1.可见性
  • 2.2.原子性。synchronized保证。
  • 2.3.VolatileDemo代码演示可见性+原子性代码
  • 2.4.有序性

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM关于同步的规斌
1线程解锁前,必须把共享变量的值刷新回主内存
2线程加锁前,必须读取主内存的最新值到自己的工作内存
3加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程
私有数据区域,而Java内存模型中规定所有变量都存储在主内存主内存是共享内存区域,所有线程都可以访问,

线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间(读取),然后对变量进行操作,操作完成后再将变量写回主内存(存储),不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

钱程A——>

  • 本地内存A
    共享变量的副本

JMM控制——>

主内存

  • 共享变量
    共享变量

8中内存交互

主存(主存变量) 工作内存(变量副本) 执行引擎

lock(锁定)——synchronized 和 ReentrantLock,锁的方式。主存变量加锁。
unlock(解锁)

read(读取):变量的值主存 传到 工作内存
load(载入):载入到 工作内存的变量副本。

  • 读取:主存——工作内存
    • 载入:工作内存——变量副本

store(存储):工作内存变量 传输到 主内存中
write(写入):放入到主内存的变量中。

  • 存储:工作内存——主存
    • 写入:主存——主存变量

use(使用):工作内存变量 传递给 执行引擎
assign(赋值):执行引擎 赋值 给工作内存的变量。

  • 使用:工作内存——执行引擎
  • 赋值:执行引擎——工作内存

2. CAS你知道吗

14.讲一讲AtomicInteger,为什么要用CAS而不是synchronized?

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

  • 靠的是:UnSafe类,CPU指令原语,保证原子性,
  • 底层思想是 CAS,
  • 比较并交换 compareAndSet,真实值和期望相等才更新成功。
    • 不一样,就失败,进入循环 重新获取 内存中 最新的值。再次 比较并交换。
    • 叫做:自旋锁
  • 参数为:当前对象,对象的内存地址偏移量,要更新的值
swap 也可以认为这个
英
/swɒp/
v.
交换,交易; 用……替换,把……换成;交换(工作或岗位);
n.
交易,交换;交易物,交换物;

expect
英
/ɪkˈspekt
v.
期待;预计;要求,指望;认为;怀孕
  • 如果线程的期望值,跟物理内存的真实值一样,就更新。
    • 不一样,就返回false,本次修改失败。
        AtomicInteger a = new AtomicInteger(5);
        boolean b = a.compareAndSet(5, 2019);
        boolean b2 = a.compareAndSet(5, 2019);

        System.out.println("修改的结果:" + b + "\t当前的值为:" + a.get());
        System.out.println("修改的结果:" + b2 + "\t当前的值为:" + a.get());
//        修改的结果:true	当前的值为:2019
//        修改的结果:false	当前的值为:2019

UnSafe和AtomicInteger

atomiclnteger.getAndIncrement();

UnSafe
CAS是什么
  • 找到 jre/lib/rt.jar
runtime
英
/ˈrʌntaɪm/
n.
执行时间,运行时间
package sun.misc;
public final class Unsafe {
    //大部分都是 native
   public native int getInt(Object var1, long var2);

    public native void putInt(Object var1, long var2, int var4);
}
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); }
    }

    //只要,value变化,其他线程都可见。
    private volatile int value;
}

1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是足native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  • native 方法会返回,native stack 本地方法栈,JVM里的。
  • 像C的指针一样直接操作内存,知道内存地址,就能操作变量。
misc
英
/ˈmɪsk/
abbr.
混杂的;各色各样混在一起;多才多艺的(miscellaneous)

2变量valueOffset,表示该变量值在内存中的偏移地址因为Unsafe就是根据内存偏移地址获取数据的。

3变量value用volatile修饰,保证了多线程之间的内存可见性。

简介和getAndIncrement

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

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

  • 必须线程 安全
        AtomicInteger a = new AtomicInteger(5);
		//i++;
        a.getAndIncrement();

public final int getAndIncrement() {
        //当前对象,内存偏移量(内存地址),
        //当前对象,这个内存地址的值是多少。
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }


    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
	//unsafe.getAndAddInt
	//对象,内存偏移量,1
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //上来就先循环获取,
            // 获取到 当前对象,主内存上的值,相当于 拷贝到自己的工作内存
            var5 = this.getIntVolatile(var1, var2);
            
            //比较并交换。当前对象的值,和 var5 一样,就用 var5+var4
            //就是+1
            //如果compareAndSwapInt方法返回的为true,在取反。就为假。结束。
            //如果 compareAndSwapInt 为假(工作空间快照值 和 主内存地址值 不一样),在进入循环。重新获取内存的值。

        } while(!this.compareAndSwapInt(var1, var2, var5, 
                                        var5 + var4));

        return var5;
    }
var1 AtomicInteger对象本身。
var2该对象值得引用地址。
var4需要变动的数量。
var5是用过var1 var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。
  • 即保证了一致性,又提高了并发性

假设线程A和线程B两个线程同时执行getAndAddInt操(作(分别跑在不同CPU上) :

1Atomiclnteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

2线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。

3线程B也通过getIntVolatle(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。

4这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

5线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

  • Unsafe类+CAS思想(自旋)

(非计算机专业的,不要求懂,可以不听,需要汇编知识)
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

(Atomic:cmpxchg(x, addr, e))==e;

先想办法拿到变量value在内存中的地址。
通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。

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

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

缺点

循环时间长开销很大。

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

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

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

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

引出来ABA问题?? ?

ABA
  • ABA
    狸猫换太子
    • 先把太子 换成狸猫,打一顿,在换回去。

3.原子类Atomiclnteger的ABA问题谈谈?原子更新引用知道吗?

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

  • 线程1 和 线程2,都拿到了 值为5,
    • 线程2,先改为了6,又改为了5
    • 线程1,还以为这个5,是最原始的那个5。认为别人没动过。

CAS会导致“ABA问题”。
CAS算法实现→个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

  • two,从A改为了B,又改为了A

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的

原子引用

  • AtomicReference
re fer en ce
英
/ˈrefrəns
n.
提及,谈到;参考,查阅;(引自书或诗歌的)引言,引文;
adj.
参考的,用于查阅的;文献索引的,参照的
v.
列出……的参考书目;提及,提到;引用,参照(某书或某作者)
        User z3 = new User("zs", 22);
        User li4 = new User("li4", 26);
        AtomicReference r = new AtomicReference();
        r.set(z3);

        //true 里面最新的值为:User(userName=li4, age=26)
        System.out.println(r.compareAndSet(z3, li4) + " 里面最新的值为:" + r.get());
        //false 里面最新的值为:User(userName=li4, age=26)
        System.out.println(r.compareAndSet(z3, li4) + " 里面最新的值为:" + r.get());
时间戳原子引用

新增一种机制,那就是修改版本号(类似时间戳)

T1
100 1
2019 2

  • 已经被T2改为了3版本,相当于 乐观锁
  • T1,需要先 拿到最新值,在进行修改。

T2
100 1
101 2
100 3

AtomicStampedReference
    
维护一个对象引用和一个整数“stamp”,可以原子地更新。

实现注意:该实现通过创建表示“盒装”[引用,整数]对的内部对象来维护加盖戳的引用。
stamp
英
/stæmp/
n.
邮票;印记,戳记;印,章,戳;跺脚(声),跺蹄(声);
vt.
跺(脚),重踩,重踏;迈着重步走,跺着脚走;在……上面印盖

timestamp
n.
时间戳;时间邮票
ABA问题代码
    static AtomicReference<Integer> ar = new AtomicReference<>(100);

        new Thread(() -> {
            ar.compareAndSet(100, 101);
            ar.compareAndSet(101, 100);
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //true 最新的值:2019。 出现ABA问题
            System.out.println(ar.compareAndSet(100, 2019) + " 最新的值:" + ar.get());

        }).start();
解决ABA
    static AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 1);
    
        new Thread(() -> {
            int stamp = asr.getStamp();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //期望值,更新值,期望版本号,更新后的版本号
            //第一次更为 101
            asr.compareAndSet(100, 101, stamp, 2);
            //第二次更新为 100
            asr.compareAndSet(101, 100, asr.getStamp(), 3);

            //t3现在的版本号为:3
            System.out.println("t3现在的版本号为:" + asr.getStamp());

        }, "t3").start();

        new Thread(() -> {
            int stamp = asr.getStamp();
            //t4拿到的版本号为:1
            System.out.println("t4拿到的版本号为:" + stamp);
            //暂停3秒钟t4线程,保证上面的t3线程完成了一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean b = asr.compareAndSet(100, 2019, stamp, 2);
            //t4 更新的结果:false 最新的值为:100
            System.out.println("t4 更新的结果:" + b + " 最新的值为:" + asr.getReference());

        }, "t4").start();

JUC 3个包

java.util.concurrent

java.util.concurrent.atomic

  • Atomiclnteger 原子引用

java.util.concurrent.locks

  • Condition
  • Lock 接口
    • ReentrantLock
  • ReadWriteLock
concurrent
英
/kənˈkʌrənt/
adj.
并存的,同时发生的;同意的,一致的;(两个或两个以上徒刑判决)同时执行的;(三条或三条以上线)共点的,会合的
n.
共点;同时发生的事件

atomic
英
/əˈtɒmɪk
adj.
原子的,与原子有关的;原子能的,核能的

ReentrantLock

ReentrantLock r = new ReentrantLock();

reen tr ant
英
/riːˈentrənt/
adj.
再进去的;凹角的
n.
凹角;再进入

reen
n.
沟(尤指排水沟)

reenter
英
/ˌriːˈentə/
v.
再次入内,重返;重新加入
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值