Java高频笔试、面试题
一、equals()和 == 的区别?
1、equals()是一个方法,只能由对象来调用,在Object类中,比较引用是否相等。但如果对象的类(如Integer、String)中重写了equals()方法,则可能变成比较内容是否相等;
2、== 当两端是对象时,比较的是地址;当两端是基本类型时,比较的是值。
二、final、finally和finalize的区别
1、final可修饰类、方法和变量。修饰类时,类不能再被继承;修饰方法时,不能再被重写;修饰变量时,如果是基本类型,相当于常量,如果是引用类型,则引用指向的对象地址不可变,但对象本身内容可变;
2、finally可配合try…catch使用,是对异常的处理。如果不在try…catch中调用System.exit(0)退出虚拟机,那finally修饰的部分是一定会被执行的;
3、finalize是指对象被GC回收前调用的方法,已经不再推荐使用了
三、String、Stringbuilder、Stringbuffer的区别
1、String定义的字符串不可变,底层实现是final char[],JDK9 之后底层变成 final byte[]。如改变字符串,则会在常量池中出现一个新的字符串对象,线程安全的;
2、Stringbuilder是可变的,线程不安全但效率高;
3、Stringbuffer是可变的,但使用了同步锁机制来保证线程安全,所以效率较低
四、Java的集合
1、Java的集合分为单列集合和双列集合,单列集合包括List、Set,双列集合包括Map。List是有序、可用索引检索、可重复的,而Set一般是无序(LinkedHashSet是有序的)、不可用索引检索和不可重复的。Map要求Key唯一,不为空;
2、List集合又分为Arraylist和Linkedlist,二者的区别在于数组和链表的区别;
3、Set主要实现是HashSet,底层实现基于HashMap,元素存放在key的位置,value存放PRESENT固定对象。
4、关于HashMap:
(1)Map主要实现类时HashMap,JDK1.8之前的结构是数组+链表,JDK1.8之后是数组+链表+红黑树。数组存放的是key的hashcode和数组长度-1相与的结果,value与该值绑定,若该值相同,则会形成链表,若链表的长度大于(>)8,并且数组的长度超过(≥)64,则链表会自动转为红黑树,以提高查询效率。
(2)HashMap不是线程安全的,多线程时插入数据可能导致数据覆盖,也没有内存屏障保证可见性,多线程读时可能读取的数据不是最新的。替代方案是使用ConcurrentHashMap,1.7版本之前由分段锁实现;之后由CAS+synchronized实现。
(3)HashMap的key可以为null,其值只有一个:0。concurrentHashMap的key不能为null
五、synchronized、volatile和Reentrantlock的区别
1、synchronized是同步锁,隐式锁,由JVM实现,只要加锁后,其余线程就无法再访问,必须等到锁自动释放;保证了原子性,有序性,可见性。
- 可见性:释放锁时会写回内存,加锁时会从内存中读取最新数据。
2、volatile是内存语义关键字,保证了可见性和有序性,但不保证原子性。
- 有序性:由内存屏障、happens-before规则保证。内存屏障是底层实现,是硬件级的约束,直接约束CPU和编译器禁止重排指令。常见内存屏障比如StoreLoad,指禁止屏障前的写操作和屏障后的读操作指令重排。JMM定义的happens-before是保证有序性的高层逻辑约束,A happens-before B表明,A在B之前执行,且A的执行结果对B可见。
- 可见性:指线程修改volatile变量后,会立即写回内存,并标记其它线程中缓存失效,若要使用该变量,必须从内存中读取。
- 使用场景:状态标记以控制线程启动和停止、双重检查锁定单例模式、独立共享变量。绝对不能用于“读-改-写”的场景,例如i++。
3、Reentrantlock是显式锁,保证原子性、可见性和有序性,支持公平锁、非公平锁、可中断,需要手动获取和释放。
六、ThreadLocal原理
每个线程都会维护一个ThreadLocalMap对象,key是ThreadLocal实例,value是线程的变量副本。ThreadLocal 常用于实现 线程隔离的上下文信息,例如事务管理器、用户会话信息等。key是弱引用,value是强引用,若key被回收,变为null,但value仍被引用导致无法回收,会造成内存泄露,需要手动remove()。
七、Java多线程创建方式
1、继承Thread类。Java只支持单继承,扩展性差;
2、实现Runnable接口。灵活性好
3、实现 Callable 接口 + FutureTask。适用于需要线程返回值的场景
4、线程池。JUC包提供了线程池框架,核心接口是ExecutorService,实现类有Executors和ThreadPoolExecutor。
-
线程池参数:核心线程数(长期保持的线程数量)、最大线程数、非核心线程的空闲存活时间、时间单位、任务队列(线程数达到核心线程数时,新任务会进入队列等待执行)、线程工厂(创建线程,可定义名称、优先级)、拒绝策略(任务队列满且达到最大线程数,采用何种策略处理新提交的任务)。
-
如果未达到核心线程数,则创建线程;达到后,先放入任务队列;任务队列满,如果没达到最大线程数,创建线程;若队列满并且达到了最大线程数,则采用拒绝策略(丢弃、抛异常)。
-
任务队列是一种阻塞队列,有3种:有界数组队列、无界链表队列和SynchronousQueue(不存储任务,直接把任务交给线程)。Executors工厂的newFixedThreadPool / newSingleThreadExecutor默认使用无界队列,易 OOM,而newCachedThreadPool 无上限线程数,易造成 CPU/内存耗尽。
-
线程池的状态:RUNNING:正常运行,可接受新任务并处理队列任务。
- SHUTDOWN:调用shutdown()后进入,不接受新任务,但会处理完队列中已有的任务。
- STOP:调用shutdownNow()后进入,不接受新任务,也不处理队列任务,且会中断正在执行的任务。
- TIDYING:所有任务处理完毕,线程数为 0 时进入,准备销毁
- TERMINATED:线程池彻底销毁。
八、线程和协程的区别
对比点 | 线程(Thread) | 协程(Coroutine) |
---|---|---|
定义 | 操作系统调度的最小单位(在 Java 中由 JVM 封装) | 用户态的轻量级线程,由程序调度 |
调度者 | 操作系统(OS)内核 | 用户代码(协程库/运行时) |
切换开销 | 线程切换需要保存/恢复寄存器、栈等,上下文切换成本高 | 协程切换只保存少量状态(函数调用栈、指令位置),开销极小 |
并发模型 | 可以利用多核 CPU,真正并行 | 单线程内的并发(协作式调度),一般不能直接利用多核 |
内存消耗 | 每个线程都有独立栈空间(JVM 默认 1M 左右),创建/销毁成本高 | 协程栈很小(几 KB),可创建百万级协程 |
阻塞 | 一个线程阻塞时,整个线程卡住 | 协程阻塞可以通过“挂起/恢复”机制避免,提升并发能力 |
Java 实现 | Thread 、Runnable 、线程池 | JDK 尚未原生支持,第三方库(如 Quasar)或 Kotlin 协程 |
适用场景 | CPU 密集型、多核并行 | I/O 密集型、大量高并发请求 |