一、Java基础
1. Java有哪些基本数据类型?
byte、short、int、long、float、double、char、boolean。
2. Java类型转换。
从小类型到大类型,直接转。
从大类型到小类型,需要在强制类型转换的变量前面加上括号,然后在括号里面标注要转换的类型。强制类型转换可能导致溢出或损失精度。
3. 自动拆装箱。
自动装箱:将基本数据类型自动转换成对应的包装类。
自动拆箱:将包装类自动转换成对应的基本数据类型。
4. 基本数据类型缓冲池
事先缓存-128至127之间的整型数字,当需要进行自动装箱时,直接使用缓存中的对象,而不是重新创建一个新对象。
这个范围可以通过 -XX:AutoBoxCacheMax=size 参数进行调整。
5. 抽象类和接口的区别?
抽象类是可以包含抽象方法的类,被abstract修饰。单继承。
接口就是接口。多实现。
6. 内部类有哪些优点?
有效实现了多继承。
匿名内部类可以很方便地定义回调。
Java内部类详解:Java 内部类详解 | 菜鸟教程
7. Java有哪些引用类型?
强、软、弱、虚。
强引用:Java默认的引用类型,垃圾回收器永远不会回收被引用的对象。
软引用:用来描述一些非必需但仍有用的对象,软引用对象在内存足够时不会被回收,在内存不足时会被系统回收。常被用来实现缓存技术。
弱引用:只要JVM进行垃圾回收,就会回收弱引用对象。经典使用场景是ThreadLocal中的ThreadLocalMap中的Entry,它的key弱引用了一个ThreadLocal对象。
虚引用:随时可能会被回收,无法通过虚引用来获取对象。虚引用必须要和引用队列ReferenceQueue一起使用。
8. 类的初始化顺序。
先静态,再非静态。
先父类,再子类。
先属性/方法,再构造器。
9. Java中的修饰符。
public、protected、default、private。
static、final、abstract、synchronized、volatile。
10. final关键字。
修饰类,类不能被继承。
修饰方法,方法不能被重写。
修饰变量,变量在初始化后不能再被赋值。
11. switch支持的数据类型。
byte、short、int、char。
enum、String。
12. 面向对象三大特性。
封装、继承、多态。
13. Java是如何实现多态的?
继承父类或实现接口。
14. 重载和重写的区别。
重载:同一个类中的同名方法,根据参数列表类型不同来区分。
重写:父类和子类中的同名方法。重写方法的访问修饰符不能比父类中被重写的方法的访问权限更低。
15. Object类有哪些常见方法?
equals():判断相等。
hashcode():获取哈希值。
toString():转字符串。
wait():线程等待。
notify():唤醒正在等待的线程。
notifyAll():唤醒所有正在等待的线程。
clone():克隆对象。
finalize():对象首次被垃圾收集器回收时,该方法被执行。
getclass():获取对象的类。
16. equals方法和==的区别。
==比较的是两个变量的值是否相等,如果比较的是两个引用,则判断两个引用是否指向同一个地址。
equals比较的是两个对象的内容是否相同,具体如何比较要看equals方法是如何实现的。Object类中的equals方法就只是用了简单的==。
17. equals方法和hashcode方法的联系。
其实没什么联系。一般把这两个方法放到一起说,是想使用奇怪的对象作为hashMap的key。
向hashMap中put键值对时,如果key已存在,就更新value,这里判断key已存在的条件是 p.hash == hash && (p.key == key || key.equals(p.key)),其中p是与新结点发生哈希碰撞的结点,hash是用hashcode计算得到的。可以看出,如果想使用奇怪的对象作为hashMap的key,必须同时重写equals方法和hashcode方法。
18. 浅拷贝和深拷贝的区别。
浅拷贝:只复制对象,不复制对象引用的其他对象。
深拷贝:复制对象的同时,把对象引用的其他对象都复制一遍。
19. String s = new String("x")创建了几个对象?
如果字符串常量池中没有"x",则创建常量对象"x",和一个String类型的引用对象s。
如果字符串常量池中已有"x",则只创建引用对象s。
20. String、StringBuffer、StringBuilder的区别。
String是不可变对象,每次对String进行操作都产生新的String对象,所以尽量不要对String进行大量的拼接操作(不过如果在编译期就可以确定字符串拼接的结果,则在编译时直接优化成拼接结果)。
StringBuffer可以用来创建字符串,不产生中间对象,并且StringBuffer的每个方法都被synchronized修饰,是线程安全的。
StringBuilder不是线程安全的,但相较于StringBuffer有一定的速度优势,因此使用得比较多。
二、Java集合
三、Java并发
1. Java线程有哪几种状态?
NEW:新建状态。创建了一个线程对象,还没有调用start方法。
RUNNABLE:可运行状态。是就绪和运行中两种状态的统称。其他线程调用线程对象的start方法后,线程对象进入RUNNABLE状态。线程未被CPU调度时为就绪状态,被CPU调度时为运行中状态,Java对这两种状态不做区分。
BLOCKED:阻塞状态,线程运行所需的资源被上锁而无法获得。
WAITING:等待状态,线程需要等其他线程做出一些特定动作(通知或中断)。
TIMED_WAITING:超时等待状态,经过指定的时间后,线程由等待状态自动转换为可运行状态。
TERMINATED:终止状态。线程执行完毕。
2. 如何创建一个线程?
继承Thread类,重写run方法,然后调用start方法启动线程。
实现Runnable接口,重写run方法。
使用线程池。
实现java.util.concurrent包中的Callable接口,重写call方法。
3. 线程池七大参数。
核心线程池大小、最大线程池大小、活跃时间、时间单位、阻塞队列、线程工厂、拒绝执行处理程序。
4. 线程池工作原理。
写太多次了,不想写了。
5. 线程池拒绝策略。
AbortPolicy:中止策略,抛出异常。
CallerRunsPolicy:调用者运行策略。
DiscardPolicy:丢弃策略。
DiscardOldestPolicy:丢弃最旧任务策略。
6. Java中常见的锁。
乐观锁、悲观锁。
偏向锁、自旋锁(轻量级锁)、重量级锁。
公平锁、非公平锁。
可重入锁、非可重入锁。
共享锁、排他锁。
7. synchronized锁的对象。
修饰普通方法:锁的是调用该方法的实例。
修饰静态方法:锁的是当前类。
直接锁对象:对象。
8. JVM对synchronized的优化。
锁膨胀:synchronized锁的四种状态为无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。会随着竞争加剧而逐渐升级。
锁消除:在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化:扩大锁的范围,避免反复加锁和解锁。
自适应自旋:自旋锁的自旋次数不再固定,而是由前一次在同一个锁上的自选时间及锁的拥有者的状态来决定。
9. synchronized锁升级过程。
新创建对象 --> 偏向锁 --> 轻量级锁 --> 重量级锁
偏向锁:给新创建对象上偏向锁:在对象的markword里记录当前线程的指针。偏向锁认为,在大部分情况下,使用synchronized上锁的对象只有一个线程要使用。只要有其他线程来争夺这个对象(轻度竞争),偏向锁就升级为轻量级锁。
轻量级锁:线程轻度竞争下,每个线程在自己的线程栈里划出一块空间,然后把对象的markword复制过来,称为锁记录(Lock Record),然后以CAS的形式尝试将对象的markword更新为指向锁记录的指针,更新成功的线程就获得了该对象的锁。
重量级锁:CAS本质上是程序在不停地循环运行,会占用CPU的资源,所以当线程之间竞争激烈的时候,轻量级锁升级为重量级锁。重量级锁将竞争激烈的线程放入等待队列,由操作系统负责线程调度。放入等待队列的线程不占用CPU资源。
10. JMM内存模型。
JMM是Java内存模型。
JMM定义程序中各个变量的访问规则,即在Java虚拟机中从内存中取出变量和将变量存储到内存中的规则。
JMM涉及到的八个原子操作为:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
11. volatile。
volatile关键字有两个作用:保证线程可见性、禁止指令重排序。
保证线程可见性:一个线程对主存的修改能及时地被其他线程观察到,这种特性被称为可见性。volatile可以保证线程可见性的原因是实现了缓存一致性协议(MESI)。
禁止指令重排序:CPU执行指令的顺序是由输入数据的可用性决定的,而不是由程序的原始数据决定的,这种技术被称为乱序执行。乱序执行在多线程下可能会出现问题。volatile可以阻止指令重排序的原因是使用了内存屏障。
12. CAS。
Compare and Swap,比较并交换。是一个原子操作。
解决ABA问题:给锁定对象加上版本号。
13. AQS。
AbstractQueuedSynchronizer,抽象队列同步器。是JUC中的类,我们常用的ReentrantLock、ReentrantReadWriteLock都是基于AQS实现的。
AQS的数据结构:volatile int + 双向链表。
AQS的内部属性:
state:volatile修饰的int类型数据,代表加锁的状态,初始为0。
exclusiveOwnerThread:Thread类型,记录当前加锁的是哪个线程,初始为null。
head:头结点,也是当前持有锁的线程。
tail:尾结点。每个新的结点进来,都插入到最后,形成了一个等待队列。
14. ThreadLocal内存泄漏的原因。
ThreadLocal是基于ThreadLocalMap实现的,其中ThreadLocalMap的Entry继承了WeakReference,Entry对象中的key使用了WeakReference封装,也就是说,Entry中的key是一个弱引用类型,对于弱引用来说,它只能存活到下次GC之前。
如果此时一个线程调用了ThreadLocalMap的set方法设置变量,当前的ThreadLocalMap就会新增一条记录,如果之前发生过一次垃圾回收,就会造成一个局面:key值被回收掉了,但是value值还在内存中,如果线程一直活着的话,它的value值就会一直存在。
解决办法:使用完key值之后,将value值通过remove方法删掉。
15. 为什么ThreadLocalMap的Entry的key要使用弱引用?
ThreadLocalMap的Entry的key是一个ThreadLocal对象,如果使用强引用,ThreadLocalMap会一直持有ThreadLocal的引用,ThreadLocal对象将无法被回收。
16. Semaphore。
信号量,可以维护当前访问自身的线程个数,并提供同步机制。
17. AtomicInteger如何保证线程安全?
通过CAS操作。
18. CountDownLatch和CyclicBarrier。
CountDownLatch通过一个计数器来实现。计数器的初始值为线程的数量,每当一个线程完成了自己的任务后,计数器的值就会减一,当计数器值达到0时,表示所有的线程都已完成任务,然后在闭锁上等待的线程就可以恢复执行任务。
CyclicBarrier的await方法每调用一次,计数便会减一,当计数减至0时,阻塞解除,所有在此CyclicBarrier上阻塞的线程开始运行。在这之后,如果再次调用await方法,新一轮重新开始。
四、Java虚拟机
五、MySQL
六、Redis
1. Redis数据结构。
字符串String、字典Hash、列表List、集合Set、有序集合ZSet。
2. Redis数据过期策略。
通常,我们在内存中创建一条缓存记录时,会指明这条记录在内存中的超时时间。否则除非用户手动删除,这条记录将永远存在于内存中,很可能造成内存泄漏。
实现缓存过期的方式一般有两种:被动方式和主动方式。两种方式需要结合使用。
被动方式:当客户端尝试访问一条缓存记录时,如果缓存记录已过期,就删掉这条记录并返回 null。
主动方式:每隔一段时间,从关联了超时时间的缓存记录中随机选取一些进行测试,删除已过期的记录。
3. Redis持久化策略。
RDB和AOF。
RDB是在指定的时间间隔内将内存中的数据集快照写入磁盘。
AOF是将每一个收到的写命令记录下来,随着AOF持久化文件越来越大,Redis会对AOF文件进行重写。
4. Redis保证原子操作。
支持执行Lua脚本。
七、Spring
1 速通Spring
2 非基础性问题
1. Spring中使用了哪些设计模式?
工厂模式:FactoryBean机制。
单例模式:bean的作用域默认是singleton。
策略模式:IoC容器采用策略模式决定以何种方式初始化bean实例,可选的策略有反射和CGLIB动态字节码生成。
代理模式:Spring AOP默认是基于动态代理技术实现的。
模板模式:JdbcTemplate中的方法封装了一系列对数据库的操作。
观察者模式:Spring事件驱动模型是经典的观察者模式应用。
适配器模式:Spring MVC中,DispatcherServlet根据请求解析到对应的Handler后,根据Handler的类型选择对应的HandlerAdapter来处理。
责任链模式:Spring MVC中,DispatcherServlet从HandlerMapping得到一条Handler链。
3 Spring Boot
1. 对Spring Boot的理解。
Spring Boot的作用是简化Spring配置。Spring Boot并不提供Spring的核心功能,而是作为Spring的脚手架框架,达到快速构建项目、预置三方配置、开箱即用的目的。
Spring Boot有如下优点:
可以快速构建项目,且项目可以独立运行。
可以对主流开发框架无配置集成。
提供运行时的应用监控。
可以与云计算天然集成。
2. Spring Boot项目的启动流程。
Spring Boot项目创建完成会默认生成一个入口类:xxxApplication,执行该类中的main方法即可启动Spring Boot项目。
3. Spring Boot自动装配过程。
我们只需要引入某个开发工具的Starters,Spring Boot项目启动时就会自动加载其相关依赖,配置其初始化参数,对该工具进行集成。
详细的过程为:
Spring Boot通过注解@EnableAutoConfiguration 开启自动配置。
加载标注了注解@Configuration 的配置类,实例化配置类中定义的bean。
将实例化的bean注入IoC容器。
八、消息队列
1. 为什么要使用消息队列?
异步、削峰、解耦。
2. 几种常见MQ的比较。
RabbitMQ:单机吞吐量-万级,可用性-高,消息可靠性-基本不会丢失数据,扩展性-pass。
ActiveMQ:单机吞吐量-万级,可用性-高,消息可靠性-低概率丢失数据,扩展性-pass。
RocketMQ:单机吞吐量-十万级,可用性-非常高,消息可靠性-可以做到0丢失,扩展性-可以支撑大量topic。
Kafka:单机吞吐量-十万级,可用性-非常高,消息可靠性-可以做到0丢失,扩展性-同等机器下,topic数量不能太多,否则必须增加更多机器资源。
不推荐使用ActiveMQ。
RabbitMQ社区活跃,中小型公司推荐使用。
RocketMQ功能更强大,但社区活跃度较低,推荐基础架构研发能力较强的大型公司使用。
大数据领域实时计算、日志采集等场景,用Kafka。
3. 生产者-消费者模式。
在生产者-消费者模型中包含两类线程,生产者线程用于生产数据,消费者线程用于消费数据。
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。生产者生成数据之后直接放置在共享数据区中,并不关心消费者的行为,消费者只需要从共享数据区中获取数据,不需要关心生产者的行为。
4. 消息队列如何保证顺序消费?
不同的消息队列产品,产生消息错乱的原因,以及解决方案是不同的。
对于RabbitMQ来说,造成消息错乱的原因是消费者的集群部署,多个消费者消费同一个queue的消息,即使后拿到消息的消费者,也有先消费完消息的可能。解决这一问题,我们可以创建多个queue,每个消费者固定消费一个queue的消息,生产者发送消息时,同一个订单号的消息发送到同一个queue中,由于同一个queue的消息一定保证有序,就保证了消息的顺序性。
对于Kafka来说,造成消息乱序的原因是消费者端需要使用多线程并发处理消息来提高吞吐量。解决这一问题,我们需要保证同一个订单号的消息只能被同一个线程处理,我们可以在线程处理前增加一个内存队列,每个线程只负责处理其中一个内存队列的消息,同一个订单号的消息发送到同一个内存队列中即可。
5. 如何保证消息不丢失?
RabbitMQ:
生产者丢消息:可以开启消息确认机制,当Broker收到消息后会返回一个ack消息,如果超过一定的时间还没收到ack消息,就重发。
Broker丢消息:设置消息持久化到磁盘,只有消息持久化到磁盘后,才会给生产者返回ack。
消费者丢消息:关闭自动ack,启用手动ack,消费者每次在确保处理完消息后,给Broker返回ack。
6. 消息队列如何保证不重复消费?
尽量使用幂等性操作。
生产者发送每条数据的时候,里面加一个全局唯一的id。消费消息时,查一下这个id是否被消费过,如果消费过,就不再处理了。
7. 消息处理失败了怎么办?
消息处理失败后,标识这条消息处理失败,然后把这条消息发送到死信队列。
监控链路中发生故障的结点,当结点恢复正常后,将死信队列中的死信重新消费。
九、计算机网络
1. 因特网协议栈如何分层?
共有5层,自顶向下依次是:应用层、运输层、网络层、链路层、物理层。
2. 你知道哪些网络协议?
应用层:HTTP、FTP、DNS。
传输层:TCP、UDP。
网络层:IP。
链路层:MAC。
3. TCP三次握手。
三次握手是指发送了3个报文段。
第一次握手:建立连接时,客户端发送SYN包到服务器,并进入SYN_SENT状态,等待服务器确认。
第二次握手:服务器收到SYN包,确认用户的SYN,向客户端发送一个SYN+ACK包,服务器进入SYN_RECV状态。
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送ACK包,客户端进入ESTABLISHED状态,服务器收到ACK包后也进入ESTABLISHED状态。TCP连接成功,完成三次握手。