Java开发工程师面试经验总集

置顶个交流群
文章觉得海星的话,可以来群里找桃子交流技术或者普通乱聊= =
挂群:820080257
文档链接:【腾讯文档】Java开发工程师
https://docs.qq.com/doc/DQlZxcG5QdURDWmhi

1 简历
1.1 ⾃我介绍
1.1.1 ⼀个好的⾃我介绍应该包含这⼏点要素:

  1. ⽤简单的话说清楚⾃⼰主要的技术栈于擅⻓的领域;

  2. 把重点放在⾃⼰在⾏的地⽅以及⾃⼰的优势之处;

  3. 重点突出⾃⼰的能⼒⽐如⾃⼰的定位的bug的能⼒特别厉害。
    例:
    ⾯试官,您好!我叫独秀⼉。我⽬前有1年半的⼯作经验,熟练使⽤Spring、MyBatis等框架、了解 Java 底层原理⽐如JVM调优并且有着丰富的分布式开发经验。离开上⼀家公司是因为我想在技术上得到更多的锻炼。在上⼀个公司我参与了⼀个分布式电⼦交易系统的开发,负责搭建了整个项⽬的基础架构并且通过分库分表解决了原始数据库以及⼀些相关表过于庞⼤的问题,⽬前这个⽹站最⾼⽀持 10 万⼈同时访问。⼯作之余,我利⽤⾃⼰的业余时间写了⼀个简单的 RPC 框架,这个框架⽤到了Netty进⾏⽹络通信, ⽬前我已经将这个项⽬开源,在 Github 上收获了 2k的 Star! 说到业余爱好的话,我⽐᫾喜欢通过博客整理分享⾃⼰所学知识,现在已经是多个博客平台的认证作者。 ⽣活中我是⼀个比较积极乐观的⼈,⼀般会通过运动打球的⽅式来放松。我⼀直都⾮常想加⼊贵公司,我觉得贵公司的⽂化和技术氛围我都⾮常喜欢,期待能与你共事!
    1.2 项⽬经历
    1.2.1 项⽬经历怎么写?
    简历上有⼀两个项⽬经历很正常,但是真正能把项⽬经历很好的展示给⾯试官的⾮常少。对于项⽬经历⼤家可以考虑从如下⼏点来写:

  4. 对项⽬整体设计的⼀个感受

  5. 在这个项⽬中你负责了什么、做了什么、担任了什么⻆⾊

  6. 从这个项⽬中你学会了那些东⻄,使⽤到了那些技术,学会了那些新技术的使⽤

  7. 另外项⽬描述中,最好可以体现⾃⼰的综合素质,⽐如你是如何协调项⽬组成员协同开发的或者在遇到某⼀个棘⼿的问题的时候你是如何解决的⼜或者说你在这个项⽬⽤了什么技术实现了什么功能⽐如:⽤redis做缓存提⾼访问速度和并发量、使⽤消息队列削峰和降流等等。
    1.2.2 项目介绍从下⾯⼏个⽅向来考虑

  8. 对项⽬整体设计的⼀个感受(⾯试官可能会让你画系统的架构图)

  9. 在这个项⽬中你负责了什么、做了什么、担任了什么⻆⾊

  10. 从这个项⽬中你学会了那些东⻄,使⽤到了那些技术,学会了那些新技术的使⽤

  11. 另外项⽬描述中,最好可以体现⾃⼰的综合素质,⽐如你是如何协调项⽬组成员协同开发的或者在遇到某⼀个棘⼿的问题的时候你是如何解决的⼜或者说你在这个项⽬⽤了什么技术实现了什么功能⽐如:⽤redis做缓存提⾼访问速度和并发量、使⽤消息队列削峰和降流等等。
    2 Java基础
    2.1 类和对象
    2.1.1 类和对象的概念、区别和联系?
    类与对象是整个面向对象中最基础的组成单元。
    类:是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法)
    对象:对象是一种个性的表示,表示一个独立的个体,每个对象拥有自己独立的属性,依靠属性来区分不同对象
    可以一句话来总结出类和对象的区别:类是对象的模板,对象是类的实例。类只有通过对象才可以使用,而在开发之中应该先产生类,之后再产生对象。类不能直接使用,对象是可以直接使用的。
    例:
    如下图:
    所以图中什么是类,什么是对象?
    男孩、女孩为类,类是一个模板,描述一类对象的行为和状态
    而具体的每个人为该类的对象,他的状态有:男性或者女性;他的行为有:名字、种族等。
    用一句话总结类与对象的区别与关系就是:类是一类对象拥有的共同的状态和行为的模板,而对象是具体的实例。
    用大白话来讲就是:只要是个人,那么就有手有脚能跑能跳,而我就是一个根据人的模板实例出来的对象,我拥有作为人该有的所有属性。
    2.2 集合
    2.2.1 说说List,Set,Map三者的区别?
    ● List 以索引来存取元素,有序的,元素是允许重复的,可以插入多个null。
    ● Set 不能存放重复元素,无序的,只允许一个null
    ● Map 保存键值对映射,映射关系可以一对一、多对一
    ● List 有基于数组、链表实现两种方式
    ● Set、Map 容器有基于哈希存储和红黑树两种方式实现
    ● Set 基于 Map 实现,Set 里的元素值就是 Map 的键值
    2.2.2 Set
    2.2.2.1 Set为什么无序、不重复?Set是如何实现不重复的?
    对于基本数据类型的数据,Set集合可以直接比较是否相等,相等就去掉重复。
    对于引用数据类型的数据,Set集合将会按照如下流程判断是否重复:

  12. Set集合每次添加元素的时候,会自动提取两个对象。

  13. 然后让两个对象调用自己的hashCode()方法(继承自Object)得到彼此的哈希值(所谓的内存地址)

  14. 然后判断两个对象的哈希值是否一样,如果不一样认为两个对象不重复,都保留。

  15. 如果两个对象的哈希值一样,Set集合会继续让两个对象进行equlas比较,

  16. 如果equlas比较结果不一样,则认为两个对象不重复,都保留。

  17. 如果equlas比较结果一样,则认为两个对象重复,保留一个。
    2.2.2.2 Set无序指的是?
    添加顺序 != 排列顺序
    会按照ASCII码进行排序,与添加顺序不一定相同,所以是“无序”。
    2.2.2.3 HashSet是如何保证不重复的?
    可以看一下HashSet的add方法,元素E作为HashMap的key,我们都知道HashMap的可以是不允许重复的。

    public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    }

2.2.2.4 说一下HashSet的实现原理?
● 不能保证元素的排列顺序,顺序有可能发生变化。
● 元素可以为null
● hashset保证元素不重复~ (这个面试官很可能会问什么原理,这个跟HashMap有关的哦)
● HashSet,需要谈谈它俩hashcode()和equles()哦~
● 实际是基于HashMap实现的,HashSet 底层使用HashMap来保存所有元素的
2.2.2.5 HashSet和TreeSet有什么区别?
● Hashset 的底层是由哈希表实现的,Treeset 底层是由红黑树实现的。
● HashSet中的元素没有顺序,TreeSet保存的元素有顺序性(实现Comparable接口)
● HashSet的add(),remove(),contains()方法的时间复杂度是O(1);TreeSet中,add(),remove(),contains()方法的时间复杂度是O(logn)
2.2.2.6 Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用还是equals()?
元素重复与否是使用equals()方法进行判断的,这个可以跟面试官说说
和equals()的区别,hashcode()和equals
2.2.3 List
2.2.3.1 Arraylist 与 LinkedList 区别
● 可以从它们的底层数据结构、效率、开销进行阐述:
● ArrayList是数组的数据结构,LinkedList是链表的数据结构。
● 随机访问的时候,ArrayList的效率比较高,因为LinkedList要移动指针,而ArrayList是基于索引(index)的数据结构,可以直接映射到。
● 插入、删除数据时,LinkedList的效率比较高,因为ArrayList要移动数据。
● LinkedList 比 ArrayList 开销更大,因为 LinkedList 的节点除了存储数据,还需要存储引用。
2.2.3.2 Collections.sort和Arrays.sort的实现原理
Collection.sort是对list进行排序,Arrays.sort是对数组进行排序。
2.2.3.3 遍历 ArrayList 时移除一个元素
foreach删除会导致快速失败的问题(fail-fast),fori顺序遍历会导致重复元素没删除,所以正确解法如下:
2.2.3.3.1 倒叙遍历删除
for(int i=list.size()-1; i>-1; i–){
if(list.get(i).equals(“jay”)){
list.remove(list.get(i));
}
}

2.2.3.3.2 迭代器删除
Iterator itr = list.iterator();
while(itr.hasNext()) {
if(itr.next().equals(“jay”) {
itr.remove();
}
}

2.2.3.3.3 fori优化后即可正常删除
原理:删除元素后,再次访问该位置即可确认是否已删除重复的元素。
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add(2);
list.add(3);
list.add(4);
list.add(4);
list.add(1);
list.add(3);
list.add(6);
list.add(4);
for(int i=0;i<list.size();i++){
if(list.get(i)==4){
list.remove(i);
i = i - 1;//只需要多添加这一行代码,待删除此位置的元素后,再访问此位置一次,就不会造成遗漏了
}
}
System.out.println(list);
}

2.2.3.4 Java 中的 LinkedList是单向链表还是双向链表?
双向链表
2.2.3.5 说一说ArrayList 的扩容机制吧
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
2.2.3.6 ArrayList的默认大小
ArrayList 的默认大小是 10 个元素
2.2.3.7 当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?
在作为参数传递之前,使用 Collections.unmodifiableCollection(Collection c) 方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
2.2.3.8 Array 和 ArrayList 有何区别?
● 定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。
● ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
2.2.3.9 说出ArrayList,LinkedList的存储性能和特性
● ArrayList,使用数组方式存储数据,查询时,ArrayList是基于索引(index)的数据结构,可以直接映射到,速度较快;但是插入数据需要移动数据,效率就比LinkedList慢一点~
● LinkedList,使用双向链表实现存储,按索引数据需要进行前向或后向遍历,查询相对ArrayList慢一点;但是插入数据速度较快。
● LinkedList比ArrayList开销更大,因为LinkedList的节点除了存储数据,还需要存储引用。
2.2.3.10 ArrayList集合加入1万条数据,应该怎么提高效率?
因为ArrayList的底层是数组实现,并且数组的默认值是10,如果插入10000条要不断的扩容,耗费时间,所以我们调用ArrayList的指定容量的构造器方法ArrayList(int size) 就可以实现不扩容,就提高了性能。
2.2.4 Map
2.2.4.1 HashMap
2.2.4.1.1 HashMap原理,java8做了什么改变?
● HashMap是以键值对存储数据的集合容器
● HashMap是非线性安全的。
● HashMap底层数据结构:数组+(链表、红黑树),jdk8之前是用数组+链表的方式实现,jdk8引进了红黑树
● Hashmap数组的默认初始长度是16,key和value都允许null的存在
● HashMap的内部实现数组是Node[]数组,上面存放的是key-value键值对的节点。HashMap通过put和get方法存储和获取。
● HashMap的put方法,首先计算key的hashcode值,定位到对应的数组索引,然后再在该索引的单向链表上进行循环遍历,用equals比较key是否存在,如果存在则用新的value覆盖原值,如果没有则向后追加。
● jdk8中put方法:先判断Hashmap是否为空,为空就扩容,不为空计算出key的hash值i,然后看table[i]是否为空,为空就直接插入,不为空判断当前位置的key和table[i]是否相同,相同就覆盖,不相同就查看table[i]是否是红黑树节点,如果是的话就用红黑树直接插入键值对,如果不是开始遍历链表插入,如果遇到重复值就覆盖,否则直接插入,如果链表长度大于8,转为红黑树结构,执行完成后看size是否大于阈值threshold,大于就扩容,否则直接结束。
● Hashmap解决hash冲突,使用的是链地址法,即数组+链表的形式来解决。put执行首先判断table[i]位置,如果为空就直接插入,不为空判断和当前值是否相等,相等就覆盖,如果不相等的话,判断是否是红黑树节点,如果不是,就从table[i]位置开始遍历链表,相等覆盖,不相等插入。
● HashMap的get方法就是计算出要获取元素的hash值,去对应位置获取即可。
● HashMap的扩容机制,Hashmap的扩容中主要进行两步,第一步把数组长度变为原来的两倍,第二部把旧数组的元素重新计算hash插入到新数组中,jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二部一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。
● HashMap大小为什么是2的幂次方?效率高+空间分布均匀
2.2.4.1.2 HashMap 是线程安全的吗,为什么不是线程安全的?死循环问题?
不是线性安全的。
并发的情况下,扩容可能导致死循环问题。
HashMap会进行resize操作,在resize操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。
1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)。
具体分析如下:
下面的代码是resize的核心内容:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

这个方法的功能是将原来的记录重新计算在新桶的位置,然后迁移过去。
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。
优化:
如果在取链表的时候从头开始取(现在是从尾部开始取)的话,则可以保证节点之间的顺序,那样就不存在这样的问题了。
综合上面两点,可以说明HashMap是线程不安全的。
2.2.4.1.3 HashMap 的扩容过程

  1. 第一步把数组长度变为原来的两倍,
  2. 第二步把旧数组的元素重新计算hash插入到新数组中。
  3. jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二步一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。
    2.2.4.1.4 为什么HashMap中String、Integer这样的包装类适合作为key?
    String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率~
    因为:
    ● 它们都是final修饰的类,不可变性,保证key的不可更改性,不会存在获取hash值不同的情况~
    ● 它们内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范
    2.2.4.1.5 如果想用Object作为hashMap的Key怎么做?
    ● 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
    ● 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
    2.2.4.1.6 HashMap在JDK1.7和JDK1.8中有哪些不同?
    2.2.4.1.7 HashMap是怎么解决哈希冲突的?
    Hashmap解决hash冲突,使用的是链地址法,即数组+链表的形式来解决。put执行首先判断 table[i] 位置,如果为空就直接插入,不为空判断和当前值是否相等,相等就覆盖,如果不相等的话,判断是否是红黑树节点,如果不是,就从 table[i] 位置开始遍历链表,相等覆盖,不相等插入。
    2.2.4.2 TreeMap
    2.2.4.2.1 TreeMap底层?
    ● TreeMap实现了SotredMap接口,它是有序的集合。
    ● TreeMap底层数据结构是一个红黑树,每个key-value都作为一个红黑树的节点。
    ● 如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。
    2.2.4.3 LinkedHashMap
    2.2.4.3.1 LinkedHashMap的应用,底层,原理
    ● LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序(insert-order)或者是访问顺序,其中默认的迭代访问顺序就是插入顺序,即可以按插入的顺序遍历元素,这点和HashMap有很大的不同。
    ● LRU算法可以用LinkedHashMap实现。
    2.2.4.4 如何决定使用 HashMap 还是TreeMap?
    这个点,主要考察HashMap和TreeMap的区别。
    TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按key的升序排序,也可以指定排序的比较器。当用Iterator遍历TreeMap时,得到的记录是排过序的。
    2.2.4.5 HashMap,HashTable,ConcurrentHash的共同点和区别?
    2.2.4.5.1 HashMap
    ● 底层由链表+数组+红黑树实现
    ● 可以存储null键和null值
    ● 线性不安全
    ● 初始容量为16,扩容每次都是2的n次幂
    ● 加载因子为0.75,当Map中元素总数超过Entry数组的0.75,触发扩容操作.
    ● 并发情况下,HashMap进行put操作会引起死循环,导致CPU利用率接近100%
    ● HashMap是对Map接口的实现
    2.2.4.5.2 HashTable
    ● HashTable的底层也是由链表+数组+红黑树实现。
    ● 无论key还是value都不能为null
    ● 它是线性安全的,使用了synchronized关键字。
    ● HashTable实现了Map接口和Dictionary抽象类
    ● Hashtable初始容量为11
    2.2.4.5.3 ConcurrentHashMap
    ● ConcurrentHashMap的底层是数组+链表+红黑树
    ● 不能存储null键和值
    ● ConcurrentHashMap是线程安全的
    ● ConcurrentHashMap使用锁分段技术确保线性安全
    ● JDK8为何又放弃分段锁,是因为多个分段锁浪费内存空间,竞争同一个锁的概率非常小,分段锁反而会造成效率低。
    2.2.5 消息队列(MQ)
    2.2.5.1 什么是消息队列(MQ)?
    MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构,是分布式系统中重要的组件。主要解决应用耦合,异步消息,流量削峰等问题。可实现高性能,高可用,可伸缩和最终一致性架构,是大型分布式系统中不可缺少的中间件。
    2.2.5.2 MQ的作用?
    消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
    解耦
    一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。
    异步
    主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。
    削峰
    高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。
    2.2.5.3 MQ有什么缺点?
    系统可用性降低
    本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了?因此,系统可用性会降低。
    系统复杂度提高
    加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
    一致性问题
    A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
    2.2.5.4 什么是Java优先级队列(Priority Queue)?
    优先队列PriorityQueue是Queue接口的实现,可以对其中元素进行排序
    ● 优先队列中元素默认排列顺序是升序排列
    ● 但对于自己定义的类来说,需要自己定义比较器
    方法
    peek()//返回队首元素
    poll()//返回队首元素,队首元素出队列
    add()//添加元素
    size()//返回队列元素个数
    isEmpty()//判断队列是否为空,为空返回true,不空返回false

特点
● 基于优先级堆
● 不允许null值
● 线程不安全
● 出入队时间复杂度O(log(n))
● 调用remove()返回堆内最小值
2.2.5.5 什么是阻塞队列?
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
2.2.5.6 poll()方法和 remove()方法的区别?
Queue队列中,poll() 和 remove() 都是从队列中取出一个元素,在队列元素为空的情况下,remove() 方法会抛出异常,poll() 方法只会返回 null 。
2.2.6 有没有有顺序的Map实现类,如果有,他们是怎么保证有序的?
● TreeMap和LinkedHashmap都是有序的。(TreeMap默认是key升序,LinkedHashmap默认是数据插入顺序)
● TreeMap是基于比较器Comparator来实现有序的。
● LinkedHashmap是基于链表来实现数据插入有序的。
2.2.7 哪些集合类是线程安全的?哪些不安全?
2.2.7.1 线性安全的
● Vector:比Arraylist多了个同步化机制。
● Hashtable:比Hashmap多了个线程安全。
● ConcurrentHashMap:是一种高效但是线程安全的集合。
● Stack:栈,也是线程安全的,继承于Vector。
2.2.7.2 线性不安全的
● Hashmap
● Arraylist
● LinkedList
● HashSet
● TreeSet
● TreeMap
2.2.8 Collection与Collections的区别是什么?
● Collection是Java集合框架中的基本接口,如List接口也是继承于它
● Collections是Java集合框架提供的一个工具类,其中包含了大量用于操作或返回集合的静态方法。如排序sort()。
2.2.9 怎么确保一个集合不能被修改?
首先 final 关键字不可行,final 修饰的这个成员变量,如果是基本数据类型,表示这个变量的值是不可改变的;如果是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。
那么,到底怎么确保一个集合不能被修改呢,看以下:
● unmodifiableMap
● unmodifiableList
● unmodifiableSet
demo:
public class Test {
private static Map<Integer, String> map = new HashMap<Integer, String>();
{
map.put(1, “jay”);
map.put(2, “tianluo”);
}

   public static void main(String[] args) {
       map =   Collections.unmodifiableMap(map);
       map.put(1, "boy");
       System.out.println(map.get(1));
   }

}

运行结果:
// 可以发现,unmodifiableMap确保集合不能修改啦,抛异常了
Exception in thread “main” java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at Test.main(Test.java:14)

2.2.10 迭代器
2.2.10.1 迭代器 Iterator 是什么?怎么用,有什么特点?
Iterator 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
2.2.10.2 Iterator 和 ListIterator 有什么区别?
● ListIterator 比 Iterator有更多的方法。
● ListIterator只能用于遍历List及其子类,Iterator可用来遍历所有集合,
● ListIterator遍历可以是逆向的,因为有previous()和hasPrevious()方法,而Iterator不可以。
● ListIterator有add()方法,可以向List添加对象,而Iterator却不能。
● ListIterator可以定位当前的索引位置,因为有nextIndex()和previousIndex()方法,而Iterator不可以。
● ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改哦。
2.2.11 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
2.2.11.1 快速失败
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
public class Test {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(2);

       Iterator iterator = list.iterator();
       while (iterator.hasNext()) {
             System.out.println(iterator.next());
           list.add(3);
           System.out.println(list.size());
       }
   }

}

运行结果:
1
Exception in thread “main” java.util.ConcurrentModificationException
3
at java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 909 ) a t j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList Itr.checkForComodification(ArrayList.java:909)atjava.util.ArrayListItr.next(ArrayList.java:859)
at Test.main(Test.java:12)

2.2.11.2 安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
public class Test {
public static void main(String[] args) {
List list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);

       Iterator iterator = list.iterator();
       while (iterator.hasNext()) {
             System.out.println(iterator.next());
           list.add(3);
           System.out.println("list   size:"+list.size());
       }
   }

}

运行结果:
1
list size:3
2
list size:4

其实,在java.util.concurrent 并发包的集合,如 ConcurrentHashMap, CopyOnWriteArrayList等,默认为都是安全失败的。
2.2.12 ArrayList 和 HashMap 的默认大小是多少?
ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。
2.3 反射机制
2.3.1 Java反射机制是什么?
反射是运行中的程序检查自己和软件运行环境的能力,它可以根据它发现的进行改变。通俗的讲就是反射可以在运行时根据指定的类名获得类的信息。
2.3.2 反射的作用是什么?

  1. 对于任意一个对象,可以拿到他的类;
  2. 对于任意一个类可以创建他的对象;
  3. 对于任意一个类,可以知道这个类有哪些属性和方法;
  4. 对于任意一个对象,可以调用它的任意一个方法;
  5. 生成动态代理。
    2.3.3 反射的原理?
    api层面去获取到jvm层装载的类信息。
    2.3.4 什么地方用到了反射?
  6. JDBC中,利用反射动态加载了数据库驱动程序。
  7. Web服务器中利用反射调用了Sevlet的服务方法(service)。
  8. 开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。
  9. 很多框架都用到反射机制,注入属性,调用方法,如Spring。
    2.3.5 反射对性能的影响?
    反射 classfor 会有一个检查过程:
    每一次反射调用会有 包装参数、可见性检查 、数据包装、匹配、java安全机制检查、性能问题,反射代码会造成java优化无法实现。
    2.4 多线程
    2.4.1 什么是线程和进程?
    进程
    是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。
    在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。
    线程
    线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
    2.4.2 ⼀句话简单了解堆和⽅法区?
    堆和⽅法区是所有线程共享的资源。

    堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存)
    ⽅法区
    ⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    2.4.3 说说并发与并⾏的区别?
    并发
    同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏);
    并⾏
    单位时间内,多个任务同时执⾏。
    2.4.4 为什么要使⽤多线程呢?
    先从总体上来说
    从计算机底层来说
    线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换 和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以同时运⾏,这减少 了线程上下⽂切换的开销。
    从当代互联⽹发展趋势来说
    现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线 程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
    再深⼊到计算机底层来探讨:
    单核时代:
    在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦: 当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我 们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了, 当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在 理想情况下达到 100%了。
    多核时代:
    多核时代多线程主要是为了提⾼ CPU 利⽤率。举个例⼦:假如我们要计算⼀个复 杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就 可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。
    2.4.5 使⽤多线程可能带来什么问题?
    并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁。
    什么是上下文切换?
    当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以 便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
    什么是线程死锁?
    多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。
    例:
    线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。
    如何避免死锁?
    产⽣死锁必须具备以下四个必要条件:
  10. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
  11. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
  12. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕 后才释放资源。
  13. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
    我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下:
  14. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  15. 破坏请求与保持条件 :⼀次性申请所有的资源。
  16. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释 放它占有的资源。
  17. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
    例:
    线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁。
    2.5 生产者和消费者
    2.5.1 什么是生产者和消费者?
    生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
    2.6 Java设计模式
    ● ⼯⼚设计模式 : Spring使⽤⼯⼚模式通过 BeanFactory 、 ApplicationContext 创建 bean 对 象。
    ● 代理设计模式 : Spring AOP 功能的实现。
    ● 单例设计模式 : Spring 中的 Bean 默认都是单例的。
    ● 包装器设计模式 : 我们的项⽬需要连接多个数据库,⽽且不同的客户在每次访问中根据需要 会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
    ● 观察者模式: Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。
    ● 适配器模式 :Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配 Controller 。
    3 网络基础
    3.1 网关
    网关就是一个网络连接到另一个网络的 “关口” 。又称网间连接器、协议转换器。
    网关是一种充当转换重任的计算机系统或设备。使用在不同的通信协议、数据格式或语言,甚至体系结构完全不同的两种系统之间,网关是一个翻译器,与网桥只是简单地传达信息不同,网关对收到的信息要重新打包,以适应目的系统的需求。
    同时,网关也可以提供过滤和安全功能。
    目前家用路由器一般使用192.168.1.1和192.168.0.1作为LAN接口的地址,这两个地址也是最常见的网关地址。
    windows查询网关指令:
    Win + R --> cmd --> ipconfig /all

3.2 网段
3.2.1 网段是什么?
IP前三段。
首先要看子网掩码是否相同:
最常用的是C类地址子网掩码,也就是255.255.255.0:
A类地址子网掩码是255.0.0.0:
只要第 1 段一样就是一个网段了,如 10.0.0.5 和 10.5.2.1 就是一个网段 ;
B类地址子网掩码是255.255.0.0:
就是说前 2 段一样就是一个网段的。
3.3 http和https
3.3.1 http和https的区别?

  1. 传输信息安全性不同
  2. 连接方式不同
  3. 端口不同
  4. 证书申请方式不同
    传输信息安全性不同
  5. http协议:是超文本传输协议,信息是明文传输。如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息。
  6. https协议:是具有安全性的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全。
    连接方式不同
  7. http协议:http的连接很简单,是无状态的。
  8. https协议:是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议。
    端口不同
  9. http协议:使用的端口是80。
  10. https协议:使用的端口是443。
    证书申请方式不同
  11. http协议:免费申请。
  12. https协议:需要到CA申请证书,一般免费证书很少,需要交费。
    3.3.2 如何理解HTTP协议是无状态的
    HTTP协议是无状态的,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系。HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接)。
    3.3.3 什么是HTTP长连接、短连接?
    在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
    而从HTTP/1.1起 默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
    Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。
3.4 DNS
3.4.1 DNS是什么?
dns是一个域名系统,是万维网上作为把域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。
意义:通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。在解析域名时,可以首先采用静态域名解析的方法,如果静态域名解析不成功,再采用动态域名解析的方法,域名是互联网上的身份标识,是不可重复的唯一标识资源; 互联网的全球化使得域名成为标识一国主权的国家战略资源。
某个区域的资源记录通过手动或自动方式更新到单个主名称服务器(称为主 DNS服务器)上,主 DNS 服务器可以是一个或几个区域的权威名称服务器。
3.5 CDN
3.5.1 CDN是什么?
CDN是指内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器。
通过中心平台的负载均衡内容分发,调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络内。
在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
3.6 TCP和UDP
3.6.1 概念
UDP

  1. 面向无连接
  2. UDP是面向报文的
  3. 有单播,多播,广播的功能
  4. 不可靠性
    TCP
  5. 面向连接
  6. 可靠的
  7. 基于字节流
    3.6.2 TCP长连接和短连接
    TCP短连接
    模拟一下TCP短连接的情况:
    client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次请求就完成了。这时候双方任意都可以发起close操作,不过一般都是client先发起close操作。上述可知,短连接一般只会在 client/server 间传递一次请求操作。
    优点
    对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
    缺点
    如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
    TCP长连接
    我们再模拟一下长连接的情况:
    client向server发起连接,server接受client连接,双方建立连接,client与server完成一次请求后,它们之间的连接并不会主动关闭 ,后续的读写操作会继续使用这个连接。
    TCP的保活功能主要为服务器应用提供。如果客户端已经消失而连接未断开,则会使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,此时服务器将永远等待客户端的数据。保活功能就是试图在服务端器端检测到这种半开放的连接。
    如果一个给定的连接在两小时内没有任何动作,服务器就向客户发送一个探测报文段,根据客户端主机响应探测4个客户端状态:
  8. 客户主机依然正常运行,且服务器可达。此时客户的TCP响应正常,服务器将保活定时器复位。
  9. 客户主机已经崩溃,并且关闭或者正在重新启动。上述情况下客户端都不能响应TCP。服务端将无法收到客户端对探测的响应。服务器总共发送10个这样的探测,每个间隔75秒。若服务器没有收到任何一个响应,它就认为客户端已经关闭并终止连接。
  10. 客户端崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
  11. 客户机正常运行,但是服务器不可达。这种情况与第二种状态类似。
    优点
    长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。
    缺点
    存活功能的探测周期太长,并且它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候。
    这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
    3.6.3 TCP建立连接的三次握手
    第一次握手
    客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。
    第二次握手
    服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
    第三次握手
    当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
    3.6.4 TCP建立连接为什么是三次握手?
    为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
    3.6.5 TCP断开连接的四次握手
    第一次握手
    若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
    第二次握手
    B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。
    第三次握手
    B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
    第四次握手
    A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
    3.6.6 TCP断开连接为什么是四次握手?
    TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。
    4 数据结构 & 算法
    4.1 讲讲红黑树的特点?
    ● 每个节点或者是黑色,或者是红色。
    ● 根节点是黑色。
    ● 每个叶子节点(NIL)是黑色。[注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
    ● 如果一个节点是红色的,则它的子节点必须是黑色的。
    ● 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
    5 常用框架
    5.1 Spring
    5.1.1 谈谈⾃⼰对于 Spring IoC 和 AOP 的理解
    IoC
    IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是 Spring ⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。
    将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀ 样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如 何被创建出来的。 在实际项⽬中⼀个 Service 类可能有⼏百甚⾄上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把⼈逼疯。如果利⽤ IoC 的话,你只需要配置好,然后在需要的地⽅引⽤就⾏了,这⼤⼤增 加了项⽬的可维护性且降低了开发难度。
    Spring 时代我们⼀般通过 XML ⽂件来配置 Bean,后来开发⼈员觉得 XML ⽂件来配置不太好, 于是 SpringBoot 注解配置就慢慢开始流⾏起来。
    AOP
    AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共 同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
    Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤ JDK Proxy ,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代 理了,这时候Spring AOP会使⽤ Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理。
    5.1.2 AOP 有几种实现方式?
    JDK动态代理实现和cglib动态代理实现
    JDK动态代理
    只能对实现了接口的类生成代理,而不是针对类,该目标类型实现的接口都将被代理。
    原理是 通过在运行期间创建一个接口的实现类来完成对目标对象的代理。
    实现过程
  12. 定义一个实现接口 InvocationHandler 的类
  13. 通过构造函数,注入被代理类
  14. 实现 invoke( Object proxy, Method method, Object[] args)方法
  15. 在主函数中获得被代理类的类加载器
  16. 使用 Proxy.newProxyInstance( ) 产生一个代理对象
  17. 通过代理对象调用各种方法
    CGLib动态代理
    CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
    如何选择?
    如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP,也可以强制使用 cglib 实现 AOP。
    如果目标对象没有实现接口,必须采用 cglib 库,Spring 会自动在 JDK 动态代理和 cglib 之间转换。
    5.2 RPC
    5.2.1 RPC框架的原理?
    5.2.2 RPC使用了哪些关键技术?
    动态代理
    生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理,Javassist字节码生成技术。
    序列化和反序列化
    在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。
    序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
    反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。
    目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。
    NIO通信
    出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。
    服务注册中心
    可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。
    6 数据库
    6.1 MySQL
    6.1.1 什么是事务?
    事务是逻辑上的⼀组操作,要么都执⾏,要么都不执⾏。
    事务最经典也经常被拿出来说例⼦就是转账了。假如⼩明要给⼩红转账1000元,这个转账会涉及 到两个关键操作就是:将⼩明的余额减少1000元,将⼩红的余额增加1000元。万⼀在这两个操 作之间突然出现错误⽐如银⾏系统崩溃,导致⼩明余额减少⽽⼩红的余额没有增加,这样就不对 了。事务就是保证这两个关键操作要么都成功,要么都要失败。
    6.1.2 事务的四⼤特性(ACID)
  18. 原⼦性(Atomicity): 事务是最⼩的执⾏单位,不允许分割。事务的原⼦性确保动作要么全部完成,要么完全不起作⽤;
  19. ⼀致性(Consistency): 执⾏事务前后,数据保持⼀致,多个事务对同⼀个数据读取的结 果是相同的;
  20. 隔离性(Isolation): 并发访问数据库时,⼀个⽤户的事务不被其他事务所⼲扰,各并发 事务之间数据库是独⽴的;
  21. 持久性(Durability): ⼀个事务被提交之后。它对数据库中数据的改变是持久的,即使数 据库发⽣故障也不应该对其有任何影响。
    6.1.3 并发事务带来哪些问题?
    在典型的应⽤程序中,多个事务并发运⾏,经常会操作相同的数据来完成各⾃的任务(多个⽤户对同⼀数据进⾏操作)。并发虽然是必须的,但可能会导致以下的问题:
    ● 脏读(Dirty read): 当⼀个事务正在访问数据并且对数据进⾏了修改,⽽这种修改还没有提 交到数据库中,这时另外⼀个事务也访问了这个数据,然后使⽤了这个数据。因为这个数据 是还没有提交的数据,那么另外⼀个事务读到的这个数据是“脏数据”,依据“脏数据”所做的 操作可能是不正确的。
    ● 丢失修改(Lost to modify): 指在⼀个事务读取⼀个数据时,另外⼀个事务也访问了该数 据,那么在第⼀个事务中修改了这个数据后,第⼆个事务也修改了这个数据。这样第⼀个事 务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事 务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被 隔离级别 脏读 不可重复读 幻影读 READ-UNCOMMITTED √ √ √ READ-COMMITTED × √ √ REPEATABLE-READ × × √ SERIALIZABLE × × × 丢失。
    ● 不可重复读(Unrepeatableread): 指在⼀个事务内多次读同⼀数据。在这个事务还没有结 束时,另⼀个事务也访问该数据。那么,在第⼀个事务中的两次读数据之间,由于第⼆个事 务的修改导致第⼀个事务两次读取的数据可能不太⼀样。这就发⽣了在⼀个事务内两次读到 的数据是不⼀样的情况,因此称为不可重复读。
    ● 幻读(Phantom read): 幻读与不可重复读类似。它发⽣在⼀个事务(T1)读取了⼏⾏数 据,接着另⼀个并发事务(T2)插⼊了⼀些数据时。在随后的查询中,第⼀个事务(T1) 就会发现多了⼀些原本不存在的记录,就好像发⽣了幻觉⼀样,所以称为幻读。
    6.1.4 解释⼀下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?
    池化设计应该不是⼀个新名词。我们常⻅的如java线程池、jdbc连接池、redis连接池等就是这类 设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好⽐你去⻝堂打饭,打饭的⼤妈会先把饭盛好⼏份放那 ⾥,你来了就直接拿着饭盒加菜即可,不⽤再临时⼜盛饭⼜打菜,效率就⾼了。除了初始化资 源,池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征 可以直接映射到java线程池和数据库连接池的成员属性中。
    数据库连接本质就是⼀个 socket 的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类的所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来 需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数 据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。在连接池中,创建连接后,将其放置在池 中,并再次使⽤它,因此不必建⽴新的连接。如果使⽤了所有连接,则会建⽴⼀个新连接并将其 添加到池中。 连接池还减少了⽤户必须等待建⽴与数据库的连接的时间。
    6.1.5 说说看,你是如何对SQL进行性能优化的?
  22. EXPLAIN
  23. SQL语句中IN包含的值不应过多
  24. SELECT语句务必指明字段名称
  25. 当只需要一条数据的时候,使用 limit 1
  26. 如果排序字段没有用到索引,就尽量少排序
  27. 如果限制条件中其他字段没有索引,尽量少用 or
  28. 尽量用union all代替union
  29. 不使用ORDER BY RAND()
  30. 区分in和exists, not in和not exists
  31. 使用合理的分页方式以提高分页的效率
  32. 分段查询
  33. 避免在 where 子句中对字段进行 null 值判断
  34. 不建议使用%前缀模糊查询
  35. 避免在where子句中对字段进行表达式操作
  36. 避免隐式类型转换
  37. 对于联合索引来说,要遵守最左前缀法则
  38. 必要时可以使用force index来强制查询走某个索引
  39. 注意范围查询语句
  40. 关于JOIN优化
    6.1.6 索引
    6.1.6.1 索引是什么?
    索引是帮助MySQL高效获取数据的排好序的数据结构(本质是一种优化查询的数据结构)。
    6.1.6.2 为什么要建立索引?
    目的就是为了减少磁盘I/O的次数,加快查询速率。
    如果我们不借助任何索引结构帮助我们快速定位数据的话,我们查找一条记录,就要逐行去查找、去比较。从最开始进行比较,发现不是,继续下一行。。。如果表很大的话,有上千万条数据,就意味着要做很多很多次磁盘I/O才能找到。速度是很慢的。
    6.1.6.3 什么时候需要建索引?
  41. 主键自动建立唯一索引
  42. 频繁作为查询条件的字段
  43. 外键关系建立索引
    6.1.6.4 什么时候不需要建索引?
  44. 表记录太少
  45. 经常增删改的表
  46. 数据列存在太多重复的内容没有必要建索引
    6.1.6.5 什么情况索引会失效?
  47. 组合索引未使用最左前缀,例如组合索引(A,B),where B=b不会使用索引;
  48. like未使用最左前缀,where A like ‘%China’;
  49. 搜索一个索引而在另一个索引上做order by,where A=a order by B,只使用A上的索引,因为查询只使用一个索引 ;
  50. or会使索引失效。如果查询字段相同,也可以使用索引。例如where A=a1 or A=a2(生效),where A=a or B=b(失效)
  51. 如果列类型是字符串,要使用引号。例如where A=‘China’,否则索引失效(会进行类型转换);
  52. 在索引列上的操作,函数(upper()等)、or、!=(<>)、not in等;
    6.1.7 分库分表之后,id 主键如何处理?
    ● UUID:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。比较适合⽤于⽣成 唯⼀的名字的标示⽐如⽂件的名字。
    ● 数据库⾃增 id : 两台数据库分别设置不同步⻓,⽣成不重复ID的策略来实现⾼可⽤。这种⽅式⽣成的 id 有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
    ● 利⽤ redis ⽣成 id : 性能比较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成 系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本。
    6.2 Redis
    6.2.1 redis的5种数据类型?他们之间的区别?
  53. string 字符串(可以为整形、浮点型和字符串,统称为元素)
  54. list 列表(实现队列,元素不唯一,先入先出原则)
  55. set 集合(各不相同的元素)
  56. hash hash散列值(hash的key必须是唯一的)
  57. sort set 有序集合
    7 搜索引擎
    7.1 Solr
    7.1.1 是什么?
    solr是基于Lucene的搜索服务器,主要用作 全文检索 。它易于安装和配置,而且附带了一个基于HTTP 的管理界面。
    Lucene
    Lucene是一个基于Java的全文信息检索工具包,它不是一个完整的搜索应用程序,而是为你的应用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta(雅加达) 家族中的一个开源项目。也是目前最为流行的基于Java开源全文检索工具包。目前已经有很多应用程序的搜索功能是基于 Lucene ,比如Eclipse 帮助系统的搜索功能。Lucene能够为文本类型的数据建立索引,所以你只要把你要索引的数据格式转化的文本格式,Lucene 就能对你的文档进行索引和搜索。
    7.1.2 大概介绍介绍?
    服务端
    安装就是解压一个war包,添加一些jar包,配置scheme.xml
    客户端
    客户端操作可以用 solrj 或者 spring-data-solr ,到时候也可以进行二次封装,也可以不需要,因为都是封装给了 service 层, controller 直接传入对象给 service 层就可以了。
    solr的客户端,主要就是学会 索引库的操作 和 各种条件的搜索
    索引库的操作
    ● 新增
    ● 删除
    ● 更新
    各种条件的搜索
    ● 普通域查询
    ● 复制域查询
    ● 动态域查询
    ● 分页查询
    ● 分组查询
    ● 高亮查询
    ● 过滤查询
    ● 区间查询
    ● 排序查询
    7.1.3 solr中文分词器
    7.1.3.1 solr中文分词器IK Analyzer的作用?
    如果没有配置IK分词器,用solr自带的text分词它会把一句话分成单个的字;
    配置IK分词器的话,它会把句子分成词组,有中文语义分析的效果, 对中文分词效果好。
    优点
    可以自定义词库,增加新词
    缺点
    分出来的垃圾词较多。
    7.1.3.2 IK分词器原理?
    本质上是词典分词,在内存中初始化一个词典,然后在分词过程中逐个读取字符,和字典中的字符相匹配,把文档中的所有词语拆分出来的过程
    7.1.4 solr的索引查询为什么比数据库要快?
    Solr使用的是Lucene API实现的全文检索。全文检索本质上是查询的索引。而数据库中并不是所有的字段都建立的索引,更何况如果使用like查询时很大的可能是不使用索引,所以使用solr查询时要比查数据库快。
    7.1.5 多张表的数据导入solr(解决id冲突)
    在schema.xml中添加uuid,然后solrconfig那边修改update的部分,改为使用uuid生成。
    7.1.6 solr如何分词,如何新增词,如何禁用词?
    分词
    schema.xml文件中配置一个IK分词器,然后域指定分词器为IK
    新增词
    新增词添加到词典配置文件中ext.dic
    禁用词
    禁用词添加到禁用词典配置文件中stopword.dic,然后在schema.xml文件中配置禁用词典
    8 分布式
    8.1 分布式锁有哪些方式?
  58. 基于数据库实现排他锁:做唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功。
  59. 基于redis实现
  60. 基于zookeeper实现
    8.2 负载均衡
    8.2.1 什么是负载均衡?
    负载均衡,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,从而协同完成工作任务。
    8.2.2 负载均衡的基本原理?
    任何的负载均衡技术都要想办法建立某种一对多的映射机制:一个请求的入口映射到多个处理请求的节点,从而实现分而治之。
    这种映射机制使得多个物理存在对外体现为一个虚拟的整体,对服务的请求者屏蔽了内部的结构。
    8.2.3 负载均衡有哪几种实现方式?
    目前最常见的负载均衡应用是Web负载均衡,根据实现的原理不同,常见的web负载均衡技术包括:DNS轮询、IP负载均衡和CDN。其中IP负载均衡可以使用硬件设备或软件方式来实现。
    DNS轮询
    DNS轮询是最简单的负载均衡方式。以域名作为访问入口,通过配置多条DNS A记录使得请求可以分配到不同的服务器。
    DNS轮询没有快速的健康检查机制,而且只支持WRR的调度策略导致负载很难“均衡”,通常用于要求不高的场景。并且DNS轮询方式直接将服务器的真实地址暴露给用户,不利于服务器安全。
    IP负载均衡
    IP负载均衡是基于特定的TCP/IP技术实现的负载均衡。比如NAT、DR、Turning等。是最经常使用的方式。
    IP负载均衡可以使用硬件设备,也可以使用软件实现。硬件设备的主要产品是F5-BIG-IP-GTM(简称F5),软件产品主要有LVS、HAProxy、NginX。其中LVS、HAProxy可以工作在4-7层,NginX工作在7层。关于三者的简单对比,可以参考这里。
    硬件负载均衡设备可以将核心部分做成芯片,性能和稳定性更好,而且商用产品的可管理性、文档和服务都比较好。唯一的问题就是价格。
    软件负载均衡通常是开源软件。自由度较高,但学习成本和管理成本会比较大。
    CDN
    CDN(Content Delivery Network,内容分发网络)。通过发布机制将内容同步到大量的缓存节点,并在DNS服务器上进行扩展,找到里用户最近的缓存节点作为服务提供节点。
    因为很难自建大量的缓存节点,所以通常使用CDN运营商的服务。目前国内的服务商很少,而且按流量计费,价格也比较昂贵。
    8.2.4 Nginx怎么实现负载均衡?
    轮询(默认)
    每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
    upstream backserver {
    server 192.168.0.14;
    server 192.168.0.15;
    }

指定轮询
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
upstream backserver {
server 192.168.0.14 weight=8;
server 192.168.0.15 weight=10;
}

IP绑定 ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
upstream backserver {
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}

8.2.5 常见的负载均衡算法有哪些?
轮询(Round Robin)法
轮询很容易实现,将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
这里通过实例化一个serviceWeightMap的Map变量来服务器地址和权重的映射,以此来模拟轮询算法的实现,其中设置的权重值在以后的加权算法中会使用到,这里先不做过多介绍,该变量初始化如下:
private static Map<String, Integer> serviceWeightMap = new HashMap<String, Integer>();
static {
serviceWeightMap.put(“192.168.1.100”, 1);
serviceWeightMap.put(“192.168.1.101”, 1);    
serviceWeightMap.put(“192.168.1.102”, 4); //权重为4
serviceWeightMap.put(“192.168.1.103”, 1);
serviceWeightMap.put(“192.168.1.104”, 1);
serviceWeightMap.put(“192.168.1.105”, 3); //权重为3
serviceWeightMap.put(“192.168.1.106”, 1);
serviceWeightMap.put(“192.168.1.107”, 2); //权重为2
serviceWeightMap.put(“192.168.1.108”, 1);
serviceWeightMap.put(“192.168.1.109”, 1);
serviceWeightMap.put(“192.168.1.110”, 1);
}

通过该地址列表,实现的轮询算法的部分关键代码如下:
private static Integer pos = 0;
public static String testRoundRobin() {
// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
Map<String, Integer> serverMap = new HashMap<String, Integer>();
serverMap.putAll(serviceWeightMap);

   //取得IP地址list
   Set<String> keySet =   serverMap.keySet();
   ArrayList<String> keyList = new   ArrayList<String>();
   keyList.addAll(keySet); 
   String server = null; 
   synchronized (pos) {
       if (pos > keySet.size()) {
           pos = 0;
       } 
       server = keyList.get(pos); 
       pos++;
   } 
   return server;

}

由于serviceWeightMap中的地址列表是动态的,随时可能由机器上线、下线或者宕机,因此,为了避免可能出现的并发问题,比如数组越界,通过在方法内新建局部变量serverMap,先将域变量拷贝到线程本地,避免被其他线程修改。这样可能会引入新的问题,当被拷贝之后,serviceWeightMap的修改将无法被serverMap感知,也就是说,在这一轮的选择服务器中,新增服务器或者下线服务器,负载均衡算法中将无法获知。新增比较好处理,而当服务器下线或者宕机时,服务消费者将有可能访问不到不存在的地址。因此,在服务消费者服务端需要考虑该问题,并且进行相应的容错处理,比如重新发起一次调用。
对于当前轮询的位置变量pos,为了保证服务器选择的顺序性,需要对其在操作时加上synchronized锁,使得同一时刻只有一个线程能够修改pos的值,否则当pos变量被并发修改,将无法保证服务器选择的顺序性,甚至有可能导致keyList数组越界。
使用轮询策略的目的是,希望做到请求转移的绝对均衡,但付出的代价性能也是相当大的。为了保证pos变量的并发互斥,引入了重量级悲观锁synchronized,将会导致该轮询代码的并发吞吐量明显下降。
随机法
通过系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。由概率概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到后台的每一台服务器,也就是轮询法的效果。
随机算法的部分关键代码如下:
public static String testRandom() {
// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
Map<String, Integer> serverMap = new HashMap<String, Integer>();
serverMap.putAll(serviceWeightMap);
//取得IP地址list
Set keySet = serverMap.keySet();
ArrayList keyList = new ArrayList();
keyList.addAll(keySet);
Random random = new Random();
int randomPos = random.nextInt(keyList.size());
String server = keyList.get(randomPos);
return server;
}

跟前面类似,为了避免并发的问题,需要将serviceWeightMap拷贝到serverMap中。通过Random的nextInt函数,取到0~keyList.size之间的随机值, 从而从服务器列表中随机取到一台服务器的地址,进行返回。根据概率统计理论,吞吐量越大,随机算法的效果越接近于轮询算法的效果。
源地址哈希法
源地址哈希法的思想是根据服务消费者请求客户端的IP地址,通过哈希函数计算得到一个哈希值,将此哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。采用源地址哈希法进行负载均衡,相同的IP客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。
源地址哈希法部分关键代码如下:
public static String testConsumerHash(String remoteIp) {
// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
Map<String, Integer> serverMap = new HashMap<String, Integer>();
serverMap.putAll(serviceWeightMap);
//取得IP地址list
Set keySet = serverMap.keySet();
ArrayList keyList = new ArrayList();
keyList.addAll(keySet);
int hashCode = remoteIp.hashCode();
int pos = hashCode % keyList.size();
return keyList.get(pos);
}

加权轮询(Weight Round Robin)法
不同的后台服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不一样。跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而配置低、负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询很好的处理了这一问题,并将请求按照顺序且根据权重分配给后端。

加权轮询法部分关键代码如下:
public static String testWeightRoundRobin() {
// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
Map<String, Integer> serverMap = new HashMap<String, Integer>();
serverMap.putAll(serviceWeightMap);
//取得IP地址list
Set keySet = serverMap.keySet();
Iterator it = keySet.iterator();
List serverList = new ArrayList();
while (it.hasNext()) {
String server = it.next();
Integer weight = serverMap.get(server);
for (int i=0; i<weight; i++) {
serverList.add(server);
}
}
String server = null;
synchronized (pos) {
if (pos > serverList.size()) {
pos = 0;
}
server = serverList.get(pos);
pos++;
}
return server;
}

与轮询算法类似,只是在获取服务器地址之前增加了一段权重计算代码,根据权重的大小,将地址重复增加到服务器地址列表中,权重越大,该服务器每轮所获得的请求数量越多。
加权随机(Weight Random)法
加权随机法跟加权轮询法类似,根据后台服务器不同的配置和负载情况,配置不同的权重。不同的是,它是按照权重来随机选取服务器的,而非顺序。
部分关键代码如下:
public static String testWeightRandom() {
// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
Map<String, Integer> serverMap = new HashMap<String, Integer>();
serverMap.putAll(serviceWeightMap);
//取得IP地址list
Set keySet = serverMap.keySet();
List serverList = new ArrayList();
Iterator it = keySet.iterator();
while (it.hasNext()) {
String server = it.next();
Integer weight = serverMap.get(server);
for (int i=0; i<weight; i++) {
serverList.add(server);
}
}
Random random = new Random();
int randomPos = random.nextInt(serverList.size());
String server = serverList.get(randomPos);
return server;
}

最小连接数法
前面我们费尽心思来实现服务消费者请求次数分配的均衡,我们知道这样做是没错的,可以为后端的多台服务器平均分配工作量,最大程度地提高服务器的利用率,但是,实际上,请求次数的均衡并不代表负载的均衡。因此我们需要介绍最小连接数法,最小连接数法比较灵活和智能,由于后台服务器的配置不尽相同,对请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。
9 Tomcat调优
Tomcat性能优化?

  1. 内存优化
  2. 并发优化
  3. 缓存优化
  4. IO优化
  5. 开启线程池
  6. 添加Listener
  7. 组件优化

猜你喜欢

转载自blog.csdn.net/weixin_46017976/article/details/118754685