Java 面试八股文(精简、纯手打)
本小册内容涵盖:Java 基础,JVM,多线程,数据库(MySQL/Redis)SSM,Dubbo,网络,MQ,Zookeeper,Netty,微服务,大数据,算法,项目,设计模式等,篇幅足足近 2 千页,大家面试前拿去提前刷刷
一、基础篇
1.接口和抽象类的区别
相似点:
(1)接口和抽象类都不能被实例化
(2)实现接口或继承抽象类的普通子类都必须实现这些抽象方法
不同点:
(1)抽象类可以包含普通方法和代码块,接口里只能包含抽象方法,静态方法和默认方法,
(2)抽象类可以有构造方法,而接口没有
(3)抽象类中的成员变量可以是各种类型的,接口的成员变量只能是 public static final 类型的,并且必须赋值
2.重载和重写的区别
重载发生在同一个类中,方法名相同、参数列表、返回类型、权限修饰符可以不同
重写发生在子类中,方法名相、参数列表、返回类型都相同,权限修饰符要大于父类方法,声明异常范围要小于父类方法,但是 final 和 private 修饰的方法不可重写
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java 面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题需要的【领取/点击】
3.==和 equals 的区别
==比较基本类型,比较的是值,==比较引用类型,比较的是内存地址
equlas 是 Object 类的方法,本质上与==一样,但是有些类重写了 equals 方法,比如 String 的 equals 被重写后,比较的是字符值,另外重写了 equlas 后,也必须重写 hashcode()方法
4.异常处理机制
(1)使用 try、catch、finaly 捕获异常,finaly 中的代码一定会执行,捕获异常后程序会继续执行
(2)使用 throws 声明该方法可能会抛出的异常类型,出现异常后,程序终止
5.HashMap 原理
1.HashMap 在 Jdk1.8 以后是基于数组+链表+红黑树来实现的,特点是,key 不能重复,可以为 null,线程不安全
2.HashMap 的扩容机制:
HashMap 的默认容量为 16,默认的负载因子为 0.75,当 HashMap 中元素个数超过容量乘以负载因子的个数时,就创建一个大小为前一次两倍的新数组,再将原来数组中的数据复制到新数组中。当数组长度到达 64 且链表长度大于 8 时,链表转为红黑树
3.HashMap 存取原理:
(1)计算 key 的 hash 值,然后进行二次 hash,根据二次 hash 结果找到对应的索引位置
(2)如果这个位置有值,先进性 equals 比较,若结果为 true 则取代该元素,若结果为 false,就使用高低位平移法将节点插入链表(JDK8 以前使用头插法,但是头插法在并发扩容时可能会造成环形链表或数据丢失,而高低位平移发会发生数据覆盖的情况)
6.想要线程安全的 HashMap 怎么办?
(1)使用 ConcurrentHashMap
(2)使用 HashTable
(3)Collections.synchronizedHashMap()方法
7.ConcurrentHashMap 原如何保证的线程安全?
JDK1.7:使用分段锁,将一个 Map 分为了 16 个段,每个段都是一个小的 hashmap,每次操作只对其中一个段加锁
JDK1.8:采用 CAS+Synchronized 保证线程安全,每次插入数据时判断在当前数组下标是否是第一次插入,是就通过 CAS 方式插入,然后判断 f.hash 是否=-1,是的话就说明其他线程正在进行扩容,当前线程也会参与扩容;删除方法用了 synchronized 修饰,保证并发下移除元素安全
8.HashTable 与 HashMap 的区别
(1)HashTable 的每个方法都用 synchronized 修饰,因此是线程安全的,但同时读写效率很低
(2)HashTable 的 Key 不允许为 null
(3)HashTable 只对 key 进行一次 hash,HashMap 进行了两次 Hash
(4)HashTable 底层使用的数组加链表
9.ArrayList 和 LinkedList 的区别
ArratList 的底层使用动态数组,默认容量为 10,当元素数量到达容量时,生成一个新的数组,大小为前一次的 1.5 倍,然后将原来的数组 copy 过来;因为数组在内存中是连续的地址,所以 ArrayList 查找数据更快,由于扩容机制添加数据效率更低
LinkedList 的底层使用链表,在内存中是离散的,没有扩容机制;LinkedList 在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高
10.如何保证 ArrayList 的线程安全?
(1)使用 collentions.synchronizedList()方法为 ArrayList 加锁
(2)使用 Vector,Vector 底层与 Arraylist 相同,但是每个方法都由 synchronized 修饰,速度很慢
(3)使用 juc 下的 CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为 list 创建一个副本,期间其它线程读取的都是原本 list,写操作都在副本中进行,写入完成后,再将指针指向副本。
11.String、StringBuffer、StringBuilder 的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。
StringBuffer 可变并且线程安全
StringBuiler 可变但线程不安全。
操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer。
12.hashCode 和 equals
hashCode()和 equals()都是 Obkect 类的方法,hashCode()默认是通过地址来计算 hash 码,但是可能被重写过用内容来计算 hash 码,equals()默认通过地址判断两个对象是否相等,但是可能被重写用内容来比较两个对象
所以两个对象相等,他们的 hashCode 和 equals 一定相等,但是 hashCode 相等的两个对象未必相等
如果重写 equals()必须重写 hashCode(),比如在 HashMap 中,key 如果是 String 类型,String 如果只重写了 equals()而没有重写 hashcode()的话,则两个 equals()比较为 true 的 key,因为 hashcode 不同导致两个 key 没有出现在一个索引上,就会出现 map 中存在两个相同的 key
13.面向对象和面向过程的区别
面向对象有封装、继承、多态性的特性,所以相比面向过程易维护、易复用、易扩展,但是因为类调用时要实例化,所以开销大性能比面向过程低
4.深拷贝和浅拷贝
浅拷贝:浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝:深拷贝会创造一个一摸一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对对象。
15.多态的作用
多态的实现要有继承、重写,父类引用指向子类对象。它的好处是可以消除类型之间的耦合关系,增加类的可扩充性和灵活性。
16.什么是反射?
反射是通过获取类的 class 对象,然后动态的获取到这个类的内部结构,动态的去操作类的属性和方法。
应用场景有:要操作权限不够的类属性和方法时、实现自定义注解时、动态加载第三方 jar 包时、按需加载类,节省编译和初始化时间;
获取 class 对象的方法有:class.forName(类路径),类.class(),对象的 getClass()
17.Java 创建对象得五种方式?
(1)new 关键字 (2)Class.newInstance (3)Constructor.newInstance
(4)Clone 方法 (5)反序列化
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java 面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击】即可免费获取
二.Java 多线程篇
1.进程和线程的区别,进程间如何通信
进程:系统运行的基本单位,进程在运行过程中都是相互独立,但是线程之间运行可以相互影响。
线程:独立运行的最小单位,一个进程包含多个线程且它们共享同一进程内的系统资源
进程间通过管道、 共享内存、信号量机制、消息队列通信
2. 什么是线程上下文切换
当一个线程被剥夺 cpu 使用权时,切换到另外一个线程执行
3.什么是死锁
死锁指多个线程在执行过程中,因争夺资源造成的一种相互等待的僵局
4.死锁的必要条件
互斥条件:同一资源同时只能由一个线程读取
不可抢占条件:不能强行剥夺线程占有的资源
请求和保持条件:请求其他资源的同时对自己手中的资源保持不放
循环等待条件:在相互等待资源的过程中,形成一个闭环
想要预防死锁,只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,还可以用银行家算法可以预防死锁
5.Synchrpnized 和 lock 的区别
(1)synchronized 是关键字,lock 是一个类
(2) synchronized 在发生异常时会自动释放锁,lock 需要手动释放锁
(3)synchronized 是可重入锁、非公平锁、不可中断锁,lock 的 ReentrantLock 是可重入锁,可中断锁,可以是公平锁也可以是非公平锁
(4)synchronized 是 JVM 层次通过监视器实现的,Lock 是通过 AQS 实现的
6.什么是 AQS 锁?
AQS 是一个抽象类,可以用来构造锁和同步类,如 ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。
AQS 的原理是,AQS 内部有三个核心组件,一个是 state 代表加锁状态初始值为 0,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以 CAS 的形式将 state 变为 1,CAS 成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断 state 是不是 0,不是 0 再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的
可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话 state+1,释放锁的时候就将 state-1。当 state 减到 0 的时候就去唤醒阻塞队列的第一个线程。
7.为什么 AQS 使用的双向链表?
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
8.有哪些常见的 AQS 锁
AQS 分为独占锁和共享锁
ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次 CAS 去抢占锁,公平锁会按队列顺序排队
Semaphore(信号量):设定一个信号量,当调用 acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用 release()时释放一个信号量,唤醒阻塞线程。
应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票
CountDownLatch(倒计数器):给计数器设置一个初始值,当调用 CountDown()时计数器减一,当调用 await() 时判断计数器是否归 0,不为 0 就阻塞,直到计数器为 0。
应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用 await() 时会计数+1 并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
应用场景:多线程计算数据,最后合并计算结果的应用场景
9.sleep()和 wait()的区别
(1)wait()是 Object 的方法,sleep()是 Thread 类的方法
(2)wait()会释放锁,sleep()不会释放锁
(3)wait()要在同步方法或者同步代码块中执行,sleep()没有限制
(4)wait()要调用 notify()或 notifyall()唤醒,sleep()自动唤醒
10.yield()和 join()区别
yield()调用后线程进入就绪状态
A 线程中调用 B 线程的 join() ,则 B 执行完前 A 进入阻塞状态
11.线程池七大参数
核心线程数:线程池中的基本线程数量
最大线程数:当阻塞队列满了之后,逐一启动
最大线程的存活时间:当阻塞队列的任务执行完后,最大线长的回收时间
最大线程的存活时间单位
阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列
线程工厂:用于生产线程
任务拒绝策略:阻塞队列满后,拒绝任务,有四种策略(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争
12.Java 内存模型
JMM(Java 内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让 Java 程序在各平台下都能达到一致的内存访问效果,它定义了 JVM 如何将程序中的变量在主存中读取
具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中 copy 变量到自己的工作区,每个线程的工作内存是相互隔离的
由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题
13.保证并发安全的三大特性?
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
有序性:JVM 对指令的优化会让指令执行顺序改变,有序性是禁止指令重排
14.volatile
保证变量的可见性和有序性,不保证原子性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
单例模式双重校验锁变量为什么使用 volatile 修饰? 禁止 JVM 指令重排序,new Object()分为三个步骤:为实例对象分配内存,用构造器初始化成员变量,将实例对象引用指向分配的内存;实例对象在分配内存后实才不为 null。如果分配内存后还未初始化就先将实例对象指向了内存,那么此时最外层的 if 会判断实例对象已经不等于 null 就直接将实例对象返回。而此时初始化还没有完成。
15.线程使用方式
(1)继承 Tread 类
(2)实现 Runnable 接口
(3)实现 Callable 接口:带有返回值
(4)线程池创建线程
16.ThreadLocal 原理
原理是为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个 Map,key 为 threadLocal 实例,value 为要保存的副本。
但是使用 ThreadLocal 会存在内存泄露问题,因为 key 为弱引用,而 value 为强引用,每次 gc 时 key 都会回收,而 value 不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除 value 或者使用 static 修饰 ThreadLocal,可以随时获取 value
17.什么是 CAS 锁
CAS 锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。这样的缺点是
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS 频繁失败导致 CPU 开销大
(3)ABS 问题:线程 1 和线程 2 同时去修改一个变量,将值从 A 改为 B,但线程 1 突然阻塞,此时线程 2 将 A 改为 B,然后线程 3 又将 B 改成 A,此时线程 1 将 A 又改为 B,这个过程线程 2 是不知道的,这就是 ABA 问题,可以通过版本号或时间戳解决
18.Synchronized 锁原理和优化
Synchronize 是通过对象头的 markwordk 来表明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,成本很高,此时这种锁叫重量级锁,在 JDK1.6 以后引入了偏向锁、轻量级锁、重量级锁
偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会升级为轻量级锁。线程会通过 CAS 方式获取锁,不会阻塞,提高性能,
重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
19.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:线程中十分消耗 Io 的线程数*2
CPU 密集型: cpu 线程数量
20.AtomicInteger 的使用场景
AtomicInteger 是一个提供原子操作的 Integer 类,使用 CAS+volatile 实来现线程安全的数值操作。
因为 volatile 禁止了 jvm 的排序优化,所以它不适合在并发量小的时候使用,只适合在一些高并发程序中使用
评论