前言
好久没更新了,我裂开来,一直在忙新项目的事情,唉! 陆陆续续也没啥时候写东西了,刚过完元旦,才有了休息时间给大家做做分享。 这个2020,程序员实在是太难了。
眼瞅着马上又要过年了,口袋里的钱是一天比一天少,谁谁谁摆满月酒,哪家邻居的二儿子要结婚了,大堂姐又要办乔迁宴了...... 一大堆的人情往来,但是你又莫得办法,这个年可能过得会有些寒冷!!!
过完年就是金三银四了,各位兄弟姐妹、英雄好汉们,我可能会换工作了,不知道你们都准备好了没有。
本篇献给所有准备在金三银四找工作的你们!!!
1. JAVA 中面向对象的特征有哪些?
主要有四大特性:封装、继承、多态、抽象(很多人也认为只有三大特性)
封装
封装的思想保证了类内部数据结构的完整性,使用户无法轻易直接操作类的内部数据,这样降低了对内部数据的影响,提高了程序的安全性和可维护性。
优点:
- 只能通过规定方法访问数据。
- 隐藏类数实现细节。
- 方便修改实现。
- 方便加入控制语句。
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或类从父 类继承方法,使得子类具有父类相同的行为。
还有一个地方需要知道的是,这里还有一个概念叫做组合,可以在面试的时候提一下。组合其实很简单,就是单纯的将某个对象引入当前类,让当前类有引入类的功能,因为 JAVA 只能单继承,所以某些场景下组合其实更合适。
特点:
- 继承父类的重用。
- 继承可以多层继承。
- 一个类只能继承一个父类。
- 父类中
private修饰的不能被继承。 - 构造方法不能被继承。
多态
多态是同一个行为具有多个不同表现形式或形态的能力。产生的场景是将子类对象赋给父类的引用,会产生多态。如:Father son = new Son() 此时的对象son只能调用Father的方法(指的是子类重写或者继承父类的那些方法),而不能调用本身定义的一些方法。因为多态中强调:编写 java 程序时,引用类型变量只能调用其编译时类型的变量,不能调用其运行时类型变量。
特点:
- 继承父类的重用。
- 继承可以多层继承。
- 一个类只能继承一个父类。
- 父类中 private 修饰的不能被继承。
- 构造方法不能被继承。
必要条件 : 继承、重写、父类引用指向子类对象。
作用:
- 不必编写每一子类的功能调用,可以直接把不同子类当父类看,屏蔽子类间的差异(也可以说隐藏了细节),提高代码的通用率/复用率。
- 父类引用可以调用不同子类的功能,提高了代码的扩充性和可维护性。
抽象
用 abstract 关键字来修饰一个类时,这个类叫作抽象类。抽象类是它的所有子类的公共属性的集合,是包含一个或多个抽象方法的类。但不意味着抽象类中只能有抽象方法,它和普通类一样,可以拥有普通的成员变量、方法。
特点:
- 抽象类不能被实例化。抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
- 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
- 构造方法,类方法(用
static修饰的方法)不能声明为抽象方法。 - 被定义为
abstract的类需要被子类继承,但是被修饰为final的类是不能被继承和改写的,这两者是不能一起用来做修饰的。
2. JAVA 中的基本数据类型有哪些,对应的大小和包装类型是什么?
八种基本数据类型: int、short、float、double、long、boolean、byte、char。
封装类分别是: Integer、Short、Float、Double、Long、Boolean、Byte、Character。
大小分别是(byte): int:4、short:2、float:4、double:8、long:8、boolean:1、byte:1、char:2。
3. Java 的引用类型有哪些?
一共有四种引用,分别是强引用、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)。
- 强引用
最普遍的一种引用方式。如String s = "abc", 变量s就是字符串abc的强引用。只要强引用存在,则垃圾回收器就不会回收这个对象。 - 软引用(
SoftReference.java)
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。 - 弱引用(
WeakReference.java)
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 - 虚引用(
PhantomReference.java)
就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
4. String、StringBuffer 和 StringBuilder 的区别有哪些?
String
String 是一个不可变(值是不可变的)的类对象,这就导致每次对String 的操作都会生成新的String对象,在操作频繁的场景中可能会出现性能问题和内存溢出的情况。
StringBuffer
StringBuffer 是一个值可变的类对象,内部维护的是字符数组。另外 StringBuffer是线程安全的,因为StringBuffer所有 public 方法都是 synchronized 修饰的。这也就会导致在数据量大的情况下的性能问题。
StringBuilder
StringBuilder 和StringBuffer 是一样的,都是字符串变量。区别在于StringBuilder的方法都没有加synchronized 修饰。所以在性能上要优于 StringBuffer ,但是StringBuilder在多线程环境下不是线程安全的。
5. 能说说进程和线程的区别吗?
每次问到能说说这种类型的问题时,我真想说一句不能。
进程
进程指的是在系统中正在运行的一个应用程序。程序一旦运行就是进程,进程是资源分配的最小单位。
线程
线程是系统分配处理器时间资源的基本单元,是程序执行的最小单位。或者说进程内独立执行的一个单元执行流。
栗子
比如我们平常用的微信其实就是一个进程。而像微信中的朋友圈、扫一扫等功能就可以理解成为一个个单独的线程。
6. 你们平时是怎么创建线程的,还有其他的方式吗?
- 通过实现
Runnable接口,将Runnable实现类当成参数来创建线程类。 - 通过继承
Thread类创建线程类(重写run方法)。 - 通过实现
Callable和Future创建线程(可以获取线程的返回值)。
7. Thread 类中的 start 和 run 方法有什么区别?
start方法被用来启动新创建的线程,而run一般是线程对应的业务逻辑。
另一方面start方法内部调用了run方法,这和直接调用run方法的效果不一样。当你调用run方法的时候,只会是在原来的线程中调用,没有新的线程启动,start方法才会启动新线程。
8. 线程池用过吗,原理是什么?
用过的,由于频繁的创建和销毁线程一方面会增加对系统资源的消耗,另一方面,会影响程序的性能。在处理多任务的场景下,可以使用池化技术将创建和销毁这过程给省略掉。 在 Java 中,常见的线程池有newSingleThreadExecutor(单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行)、newFixedThreadPool(定长线程池,可控制线程最大并发数,超出的线程会在队列中等待)、newCachedThreadPool(可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程)、newScheduledThreadPool(定长线程池,支持定时及周期性任务执行)。
原理
这个的话个人认为把整个线程池执行的过程讲清楚就可以了,当然可以先把创建线程池的线程池的七个参数讲一下(上面几个常用的线程池也是通过指定默认的几个参数来创建的)。
-
maximumPoolSize最大线程数。 -
corePoolSize核心线程数。 -
keepAliveTime线程活跃时间。 -
TimeUnit线程活跃时间单位。 -
workQueue阻塞队列。 -
RejectedExecutionHandler拒绝策略。 -
ThreadFactory创建线程的工厂。
还是一样,自己根据对线程池的理解组织一下话术。[图片上传中...(image-6969e6-1609767060566-50)]
9. 那线程池的拒绝策略有哪些?
当线程池不能再创新的线程后(参考上图),线程池就会执行拒绝策略。拒绝策略主要有一下(当然排查自定义的策略, 如果有面试官问这个那还是问的比较深比较变态的):
-
AbrtPolicy直接丢弃任务,抛出异常,这是默认策略。 -
CallerRunsPolicy只用调用者所在的线程来处理任务。 -
DiscardOldestPolicy丢弃等待队列中最旧的任务,并执行当前任务。 -
DiscardPolicy直接丢弃任务,也不抛出异常。
10. 简单讲一下 wait 和 sleep 的区别?
| wait | sleep | |
|---|---|---|
| 调用时机 | 只能在同步上下文中调用,否则抛出IllegalMonitorStateException异常 |
不需要在同步上下文中调用 |
| 作用对象 | 定义在Object类中,作用于对象本身 |
定义在Thread中,作用于当前线程 |
| 释放资源 | 被调用时释放锁资源 | 被调不释放资源,当前线程阻塞 |
| 唤醒方式 | 调用notify、notifyAll或者超时 |
调用interrupt或超时 |
| 方法属性 | 实例方法 | 静态方法 |
11. volatile 关键字原理有了解过吗?
首先使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见,并且可以防止指令重排序。
原理
这个问题的我会先回答上面的作用,然后在将一下MESI 协议 和 Java 的内存模型。至于指令重排的原理我会忽略性不讲。因为这一块东西太多了,扯下去的话时间会很久也比较难讲清楚。
MESI 协议
多核CPU 在读/写取数据的过程大致是CPU -> CPU缓存(L1、L2、L3) -> 内存,MESI(Modified、Exclusive、Shared、Invalid 首字母缩写) 协议就是用于保证这个过程中每个缓存中使用的共享变量的副本和内存中是一致的。当CPU写数据时,如果发现操作的变量是共享变量,先将变量设置为独占(Exclusive)状态,当进行设值操作是把变量状态设置为修改(Modified)状态,然后通过总线嗅探机制发出信号通知其他CPU将该变量的缓存行置为无效(Invalid)状态。设置完成后将变量状态修改为共享(Shared)。其他CPU需要读取这个变量时,会发现自己缓存中缓存该变量的缓存行是无效的(Invalid),那么它就会从内存重新读取。
[图片上传中...(image-d0ac1a-1609767060565-49)]
volatile 原理
首先,Java 的内存模型(JMM)和上图基本思想是一致的。在JMM中同样也区分了主内存(主存)、线程副本(缓存)。当修改volatile变量时会强制将修改后的值刷新的主内存中。修改volatile变量后会导致其他线程工作内存中对应的变量值失效(与上面是一致的)。因此,再读取该变量值的时候就需要重新从读取主内存中的值(保证可见性)。
12. synchronized 的作用和原理能讲一讲吗?
synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。synchronized的作用主要有三个:
- 确保线程互斥的访问同步代码。
- 保证共享变量的修改能够及时可见。
- 有效解决重排序问题。
原理
使用synchronized之后,通过编译后的字节码会发现在同步的代码块前后会加上monitorenter和monitorexit字节码指令,在同步方法中会添加ACC_SYNCHRONIZED标记。然后在 Java 中每个对象都会有一个属于自己的监视器锁(monitor),执行monitorenter 指令(ACC_SYNCHRONIZED标记执行逻辑也是一样的)时就是尝试获取 monitor 所有权的过程。如果对象没有被锁定或者已经获得了锁(可重入性),锁的计数器 +1,此时其他竞争锁的线程则会进入等待队列中。
monitorexit 指令只能被持有 monitor 锁的线程执行。当monitorexit 指令被执行时,monitor 锁的计数器就会做-1操作。当计数器值为0时,则释放锁,并且唤醒处于等待队列中的线程再继续竞争锁。
13. synchronized 锁优化了解过吗?
在 JDK1.6 版本前,synchronized 锁是一把重量级锁。从JDK1.6版本之后,对synchronized 锁经过了一系列的优化。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。简单点来说就是使用synchronized 锁一开始会变成偏向锁而不是重量级锁,然后经过锁竞争会有一个从无锁->偏向锁->轻量级锁->重量级锁 的锁升级过程。
自旋锁
自旋锁主要发生在线程阻塞等待期间,由于大部分时候锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态。
自适应锁
自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除
锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化
锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
CAS 无锁机制
CAS(Compare And Swap/Set) 是一种乐观锁的实现方式,从字面意思上来说是比较并交换/赋值。CAS机制当中使用了内存地址V、旧的预期值A、要修改的新值B3 个基本操作数,更新一个变量的时候,只有当旧的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
偏向锁
当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程 ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。
轻量级锁
JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM 将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
锁升级过程
锁升级的过程是非常复杂的,好哥哥们自己总结一下语言,尽量简单一点。
14. 有用过 ThreadLocal 吗,原理有了解过吗?
这种问题真想回一个没用过,不会(然后大结局)
首先ThreadLocal是一个存储泛型变量数据类,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。实际上是一种数据隔离(牺牲内存空间)的方式来达到线程安全的目的。
原理
这个的话比较简单,实际上ThreadLocal内部维护了一个Map变量(ThreadLocalMap,并不是HashMap),当调用set 方法时,会将当前线程作为key(实际上这里并不那么准确,应该是将ThreadLocal对象本身作为key,ThreadLocal对象中包含了当前线程),参数作为value,然后设值给ThreadLocal 的静态内部类Map(ThreadLocalMap)中。取值的话就是讲当前线程作为key然后从ThreadLocalMap获取。
最后
本期的内容就到这啦,有需要补充的地方 欢迎大家在评论区留言指出!
由于细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容! 全套2021大厂面试合集给大家整理出来了,需要的请扣一。
获取资料方式:关注+转发,私信“书籍”免费领取
喜欢的话可以给小编三连一下哦