Java开发常见面试题详解(并发,JVM)

预览

并发

问题 详解
请谈谈你对volatile的理解 link
CAS你知道吗? link
原子类Atomiclnteger的ABA问题谈谈?原子更新引用知道吗? link
我们知道ArrayList是线程不安全,请编码写一个不安全的案例并给出解决方案 link
公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁 link
CountDownLatch/CyclicBarrier/Semaphore使用过吗? link
阻塞队列知道吗? link
线程池用过吗?ThreadPoolExecutor谈谈你的理解? link
线程池用过吗?生产上你如何设置合理参数 link
死锁编码及定位分析 link

JVM

问题 详解
JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots link
你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值 link
你平时工作用过的JVM常用基本配置参数有哪些? link
强引用、软引用、弱引用、虚引用分别是什么?请谈谈你对OOM的认识 link
GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈怎么查看服务器默认的垃圾收集器是那个? link
生产上如何配置垃圾收集器的? link
谈谈你对垃圾收集器的理解?G1垃圾收集器 link
生产环境服务器变慢,诊断思路和性能评估谈谈? link
假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位 link
对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的? link

- - -
预览 01_本课程前提要求和说明 02_volatile是什么
03_JMM内存模型之可见性 04_可见性的代码验证说明 05_volatile不保证原子性
06_volatile不保证原子性理论解释 07_volatile不保证原子性问题解决 08_volatile指令重排案例1
09_volatile指令重排案例2 10_单例模式在多线程环境下可能存在安全问题 11_单例模式volatile分析
12_CAS是什么 13_CAS底层原理-上 14_CAS底层原理-下
15_CAS缺点 16_ABA问题 17_AtomicReference原子引用
18_AtomicStampedReference版本号原子引用 19_ABA问题的解决 20_集合类不安全之并发修改异常
21_集合类不安全之写时复制 22_集合类不安全之Set 23_集合类不安全之Map
24_TransferValue醒脑小练习 25_java锁之公平和非公平锁 26_java锁之可重入锁和递归锁理论知识
27_java锁之可重入锁和递归锁代码验证 28_java锁之自旋锁理论知识 29_java锁之自旋锁代码验证
30_java锁之读写锁理论知识 31_java锁之读写锁代码验证 32_CountDownLatch
33_CyclicBarrierDemo 34_SemaphoreDemo 35_阻塞队列理论
36_阻塞队列接口结构和实现类 37_阻塞队列api之抛出异常组 38_阻塞队列api之返回布尔值组
39_阻塞队列api之阻塞和超时控制 40_阻塞队列之同步SynchronousQueue队列 41_线程通信之生产者消费者传统版
42_Synchronized和Lock有什么区别 43_锁绑定多个条件Condition 44_线程通信之生产者消费者阻塞队列版
45_Callable接口 46_线程池使用及优势 47_线程池3个常用方式
48_线程池7大参数入门简介 49_线程池7大参数深入介绍 50_线程池底层工作原理
51_线程池的4种拒绝策略理论简介 52_线程池实际中使用哪一个 53_线程池的手写改造和拒绝策略
54_线程池配置合理线程数 55_死锁编码及定位分析 56_JVMGC下半场技术加强说明和前提知识要求
57_JVMGC快速回顾复习串讲 58_谈谈你对GCRoots的理解 59_JVM的标配参数和X参数
60_JVM的XX参数之布尔类型 61_JVM的XX参数之设值类型 62_JVM的XX参数之XmsXmx坑题
63_JVM盘点家底查看初始默认值 64_JVM盘点家底查看修改变更值 65_堆内存初始大小快速复习
66_常用基础参数栈内存Xss讲解 67_常用基础参数元空间MetaspaceSize讲解 68_常用基础参数PrintGCDetails回收前后对比讲解
69_常用基础参数SurvivorRatio讲解 70_常用基础参数NewRatio讲解 71_常用基础参数MaxTenuringThreshold讲解
72_强引用Reference 73_软引用SoftReference 74_弱引用WeakReference
75_软引用和弱引用的适用场景 76_WeakHashMap案例演示和解析 77_虚引用简介
78_ReferenceQueue引用队列介 79_虚引用PhantomReference 80_GCRoots和四大引用小总结
81_SOFE之StackOverflowError 82_OOM之Java heap space 83_OOM之GC overhead limit exceeded
84_OOM之Direct buffer memory 85_OOM之unable to create new native thread故障演示 86_OOM之unable to create new native thread上限调整
87_OOM之Metaspace 88_垃圾收集器回收种类 89_串行并行并发G1四大垃圾回收方式
90_如何查看默认的垃圾收集器 91_JVM默认的垃圾收集器有哪些 92_GC之7大垃圾收集器概述
93_GC之约定参数说明 94_GC之Serial收集器 95_GC之ParNew收集器
96_GC之Parallel收集器 97_GC之ParallelOld收集器 98_GC之CMS收集器
99_GC之SerialOld收集器 100_GC之如何选择垃圾收集器 101_GC之G1收集器
102_GC之G1底层原理 103_GC之G1参数配置及和CMS的比较 104_JVMGC结合SpringBoot微服务优化简介
105_Linux命令之top 106_Linux之cpu查看vmstat 107_Linux之cpu查看pidstat
108_Linux之内存查看free和pidstat 109_Linux之硬盘查看df 110_Linux之磁盘IO查看iostat和pidstat
111_Linux之网络IO查看ifstat 112_CPU占用过高的定位分析思路 113_GitHub骚操作之开启
114_GitHub骚操作之常用词 115_GitHub骚操作之in限制搜索 116_GitHub骚操作之star和fork范围搜索
117_GitHub骚操作之awesome搜索 118_GitHub骚操作之#L数字 119_GitHub骚操作之T搜索
120_GitHub骚操作之搜索区域活跃用户 - -

01_本课程前提要求和说明

教学视频

一些大厂的面试题

蚂蚁花呗一面:

  1. Java容器有哪些?哪些是同步容器,哪些是并发容器?
  2. ArrayList和LinkedList的插入和访问的时间复杂度?
  3. java反射原理,注解原理?
  4. 新生代分为几个区?使用什么算法进行垃圾回收?为什么使用这个算法?
  5. HashMap在什么情况下会扩容,或者有哪些操作会导致扩容?
  6. HashMap push方法的执行过程?
  7. HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头?
  8. 1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?
  9. https和http区别,有没有用过其他安全传输手段?
  10. 线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻塞队列的作用是什么?
  11. linux怎么查看系统负载情况?
  12. 请详细描述springmvc处理请求全流程?spring 一个bean装配的过程?
  13. 讲一讲AtomicInteger,为什么要用CAS而不是synchronized?

美团一面:

  1. 最近做的比较熟悉的项目是哪个,画一下项目技术架构图。
  2. JVM老年代和新生代的比例?
  3. YGC和FGC发生的具体场景?
  4. jstack,jmap,jutil分别的意义?如何线上排查JVM的相关问题?
  5. 线程池的构造类的方法的5个参数的具体意义?
  6. 单机上一个线程池正在处理服务如果忽然断电怎么办(正在处理和阻塞队列里的请求怎么处理)?
  7. 使用无界阻塞队列会出现什么问题?接口如何处理重复请求?

百度一面:

  1. 介绍一下集合框架?
  2. hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢?
  3. hashmap和treemap什么区别?低层数据结构是什么?
  4. 线程池用过吗都有什么参数?底层如何实现的?
  5. sychnized和Lock什么区别?sychnize 什么情况情况是对象锁?什么时候是全局锁为什么?
  6. ThreadLocal 是什么底层如何实现?写一个例子呗?
  7. volitile的工作原理?
  8. cas知道吗如何实现的?
  9. 请用至少四种写法写一个单例模式?
  10. 请介绍一下JVM内存模型?用过什么垃圾回收器都说说呗线上发送频繁full gc如何处理?CPU使用率过高怎么办?如何定位问题?如何解决说一下解决思路和处理方法
  11. 知道字节码吗?字节码都有哪些?Integer x =5,int y =5,比较x =y 都经过哪些步骤?讲讲类加载机制呗都有哪些类加载器,这些类加载器都加载哪些文件?
  12. 手写一下类加载Demo
  13. 知道osgi吗?他是如何实现的?
  14. 请问你做过哪些JVM优化?使用什么方法达到什么效果?
  15. classforName(“java.lang.String”)和String classgetClassLoader() LoadClass(“java.lang.String”)什么区别啊?

今日头条

  1. HashMap如果一直put元素会怎么样? hashcode全都相同如何?
  2. ApplicationContext的初始化过程?
  3. GC 用什么收集器?收集的过程如何?哪些部分可以作为GC Root?
  4. Volatile关键字,指令重排序有什么意义 ?synchronied,怎么用?
  5. Redis数据结构有哪些?如何实现sorted set?
  6. 并发包里的原子类有哪些,怎么实现?
  7. MvSql索引是什么数据结构? B tree有什么特点?优点是什么?
  8. 慢查询怎么优化?
  9. 项目: cache,各部分职责,有哪些优化点

京东金融面试

  1. Dubbo超时重试;Dubbo超时时间设置
  2. 如何保障请求执行顺序
  3. 分布式事务与分布式锁(扣款不要出现负数)
  4. 分布式Session设置
  5. 执行某操作,前50次成功,第51次失败a全部回滚b前50次提交第51次抛异常,ab场景分别如何设计Spring (传播特性)
  6. Zookeeper有却些作用
  7. JVM内存模型
  8. 数据库垂直和水平拆分
  9. MyBatis如何分页;如何设置缓存;MySQL分页

蚂蚁金服二面

  1. 自我介绍、工作经历、技术栈
  2. 项目中你学到了什么技术?(把三项目具体描述了很久)
  3. 微服务划分的粒度
  4. 微服务的高可用怎么保证的?
  5. 常用的负载均衡,该怎么用,你能说下吗?
  6. 网关能够为后端服务带来哪些好处?
  7. Spring Bean的生命周期
  8. HashSet是不是线程安全的?为什么不是线程安全的?
  9. Java 中有哪些线程安全的Map?
  10. Concurrenthashmap 是怎么做到线程安全的?
  11. HashTable你了解过吗?
  12. 如何保证线程安全问题?
  13. synchronized、lock
  14. volatile 的原子性问题?为什么i++这种不支持原子性﹖从计算机原理的设计来讲下不能保证原子性的原因
  15. happens before 原理
  16. cas操作
  17. lock和 synchronized 的区别?
  18. 公平锁和非公平锁
  19. Java读写锁
  20. 读写锁设计主要解决什么问题?

02_volatile是什么

volatile是JVM提供的轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排(保证有序性)

03_JMM内存模型之可见性

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

JMM关于同步的规定:

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

由于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类的成员变量numbermain线程将会卡在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指令,它的作用有两个:

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

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

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

对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);
    }
    
    ...
}
    

UnSafe

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是用过var1var2找出的主内存中真实的值。
  • 用该对象当前的值与var5比较:
    • 如果相同,更新var5+var4并且返回true,
    • 如果不同,继续取值然后再比较,直到更新完成。

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

  1. Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程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()

21_集合类不安全之写时复制

上一节程序导致抛java.util.ConcurrentModificationException的原因解析

先观察下抛错打印栈堆信息:

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.lun.collection.ArrayListNotSafeDemo.lambda$0(ArrayListNotSafeDemo.java:20)
	at java.lang.Thread.run(Thread.java:748)

可看出toString(),Itr.next(),Itr.checkForComodification()后抛出异常,那么看看它们next(),checkForComodification()源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
   
    
    ...
    
	private class Itr implements Iterator<E> {
   
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//modCount在AbstractList类声明

        Itr() {
   }

        ...

        @SuppressWarnings("unchecked")
        public E next() {
   
            checkForComodification();
			...
        }

        final void checkForComodification() {
   
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();//<---异常在此抛出
        }
    }
    
    
    public boolean add(E e) {
   
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
   
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
   
        modCount++;//添加时,修改了modCount的值

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
	...
}
public abstract class AbstractList<E
  • 234
    点赞
  • 1270
    收藏
    觉得还不错? 一键收藏
  • 37
    评论
Java开发常见面试题是面试中经常会被问到的问题。这些问题通常涵盖Java语言的基础知识、面向对象编程、集合框架、多线程、异常处理、IO操作、数据库等方面。以下是一些可能被问到的Java开发常见面试题的例子: 1. 什么是Java虚拟机(JVM)?它的作用是什么? 2. 什么是面向对象编程(OOP)?Java中的类和对象有什么区别? 3. Java中的集合框架有哪些?分别介绍一下它们的特点和用途。 4. 什么是多线程?Java中如何创建和管理线程?如何实现线程间的通信? 5. 什么是异常处理?Java中的异常分类是什么?如何处理异常? 6. Java中的IO操作有哪些?分别介绍一下它们的特点和用途。 7. 如何连接和操作数据库?介绍一下JDBC的基本用法。 8. 什么是Java的反射机制?如何使用反射实现动态加载类和调用方法? 9. 什么是Java的注解(Annotation)?如何自定义注解? 10. 什么是Java的设计模式?列举一些常见的设计模式及其应用场景。 以上只是一些常见Java开发面试题的例子,面试时可能还会有其他问题涉及到更深入的知识。建议在面试前充分准备,熟悉Java语言的基础知识和常用的开发技术,以便能够给出清晰且准确的答案。引用<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java面试题大全带答案 110道(持续更新)](https://blog.csdn.net/u012889902/article/details/121614968)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值