个人面试笔记整理
JAVA基础知识
1. finalize方法
该方法是Object类的一个方法,当该对象不在呗任何对象引用时,JVM执行GC调用该对象的finalize()方法。子类可以覆盖该方法来实现资源的回收(例如文件的关闭等)。也可以在finalize中再次将该对象被引用,避免被GC回收。注意:finalize中抛出的异常会被忽略同事方法终止。当finalize被调用之后,JVM回再一次检测该对象是否能被存活的线程所调用,如果不能,则清楚该对象。finalize只能够被调用一次,也就是说,覆盖finalize之后jvm会执行两次才能够清楚该对象。
2. String、StringBuffer、StingBuider详解
-
String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。
-
StringBuffer、StingBuider是可修改字符串,tringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,两者对象在进行构造时,会先按照默认大小申请一个字符数组,在不断加入新数据的过程中,如果超过默认长度会重新创建一个更大的数组,丢弃原有的数组,将原有的数据复制到新数组中。
唯一需要注意的是:StringBuffer是线程安全的,但是StringBuilder是线程不安全的。StringBuffer的底层都有synchronize关键字。因此,StringBuffer的性能要低于StringBuilder。
String字符串重点解析: String字符串不属于八中基本数据类型,是一个对象,并且是final的,不可被改变。
String有两种创建方式:
- String str=“string”;直接创建方式,字面量形式创建。该字符串在创建初始,会在堆内存的常量池中寻找是否存在字符常量“string”,如果存在则,直接引用到String,如果不存在,则创建新的字符串对象,并返回引用。
- String str1 = new String(“String”);构造器创建方式,不论是否存在该字符串,都会重新创建一个新的字符串对象。如果使用字符串String的intern方法:String str1 = str.intern(),调用inert方法之后,首先检查字字符串常量池中是否有该对象的引用,如果存在,则返回该引用,否则则创建新的引用返回。
字符串常量池中存储的是字符串对象的引用而不是字符串对象本身
问题:字符串对象和常量池存在于何处?JVM内存划分解析
3. IO
Java 中的流
再熟悉了IO流的结构之后,说明字节流和字符流之间怎样转换。通过InputStreamReader和OutputStreamWriter实现。
完成以下案例:实现java对象的序列化
//对象输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("D://obj")));//创建对象
objectOutputStream.writeObject(new User("zhangsan", 100));获取对象数据,并开始写入
objectOutputStream.close();//关闭资源
//对象输入流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D://obj")));//创建对象
User user = (User)objectInputStream.readObject();读取输入流对象,墙砖为user对象
sout.user;//打印对象
objectOutputStream.close();//关闭资源
一、数据结构
线性结构:数组、链表、队列、栈;树结构;哈希结构:图结构:
- 首先明白链表和数组的区别:
链表:存储空间不连续,查询慢O(n),增删快O(1)。
单向链表结构:head-[数据,地址]-[数据,地址]-[数据,地址]。每一个数据节点都有数据部分和下一个数据节点地址构成。
数组:连续的内存空间,查询快O(1),增删慢O(n)。
1.1 栈
线性结构,底层为数组,继承自Vector,先进后出,创建对象默认容量为为10,超出范围自动扩容。
动态解析图
应用:逆序输出、语法检查(符号成对出现)、进制转换(倒序输出余数)
1.2 堆
在数据结构中,堆是特殊的二叉树,满足完全二叉树的条件,通常由数组时间,每个节点都能够与数组与之对应。
在java中,堆是jvm虚拟机的一块内存区域,是程序员能够使用的内存区,能够通过new 对象来获得一定的内存空间。
1.3 队列
线性结构,可以有链表和数组实现。数据元素遵循先进先出的原则。其他与栈类似。动态解析图
应用:阻塞队列,队列在高并发场景中可做为数据缓冲区。
1.4 集合
LIST
线性集合,元素可重复,存储有序,存在索引。
ArrayList:非线程安全,内部使用数组进行存储;扩容时需要创建新的数组,并复制数据。访问数据较快,插入和删除较慢;
LinkedList详解:基于双向链表机制,所谓双向链表机制,就是集合中的每个元素都知道其前一个元素及其后一个元素的位置。在LinkedList中,以一个内部的Entry类来代表集合中的元素,元素的值赋给element属性,Entry中的next属性指向元素的后一个元素,Entry中的previous属性指向元素的前一个元素,基于这样的机制可以快速实现集合中元素的移动。
总结:
- 1,LinkedList基于双向链表机制实现;
- 2,LinkedList在插入元素时,须创建一个新的Entry对象,并切换相应元素的前后元素的引用;在查找元素时,须遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可,此时原有的前后元素改变引用连在一起;
- 3,LinkedList是非线程安全的。
SET
元素不可重复,存储无序。
HashSet是使用HashMap来实现的;
TreeSet是使用TreeMap实现的;
LinkedHashSet继承自HashSet,具有HashSet的优点,内部使用链表维护元素的插入顺序。
Map
重点讲解hashMap:部通过数组 + 单链表的方式实现. jdk8中引入了红黑树对长度 > 8的链表进行优化;
当map中元素超出设定的阈值后, 会进行resize (length * 2)操作, 扩容过程中对元素一通操作, 并放置到新的位置
具体操作如下:
- 在jdk7中对所有元素直接rehash, 并放到新的位置
- 在jdk8中判断元素原hash值新增的bit位是0还是1, 0则索引不变, 1则索引变成"原索引 + oldTable.length"
动态解析
HashMap空构造,将loadFactor设为默认的0.75,threshold设置为12,并创建一个大小为16的Entry对象数组。
面试题:
已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。请写一个方法实现对HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,返回类型HashMap<IntegerUser>,要求对 HashMap 中的 User 的 age 倒序进行排序。排序时 key=value 键值对不得拆散。
答案解析:
public class HashMapSortDemo1 {
public static void main(String[] args) {
//创建测试环境
HashMap<Integer,User> map = new HashMap<Integer, User>();
map.put(1,new User("张三",1));
map.put(2,new User("李四",2));
map.put(3,new User("王五",3));
//测试排序
HashMap<Integer, User> map1 = sortMap(map);
System.out.println(map1);
}
/**
* 排序方法
*/
public static HashMap<Integer,User> sortMap(HashMap<Integer,User> map){
//获取map集合所有元素
Set<Map.Entry<Integer, User>> entrySet = map.entrySet();
//将set集合转换成List集合,使用collections方法
List<Map.Entry<Integer, User>> entryList = new ArrayList<Map.Entry<Integer, User>>(entrySet);
//排序
Collections.sort(entryList, new Comparator<Map.Entry<Integer, User>>() {
public int compare(Map.Entry<Integer, User> o1, Map.Entry<Integer, User> o2) {
return o2.getValue().getAge()-o1.getValue().getAge();//根据年龄倒序
}
});
LinkedHashMap<Integer,User> linkedHashMap = new LinkedHashMap<Integer, User>();
for (Map.Entry<Integer, User> entry : entryList) {
linkedHashMap.put(entry.getKey(),entry.getValue());
}
return linkedHashMap;
}
}
1.5 线程安全的集合
- Vector 线程安全:
- HashTable 线程安全:无论是key还是value都不允许有null值的存在
- StringBuffer 线程安全:
1.6 CopyOnWrite
简称cow,是计算机程序设计领域中的一种优化策略,也是一种思想,即写入式复制思想。
List并发集合(CopyOnWriteArrayList)
基本实现原理介绍:写入式复制思想
在向集合中添加元素时:对add方法添加ReentrantLock锁,实现对add方法的安全控制。在当前线程完成操作之后,复制到一个新的数组中,并且将原数组指针指向当前数组。实现复制替换。
注意:CopyOnWriteArrayList本身是不存在锁。因此需要进行复制操作,保证原有的数组对象还存在。CopyOnWriteArrayList只能保证最终的数据一致性。
ConcurrentHashMap
jdk1.7之前:
从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。、
简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。
jdk1.8之后:
采用CAS和synchronized实现。详细实现方式,源码看不懂。
CAS基本介绍:在进行修改操作时,假定存在三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
缺点:会发生ABA操作。假定线程1从内存中读取A,线程2也在内存中读取A,并且线程2进行了一些操作将内存中的值变成了B,然后线程2又将内存X中的数据变成A,此时,线程1认为内存中的值任然为A,则线程1操作成功。但事实上,整个操作过程是存在线程安全问题的,忽略了线程2中的过程操作。
二、线程
2.1 创建线程的三种方式:
继承Thread,实现runnerble接口,使用Callable和Future创建线程。重点说明第三种方式。
三种线程创建的区别:callable的call’方法可以向外抛出异常。同时可存在返回值。
FutureTask实现了Future接口、Runnable接口 ,可以作为 Thread对象的target:关系如下
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
CallableTest callableTest = new CallableTest() ;
//因为Callable接口是函数式接口,可以使用Lambda表达式
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i = 0 ;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName() + "的循环变量i的值 :" + i);
}
return i;
});
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" 的循环变量i : + i");
if(i==20){
new Thread(task,"有返回值的线程").start();
}
}
try{
System.out.println("子线程返回值 : " + task.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
2.2 线程的生命周期
和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量。
就绪 ----当线程对象调用了start()方法之后,该线程处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器。此时表示当前线程可以准备运行,至于何时运行取决于jvm虚拟机的调用。
运行 ---- 当就绪状态的线程对象获得了cpu使用权,执行run方法,进入运行状态。
运行状态的线程处于以下状态时会出现阻塞状态:
1、线程调用sleep方法。
2、线程wait等待。
3、线程自闭,调用join方法,此时该线程会进入阻塞状态,另一个线程会运行,直到运行结束
后,原线程才会进入就绪状态。
4、调用io阻塞方法。下图是线程调用io阻塞时的情况。
5、调用suspend方法,挂起当前线程。调用resume唤醒;由于这种比较容易出现死锁现象,所以jdk1.5之后就已经被废除了
阻塞 ----当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会了。被阻塞的线程会在合适时候重新进入就绪状态,注意是就绪状态而不是运行状态。
死亡 ----执行结束或者线程抛出一个未捕获的Exception或Error。
直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
2.3 线程池
2.3.1 线程池的实现原理
线程池内部维护了一个线程列表,我们使用线程池只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行
2.3.2 线程池的四种方式
- newFixedThreadPool():初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列。
特点:即使当线程池没有可执行任务时,也不会释放线程。 - newCachedThreadPool():初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
特点:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; - newSingleThreadExecutor():初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。
如果该线程异常结束LLLL,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行 - newScheduledThreadPool():初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
提交任务给线程池
- Executor.execute(Runnable command);
- ExecutorService.submit(Callable task);
线程池的关闭
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
总结:线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到maxPoolSize,这时再有任务来,只能执行reject()处理该任务;
面试问题:多个线程同时读写,读线程的数量远远⼤于写线程,你认为应该如何解决并发的问题?你会选择加什么样的锁?
采用Read-Write Lock Pattern是一种将对于共享资源的访问与修改操作分离,称为读写分离。用单独的线程来处理读写,允许多个读线程同时读,但是不能多个写线程同时写。
2.3.3 乐观锁和悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。乐观锁的两种实现机制:版本version控制和CAS。
CAS实现,以ava.util.concurrent 中AtomicInteger 为例。
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
//获取当前对象
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
//通过compareAndSet方法对内存中的value变量和+1之后进行比较。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
参考:https://blog.csdn.net/caisongcheng_good/article/details/79916873
三、设计模式
3.1 单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
主要实现方法:
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
单例模式的写法
饿汉式(静态常量) 此方法还可以将final变为静态代码块
//在类加载的时候创建final实体,通过唯一外部方法获取静态化实体。
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
//将final去掉:类加载器在创建的时候会执行一次静态代码块,利用此原理创建对象。
static {
instance = new Singleton();
}
懒汉式()
//每次在创建实例的时候判断当前实例是否为第一次创建,如果是,则完成对实体的创建。如果否,则返回当前实例。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
//存在多线程不安全问题。可以再静态方法上添加synchronized锁。不推荐使用。
双重检查
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}//在加锁钱和加锁后都进行判断
静态内部内实现方式-借鉴饿汉式
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
枚举-在此处就不写了,很简单。
更多设计模式参考https://www.cnblogs.com/pony1223/p/7608955.html
四、框架
扩展:servlet生命周期
加载servlet实例,在servlet容器检测到第一次请求时,创建该servlet’实例。
init初始化,在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象。
请求处理:,Servlet容器调用Servlet的service()方法对请求进行处理。
服务终止,当容器检测到一个Servlet实例应该从服务中被移除的时候,容器就会调用实例的destroy()方法,释放资源,保存数据。
4.1 springMVC
执行流程
1、用户请求发送至DispatcherServlet类进行处理。
2、DispatcherServlet类遍历所有配置的HandlerMapping类请求查找Handler。
3、HandlerMapping类根据request请求的URL等信息查找能够进行处理的Handler,以及相关拦截器interceptor并构造HandlerExecutionChain。
4、HandlerMapping类将构造的HandlerExecutionChain类的对象返回给前端控制器DispatcherServlet类。
5、前端控制器拿着上一步的Handler遍历所有配置的HandlerAdapter类请求执行Handler。
6、HandlerAdapter类执行相关Handler并获取ModelAndView类的对象。
7、HandlerAdapter类将上一步Handler执行结果的ModelAndView 类的对象返回给前端控制器。
8、ViewResolver类进行视图解析并获取View对象。
9、ViewResolver类向前端控制器返回上一步骤的View对象。
10、DispatcherServlet类进行视图View的渲染,填充Model。
11、DispatcherServlet类向用户返回响应.
springMVC常见面试题;https://blog.csdn.net/a745233700/article/details/80963758
3.2 spring
IOC 控制反转:Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。类与类之间的依赖关系交给spring来控制,而不是直接调用。实现了程序的解耦合。以前是我们主动去寻找,获取一个类来供我们使用,现在是spring给我们一个类,从主动变为被动。
DI 依赖注入:IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。spring就是通过反射来实现注入的。
ioc和di其实就是同一个概念的不同角度描述。一个控制,一个依赖。
AOP:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。(具体理解请自行查看相关文档)
spring事务控制:
Spring事务机制主要包括声明式事务和编程式事务,此处侧重讲解声明式事务。
了解spring事务控制的相关对象:
PlatformTransactionManerge 事务管理者
TransactionDefinition 事务属性 -超时时间、传播行为、隔离级别、只读。
TransactionStatus 事务运行状态
spring在TransactionDefinition接口中定义这些属性,以供PlatfromTransactionManager使用,。
基于xml配置的配置步骤:
- 配置数据源和模板类JdbcTemplate
- 配置事务管理器
- 配置事务通知
- 事务aop织入配置,(事务控制地点),在那个类那个方法上添加事务控制。
- 完成切入点表达式和事务通知的对应关系。
基于注解配置的配置步骤:
- 配置数据源和模板类JdbcTemplate
- 配置事务管理器
- 组件扫描(包扫描)
- 事务注解驱动tx:annotation-driven/
##五、分布式事务
参考文档:架构篇,分布式事务解决方案
本地消息表
高并发和分布式中的幂等处理
六、其他
###1. redis
redis数据类型:String ,List,Set,Sorted Set(Zset),Hash 。
Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的万一 Redis 挂掉,数据会消失掉。
redis缓存穿透:是指查询一个数据库一定不存在的数据。不断请求。
解决方案:初步校验;当查询不到数据时,设置key为0,存入缓存,同时设置较短的超时时间。
redis缓存雪崩,是指在某一个时间段,缓存集中过期失效。
解决方案:随机过期时间。缓存数据永不过期,缓存预热。
redis缓存击穿,缓存中不存在数据,并发请求量大,在读取不到缓存时,读取数据库。引起数据库压力瞬间增大。
解决方案:设置缓存数据永不过期。缓存预热。
魂村预热解决方案:缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
(1)直接写个缓存刷新页面,上线时手工操作下;
(2)数据量不大,可以在项目启动的时候自动进行加载;
(3)定时刷新缓存;
2. rabbitMQ
工作模式:
- 简单模式、工作队列模式//两种模式不涉及交换机
- 订阅模式:发布订阅模式(交换机)、路由模式(绑定routingkey)、通配符模式(添加通配符)
如何保证消息中间件消息不丢失
1) 保证消息生产者发送消息成功 --- 事务或者confirm确认机制
2) 开启交换机,队列,消息持久化
3) 关闭自动应答,开启手动应答
如何防止消息重复消费
RabbitMQ没有提高相应的解决方案。
1) 在消息发送方给每一个消息指定一个唯一的 -- { msgID:UUID,data:{ ... } }
2 ) 消费者如果把消息成功消费了,同时就向消息日志表记录当前消费的消息msgID
如何确保消费者成功处理了消息
1)关闭rabbitMq的自动应答机制
2)第二消费者正确处理完消息后手动应答。