写点什么

2021 年 10 月最新版 Java 面试真题 + 视频解析(价值 24980 赶紧收藏码住!

  • 2021 年 11 月 12 日
  • 本文字数:6820 字

    阅读完需:约 22 分钟

StringBuffer 方法都是 synchronized 修饰的性能:StringBuilder > StringBuffer > String


场景:经常需要改变字符串内容时使用后面两个


优先使用 StringBuilder,多线程使用共享变量时使用 StringBuffer


抽象类是对类本质的抽象,表达的是 is a 的关系,比如: BMW is a Car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。


而接口是对行为的抽象,表达的是 like a 的关系。比如: Bird like a Aircraft (像飞行器一样可以飞),但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。


使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。


抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也 是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功 能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计 阶段会降低难度


List 和 Set 的区别


===========


List:有序,按对象进入的顺序保存对象,可重复,允许多个 Null 元素对象,可以使用 Iterator 取出 所有元素,在逐一遍历,还可以使用 get(int index)获取指定下标的元素


Set:无序,不可重复,最多允许有一个 Null 元素对象,取元素时只能用 Iterator 接口取得所有元 素,在逐一遍历各个元素


ArrayList 和 LinkedList 区别


======================


ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固 定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚 至超过 linkedList(需要创建大量的 node 对象)


LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历


遍历 LinkedList 必须使用 iterator 不能使用 for 循环,因为每次 for 循环体内通过 get(i)取得某一元素时都需 要对 list 重新进行遍历,性能消耗极大。


另外不要试图使用 indexOf 等返回元素索引,并利用其进行遍历,使用 indexlOf 对 list 进行了遍历,当结 果为空时会遍历整个列表。


HashMap 和 HashTable 有什么区别?其底层实现是什么?


================================


区别 :


(1) HashMap 方法没有 synchronized 修饰,线程非安全,HashTable 线程安全;


(2) HashMap 允许 key 和 value 为 null,而 HashTable 不允许


2.底层实现:数组+链表实现


jdk8 开始链表高度到 8、数组长度超过 64,链表转变为红黑树,元素以内部类 Node 节点存在计算 key 的 hash 值,二次 hash 然后对数组长度取模,对应到数组下标,


如果没有产生 hash 冲突(下标位置没有元素),则直接创建 Node 存入数组,


如果产生 hash 冲突,先进行 equal 比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到 8,并且数组长度到 64 则转变为红黑树,长度低于 6 则将红黑树转回链表


key 为 null,存在下标 0 的位置


数组扩容


ConcurrentHashMap 原理,jdk7 和 jdk8 版本的区别


==================================


jdk7:


数据结构:ReentrantLock+Segment+HashEntry,一个 Segment 中包含一个 HashEntry 数组,每个


HashEntry 又是一个链表结构


元素查询:二次 hash,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部


锁:Segment 分段锁 Segment 继承了 ReentrantLock,锁定操作的 Segment,其他的 Segment 不受影响,并发度为 segment 个数,可以通过构造函数指定,数组扩容不会影响其他的 segment


get 方法无需加锁,volatile 保证 jdk8:


数据结构:synchronized+CAS+Node+红黑树,Node 的 val 和 next 都用 volatile 修饰,保证可见性


查找,替换,赋值操作都使用 CAS


锁:锁链表的 head 节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容


读操作无锁:


Node 的 val 和 next 使用 volatile 修饰,读写线程对该变量互相可见数组用 volatile 修饰,保证扩容时被读线程感知


什么是字节码?采用字节码的好处是什么?


===================


java 中的编译器和解释器:


==============


Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。


编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系 统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。


每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节 码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机 器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。


Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器 >机器可执


行的二进制机器码 >程序运行。


采用字节码的好处:


=========


Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器, 因此,Java 程序无须重新编译便可在多种不同的计算机上运行。


Java 中的异常体系


==========


Java 中的所有异常都来自顶级父类 Throwable。Throwable 下有两个子类 Exception 和 Error。


Error 是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。


Exception 不会导致程序停止,又分为两个部分 RunTimeException 运行时异常和 CheckedException 检 查异常。


RunTimeException 常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException 常常发生在程序编译过程中,会导致程序编译不通过。


Java 类加载器


========


JDK 自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。


BootStrapClassLoader 是 ExtClassLoader 的父类加载器,默认负责加载 %JAVA_HOME%lib 下的 jar 包和 class 文件。


ExtClassLoader 是 AppClassLoader 的父类加载器,负责加载 %JAVA_HOME%/lib/ext 文件夹下的 jar 包和 class 类 。 AppClassLoader 是自定义类加载器的父类,负责加载 classpath 下的类文件。系统类加载器,线程上下 文加载器


继承 ClassLoader 实现自定义类加载器


双亲委托模型


======



双亲委派模型的好处:


主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String。


同时也避免了类的重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类


GC 如何判断对象可以被回收


=============


引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计 数为 0 时可以回收,


可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC


Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。


引用计数法,可能会出现 A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收。


GC Roots 的对象有:


虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象


方法区中常量引用的对象


本地方法栈中 JNI(即一般说的 Native 方法)引用的对象


可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至 少要经历两次标记过程:第一次是经过可达性分析发现没有与 GCRoots 相连接的引用链,第二次是在由虚拟机自动建立的 Finalizer 队列中判断是否需要执行 finalize()方法。


当对象变成(GC Roots)不可达时,GC 会判断该对象是否覆盖了 finalize 方法,若未覆盖,则直接将其回收。否则,若对象未执行过 finalize 方法,将其放入 F-Queue 队列,由一低优先级线程执行该队列中对象的 finalize 方法。执行 finalize 方法完毕后,GC 会再次判断该对象是否可达,若不可达,则进行回收,否 则,对象“复活”


每个对象只能触发一次 finalize()方法


由于 finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。


线程、并发相关


=======


线程的生命周期?线程有几种状态


===============


1. 线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。


2. 阻塞的情况又分为三种:


(1) 、等待阻塞:运行的线程执行 wait 方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify 或 notifyAll 方法才能被唤醒,wait 是 object 类的方法


(2) 、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入“锁池”中。


(3) 、其他阻塞:运行的线程执行 sleep 或 join 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状 态。当 sleep 状态超时、join 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。


sleep 是 Thread 类的方法


1. 新建状态(New):新创建了一个线程对象。


2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。


3. 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。


4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。


5. 死亡状态(Dead):线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。


对线程安全的理解


========


不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问


是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分 配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了 要还给操作系统,要不然就是内存泄漏。


在 Java 中,堆是 Java 虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚 拟机启动时创建的。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及 数组都在这里分配内存。


是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语 言里面显式地分配和释放。


目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己 的内存空间,而不能访问别的进程的,这是由操作系统保障的。


在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。


对守护线程的理解


========


守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个 JVM 中所有非守护线程的保姆;


守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪 天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;


注意: 由于守护线程的终止是自身无法控制的,因此千万不要把 IO、File 等重要操作逻辑分配给它;因为它不靠谱;


守护线程的作用是什么?


举例, GC 垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的 Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线 程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。


应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要 正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都 是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。


thread.setDaemon(true)必须在 thread.start()之前设置,否则会跑出一个


IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程。


在 Daemon 线程中产生的新线程也是 Daemon 的。


守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作 的中间发生中断。


Java 自带的多线程框架,比如 ExecutorService


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


,会将守护线程转换为用户线程,所以如果要使用后台线 程就不能用 Java 的线程池。


ThreadLocal 的原理和使用场景


===================


每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有 ThreadLocal 对象及其对应的值


ThreadLocalMap 由一个个 Entry 对象构成


Entry?继承自 WeakReference<ThreadLocal<?>>?,一个 Entry?由 ThreadLocal?对象和 Object 构成。由此可见, Entry?的 key 是 ThreadLocal 对象,并且是一个弱引用。当没指向 key 的强引用后,该


key 就会被垃圾收集器回收


当执行 set 方法时,ThreadLocal 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对象。再以当前 ThreadLocal 对象为 key,将值存储进 ThreadLocalMap 对象中。


get 方法执行过程类似。ThreadLocal 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap


对象。再以当前 ThreadLocal 对象为 key,获取对应的 value。


由于每一条线程均含有各自私有的 ThreadLocalMap 容器,这些容器相互独立互不影响,因此不会存在 线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。


使用场景:


1、在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束。


2、线程间数据隔离


3、进行事务操作,用于存储线程事务信息。


4、数据库连接,Session 会话管理。


ThreadLocal 内存泄露原因,如何避免


======================


内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露 堆积后果很严重,无论多少内存,迟早会被占光,


不是被使用的对象或者变量占用的内存不能被回收,就是内存泄露。


强引用:使用最普遍的引用(new),一个对象具有强引用能力,不会被垃圾回收器回收。当然内存空间不足,


Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。


如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为 null,这样可以使 JVM 在合适的时间就会回收该对象。


弱引用:JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用


java.lang.ref.WeakReference 类来表示。可以在缓存中使用弱引用。


ThreadLocal 的实现原理,每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal


实例,value 为线程变量的副本



hreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 不存在外部强引用时, Key(ThreadLocal)势必会被 GC 回收,这样就会导致 ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,只有 thead 线程退出以后,value 的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这 些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链(红色链条)


key 使用强引用


当 hreadLocalMap 的 key 为强引用回收 ThreadLocal 时,因为 ThreadLocalMap 还持有 ThreadLocal 的强 引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。


key 使用弱引用


当 ThreadLocalMap 的 key 为弱引用回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的弱 引用,即使没有手动删除,ThreadLocal 也会被回收。当 key 为 null,在下一次 ThreadLocalMap 调用 set(),get(),remove()方法的时候会被清除 value 值。


因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。


ThreadLocal 正确的使用方法


每次使用完 ThreadLocal 都调用它的 remove()方法清除数据


将 ThreadLocal 变量定义成 private static,这样就一直存在 ThreadLocal 的强引用,也就能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉 。


说?下 ArrayList 和 LinkedList 区别


=========================


1.?先,他们的底层数据结构不同,ArrayList 底层是基于数组实现的,LinkedList 底层是基于链表实现的


2. 由于底层数据结构不同,他们所适?的场景也不同,ArrayList 更适合随机查找,LinkedList 更适合删 除和添加,查询、添加、删除的时间复杂度不同


3. 另外 ArrayList 和 LinkedList 都实现了 List 接?,但是 LinkedList 还额外实现了 Deque 接?,所以 LinkedList 还可以当做队列来使?

评论

发布
暂无评论
2021年10月最新版Java面试真题+视频解析(价值24980赶紧收藏码住!