Java面试必问到的10道面试题

1、[Atomic]原子类了解多少 原理是什么 难度系数:⭐

Atomic[原子类]的原理主要基于以下几个方面:

  1. 使用CAS(Compare And Swap)操作:CAS是一种乐观锁机制,包含三个参数:内存位置(变量的内存地址)、期望值和新值。该操作会先比较内存位置上的值是否等于期望值,如果相等,则将内存位置上的值修改为新值;如果不相等,则说明该变量已经被其他线程修改过,操作失败。Java中的Atomic类使用CAS操作来实现原子性。
  2. 使用volatile关键字:volatile关键字可以确保多线程环境下变量的可见性和有序性。
  3. 底层的本地方法:Atomic类还利用了一些底层的本地方法来实现其原子性。

根据使用范围,Atomic原子类可以分为以下四种类型:

  1. 原子更新基本类型 :如AtomicIntegerAtomicLong等,用于原子地更新基本数据类型的值。
  2. 原子更新数组 :包括AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray等,用于原子地更新数组中的元素。
  3. 原子更新引用 :如AtomicReference,用于原子地更新对象引用。
  4. 原子更新属性 :如果需要更新某个对象中的某个字段,可以使用更新对象字段的原子类,如AtomicIntegerFieldUpdater等。

2、synchronized底层实现是什么 底层是什么 有什么区别 难度系数:⭐⭐⭐

synchronizedLock 都是 Java 中用于解决并发编程时线程安全问题的工具,但它们有不同的实现方式和特点。

synchronized 底层实现

synchronized 是 Java 中的一个关键字,它的实现是基于 JVM 层面的锁机制。

  1. JVM 锁机制 :JVM 通过内部的监控机制来识别和管理被 synchronized 修饰的代码块或方法。当一个线程尝试进入一个 synchronized 代码块或方法时,它会尝试获取锁;如果锁被其他线程持有,则当前线程会阻塞,直到锁被释放。
  2. 对象锁和类锁synchronized 可以作用于实例方法或静态方法,以及代码块。作用于实例方法时,它锁定的是当前实例对象;作用于静态方法时,它锁定的是 Class 对象;作用于代码块时,它锁定的是指定的对象。
  3. 可重入性synchronized 锁是可重入的,即同一个线程可以多次获得同一个锁。
Lock 底层实现

Lock 是 Java 并发包 java.util.concurrent.locks 中的一个接口,它的实现通常是基于 AQS(AbstractQueuedSynchronizer)的。

  1. AQS:AQS 是一个用于构建锁和同步器的框架,它使用一个 int 类型的变量来表示状态,并通过 CAS(Compare-and-Swap)操作来确保状态更新的原子性。AQS 维护了一个 FIFO 的等待队列,用于管理等待获取锁的线程。
  2. 显式的获取和释放锁 :与 synchronized 的隐式锁机制不同,Lock 需要显式地调用 lock() 方法来获取锁,并在合适的时候调用 unlock() 方法来释放锁。
  3. 更灵活的锁策略Lock 接口提供了更多的方法,如 tryLock()(尝试获取锁,如果锁不可用则立即返回)、lockInterruptibly()(可中断地获取锁)等,使得锁策略更加灵活。
区别
  1. 锁获取方式synchronized 是隐式的,而 Lock 是显式的。
  2. 等待可中断Lock 提供了可中断的获取锁的方式,而 synchronized 不可中断,除非加锁的代码块抛出异常或正常执行完毕。
  3. 锁绑定多个条件Lock 可以绑定多个条件(Condition 对象),从而实现更复杂的线程同步控制,而 synchronized 不行。
  4. 公平性和非公平性Lock 接口可以支持公平锁和非公平锁,而 synchronized 只能是非公平的。

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【扫一扫】 即可免费获取**

3、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理 难度系数:⭐⭐

区别对比一(HashMap 和 HashTable 区别):

1、HashMap 是非线程安全的,HashTable 是线程安全的。

2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。

3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。

4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线

程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ①

是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,

现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用

HashTable。

区别对比二(HashTable 和 ConcurrentHashMap 区别):

HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是

JDK1.7 使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap 取消了

Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8

的结构类似,数组+链表/红黑二叉树。

synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就

不会产生并发,效率又提升 N 倍。

4、Concurrent[HashMap底层原理] 难度系数:⭐⭐⭐

ConcurrentHashMap 是 Java 并发包中提供的一个线程安全的哈希表实现。它支持高并发场景下的快速读写操作,并且具有相对较低的锁争用。下面我们来详细探讨 ConcurrentHashMap 的底层原理:

分段锁

ConcurrentHashMap 的核心原理是分段锁(Segmentation Lock),它将整个哈希表分割成多个段(Segment),每个段维护着哈希表的一部分数据。每个段都有自己的锁,这样多个线程可以同时访问不同的段,从而实现真正的并发访问。

每个段内部实际上是一个小的哈希表,其内部的数据结构(如数组和链表)与普通的 HashMap 类似。当需要访问某个键时,ConcurrentHashMap 会根据哈希值定位到相应的段,并在该段上进行操作。由于每个段都有自己的锁,因此不同线程可以同时访问不同的段,而不会相互干扰。

读写锁分离

除了分段锁之外,ConcurrentHashMap 还采用了读写锁分离的策略。对于读操作,ConcurrentHashMap 允许多个线程同时访问同一个段,因为读操作不会修改数据,所以不会引发数据不一致的问题。而对于写操作(如 put 和 remove),则需要获取相应段的写锁,以确保在修改数据时的线程安全。

CAS 操作

在某些情况下,ConcurrentHashMap 还会使用 CAS(Compare-and-Swap)操作来确保操作的原子性。CAS 是一种无锁技术,它可以在多线程环境下实现无锁的数据修改。通过比较内存位置的值和期望值,如果相等则更新为新值,否则重试,这种方式可以避免锁的竞争,提高并发性能。

扩容机制

ConcurrentHashMap 中的元素数量超过某个阈值时,它会触发扩容操作。与普通的 HashMap 不同,ConcurrentHashMap 的扩容是逐步进行的,而不是一次性重新分配整个哈希表。这种逐步扩容的方式可以减少扩容过程中对性能的影响。

总结

ConcurrentHashMap 通过分段锁、读写锁分离、CAS 操作和逐步扩容等机制,实现了高并发场景下的线程安全和高效访问。这使得它在处理大量并发读写操作时具有出色的性能表现。

  1. public V put(K key, V value) {
  2. Segment<K,V> s;
  3. if (value == null)
  4. throw new NullPointerException();
  5. int hash = hash(key);
  6. // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算
  7. // 其实也就是把高4位与segmentMask(1111)做与运算
  8. // this.segmentMask = ssize - 1;
  9. //对hash值进行右移segmentShift位,计算元素对应segment中数组下表的位置
  10. //把hash右移segmentShift,相当于只要hash值的高32-segmentShift位,右移的目的是保留了hash值的高位。然后和segmentMask与操作计算元素在segment数组中的下表
  11. int j = (hash >>> segmentShift) & segmentMask;
  12. //使用unsafe对象获取数组中第j个位置的值,后面加上的是偏移量
  13. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  14. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  15. // 如果查找到的 Segment 为空,初始化
  16. s = ensureSegment(j);
  17. //插入segment对象
  18. return s.put(key, hash, value, false);
  19. }
  20. /**
  21. * Returns the segment for the given index, creating it and
  22. * recording in segment table (via CAS) if not already present.
  23. *
  24. * @param k the index
  25. * @return the segment
  26. */
  27. @SuppressWarnings("unchecked")
  28. private Segment<K,V> ensureSegment(int k) {
  29. final Segment<K,V>[] ss = this.segments;
  30. long u = (k << SSHIFT) + SBASE; // raw offset
  31. Segment<K,V> seg;
  32. // 判断 u 位置的 Segment 是否为null
  33. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
  34. Segment<K,V> proto = ss[0]; // use segment 0 as prototype
  35. // 获取0号 segment 里的 HashEntry<K,V> 初始化长度
  36. int cap = proto.table.length;
  37. // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的
  38. float lf = proto.loadFactor;
  39. // 计算扩容阀值
  40. int threshold = (int)(cap * lf);
  41. // 创建一个 cap 容量的 HashEntry 数组
  42. HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
  43. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck
  44. // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作
  45. Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
  46. // 自旋检查 u 位置的 Segment 是否为null
  47. while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
  48. == null) {
  49. // 使用CAS 赋值,只会成功一次
  50. if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
  51. break;
  52. }
  53. }
  54. }
  55. return seg;
  56. }
  57. final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  58. // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。
  59. HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
  60. V oldValue;
  61. try {
  62. HashEntry<K,V>[] tab = table;
  63. // 计算要put的数据位置
  64. int index = (tab.length - 1) & hash;
  65. // CAS 获取 index 坐标的值
  66. HashEntry<K,V> first = entryAt(tab, index);
  67. for (HashEntry<K,V> e = first;;) {
  68. if (e != null) {
  69. // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value
  70. K k;
  71. if ((k = e.key) == key ||
  72. (e.hash == hash && key.equals(k))) {
  73. oldValue = e.value;
  74. if (!onlyIfAbsent) {
  75. e.value = value;
  76. ++modCount;
  77. }
  78. break;
  79. }
  80. e = e.next;
  81. }
  82. else {
  83. // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。
  84. if (node != null)
  85. node.setNext(first);
  86. else
  87. node = new HashEntry<K,V>(hash, key, value, first);
  88. int c = count + 1;
  89. // 容量大于扩容阀值,小于最大容量,进行扩容
  90. if (c > threshold && tab.length < MAXIMUM_CAPACITY)
  91. rehash(node);
  92. else
  93. // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
  94. setEntryAt(tab, index, node);
  95. ++modCount;
  96. count = c;
  97. oldValue = null;
  98. break;
  99. }
  100. }
  101. } finally {
  102. unlock();
  103. }
  104. return oldValue;
  105. }

5、了解volatile关键字不 难度系数:⭐

volatile 是 Java 中的一个关键字,主要用于确保多线程环境下变量的可见性和禁止指令重排。以下是 volatile 关键字的详细解释:

可见性

在多线程环境中,每个线程都有自己的工作内存(本地缓存),当线程从主内存中读取一个变量到工作内存后,本地内存中的变量副本就独立于主内存中的变量,线程对变量的修改如果没有写回到主内存,其他线程是感知不到的。

volatile 关键字的作用之一就是保证变量的可见性。当一个变量被声明为 volatile,它会确保所有线程看到这个变量的值是一致的。当一个线程修改了这个变量的值,新值对其他线程来说是立即可见的。这主要是因为 volatile 修饰的变量在每次被线程访问时,都会直接从主内存中读取,而不是从线程的本地缓存中读取。同样,对该变量的修改也会立即写回主内存,而不是留在本地缓存中。

禁止指令重排

编译器和处理器为了提高性能,会对输入的代码进行指令重排(Instruction Reordering)。但是,这种重排可能会破坏多线程程序的语义。volatile 关键字的另一个作用就是禁止指令重排,从而确保程序按照预期的顺序执行。

6、synchronized和volatile有什么区别 难度系数:⭐⭐

volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从

主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线

程被阻塞住。

volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证

变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【扫一扫】 即可免费获取**

7、Java类加载过程 难度系数:⭐

Java类加载过程是一个复杂但重要的机制,它涉及了类的生命周期中的多个阶段。以下是Java类加载过程的主要步骤:

  1. 加载(Loading):

    • 这是类加载的第一个阶段。在这个阶段,Java虚拟机(JVM)通过类的全名获取类的二进制字节流。
    • 这个字节流可以从网络上获取,或者从本地文件系统、压缩包中等多种方式获得。
    • 获取到字节流后,JVM会将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  2. 链接(Linking):

    • 链接阶段又可以分为验证、准备和解析三个子阶段。
    • 验证(Verification):确保被加载的类的正确性和安全性。
    • 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值(如int类型初始化为0,引用类型初始化为null)。
    • 解析(Resolution):把类中的符号引用转换为直接引用。
  3. 初始化(Initialization):

    • 这是类加载过程的最后一步。
    • 在这个阶段,执行类构造器<clinit>()方法的方法体。此方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块)中的语句合并产生的。
    • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,确保只会有一个线程去执行这个类的<clinit>()方法。

8、什么是类加载器,类加载器有哪些 难度系数:⭐

类加载器(ClassLoader)是Java语言中的一种机制,负责加载字节码文件(.class)到Java虚拟机(JVM)中,使得这些类能够被JVM执行。在Java中,每个类都是由类加载器加载的,而类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。

类加载器在Java中主要有以下几种:

  1. 启动类加载器(BootstrapClassLoader) :用于加载Java的核心类库,如rt.jar中的JDK类文件。它是所有类加载器的父加载器,无法被Java程序直接引用。
  2. 扩展类加载器(ExtensionClassLoader) :用于加载Java的扩展库。它会在Java虚拟机提供的扩展库目录里面查找并加载Java类。如果没有成功加载,还会从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。
  3. 系统类加载器(SystemClassLoader) :也被称为应用类加载器(ApplicationClassLoader)。它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。
  4. 用户自定义类加载器:这是开发人员根据自己的需求自定义的类加载器,通过继承java.lang.ClassLoader类的方式实现。它们具有更高的灵活性,可以实现特殊的类加载策略,例如从网络加载类或者实现类的热部署等。

类加载器在Java类加载过程中起着至关重要的作用,它们按照特定的规则和顺序来加载类,确保了Java程序的正常运行。同时,类加载器的双亲委派模型也保证了Java类库的统一性和安全性。

9、如何查看java死锁 难度系数:⭐

复制代码
  1. ####演示死锁
  2. package com.ssg.mst;
  3. public class 死锁 {
  4. private static final String lock1 = "lock1";
  5. private static final String lock2 = "lock2";
  6. public static void main(String[] args) {
  7. Thread thread1 = new Thread(() -> {
  8. while (true) {
  9. synchronized (lock1) {
  10. try {
  11. System.out.println(Thread.currentThread().getName() + lock1);
  12. Thread.sleep(1000);
  13. synchronized (lock2){
  14. System.out.println(Thread.currentThread().getName() + lock2);
  15. }
  16. } catch (InterruptedException e) {
  17. throw new RuntimeException(e);
  18. }
  19. }
  20. }
  21. });
  22. Thread thread2 = new Thread(() -> {
  23. while (true) {
  24. synchronized (lock2) {
  25. try {
  26. System.out.println(Thread.currentThread().getName() + lock2);
  27. Thread.sleep(1000);
  28. synchronized (lock1){
  29. System.out.println(Thread.currentThread().getName() + lock1);
  30. }
  31. } catch (InterruptedException e) {
  32. throw new RuntimeException(e);
  33. }
  34. }
  35. }
  36. });
  37. thread1.start();
  38. thread2.start();
  39. }
  40. }

Java死锁是并发编程中一个常见且重要的问题。当两个或更多线程在等待对方释放资源时,会发生死锁,导致这些线程都无法继续执行。死锁会导致程序性能下降,甚至使程序完全停止响应。因此,正确识别、预防和处理Java死锁是确保并发程序稳定运行的关键。

以下是对Java死锁的一些看法:

  1. 识别死锁:首先,要能够识别出程序中是否存在死锁。Java提供了多种工具和技术来帮助我们检测死锁,如使用jstack工具查看线程堆栈信息,或者使用IDE中的调试功能。通过观察线程堆栈,我们可以发现哪些线程相互等待对方释放资源,从而确定是否存在死锁。

  2. 预防死锁:预防死锁是更好的选择,因为一旦死锁发生,解决起来可能会比较困难。预防死锁的策略包括:

    • 避免嵌套锁:尽量保持锁的获取顺序一致,避免嵌套锁导致的循环等待。
    • 使用定时锁:使用带超时的锁获取方法,当无法获取锁时,线程可以放弃等待,从而避免死锁。
    • 检测死锁并恢复:在程序中实现死锁检测机制,当检测到死锁时,主动放弃一些资源或重启线程,以打破死锁状态。
  3. 处理死锁:如果程序中发生了死锁,我们需要迅速定位并解决它。处理死锁的方法包括:

    • 分析线程堆栈:使用jstack等工具分析线程堆栈,找出导致死锁的线程和资源。
    • 释放资源:尝试手动释放被死锁线程持有的资源,以打破循环等待。
    • 重启程序:如果无法快速解决死锁,可以考虑重启程序以恢复正常运行。但这种方法可能会导致数据丢失或不一致,因此需要谨慎使用。
  4. 优化并发设计:为了减少死锁的可能性,我们应该对并发设计进行优化。例如,使用更细粒度的锁来减少线程间的竞争;使用读写锁等高级并发工具来提高并发性能;对共享资源进行合理划分和隔离,以减少线程间的依赖关系。

10、Java死锁如何避免 难度系数:⭐

造成死锁的四个主要原因确实如您所述:

  1. 互斥条件:一个资源每次只能被一个线程使用。这是大多数同步机制的基础,确保了资源在某一时刻只能被一个线程访问。
  2. 持有并等待条件:一个线程在阻塞等待某个资源时,不释放已占有的资源。这通常发生在线程试图获取多个资源时,它已经持有部分资源,但还在等待其他资源。
  3. 不可剥夺条件:一个线程已经获得的资源,在未使用完之前,不能被强行剥夺。这意味着线程必须主动释放资源,否则其他线程无法获取。
  4. 循环等待条件:若干线程形成头尾相接的循环等待资源关系。即每个线程都在等待下一个线程释放资源,而最后一个线程又在等待第一个线程释放资源,形成一个闭环。

为了避免死锁,在开发过程中可以采取以下策略:

  1. 注意加锁顺序:确保每个线程都按照一致的顺序请求锁。这样可以消除循环等待条件,因为每个线程都按照相同的顺序等待锁,不会出现头尾相接的循环等待。
  2. 设置加锁时限:使用带有超时的锁获取方法。如果线程在超时时间内无法获取锁,则放弃等待,从而避免无限期的阻塞。这有助于打破持有并等待条件,因为线程在等待一段时间后会主动放弃已持有的资源。
  3. 实施死锁检测与恢复:在程序中实现死锁检测机制,定期或不定期地检查线程状态和资源占用情况,以便及时发现死锁。一旦发现死锁,可以采取措施如放弃部分资源、终止部分线程或重启系统来打破死锁状态。
  4. 避免嵌套锁:尽量减少锁的嵌套使用,以减少线程间的依赖关系。如果必须使用嵌套锁,确保内部锁总是在外部锁释放之前被释放。
  5. 使用高级并发工具 :利用Java并发包中提供的高级并发工具,如SemaphoreCountDownLatchCyclicBarrier等,它们提供了更灵活的同步机制,有助于避免死锁。
  6. 充分理解业务需求:在设计并发程序时,要深入理解业务需求,合理划分任务和资源,确保线程间的协作和同步是高效且安全的。
相关推荐
快来卷java16 分钟前
MySQL篇(一):慢查询定位及索引、B树相关知识详解
java·数据结构·b树·mysql·adb
浪遏26 分钟前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
凸头1 小时前
I/O多路复用 + Reactor和Proactor + 一致性哈希
java·哈希算法
慵懒学者1 小时前
15 网络编程:三要素(IP地址、端口、协议)、UDP通信实现和TCP通信实现 (黑马Java视频笔记)
java·网络·笔记·tcp/ip·udp
anda01091 小时前
11-leveldb compact原理和性能优化
java·开发语言·性能优化
Pasregret2 小时前
04-深入解析 Spring 事务管理原理及源码
java·数据库·后端·spring·oracle
Micro麦可乐2 小时前
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
java·spring boot·后端·spring·intellij-idea·spring security
csjane10792 小时前
Redis原理:rename命令
java·redis
拉不动的猪2 小时前
vue与react的简单问答
前端·javascript·面试
牛马baby2 小时前
Java高频面试之并发编程-02
java·开发语言·面试