JAVA项目面试常谈问题以及个人开场介绍模板

JAVA人力、技能、项目面试常谈问题以及个人开场介绍模板





关于JAVA问题 以及 完整项目源码获取 欢迎私信我,或者QQ995829376…在开源的时代,一起互相学习进步


--------------------------------------------------------------华丽的分割线----------------------------------------------------
-大道理想必大家都懂,所以直入主题,不说概念性的废话(本来要写的内容就很多,全文目前88289字,2460行);
-【提示!!!】文章内容会逐渐越来越多,若想针对性的寻觅想要的内容,可尝试CTRL+F找到自己感兴趣的内容!
-【文章结构】=【百道人力面试】+【几篇介绍模板】+【模块化技能面试】+【面试项目问题】+【扩展内容】,详情请先参考文章目录!!!选择性拿!!!
--------------------------------------------------------------华丽的分割线----------------------------------------------------



JAVA 人力面试(目前101个)常谈问题


后面的技术问题会两三天更新一次!!!不断完善 本次更新时间【2023-07-10】
小声哔哔:写就一个人写,看就大家一起看?!
在这里插入图片描述在这里插入图片描述

JAVA 技能面试项目常谈问题

开场白——模棱两可没把握的不要写,切勿给自己挖坑!

至于是说老师,面试官还是考官,因自己个人情况而异;

一、简短版

  1.        (各位)老师好,我叫XXX,来应聘JAVA开发工作岗位,今年XX岁,20XX年毕业于XXX大学,在校期间积极参与项目活动,从中学到一些实用的开发经验和积累了一些个人心得;本人性格温和,谦虚,有较强的社会适应能力和一定的抗压能力,也能快速的融入团队开发工作中,所以希望能够加入贵公司,一起努力奋斗,共创辉煌,谢谢!

  2.        (各位)老师好,大家好,我叫XXX,毕业于XXX大学,在校期间曾担任XX。我来应聘的是职位是JAVA开发工程师。我所拥有的是年轻和知识。年轻也许意味着欠缺经验,但是年轻也意味着热情和活力,我自信能凭自己的能力和学识在毕业以后的工作和生活中克服各种困难,不断实现自我的人生价值和追求的目标。

  3.        (各位)老师好,我叫XXX,我来应聘的是职位是JAVA开发工程师。我是一个性格开朗、自信,不轻易服输的人。生活中我喜欢与人交往,通过与各种朋友相处,学习到了很多自身缺乏的优点。在学习上我积极认真,努力掌握专业知识,现在已经具有相关的办公室事务管理、文书拟写与处理公共关系策划与活动组织等能力。


二、简短项目版

  1.        (各位)老师好,我叫XXX,毕业于XXX大学,今年XX岁,从事JAVA开发已有X年,参与了(简历上的项目名称)等项目开发,主要负责(简历项目上拿手的业务,HOLD住的话就谈亮眼的地方)等业务,在项目中也积累了一些个人开发心得和经验,

三、加长带项目版—(涉及内容过多,if you really hold 住)

  1.        (各位)老师好,我叫XXX,今年XX岁,今天来面试贵公司JAVA开发工程师,之前在(曾经工作的公司)任职,主要的任务是(1、与项目经理到客户现场沟通并确认需求,2、参与需求文档的编写,3、后端业务模块的逻辑实现…根据面试岗位需求针对性回答),从事这一行已经有X年。这X年的开发主要涉及(项目领域)等,在开发过程中,也用过些好的框架,如(dubbo+zk,springboot,mvc,spring…)等框架,熟练掌握框架之间的整合技术;有时候因为项目需求或是为了开发的高效性,自己也会研究一些技术,使用一些常用的主流java技术,例如:(你觉得什么牛皮就吹什么);闲暇时喜欢钻研感兴趣的技术,例如(数据结构中的什么,什么算法,架构思想等等),并把自己研究的过程以及心得与同事分享交流,因为我一直坚信一个人的能力是有限的,只有团队协作才会给公司带来更大的价值;
           我承认自己还不是最优秀的人才,但是我希望自己是此岗位最合适的人选,希望有幸能够被贵公司领导予以特别考虑。今后我在好的方面再接再厉,不足之处有所改善。我的介绍到此结束。谢谢!


在这里插入图片描述

技能部分

基础知识(有待归类)

1 什么是字符串常量池?
创建字符串的方式

 String str = new String"hello";
 String str = "hello";

使用相同的字符序列而不是使用new关键字创建的两个字符串会创建指向Java字符串常量池中的同一个字符串的指针。字符串常量池是Java节约资源的一种方式

字符串常量池: JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中, 就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突 进行共享;每一个字符串常量都是指向一个字符串类实例的引用。字符串对象有一个固定值。字符串常量,或者一般的说,常量表达式中的字符串都被使用方法 String.intern进行保留来共享唯一的实例;

2 String为什么是不可变的?
不可变的是什么: 指的是字符串的值不可变;
为什么不可变: 从源码来看, String类内部是用char数组来保存字符串的值, 并且char[]是final的,所以value必须在构造时为其赋值,
赋值后value的引用不能再变

3 String s = new String(“xyz”);究竟产生了几个对象,从JVM角度谈谈?
jvm 首先在 string 池内里面看找不找得到字符串 , 如果找得到, 不做任何事情,
否则的话就会创建新的 string 对象,放到 string 池里面。
由于遇到了 new,还会在内存上(不是 string 池里面)创建 string 对象存储;

所以总共是 1个 或者 2个对象

  1. 首先在 string 池内找,找到了,不创建 string 对象,否则创建一个对象;【这里创建0个或1个】
  2. 遇到 new 运算符号了,在内存上创建 string 对象,并将其返回给 s,又一个对象;【这里创建1个】

new String(…)创建了第二个对象

4 String拼接字符串效率低,你知道原因吗?
String的+在编译后会被编译为StringBuilder来运行,如果你对JVM有所了解,凡是new出来的对象绝对不会放在常量池中,toString会发生一次内容拷贝,但是也不会在常量池中,所以在这里常量池String+放在了堆中;每做一次"+"就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后append字符串,如此循环直至结束。如果我们直接采用StringBuilder对象进行append的话,我们可以节省创建和销毁对象的时间。

如果只是简单的字面量拼接或者很少的字符串拼接,性能都是差不多的。

5 你真的了解String的常见API吗?

6 Java中的subString()真的会引起内存泄露么?
1 这个bug发生在jdk1.6版本,bai后续修复了du。原因是比如我们有一个1G的字符串a,我们使用substring(0,2)得到了一个只有两个字符的字符串b,如果b的生命周期要长于a或者手动设置a为null,当垃圾回收进行后,a被回收掉,b没有回收掉,那么这1G的内存占用依旧存在,因为b持有这1G大小的字符数组的引用。
2 所以推荐的方式是substring中生成的字符串与原字符串共享内容数组,这样避免了每次进行substring重新进行字符数组复制。
3 jdk1.7之后substring的实现抛弃了之前的内容字符数组共享的机制,对于子字符串(自身除外)采用了数组复制实现单个字符串持有自己的应该拥有的内容来避免内存泄漏。

7 浅析Java中的final关键字?
1 修饰类
当用final修饰一个类时,表明这个类不能被继承。但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

2 修饰方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
 注:类的private方法会隐式地被指定为final方法。

3 修饰变量(包括成员变量和局部变量)
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

8 浅析Java中的static关键字?
1 在Java中static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,当然也可以修饰代码块;
2 在JVM加载一个类的时候,若该类存在static修饰的成员变量和成员方法,则会为这些成员变量和成员方法在固定的位置开辟一个固定大小的内存区域(只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们);
3 同时被static修饰的成员变量和成员方法是独立于该类的,它不依赖于某个特定的实例变量,也就是说它被该类的所有实例共享。所有实例的引用都指向同一个地方,任何一个实例对其的修改都会导致其他实例的变化。

静态变量:
 static修饰的变量我们称之为静态变量,没有用static修饰的变量称之为实例变量,他们两者的区别是:
 静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
 所以我们一般在这两种情况下使用静态变量:对象之间共享数据、访问方便。

静态方法:
 static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。
   因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

总结一下,对于静态方法需要注意以下几点:
 (1)它们仅能调用其他的static 方法。
 (2)它们只能访问static数据。
 (3)它们不能以任何方式引用this 或super。

静态代码块:
 static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次;
 特性:只会在类加载的时候执行一次;

9 你对Java中的volatile关键字了解多少?
VOLATILE关键字:保证内存模型中的可见性与有序性,不保证原子性;
 可见性 : 是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
 有序性 :
 原子性 :
为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。

12 请谈谈什么是CAS?
一种高效实现线程安全性的方法
支持原子更新操作,适用于计数器,序列发生器(就是给变量自增的工具)等场景
属于乐观锁机制,号称lock-free(底层也是有加锁行为的)
CAS操作失败是,由开发者决定是否继续尝试,还是其他操作,因此争用CAS失败的线程不会被阻塞挂起;

CAS思想:
包含三个操作数——内存位置(V)、预期原值(A)和新值(B):当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给A,基于A进行计算,得到新值B,在用预期原值A和内存中的共享变量值进行比较(这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成),如果相同就认为其他线程没有进行修改,而将新值写入内存;

CAS的缺点:
1 CPU开销比较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。

2 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3 ABA问题:线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。

CAS引起的ABA问题:
比如说我要取100,但是由于机器故障开启了两次取钱的线程,由于采用的是CAS操作,线程一如果执行成功,而线程二由于某些原因阻塞了,并且在这个阶段中有其他用户向我的账户中转了50元,并且该线程执行成功,那么线程二恢复的时候,会发现地址的值V=100,线程二比较V、A发现相同,就更新余额为50.这样就发生错误。

CAS的ABA问题解决方案:
不是只是更新某个引用的值, 而是更新两个值,包含一个引用和一个版本号。
AtomicStampedReference以及AtomicMarkableReference支持在两个变量上执行原子的条件更新。
AtomicStampedReference将更新一个“对象引用——版本号”二元组,通过在引用上加上“版本号”,从而避免ABA问题(reference——stamp)
AtomicMarkableReference将更新一个“对象引用——布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除节点”(reference——mark)

List Set 与Map的区别
一、List接口
List是一个继承于Collection的接口,即List是集合中的一种。List是有序的队列,List中的每一个元素都有一个索引;第一个元素的索引值是0,往后的元素的索引值依次+1。和Set不同,List中允许有重复的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

(1)ArrayList
ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量:

private static final int DEFAULT_CAPACITY = 10;

随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

ArrayList擅长于随机访问。同时ArrayList是非同步的。

(2)LinkedList
同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。

由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端,节约一半时间)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

List list = Collections.synchronizedList(new LinkedList(…));

(3)Vector
与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

(4)Stack
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

二、Set接口
Set是一个继承于Collection的接口,Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样运行null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,关于API方面。Set的API和Collection完全一样。实现了Set接口的集合有:HashSet、TreeSet、LinkedHashSet、EnumSet。

(1)HashSet
HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。集合元素可以是null,但只能放入一个null。它内部元素的顺序是由哈希码来决定的,所以它不保证set的迭代顺序;特别是它不保证该顺序恒久不变。

(2)TreeSet
TreeSet是二叉树实现的,基于TreeMap,生成一个总是处于排序状态的set,内部以TreeMap来实现,不允许放入null值。它是使用元素的自然顺序对元素进行排序,或者根据创建Set时提供的 Comparator 进行排序,具体取决于使用的构造方法。

(3)LinkedHashSet
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

三、Map接口
Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。实现map的集合有:HashMap、HashTable、TreeMap、WeakHashMap。

(1)HashMap
以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。

(2)HashTable
也是以哈希表数据结构实现的,解决冲突时与HashMap也一样也是采用了散列链表的形式。HashTable继承Dictionary类,实现Map接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类。每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键至多与一个值相关联。Map是”key-value键值对”接口。 HashTable采用”拉链法”实现哈希表不过性能比HashMap要低。

(3)TreeMap
有序散列表,实现SortedMap接口,底层通过红黑树实现。

(4)WeakHashMap
谈WeakHashMap前先看一下Java中的引用(强度依次递减)

强引用:普遍对象声明的引用,存在便不会GC
软引用:有用但并非必须,发生内存溢出前,二次回收
弱引用:只能生存到下次GC之前,无论是否内存足够
虚引用:唯一目的是在这个对象被GC时能收到一个系统通知
以弱键实现的基于哈希表的Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。null值和null键都被支持。该类具有与HashMap类相似的性能特征,并具有相同的效能参数初始容量和加载因子。像大多数集合类一样,该类是不同步的。

四、总结
1、List、Set都是继承自Collection接口,Map则不是
2、List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
3、Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
4、Map适合储存键值对的数据
5、线程安全集合类与非线程安全集合类 :
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
StringBuilder是非线程安全的,StringBuffer是线程安全的。

13 HashMap每次扩容和初始容量为什么都是2的次幂?
HashMap每次扩容和初始容量都是2的次幂都是为了降低hash碰撞,输入元素尽可能的均匀分布在数组上,减少为避免冲突而建立链表造成的损耗。

13 从源码角度看看ArrayList的实现原理?
添加元素是一个一个添加的,当添加元素的时候,发现装不下了,才会进行扩容,扩容后的数组长度是原来的 1.5 倍。

特点:
1 快速随机访问
2 允许存放多个null元素
3 底层是Object数组
4 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),改对应索引元素比较快

总结
1 底层是Object数组存储数据;
2 扩容机制:默认大小是10,扩容是扩容到之前的1.5倍的大小,每次扩容都是将原数组的数据复制进新数组中. 我的领悟:如果是已经知道了需要创建多少个元素,那么尽量用new ArrayList<>(13)这种明确容量的方式创建ArrayList.避免不必要的浪费;
3 添加:如果是添加到数组的指定位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制;如果是添加到末尾的话,那么只可能触发扩容机制;
4 删除:如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的. ArrayList里面的删除元素,其实是将该元素置为null;
5 查询和改某个位置的元素是非常快的( O(1) );

14 手写LinkedList的实现,彻底搞清楚什么是链表?

15 Java中方法参数的传递规则?

16 Java中throw和throws的区别是什么?

17 重载(Overload) 和重写(Override)的区别?
1.重写必须继承,重载不用。
2.重写的方法名,参数数目相同,参数类型兼容,重载的方法名相同,参数列表不同
3.重写的方法修饰符大于等于父类的方法,重载和修饰符无关。
4.重写不可以抛出父类没有抛出的一般异常,可以抛出运行时异常
18 手写ArrayList的实现,在笔试中如何过关斩将?

19 finally语句块你踩过哪些坑?

20 为什么重写equals方法需同时重写hashCode方法?

21 equals() 与 == 的区别?
一、对象类型不同
1、equals():是超类Object中的方法。
2、==:是操作符。

二、比较的对象不同
1、equals():用来检测两个对象是否相等,即两个对象的内容是否相等。
2、==:用于比较引用和比较基本数据类型时具有不同的功能,具体如下:
(1)、基础数据类型:比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。
(2)、引用数据类型:比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。

三、运行速度不同
1、equals():没有运行速度快。
2、
:运行速度比equals()快,因为==只是比较引用。

22 String StringBuffer和StringBuilder的区别,从源码角度分析?
(1)String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁(底层使用 final char[]存储,因此String中的字符内容不可变);
(2)StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。所以说StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串;
(3)StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高;

23 HashMap
1 底层数组+链表实现,可以存储null键和null值,线程不安全
2 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
3 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
4 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
5 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
6 计算index方法:index = hash & (tab.length – 1)

23 你知道HashMap的数据结构吗?
通常在我们的应用中,HashMap是用到最多的数据结构之一,在JDK1.8之前,它的底层结构是数组+链表,而在JDK1.8之后,为了查询效率的优化(主要是当哈希碰撞较多的时候),它的底层结构变成了数组+链表+红黑,以增强查询效率;

简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,
也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry;Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

34 HashMap是线程安全的吗?如何实现线程安全?
HashMap并非线程安全,多线程使用时,会导致值覆盖,会导致ConcurrentModificationException错误;如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问。

那么在Java中线程安全的map有没有呢?答案是肯定的,有以下几种方式:
HashTable
(1)底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化;
(2)初始size为11,扩容:newsize = olesize*2+1;
(3)计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length;
Synchronized Map

ConcurrentHashMap
(1) 底层采用分段的数组+链表实现,线程安全;
(2) 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
(3) Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术;
(4) 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁;
(5) 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容;

其中前两种方式是同步加锁的方式,会影响程序运行效率,第三种ConcurrentHashMap是Java 中支持高并发,高吞吐量的hashMap实现。ConcurrnetHashMap是基于线程安全的一个类;

HashMap遍历方式:

Map map = new HashMap();
  Iterator iter = map.entrySet().iterator();
  while (iter.hasNext()) {
  Map.Entry entry = (Map.Entry) iter.next();
  Object key = entry.getKey();
  Object val = entry.getValue();
  }

HashMap如何将数据存入
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标), 如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上;
24 为何HashMap的数组长度一定是2的次幂?
HashMap每次扩容和初始容量都是2的次幂都是为了降低hash碰撞,输入元素尽可能的均匀分布在数组上,减少为避免冲突而建立链表造成的损耗。

25 HashMap何时扩容以及它的扩容机制?
什么时候扩容:
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table。当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容;

扩容过程:
(1)若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16;
(2)默认的负载因子大小为0.75,当一个map填满了75%的bucket时候,就会扩容,扩容后的table大小变为原来的两倍;
(3)假设扩容前的table大小为2的N次方,有上述put方法解析可知,元素的table索引为其hash值的后N位确定;
(4)扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位;
(5)重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing;

26 HashMap的key一般用字符串,能用其他对象吗?

27 HashMap的key和value都能为null么?如果key能为null,那么它是怎么样查找值的?

29 从源码角度分析HashSet实现原理?

30 HashTable与HashMap的区别?
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有: 线程安全性,同步(synchronization),以及速度。      HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。      HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。      另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。      由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。      HashMap不能保证随着时间的推移Map中的元素次序是不变的。

术语tips:
sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。
Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。
结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。

31 String方法intern() 你真的会用吗?

32 什么是自动拆装箱?

33 String.valueOf和Integer.toString的区别?

34 反射原理,反射创建类实例有哪几种方式?
动态获取的信息以及动态调用对象的方法的功能称为java语言的反射

通过类名.Class的方式
类的实例对象的getClass方法
Class.forName+类名全路径的方式

35 反射中 Class.forName和ClassLoader区别

(1)class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。当然还可以指定是否执行静态块。
(2)classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

36 有没有顺序的Map实现类,如果有,它们是怎么保证有序的

37 描述动态代理的几种实现方式,分别有哪写优缺点
1.基于接口的动态代理:使用Proxy类中的newProxyInstance方法
2.基于子类的动态代理:用Enhancer类中的create方法

38 泛型的存在是用来解决什么问题的

39 Java中的HashSet内部是如何工作的

41 SpringAOP实现原理
Spring AOP 的实现原理是基于动态代理和字节码操作的。
在编译时, Spring 会使用 AspectJ 编译器将切面代码编译成字节码文件。在运行时, Spring 会使用 Java 动态代理或 CGLIB 代理生成代理类,这些代理类会在目标对象方法执行前后插入切面代码,从而实现AOP的功能。
Spring AOP 可以使用两种代理方式:JDK动态代理和 CGLIB 代理。如果目标对象实现了至少一个接口,则使用JDK动态代理;否则,使用 CGLIB 代理。下面分别介绍这两种代理方式的实现原理。

JDK 动态代理是 Java 自带的动态代理实现方式。使用JDK动态代理时,需要目标对象实现至少一个接口。JDK 动态代理会在运行时生成一个实现了目标对象接口的代理类,该代理类会在目标对象方法执行前后插入切面代码
CGLIB 代理是一个基于字节码操作的代理方式,它可以为没有实现接口的类创建代理对象。CGLIB 代理会在运行时生成一个目标对象的子类,并覆盖其中的方法,以实现AOP的功能

42 Spring怎么配置事务(具体说一些关键的XML元素)

43 SpringMVC中DispatcherServlet初始化过程

44 Lock和Synchronized区别
(1)来源:lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
(2)异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
(3)是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
(4)是否知道获取锁:Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
(5)Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
(6)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
(7)synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,
45 Spring的controller是单例还是多例,怎么保证并发安全的

46 你所了解的几种简单的HASH算法

47 如何将已经构建好的TreeSet完成倒排序

48 数据库隔离级别有哪些,各自含义是什么,Mysql默认的隔离级别是什么

49 聚集索引和非聚集索引的区别是什么

50 final,finally,finalize的区别?
(1)final它主要应用于:定义变量,包括静态的和非静态的。定义方法的参数。定义方法。定义类。
(2)finally只能用在try/catch语句中并且附带着一个语句块,表示这段语句最终总是被执行。
(3)finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。

51 &和&&的区别?

19 两个对象值相同(x.equals(y)==true),但却可有不同的hashcode,这句话对不对?

不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降);

20 try{}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行

21 swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?

22 构造器Constructor是否可被override?

23 是否可以继承String类?

24 数组有没有length()这个方法?String有没有length()这个方法?

25 abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized?

26 接口是否可继承接口?抽象类是否可实现(implements)接口?抽象类是否可继承实体类(concreteclass)?

28 Java有没有goto?

30 什么时候用assert?

33 运行时异常与一般异常有何异同?
1,从机制角度来讲:
运行时异常:
  在定义方法时不需要声明会抛出runtime exception;
  在调用这个方法时不需要捕获这个runtime exception;
  runtime exception是从java.lang.RuntimeException或java.lang.Error类衍生出来的。
一般异常:
  定义方法时必须声明所有可能会抛出的checked exception;
  在调用这个方法时,必须捕获它的checked exception,不然就得把它的exception传递下去;
  checked exception是从java.lang.Exception类衍生出来的。
  
2,从逻辑的角度来说,
运行时异常和一般异常是有不同的使用目的的。一般异常用来指示一种调用方能够直接处理的异常情况。而运行时则用来指示一种调用方本身无法处理或恢复的程序错误。

34 int和Integer有什么区别?
1.Integer是int的包装类,int则是java的一种基本的数据类型;
2.Integer变量必须实例化之后才能使用,而int变量不需要实例化;
3.Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向对象,而int则直接存储数值
4.Integer的默认值是null,而int的默认值是0

35 List,Set,Map是否继承自Collection接口?

36 Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用==还是equals()?它们有何区别?用contains来区分是否有重复的对象,还是都不用。

37 Collection和Collections的区别。

38 说出ArrayList,Vector,LinkedList的存储性能和特性HashMap和Hashtable的区别


JVM

1 谈谈你对JVM调优的理解
JVM内存的调优主要的目的是减少Minor GC的频率和Full GC的次数,过多的Minor GC和Full GC是会占用很多的系统资源,影响系统的吞吐量。

**Full GC:**会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。

原则上:
大多数的java应用不需要GC调优
大部分需要GC调优的的,不是参数问题,是代码问题
在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
GC调优是最后的手段

导致Full GC的原因:
(1)老年代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
(2)持久代(永久代)Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
(3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制

常用的性能优化手段
避免过早优化:不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源;

前端优化常用手段
1 使用客户端缓冲: 静态资源文件(css、图标等)缓存在浏览器中,需要更新,则通过改变文件名来解决。
2 启用压缩: 减少网络传输量,但会给浏览器和服务器带来性能的压力,需要权衡使用。
3 资源文件加载顺序: css放在页面最上面,js放在最下面。这样页面的体验才会比较好。浏览器会加载完CSS才会对页面进行渲染。JS只要加载后就会立刻执行;
4 减少Cookie传输: cookie包含在每次的请求和响应中,因此哪些数据写入cookie需要慎重考虑;
5 CDN加速: CDN,又称内容分发网络,本质是一个缓存,而且是将数据缓存在用户最近的地方。无法自行实现CDN的时候,可以根据经济实力考虑商用CDN服务。
6 反向代理缓存: 将静态资源文件缓存在反向代理服务器上,一般是Nginx。
7 WEB组件分离: 将js,css和图片文件放在不同的域名下。可以提高浏览器在下载web组件的并发数。因为浏览器在下载同一个域名的的数据存在并发数限制。
应用服务性能优化:
1 缓存: 网站性能优化第一定律:优先考虑使用缓存优化性能;优先原则:缓存离用户越近越好;缓存是将数据存在访问速度较高的介质中。可以减少数据访问的时间,同时避免重复计算。频繁修改的数据,尽量不要缓存,读写比2:1以上才有缓存的价值。
2 一致性哈希:
(1)首先求出服务器(节点)的哈希值,并将其配置到0~2的32次方的圆(continuum)上。
(2)然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
(3)然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就会保存到第一台服务器上。
一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
3 数据倾斜:
集群:
1 异步:
同步和异步,阻塞和非阻塞:
同步和异步关注的是结果消息的通信机制
同步:同步的意思就是调用方需要主动等待结果的返回
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
阻塞和非阻塞主要关注的是等待结果返回调用方的状态
阻塞:是指结果返回之前,当前线程被挂起,不做任何事
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。

1)同步阻塞:同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,你去了之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包括看手机),等着商家进货,直到有货为止,这个效率很低,jdk里的BIO就属于 同步阻塞。
2)同步非阻塞:同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣服卖完了,这个时候不需要傻傻的等着,你可以去其他地方比如奶茶店,买杯水,但是你还是需要时不时的去商店问老板新衣服到了吗。jdk里的NIO就属于 同步非阻塞
3)异步阻塞:异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit然后马上future.get(),这样线程其实还是挂起的。有点像你去商店买衣服,这个时候发现衣服没有了,这个时候你就给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一直等着他响什么事也不做。这样感觉的确有点傻,所以这个模式用得比较少。
4)异步非阻塞:好比你去商店买衣服,衣服没了,你只需要给老板说这是我的电话,衣服到了就打。然后你就随心所欲的去玩,也不用操心衣服什么时候到,衣服一到,电话一响就可以去买衣服了。jdk里的AIO就属于异步。

常见异步的手段:
1 Servlet异步:servlet3中才有,支持的web容器在tomcat7和jetty8以后。
2 多线程
3 消息队列

调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的。也没有一个放之四海而皆准的配置或者公式。即使是jvm参数也是如此,再比如说性能有关的操作系统工具,和操作系统本身相关的所谓大页机制,都需要平时去积累,去观察,去实践。



2 JVM内存区域如何划分?
在这里插入图片描述

最常谈到的区域:
(1)方法区: 这也是所有线程共享的一块内存区域,用于存储对象的类型数据,例如类结构信息,静态变量,以及常量池、字段、方法代码等。JVM对永久代垃圾回收(如,常量池回收、对象类型的卸载)非常不积极,所以当我们不断添加新类型的时候,永久代会出现OutOfMemoryError,尤其是在运行时存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题;
(2)JAVA堆: Java 堆是被所有线程共享的一块内存区域,在JVM启动时创建,随着JVM的结束而消亡。此内存区域所占的面积最大,他主要是用于存放对象实例,几乎所有的对象实例都在这里分配内存。因此他也是垃圾回收器主要关注的区域,当创建的对象实例特别多导致顿内存空间不够时,此区域会发上OOM异常。堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
(3)本地方法栈: 它和Java虚拟机栈的内存机制相同,本地方法栈则是为执行本地方法(Native Method)服务的。
(4)JAVA虚拟机栈: 每个线程在创建时都会创建一个虚拟机栈,因此它是线程私有的,其内部保存一个个的栈帧,对应着一次次的Java方法调用。当有方法调用的时候就会将方法封装到一个栈帧中进行压栈,方法执行完成后弹栈,某一时间对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。在每一个栈帧中都有一个区域存放本地变量表用于存放此方法执行过程所需要的基本类型和对象引用(注:这部分空间是在预编译阶段可预测的)。
(5)程序计数器: 程序计数器是很少的一部分内存空间,它保存的是程序当前执行的指令的地址,当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令;

注:1.8版本取消了老年代,取而代之的是元空间;

还会用到的空间:
(1)直接内存: 并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据;
(2)Code Cache : JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT 编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面;GC等功能。需要运行在本地线程之中,类似部分都需要占用内存空间;



3 JVM堆中对象是如何创建的?
步骤:
(1)类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
(2)分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
(3)初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
(4)设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
(5)执行init方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,(init) 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 (init)方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。



4 JVM对象的结构?

对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding);
在这里插入图片描述

对象头(Header): 记录了对象在运行过程中所需要使用的一些数据:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳;
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。
实例数据(Instance Data): 实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
对齐填充(Padding): 用于确保对象的总长度为 8 字节的整数倍。HotSpot VM(JVM默认虚拟机) 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。



5 对象访问的定位方式
(1) 句柄访问方式:堆中需要有一块叫做“句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。引用类型的变量存放的是该对象的句柄地址(reference)。访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。
(2) 直接指针访问方式:引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象。但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。

注:HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍。但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。



5 如何判断一个对象是否存活(如何判断对象是否是垃圾对象)
(1)引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不能再被引用的。
(2)可达性分析算法: 可达性分析算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则证明此对象是不可用的。

  即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中。并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它。这里所谓“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果有一个对象在finalize()方法中执行缓慢,或者发生死循环,将可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
  
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象这个时候,未被重新引用,那它基本上就真的被回收了。



**5 **

6 JVM垃圾回收算法有哪些?

6 垃圾回收的优点和原理

7 JVM垃圾收集器有哪些?

8 JVM内存是如何分配的?

16 描述一下JVM加载class文件的原理机制?
  虚拟机(jvm)把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
  java中的所有类,都需要有由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把Class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显示的加载所需要的类。

类装载的方式,有两种:
1、隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器对应的类到jvm中
2、显示装载,通过class.forname()等方式,显示加载需要的类

java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保存程序运行的基础类(像是基类)完全加载到JVM中,至于其他类,则在需要的时候才才加载。这当然就是为了节省内存开销

9 分析类的加载过程?
在这里插入图片描述
加载是整个“类加载”,通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

验证目的是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全,包括文件格式验证、元数据验证、字节 码验证和符号引用验证;

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段,通常情况下,初始值是数据类型的零值

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序

10 JVM双亲委派机制?
  双亲委派一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,如果此时父类不能加载,反馈给子类,由子类去完成类的加载
  采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层进行加载,这样就保证了使用不同的类加载器最终得到的都是一个Object对象

11 JVM可以作为GC Root的对象有哪些?

12 请写出几段可以导致内存溢出、内存泄漏、栈溢出的代码?

13 哪些情况会导致Full GC?

14 频繁GC问题或内存溢出问题,如何定位?

15 char型变量中能不能存贮一个中文汉字?为什么?

17 垃圾回收的优点和原理。并考虑2种回收机制。

18 Java中的异常处理机制的简单原理和应用。

39 JVM参数有哪些
-xms: 堆内存的初始化大小
-xmx: 堆内存的最大空间
-xmn: 年轻代的空间
-xx:PermSize 方法区初始化大小
-xx:MaxPermSize 方法区最大空间
-XX:PretenureSizeThreshold 令大于这个设置值的对象直接在老年代分配
-MaxTenuringThreshold :年龄大于这个值的直接进入老年代

29 GC是什么?为什么要有GC?

27 给我一个你最常见到的runtimeexception


多线程及其安全问题

在这里插入图片描述
1 (1次)谈谈你对多线程安全问题的理解

怎样才算是线程安全的: 当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的(另一种说法:如果多线程的程序的运行过程和单线程运行的结果一致,则认为它是线程安全的);
如果所有的线程都是读操作,而没有写操作,那么我们可以认为也是线程安全的,如果多个线程读写操作都有的话才会容易产生线程安全问题。这个时候我们就需要采取加锁,同步等手段来解决线程安全问题。
怎样做到线程安全: 实现线程安全的方式有多种,其中在源码中常见的方式是,采用synchronized关键字给代码块或方法加锁,比如StringBuffer(如果是多个线程访问同一个资源,那么就需要上锁,才能保证数据的安全性;这个时候如果使用的是非线程安全的对象,比如StringBuilder,那么就需要借助外力,给他加synchronized关键字。或者直接使用线程安全的对象StringBuffer)
什么时候需要考虑使用线程安全:
(1)多个线程访问同一个资源;
(2)资源是有状态的,比如字符串的拼接,这个时候数据是会有变化的;

2 程序,进程,线程

程序(进程(线程))

程序: 是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码;
进程: 是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等;
线程: 是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响;

3 多线程生命周期包括哪几个阶段

4 多线程有几种实现方式
(1)继承thread
(2)实现runnable
(3)实现callable
(4)使用线程池

5 多线程的分类
用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程;

守护线程:运行在后台,为其他前台线程服务;
特点: 一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作;
应用: 数据库连接池中的检测线程,JVM虚拟机启动后的检测线程;
最常见的守护线程: 垃圾回收线程;

可以通过调用 Thead 类的 setDaemon(true) 方法设置当前的线程为守护线程。

5 启动线程是用start()方法还是run()方法?

调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。 而直接执行run()方法,会把run方法当成一个mian线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行;

6 说说线程安全问题,什么实现线程安全,如何实现线程安全
线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕;

解决办法:使用多线程之间使用关键字synchronized、或者使用锁(lock),或者volatile关键字。
①synchronized(自动锁,锁的创建和释放都是自动的);
②lock 手动锁(手动指定锁的创建和释放)。
③volatile关键字
为什么能解决?如果可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题

7 什么是线程阻塞,分别有哪几种阻塞
阻塞: 阻塞状态是指线程因为某种原因放弃了cpu使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。(timeslice:时间片是操作系统分配给每个线程在CPU上计算的时间,并非CPU分配,CPU只管计算)

阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waiting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

7 sychronized和Lock的区别?
(1)来源:lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
(2)异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
(3)是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
(4)是否知道获取锁:Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
(5)Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
(6)在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
(7)synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

8 sleep()和wait()的区别?
(1) 两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁 。
(2) 两者都可以暂停线程的执行。
(3) wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
(4) wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。
9 深入分析ThreadLocal的实现原理?

10 你看过AbstractQueuedSynchronizer源码阅读吗,请说说实现原理?

11 谈谈对synchronized的偏向锁、轻量级锁、重量级锁的理解?
偏量锁(可以配置为没有)
适用场景:适用于单个线程,
原理:当加锁时,直接在对象头和栈帧中的锁记录里储存偏向锁的ID。下次自己的线调用的时候,查看对象头锁存在的还是自己的ID就直调用。
轻量级锁-——自旋锁(适合线程少时)
在无竞争的情况下,轻量级锁使用CAS操作来实现锁的获取和释放,避免了线程的阻塞和唤醒,从而提高了并发性能;
初始状态:对象的对象头中的锁标志位为无锁状态。
获取锁:当线程尝试获取轻量级锁时,首先会将对象头中的锁标志位复制到线程的栈帧中的锁记录(Lock Record)中。
CAS操作:线程使用CAS操作将对象头中的锁标志位替换为指向线程栈帧的指针。如果CAS操作成功,表示当前线程成功获取了锁,并进入临界区代码执行。如果CAS操作失败,说明存在竞争,进入下一步操作。
重量级锁(适合高并发)
如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁;

12 什么是悲观 乐观锁?
乐观锁
大多是基于数据版本(version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 version字段来实现。读取数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。

悲观锁
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。
悲观锁它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

12 通过三种方式实现生产者消费者模式?

13 JVM层面分析sychronized如何保证线程安全的?

14 JDK层面分析sychronized如何保证线程安全的?

15 如何写一个线程安全的单例?

16 通过AQS实现一个自定义的Lock?

17 ThreadLocal什么时候会出现OOM的情况?为什么?

18 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

19 你真的理解CountDownLatch与CyclicBarrier使用场景吗?

20 出现死锁,如何排查定位问题?

21 notify和notifyAll的区别?

22 线程池启动线程submit和execute有什么不同?

23 SimpleDateFormat是线程安全的吗?如何解决?

24 请谈谈ConcurrentHashmap底层实现原理?

25 使用synchronized修饰静态方法和非静态方法有什么区别?

26 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其方法?

27 线程池的原理,为什么要创建线程池?创建线程池的方式?

一:什么是线程池:
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池。

二:线程池的特点是什么
1:主要特点为:线程服用,控制最大的并发数,管理线程。
第一:降低资源消耗,通过重复利用己创建的线程降低线程创建和销毁造成的消耗
第二:提高响应速度,当任务到达时,任务可以不需要的等到线程创建就能立即执行
第三:提高线程的客观理性,线程时稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

三:线程池的创建方式:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求

28 创建线程池有哪几个核心参数? 如何合理配置线程池的大小?

29 Synchronized修饰的静态方法和非静态方法有什么区别?

30 什么时候考虑多线程安全;
多个线程访问同一个资源;

31 多线程的安全隐患以及synchronized监视器(锁)

32 怎样保证线程安全
不要跨线程访问共享变量;
使共享变量是final类型的;
使共享变量只读;
将共享变量的操作加上同步;

32 i++是线程安全的吗?如何解决线程安全性?

33 从字节码角度深度解析 i++ 和 ++i 线程安全性原理?

34 HashMap是线程安全的吗?如何实现线程安全?
HashMap并非线程安全,多线程使用时,会导致值覆盖,会导致ConcurrentModificationException错误;如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedMap方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问。

那么在Java中线程安全的map有没有呢?答案是肯定的,有以下几种方式:
HashTable
(1)底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化;
(2)初始size为11,扩容:newsize = olesize*2+1;
(3)计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length;
Synchronized Map

ConcurrentHashMap
(1) 底层采用分段的数组+链表实现,线程安全;
(2) 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
(3) Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术;
(4) 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁;
(5) 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容;

其中前两种方式是同步加锁的方式,会影响程序运行效率,第三种ConcurrentHashMap是Java 中支持高并发,高吞吐量的hashMap实现。ConcurrnetHashMap是基于线程安全的一个类;

证明类是线程不安全,证明的方法一般有如下几种:

  1. 对象不能安全发布,构造过程逃逸;
  2. 内存的可见性,内容不能及时发布;
  3. 操作不是原子的;
  4. 读写不能同步;
  5. 存在死锁的可能性;

数据库

Mysql与Oracle的区别
(1) 对事务的提交
MySQL默认是自动提交,而Oracle默认不自动提交,需要用户手动提交,需要在写commit;指令或者点击commit按钮;
(2) 分页查询
MySQL是直接在SQL语句中写"select… from …where…limit x, y",有limit就可以实现分页;而Oracle则是需要用到伪列ROWNUM和嵌套查询;
(3) 事务隔离级别
MySQL是read commited的隔离级别,而Oracle是repeatable read的隔离级别,同时二者都支持serializable串行化事务隔离级别,可以实现最高级别的
读一致性。每个session提交后其他session才能看到提交的更改。Oracle通过在undo表空间中构造多版本数据块来实现读一致性,每个session查询时,如果对应的数据块发生变化,Oracle会在undo表空间中为这个session构造它查询时的旧的数据块,MySQL没有类似Oracle的构造多版本数据块的机制,只支持read commited的隔离级别。一个session读取数据时,其他session不能更改数据,但可以在表最后插入数据。session更新数据时,要加上排它锁,其他session无法访问数据;
(4) 对事务的支持
MySQL在innodb存储引擎的行级锁的情况下才可支持事务,而Oracle则完全支持事务;
(5) 保存数据的持久性
MySQL是在数据库更新或者重启,则会丢失数据,Oracle把提交的sql操作线写入了在线联机日志文件中,保持到了磁盘上,可以随时恢复;
(6) 并发性
MySQL以表级锁为主,对资源锁定的粒度很大,如果一个session对一个表加锁时间过长,会让其他session无法更新此表中的数据。虽然InnoDB引擎的表可以用行级锁,但这个行级锁的机制依赖于表的索引,如果表没有索引,或者sql语句没有使用索引,那么仍然使用表级锁;Oracle使用行级锁,对资源锁定的粒度要小很多,只是锁定sql需要的资源,并且加锁是在数据库中的数据行上,不依赖与索引。所以Oracle对并发性的支持要好很多;
(7) 逻辑备份
MySQL逻辑备份时要锁定数据,才能保证备份的数据是一致的,影响业务正常的dml使用,Oracle逻辑备份时不锁定数据,且备份的数据是一致
(8) 复制
MySQL:复制服务器配置简单,但主库出问题时,丛库有可能丢失一定的数据。且需要手工切换丛库到主库。
Oracle:既有推或拉式的传统数据复制,也有dataguard的双机或多机容灾机制,主库出现问题是,可以自动切换备库到主库,但配置管理较复杂。
(9) 性能诊断
MySQL的诊断调优方法较少,主要有慢查询日志。
Oracle有各种成熟的性能诊断调优工具,能实现很多自动分析、诊断功能。比如awr、addm、sqltrace、tkproof等
(10)权限与安全
MySQL的用户与主机有关,感觉没有什么意义,另外更容易被仿冒主机及ip有可乘之机。
Oracle的权限与安全概念比较传统,中规中矩。
(11)分区表和分区索引
MySQL的分区表还不太成熟稳定。
Oracle的分区表和分区索引功能很成熟,可以提高用户访问db的体验。
(12)管理工具
MySQL管理工具较少,在linux下的管理工具的安装有时要安装额外的包(phpmyadmin, etc),有一定复杂性。
Oracle有多种成熟的命令行、图形界面、web管理工具,还有很多第三方的管理工具,管理极其方便高效。
(13)最重要的区别
MySQL是轻量型数据库,并且免费,没有服务恢复数据。
Oracle是重量型数据库,收费,Oracle公司对Oracle数据库有任何服务。

1 (1次)数据库三范式
1FS:原子性 ,字段数据不可再分;
2FS:唯一性 ,使得每一行 数据具有唯一性,并消除数据之间的部分依赖;
3FS:独立性 ,使得每个字段都独立依赖与主键字段,消除传递依赖;

2 数据库的事务、ACID及隔离级别?

数据库事务:
数据库事务是构成单一逻辑工作单元的操作集合;

基本要素ACID:
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位;
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到;
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账;
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚;

事务的并发问题:

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

MYSQL的事务隔离级别:

事务隔离级别脏读不可重复读幻读
读未提交
不可重复读
可重复读
串行化(serializable)

3 不考虑事务的隔离性,容易产生哪三种情况?
4 数据库连接池原理?
首先为什么要用数据库连接池:因为数据库连接对象的创建比较消耗性能;
1.原理:一般来说,java应用程序访问数据库的过程是:
  ①装载数据库驱动程序;
  ②通过jdbc建立数据库连接;
  ③访问数据库,执行sql语句,处理结果集;
  ④断开数据库连接;

5 什么是BTree?
BTree 即二叉搜索树
在这里插入图片描述
B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;如果B树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变B树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销;实际使用的B树都是在原B树的基础上加上平衡算法,即“平衡二叉树”;如何保持B树结点分布均匀的平衡算法是平衡二叉树的关键;平衡算法是一种在B树中插入和删除结点的策略。常见的平衡二叉树有:AVL,RBT,Treap,Splay Tree。

5 什么是B-Tree?
是一种多路搜索树(并不是二叉的):
1.定义任意非叶子结点最多只有M个儿子;且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2,M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
如:(M=3)
在这里插入图片描述

B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;

B-树的特性:
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为:log2N , 所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;

6 什么是B+Tree?
B+树是B-树的变体,也是一种多路搜索树:

主要特点:
a、 非叶子节点不存储数据data只存储键值,只有叶子节点才会存储数据data;
b、 叶子节点包含所有的索引字段;
c、 为了提高区间的访问性能,叶子节点之间使用双向指针进行连接,使所有的叶子节点形成了一个双向且有序的链表结构

   1.其定义基本与B-树同,除了:
   2.非叶子结点的子树指针与关键字个数相同;
   3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
   5.为所有叶子结点增加一个链指针;
   6.所有关键字都在叶子结点出现;
   如:(M=3)

在这里插入图片描述

B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;

   B+的特性:
   1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
   2.不可能在非叶子结点命中;
   3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
   4.更适合文件索引系统;

小结
(1) B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;
(2) B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;
(3) 所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
(4) B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

7 MySQL数据库索引结构?
索引常用的数据结构有:二叉树、红黑树(平衡二叉查找树)、Hash表、B-Tree/B+Tree等;
MySQL的都知道,其索引底层采用的是B+Tree结构

7.1 MySQL为什么采用B+Tree作为索引常用底层数据结构?
MySQL 的数据是持久化的,由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大。

所以,要设计一个适合 MySQL 索引的数据结构,至少满足以下要求:
-----能在尽可能少的磁盘的 I/O 操作中完成查询工作;
-----要能高效地查询某一个记录,也要能高效地执行范围查找;

MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

1、B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
2、B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
3、B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

7.5 位图索引:
7.6 Hash索引:
索引列会被存储在匹配到的hash bucket里面的表里,这个表里会有实际的数据行指针,再根据实际的数据行指针查找对应的数据行;

概括来说,要查找一行数据或者处理一个where子句,SQL Server引擎需要做下面几件事:
1、根据where条件里面的参数生成合适的哈希函数;
2、索引列进行匹配,匹配到对应hash bucket,找到对应hash bucket意味着也找到了对应的数据行指针(row pointer);
3、读取数据;
哈希索引比起B树索引简单,因为它不需要遍历B树,所以访问速度会更快;

Hash索引的缺点:
1、因为Hash索引比较的是经过Hash计算的值,所以只能进行等式比较,不能用于范围查询;
2、由于哈希值是按照顺序排列的,但是哈希值映射的真正数据在哈希表中就不一定按照顺序排列,所以无法利用Hash索引来加速任何排序操作;
3、不能用部分索引键来搜索,因为组合索引在计算哈希值的时候是一起计算的;
4、当哈希值大量重复且数据量非常大时,其检索效率并没有Btree索引高的;

8 什么是索引?什么条件适合建立索引?什么条件不适合建立索引?
索引: 索引用来快速地寻找那些具有特定值的记录,所有MySQL索引都以B-树的形式保存。如果没有索引,执行查询时MySQL必须从第一个记录开始扫描整个表的所有记录,直至找到符合要求的记录。表里面的记录数量越多,这个操作的代价就越高。如果作为搜索条件的列上已经创建了索引,MySQL无需扫描任何记录即可迅速得到目标记录所在的位置。如果表有1000个记录,通过索引查找记录至少要比顺序扫描记录快100倍;

索引的类型:
普通索引
这是最基本的索引类型,而且它没有唯一性之类的限制。普通索引可以通过以下几种方式创建:
创建索引:例如CREATE INDEX <索引的名字> ON tablename (列的列表);
修改表:例如ALTER TABLE tablename ADD INDEX [索引的名字] (列的列表);
创建表的时候指定索引,例如CREATE TABLE tablename ( […], INDEX [索引的名字] (列的列表) );

唯一性索引
这种索引和前面的“普通索引”基本相同,但有一个区别:索引列的所有值都只能出现一次,即必须唯一。唯一性索引可以用以下几种方式创建:
创建索引,例如CREATE UNIQUE INDEX <索引的名字> ON tablename (列的列表);
修改表,例如ALTER TABLE tablename ADD UNIQUE [索引的名字] (列的列表);
创建表的时候指定索引,例如CREATE TABLE tablename ( […], UNIQUE [索引的名字] (列的列表) );

主键
主键是一种唯一性索引,但它必须指定为“PRIMARY KEY”。如果你曾经用过AUTO_INCREMENT类型的列,你可能已经熟悉主键之类的概念了。主键一般在创建表的时候指定,例如“CREATE TABLE tablename ( […], PRIMARY KEY (列的列表) ); ”。但是,我们也可以通过修改表的方式加入主键,例如“ALTER TABLE tablename ADD PRIMARY KEY (列的列表); ”。每个表只能有一个主键。

全文索引
MySQL从3.23.23版开始支持全文索引和全文检索。在MySQL中,全文索引的索引类型为FULLTEXT。全文索引可以在VARCHAR或者TEXT类型的列上创建。它可以通过CREATE TABLE命令创建,也可以通过ALTER TABLE或CREATE INDEX命令创建。对于大规模的数据集,通过ALTER TABLE(或者CREATE INDEX)命令创建全文索引要比把记录插入带有全文索引的空表更快

适合建立索引:
1.主键自动建立唯一索引;
2.频繁作为查询条件的字段应该创建索引;
3.查询中与其他表有关联的字段,例如外键关系;
4.高并发的情况下一般选择复合索引;
5.查询中统计或者分组的字段;
6.经常增删改的表;
7.查询中排序的字段创建索引将大大提高排序的速度(索引就是排序加快速查找);
8.数据量超过300的表应该有索引;

● 对于单值索引,尽量选择针对当前query过滤性更好的索引。
● 在选择复合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
● 在选择复合索引的时候,尽量选择可以能够包含当前query中的where子句中更多字段的索引。
● 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的。

不适合简历索引:
1.频繁更新的字段不适合创建索引,因为每次更新不单单是更新记录,还会更新索引,保存索引文件;
2.where条件里用不到的字段,不创建索引;
3.表记录太少,不需要创建索引;

Mysql 四种连接

详情参考这里 0

内连接
  inner join 或者 join
外连接
  左连接 left join 或者 left outer join
  右连接 right join 或者 right outer join
  完全外连接 full join 或者 full outer join

左外连接: 左外连接会把左面表中的数据全部取出来,而右边表中的数据,如果有相等的就像是出来,如果没有就补充NULL;

mysql> select * from person left  join card on person.cardid=card.id;
+------+------+--------+------+--------+
| id   | name | cardid | id   | name   |
+------+------+--------+------+--------+
|    1 | 张三 |      1 |    1 | 饭卡   |
|    2 | 李四 |      3 |    3 | 农行卡 |
|    3 | 王五 |      6 | NULL | NULL   |
+------+------+--------+------+--------+

右外连接: 右外连接会把右面表中的数据全部取出来,而左边表中的数据,如果有相等的就像是出来,如果没有就补充NULL;

mysql> select * from person right  join card on person.cardid=card.id;
+------+------+--------+------+--------+
| id   | name | cardid | id   | name   |
+------+------+--------+------+--------+
|    1 | 张三 |      1 |    1 | 饭卡   |
|    2 | 李四 |      3 |    3 | 农行卡 |
| NULL | NULL |   NULL |    2 | 建行卡 |
| NULL | NULL |   NULL |    4 | 工商卡 |
| NULL | NULL |   NULL |    5 | 邮政卡 |
+------+------+--------+------+--------+

内连接: (典型的联接运算,使用像 = 或 <> 之类的比较运算符)。包括相等联接和自然联接。
内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如,检索 students和courses表中学生标识号相同的所有行;内连接查询就是两张表中的数据,通过某个相等的字段进行查询,查询出的结果是两张表都有的数据,即查询两张表的交集;

mysql> select * from person inner join card on person.cardid=card.id;
+------+------+--------+------+--------+
| id   | name | cardid | id   | name   |
+------+------+--------+------+--------+
|    1 | 张三 |      1 |    1 | 饭卡   |
|    2 | 李四 |      3 |    3 | 农行卡 |
+------+------+--------+------+--------+

全连接: Mysql不支持全连接(full join),要想实现全连接,可以使用左外连接和右外连接的并集union进行连接;

mysql> select * from person right  join card on person.cardid=card.id
 union 
select * from person left join card on person.cardid=card.id;
+------+------+--------+------+--------+
| id   | name | cardid | id   | name   |
+------+------+--------+------+--------+
|    1 | 张三 |      1 |    1 | 饭卡   |
|    2 | 李四 |      3 |    3 | 农行卡 |
| NULL | NULL |   NULL |    2 | 建行卡 |
| NULL | NULL |   NULL |    4 | 工商卡 |
| NULL | NULL |   NULL |    5 | 邮政卡 |
|    3 | 王五 |      6 | NULL | NULL   |
+------+------+--------+------+--------+

注:On后面条件与Where后面条件的区别:
ON条件:是过滤两个链接表笛卡尔积形成中间表的约束条件。
WHERE条件:在有ON条件的SELECT语句中是过滤中间表的约束条件。在没有ON的单表查询中,是限制物理表或者中间查询结果返回记录的约束。在两表或多表连接中是限制连接形成最终中间表的返回结果的约束。
从这里可以看出,将WHERE条件移入ON后面是不恰当的。推荐的做法是:
ON只进行连接操作,WHERE只过滤中间表的记录。

总结:
总结一下两表连接查询选择方式的依据:
1、 查两表关联列相等的数据用内连接。
2、 Col_L是Col_R的子集时用右外连接。
3、 Col_R是Col_L的子集时用左外连接。
4、 Col_R和Col_L彼此有交集但彼此互不为子集时候用全外。
5、 求差操作的时候用联合查询。
多个表查询的时候,这些不同的连接类型可以写到一块。例如:
SELECT T1.C1,T2.CX,T3.CY
FROM TAB1 T1
INNER JOIN TAB2 T2 ON (T1.C1=T2.C2)
INNER JOIN TAB3 T3 ON (T1.C1=T2.C3)
LEFT OUTER JOIN TAB4 ON(T2.C2=T3.C3);
WHERE T1.X >T3.Y;

9 索引失效的原因有哪些?如何优化避免索引失效?

10 MySQL如何启动慢查询日志?

11 MySQL如何使用show Profile进行SQL分析?
SHOW PROFILE 是MySQL提供可以用来分析当前会话语句执行的资源消耗情况,可以用于SQL 的调优测评依据。在默认的情况下,参数处于关闭状态,并保存着最近15 次的运行结果。
我们可以通过 SHOW VARIABLES LIKE ‘PROFILING’; 命令查看参数的开关情况;通过 SET PROFILING = ON; 命令设置开启状态。

12 一条执行慢的SQL如何进行优化,如何通过Explain+SQL分析性能?
1慢查询的开启并捕获
2 explain+慢SQL分析
3 show profile查询SQL在Mysql服务器里面的执行细节和生命周期情况4 SQL数据库服务器的参数调优。

13 什么是行锁、表锁、读锁、写锁,说说它们各自的特性?
按照粒度划分:行锁、表锁、间隙锁
行锁:每次操作锁住一行或多行记录,锁定粒度最小,发生锁冲突概率最低,并发读最高。
表锁:每次锁住整张表。锁定粒度大,发生冲突的概率最高,并发值最低。
间隙锁:每次锁定相邻的一组记录,锁定粒度结余行锁和表锁之间。

按操作类型可分为:读锁和写锁
读锁(S锁):共享锁,针对同一份数据,多个事务可以对其添加读锁,其他事务无法进行修改数据(其他事务无法添加写锁)。
写法:SELECT … LOCK IN SHARE MODE

写锁(X锁):排他锁,针对同一份数据,在当前上锁事务完成前,会阻塞其他事务的写锁或读锁。(仍旧是可以进行查询操作的,只不过不能加锁)。
写法:SELECT … FOR UPDATE

14 什么情况下行锁变表锁?
15 什么情况下会出现间隙锁?
16 谈谈你对MySQL的in和exists用法的理解?
17 MySQL的数据库引擎有哪些,如何确定在项目中要是用的存储引擎?
18 count(*)、count(列名)和count(1)的区别?
19 union和union all的区别?
20 一张表五条记录,分别有三门不同的成绩,用一条sql语句求出每门成绩最高和最低的和对应的学生姓名

详情参考here sql查询成绩表中每一科成绩最高的分数以及这个学生的名字,学科名
SELECT “最高分”,student.*
FROM student ,(SELECT MAX(score) AS score,subject FROM student GROUP BY subject)b
WHERE student.score = b.score
AND student.subject = b.subject
UNION
SELECT “最低分”,student.*
FROM student ,(SELECT MIN(score) AS score,subject FROM student GROUP BY subject)b
WHERE student.score = b.score
AND student.subject = b.subject;


WEB知识

1 什么是Servlet,Servlet生命周期方法?单例还是多例?
Servlet: Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。使用 Servlet,您可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页;

servlet是单例的,严格地说是一个ServletMapping对应一个单例实例(如果一个Servlet被映射了两个URL地址,会生成两个实例)。早期的CGI模式是原型式的,例如同时并发2000次请求一个Servlet,如果不是单例的,内存瞬间要创建2000个对象,同时为了线程安全还得阻塞对方线程,其性能非常之差。
要维护Servlet线程安全有很多办法,通常是使用同步块(或方法)来保护共享数据,其次可以volatile、Lock一些锁机制,还可以使用ThreadLocal来打通安全通道,另外还有原子操作也是用来保护数据安全,有非常多的选择。

生命周期: Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:

(1)Servlet 通过调用 init () 方法进行初始化。
(2)Servlet 调用 service() 方法来处理客户端的请求。
(3)Servlet 通过调用 destroy() 方法终止(结束)。
(4)最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

扩展:
init(): init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。

service(): service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。doGet() 和 doPost() 方法是每次服务请求中最常用的方法。

destroy(): destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。

2 什么Session和Cookie,它们之间有什么联系?

**Session(会话跟踪):**指用户登录网站后的一系列动作,比如浏览商品添加到购物车并购买。会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。
session共享:
对于多网站(同一父域不同子域)单服务器,我们需要解决的就是来自不同网站之间SessionId的共享。由于域名不同(aaa.test.com和bbb.test.com),而SessionId又分别储存在各自的cookie中,因此服务器会认为对于两个子站的访问,是来自不同的会话。解决的方法是通过修改cookies的域名为父域名达到cookie共享的目的,从而实现SessionId的共享。带来的弊端就是,子站间的cookie信息也同时被共享了。

常用的会话跟踪技术:session和cookie——Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。客户端浏览器再次访问服务端时只需要从该Session中查找该客户的状态就可以了。

Cookie : Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。

**cookie主要内容包含:**名字,值,过期时间,路径和域(路径和域一起构成cookie的作用范围);

 HttpCookie cookie = new HttpCookie("MyCook");//初使化并设置Cookie的名称
 DateTime dt = DateTime.Now;
 TimeSpan ts = new TimeSpan(0, 0, 1, 0, 0);//过期时间为1分钟
 cookie.Expires = dt.Add(ts);//设置过期时间
 cookie.Values.Add("userid", "value");
 cookie.Values.Add("userid2", "value2");
 Response.AppendCookie(cookie);

**会话Cookie:**若不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,关闭浏览器窗口,cookie就消失。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。

**持久Cookie:**若设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在浏览器的不同进程间共享。

Cookie具有不可跨域名性: cookie不可跨浏览器,例如访问百度不会带上Google的Cookie;

总结:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

3 JSP的八个隐含对象有哪些?

4 JSP的四个域对象的作用范围?

5 Post和Get请求的区别?

GET和POST是什么:HTTP协议中的两种发送请求的方法;
HTTP又是什么:HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议;

1.安全性:Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的;
2.数据量: Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制;
3.字符: Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集
4. 效率:Get执行效率却比Post方法好。Get是form提交的默认方法;
5. GET产生一个TCP数据包;POST产生两个TCP数据包;

6 转发和重定向有什么区别?

7 JSP自定义标签,如何实现循环打印功能?

8 Http1.0和Http1.1的区别是什么?

10 (1次)HTTP中的TCP三次握手

三次握手主要是规避网络传输当中延迟而导致服务器开销的一些问题
第一次:客户端发送一个TCP的SYN标志位置1的包指明发送的服务器端口,以及初始化序号 X(客户端什么都不确定。服务端确认对方发送正常)
第二次:服务器返回确认包ACK应答,及SYN标志位和ACK标志位均为1,同时将确认序号设置为 X+1(客户端发送/接受正常,对方发送接收正常。服务端确认自己发送正常,客户端发送正常。)
第三次:客户端再次发送确认(ACK) SYN标志为0,ACK标志为1,并把服务器发送过来的ACK序号字段+1(客户端发送/接受正常,服务端发送接受正常。服务端确认自己发送/接受正常,客户端发送接受正常。)

11 TCP四次挥手

第一次:客户端请求断开FIN,seq=u
第二次:服务器确认客户端的断开请求ACK,ack=u+1,seq=v
第三次:服务器请求断开FIN,seq=w,ACK,ack=u+1
第四次:客户端确认服务器的断开ACK,ack=w+1,seq=u+1

12 HTTP与TCP协议区别
1 TCP协议对应于传输层,而HTTP协议对应于应用层
2 TCP是有状态的长连接,而HTTP是无状态的连接

13 (1次)使用过的前端框架

14 当在地址栏输入www.XX.com的时候会发生什么
1、DNS解析地址
2、找到相应的服务器
3、TCP的三次握手建立TCP连接
4、找到入口文件
5、解析入口文件
6、TCP的四次挥手
7、返回资源页面

15 ajax是什么?如何创建一个ajax?

有关Ajax面试题或者复习请参考这儿

ajax并不算是一种新的技术,全称是asynchronous javasript and xml,可以说是已有技术的组合,主要用来实现客户端服务器的异步通信效果,实现页面的局部刷新,早期的浏览器并不能原生支持ajax,可以使用隐藏帧(iframe)方式变相实现异步效果,后来的浏览器提供了对ajax的原生支持。

//ajax编写步骤
// 1、创建XMLHttpRequest对象
    var xhr = new XMLHttpRequest();
// 2、设置请求参数
    xhr.open(请求方式,请求地址,异步或同步);
// 3、设置回调函数
    xhr.onreadystatechange = function(){
        if(xhr.reasyState===4){
            if(xhr.status === 200) {
                //5、接受响应
                console.log(xhr.responseText);
            }
        }
    }
// 4、发送请求
    xhr.send();

16 同步和异步的区别?
  同步:浏览器向服务器请求数据,服务器比较忙,浏览器一直等着(页面白屏),直到服务器返回数据,浏览器才能显示页面。

异步:浏览器向服务器请求数据,服务器比较忙,浏览器可以自如的干原来的事情(显示页面),服务器返回数据的时候通知浏览器一声,浏览器把返回的数据再渲染到页面,局部更新。

17 如何理解跨域?如何解决跨域问题?
  理解跨域的概念:协议、域名、端口都相同才同域,否则都是跨域;
  
需要使用一些跨域请求的技术:
一:
利用JQuery的方法,使用JSONP模式访问,dataType:‘jsonp’并且再url后传入callback=?
JQuery会生成随机回调函数名称,或者你自己起名字。
后台会获取callback的值,连接上() 把数据放入() 中,返回页面,
相当于调用函数function名(data);
注::jsonp 只能解决get跨域(问的最多);

二:
使用js标签加载方式,动态创建一个script标签。利用script标签的src属性不受同源策略限制。因为所有的src属性和href属性都不受同源策略限制。可以请求第三方服务器数据内容;
利用script标签 src写想要请求的URL,地址后面连接上参数?callback = 函数名
后台会获取callback的值,连接上() 把数据放入() 中,返回页面,
相当于调用函数function名(data);

步骤:
(1)去创建一个script标签;
(2)script的src属性设置接口地址;
(3)接口参数,必须要带一个自定义函数名 要不然后台无法返回数据;
(4)通过定义函数名去接收后台返回数据;

//去创建一个script标签
var  script = document.createElement("script");
//script的src属性设置接口地址 并带一个callback回调函数名称
script.src = "http://127.0.0.1:8888/index.html?callback=jsonpCallback";
//插入到页面
document.head.appendChild(script);
//通过定义函数名去接收后台返回数据
function jsonpCallback(data){
    //注意  jsonp返回的数据是json对象可以直接使用
    //ajax  取得数据是json字符串需要转换成json对象才可以使用。
}

三:
后台直接开启同源策略的访问限制,设置响应头信息。
response.setHeader(“Access-Control-Allow-Origin”, “*”);

出于安全考虑,服务器不允许ajax跨域获取数据,但是可以跨域获取文件内容,所以基于这一点,可以动态创建script标签,使用标签的src属性访问js文件的形式获取js脚本,并且这个js脚本中的内容是函数调用,该函数调用的参数是服务器返回的数据,为了获取这里的参数数据,需要事先在页面中定义回调函数,在回调函数中处理服务器返回的数据,这就是解决跨域问题的主流解决方案。

18 CORS:跨域资源共享
原理:服务器设置Access-Control-Allow-OriginHTTP响应头之后,浏览器将会允许跨域请求;

限制:浏览器需要支持HTML5,可以支持POST,PUT等方法兼容ie9以上;

18 请解释一下javaScript的同源策略
  同源策略是客户端脚本的重要安全度量标准,所谓同源指的是:协议,域名,端口相同,同源策略是一种安全协议,指一段脚本只能读取来自同一来源的窗口和文档的属性。

18 Ajax几种请求,优缺点都是啥
常用的post,get,delete。不常用copy、head、link等等;

代码上的区别
1:get通过url传递参数;
2:post设置请求头 规定请求数据类型;

使用上的区别
1:post比get安全;
(因为post参数在请求体中。get参数在url上面)
2:get传输速度比post快 根据传参决定的;
(post通过请求体传参,后台通过数据流接收。速度稍微慢一些。而get通过url传参可以直接获取)
3:post传输文件大理论没有限制 get传输文件小大概7-8k ie4k左右;
4:get获取数据 post上传数据;
(上传的数据比较多 而且上传数据都是重要数据。所以不论在安全性还是数据量级 post是最好的选择)

19 GET和POST的区别,何时使用POST?
  GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符,有的浏览器是8000个字符。

POST:一般用于修改服务器上的资源,对所发送的信息没有限制。

以下情况中,请使用POST请求:

①、无法使用缓存文件(更新服务器上的文件或数据库)

②、向服务器发送大量数据(POST没有数据量限制)

③、发送包含未知字符的用户输入时,POST比GET更稳定也更可靠。

20 ajax的最大特点是什么?
  ajax可以实现异步通信效果,实现页面局部刷新,带来更好的用户体验;按需要获取数据,节约带宽资源。

21 ajax的缺点?
①、ajax不支持浏览器back按钮。
②、安全问题ajax暴露了与服务器交互的细节。
③、对搜索引擎的支持比较弱。
④、破坏了程序的异常机制。

22 解释jsonp的原理,以及为什么不是真正的ajax?
  jsonp并不是一种数据格式,而是json是一种数据格式,jsonp是用来解决跨域获取数据的一种解决方案,具体是通过动态创建script标签,然后通过标签src属性获取js文件中的js脚本,该脚本的内容是一个函数调用,参数就是服务器返回的数据,为了处理这些返回的数据,需要事先在页面定义好回调函数,本质上使用的并不是ajax技术。

23 HTTP状态码都有哪些?   
  200 OK 客户端请求成功
  301 资源(网页等)被永久转移到其他URL
  400 Bad Request 客户端请求有语法错误,不能被服务器所理解
  403 Forbidden 服务器收到请求,但是拒绝提供服务
  404 Not Found 请求资源部存在,输入了错误的URL
  500 Internal Server Error 服务器发生不可预期的错误
  503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常。

24 为什么利用多个域名来存储网站资源会更有效?
  确保用户在不同地区能用最快的速度打开网站,其中某个域名崩溃用户也能通过其他域名访问网站,并且不同的资源放到不同的服务器上有利于减轻单台服务器的压力;

25 为什么异步加载JS文件?加载方式?
平时常用的引入JS方式,是同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,也就是说,浏览器在下载或执行该js代码块时,后面的标签不会被解析。
异步加载(async)JS文件,允许页面内容异步加载,仅适用于外部脚本。
延迟加载(defer) 属性规定是否对脚本执行进行延迟,直到页面加载为止。

26 什么是闭包?
简单理解成:定义在一个函数内部的函数;
闭包本质:将函数内部和函数外部连接起来的一座桥梁;
最大用处:
1、可以读取函数内部变量;
2、就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在;

27 json字符串转换集json对象、json对象转换json字符串

Json串转化为对象
Json.parse(json);
对象转化为JsonJson.Stringify(json);

前端框架知识

1 说说你理解中的bootstrap
Bootstrap是基于HTML5和CSS3开发的,它在jQuery的基础上进行了更为个性化和人性化的完善,只需要给标签起上响应的Class名称,就可以形成一套Bootstrap自己独有的网站风格,并兼容大部分jQuery插件;

后端框架知识

Spring框架

spring的理解

1 (1次)谈谈你对Spring的理解
Spring 是一个轻量级的 DI / IoC 和 AOP 容器的开源框架,提倡以“最少侵入”的方式来管理应用中的代码,这意味着我们可以随时安装或者卸载 Spring;

1.狭义上 spirng 就是指 Spring Framework,特别是其中的核心思想控制反转和依赖注入、事务处理等特性,在进行大型项目开发的时候能够帮助我们更好的管理对象。
2.广义上 spring 就是指 spring 家族的一些列产品:如 SpringMvc,SpringBoot,SpringJPA,SpringCloud 等等。Spring 是项目开发中最重要的框架,可以帮助我们更好的管理对象,减少依赖,提高开发效率

1加粗样式.降低了组件之间的耦合性 ,实现了软件各层之间的解耦 ;
2.可以使用容易提供的众多服务,如事务管理,消息服务等 ;
3.容器提供单例模式支持 ;
4.容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能 ;
5.容器提供了众多的辅助类,能加快应用的开发 ;
6.spring对于主流的应用框架提供了集成支持,如hibernate,JPA,Struts等 ;
7.spring属于低侵入式设计,代码的污染极低 ;
8.独立于各种应用服务器 ;
9.spring的DI机制降低了业务对象替换的复杂性 ;
10.Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选择spring的部分或全部 ;

spring的不足:
1 spring基于大量的xml 配置文件,使得我们花了大量的时间放在配置上,拖慢了开发的进度;
2 spring 的内容太庞大,随便打断点查看的时候会出现十几二十层代码,阅览性不强,在实际开发的过程中spring的角色更像是胶水一样,充当整合各种技术的角色;

2 (1次)Spring的IOC和AOP机制?

IOC
 控制反转也叫依赖注入,IOC利用java反射机制,AOP利用代理模式。所谓控制反转是指,本来被调用者的实例是由调用者来创建的,这样的缺点是耦合性太强,IOC则是统一交给spring来管理创建,将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。

AOP

关于AOP的文章,详情参考另一篇AOP

 AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理;

一 、面向切面编程(AOP)完善spring的依赖注入(DI),面向切面编程在spring中主要表现为两个方面:
1.面向切面编程提供声明式事务管理 ;
2.spring支持用户自定义的切面;

二 、面向切面编程(aop)是对面向对象编程(oop)的补充,面向对象编程将程序分解成各个层次的对象,面向切面编程将程序运行过程分解成各个切面。 AOP从程序运行角度考虑程序的结构,提取业务处理过程的切面,oop是静态的抽象,aop是动态的抽象,
是对应用执行过程中的步骤进行抽象,从而获得步骤之间的逻辑划分。

三 、aop框架具有的两个特征:
1.各个步骤之间的良好隔离性
2.源代码无关性

3 Spring中Autowired和Resource关键字的区别?
@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。

1、共同点
两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
2、不同点
(1)@Autowired
@Autowired为Spring提供的注解,需要导入包
org.springframework.beans.factory.annotation.Autowired;只按照byType注入。
@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。
(2)@Resource
@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。
@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的
名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策
略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通
过反射机制使用byName自动注入策略。
注:最好是将@Resource放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。

4 依赖注入的方式有几种,各是什么?
(1)依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念,具体的讲:当某个角色需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在spring中创建被调用者的工作不再由调用者来完成,因此称为控制反转。创建被调用者的工作由spring来完成,然后注入调用者
因此也称为依赖注入。
spring以动态灵活的方式来管理对象 , 注入的三种方式,设置(setter)注入、注解注入和构造注入。
(2)设置注入的优点:直观,自然
(3)注解注入:
(4)构造注入的优点:可以在构造器中决定依赖关系的顺序。

5 Spring容器对Bean组件是如何管理的?

6 Spring容器如何创建?

7 Spring事务分类?

8 Spring事务的管理
Spring的事务管理机制实现的原理,就是通过一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。
Spring中的AOP实现更为复杂和灵活,不过基本原理是一致的。

Spring事务的4种特性 ACID
**(1)原子性(Atomicity):**事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
(2)一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
**(3)隔离性(Isolation):**可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
**(4)持久性(Durability):**一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

8 Spring事务的7种传播行为

spring事务的传播行为说的是当一个方法调用另一个方法时,事务该如何操作。因此:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播;
(1)PROPAGATION_MANDATORY:该方法必须运行在一个事务中。如果当前事务不存在则抛出异常。
(2)PROPAGATION_NESTED:如果当前存在一个事务,则该方法运行在一个嵌套的事务中。被嵌套的事务可以从当前事务中单独的提交和回滚。如果当前不存在事务,则开始一个新的事务。各厂商对这种传播行为的支持参差不齐,使用时需注意。
(3)PROPAGATION_NEVER:当前方法不应该运行在一个事务中。如果当前存在一个事务,则抛出异常。
(4)PROPAGATION_NOT_SUPPORTED:当前方法不应该运行在一个事务中。如果一个事务正在运行,它将在该方法的运行期间挂起。
(5)PROPAGATION_REQUIRED:该方法必须运行在一个事务中。如果一个事务正在运行,该方法将运行在这个事务中。否则,就开始一个新的事务。
(6)PROPAGATION_REQUIRES_NEW:该方法必须运行在自己的事务中。它将启动一个新的事务。如果一个现有的事务正在运行,将在这个方法的运行期间挂起。
(7)PROPAGATION_SUPPORTS:当前方法不需要事务处理环境,但如果一个事务已经在运行的话,这个方法也可以在这个事务里运行。

在现实的应用中,由于业务上的需要,要求不同表中的数据保持一致,即要求不同表中的数据同时更新或者出错时同时回滚,事务的本质其实就是为了解决这样的问题。

9 Spring事务的5种隔离级别?(不要与Mysql4种隔离级别混淆)

并发事务引起的问题:
**脏读(Dirty read):**脏读发生在一个事务读取了被另一个事务改写但还未提交的数据时。如果这些改变在稍后被回滚,那么之前的事务读取的到数据就是无效的。
**不可重复读(Nonrepeatable read):**不可重复读发生在一个事务执行相同的查询两次或两次以上,但每一次的查询结果不同时。这通常是由于另一个并发的事务在两次查询之间更新了数据。
**幻读(Phantom read):**幻读是一个事务读取几行记录后,另一个事务插入了一些记录,幻读就发生了。在后来的查询中第一个事务就会发现有一些原来没有的额外的记录。

(1)ISOLATION_DEFAULT:使用数据库默认的隔离级别。
(2)ISOLATION_READ_UNCOMMITTED:允许读取改变了的还未提交的数据,可能导致脏读、不可重复读和幻读。
(3)ISOLATION_READ COMMITTED:允许并发事务提交之后读取,可以避免脏读,可能导致重复读和幻读。
(4)ISOLATION_REPEATABLE_READ:对相同字段的多次读取结果一致,可导致幻读。
(5)ISOLATION_SERIALIZABLE:完全服从ACID的原则,确保不发生脏读、不可重复读和幻读。
可以根据自己的系统对数据的要求采取适应的隔离级别,因为隔离牵涉到锁定数据库中的记录,对数据正性要求越严格,并发的性能也越差。

10 Spring中AOP的通知类型有哪些?

关于AOP通知类型的文章,详情参考另一篇AOP详解

(1)前置通知(Before advice):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常);
(2)返回后通知(After returning advice):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回;
(3)抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知;
(4)后通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出);
(5)环绕通知(Around Advice):在目标方法执行之前和之后都可以执行额外代码的通知。在环绕通知中必须显式的调用目标方法,否则目标方法不会执行。这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,这个参数必须处在环绕通知的第一个形参位置;
要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint;

五种通知类型执行顺序:
在目标方法没有抛出异常的情况下:
1 前置通知
2 环绕通知的调用目标方法之前的代码——取决于配置顺序
3 目标方法
4 环绕通知的调用目标方法之后的代码
5 后置通知——取决于配置顺序
6 最终通知——最终通知 后置通知和环绕通知结束取决于配置反向顺序

在目标方法抛出异常的情况下:
1 前置通知
2 环绕通知的调用目标方法之前的代码——取决于配置顺序
3 目标方法 抛出异常
4 异常通知
5 最终通知
如果存在多个切面:
 多切面执行时,采用了责任链设计模式;

五种通知常用场景:
前置通知 :记录日志(方法将被调用)
环绕通知 :控制事务 权限控制
后置通知 :记录日志(方法已经成功调用)
异常通知 :异常处理 控制事务
最终通知 :记录日志(方法已经调用,但不一定成功)

9 Spring拦截器与Servlet过滤器(Filter)的区别?
共同点: 两者都是AOP编程思想的体现,都能实现权限检查、日志记录等;
不同点:
(1)范围不同:Filter是Servlet规范规定的,只能用于Web程序中;而拦截器既可以用于Web程序,也可以用于Application、Swing程序中;
(2)规范不同:Filter是在Servlet规范中定义的,是Servlet容器支持的;而拦截器是在Spring容器内的,是Spring框架支持的;
(3)资源不同:拦截器是一个Spring组件,归Spring管理,配置在Spring文件中,因此能适用Spring里的任何资源、对象,例如Service对象、数据源、事务管理等,通过IOC注入到拦截器即可;而Filter不能;
(4)深度不同:Filter只在Servlet前后起作用;而拦截器能够深入到方法前后、异常抛出前后等;因此拦截器的使用具有更大的弹性,所以在Spring架构程序中,要优先使用拦截器;


SpringMVC框架

1 (1次)SpringMVC完整工作流程,熟读源码流程?
在这里插入图片描述
1 用户发起请求到前端控制器(DispatcherServlet),该控制器会过滤出哪些请求可以访问Servlet、哪些不能访问。就是url-pattern的作用,并且会加载springmvc.xml配置文件。
2 前端控制器会找到处理器映射器(HandlerMapping),通过HandlerMapping完成url到controller映射的组件,简单来说,就是将在springmvc.xml中配置的或者注解的url与对应的处理类找到并进行存储,用map<url,handler>这样的方式来存储。
3 HandlerMapping有了映射关系,并且找到url对应的处理器,HandlerMapping就会将其处理器(Handler)返回,在返回前,会加上很多拦截器。
4 DispatcherServlet拿到Handler后,找到HandlerAdapter(处理器适配器),通过它来访问处理器,并执行处理器。
5 执行处理器
6 处理器会返回一个ModelAndView对象给HandlerAdapter
7 通过HandlerAdapter将ModelAndView对象返回给前端控制器(DispatcherServlet)
8 前端控制器请求视图解析器(ViewResolver)去进行视图解析,根据逻辑视图名解析成真正的视图(jsp),其实就是将ModelAndView对象中存放视图的名称进行查找,找到对应的页面形成视图对象
9 返回视图对象到前端控制器。
10 视图渲染,就是将ModelAndView对象中的数据放到request域中,用来让页面加载数据的。
11 通过第8步,通过名称找到了对应的页面,通过第10步,request域中有了所需要的数据,那么就能够进行视图渲染了。最后将其返回即可

2 SpringMVC如何处理JSON数据?

3 SpringMVC拦截器原理,如何自定义拦截器?
 SpringMVC 拦截器也是Aop(面向切面)思想构建,但不是 Spring Aop 动态代理实现的,主要采用责任链和适配器的设计模式来实现,直接嵌入到 SpringMVC 入口代码里面。

 DispatcherServlet 执行调用 doService(request, response) 作为 Servlet 主要执行者,doService(request, response) 通过调用 doDispatch(request, response) 来真正执行请求处理;

拦截器原理:
(1) 通过 getHandler(HttpServletRequest request) 获取到 HandlerExecutionChain 处理器执行链;
(2)将拦截器注入到 HandlerExecutionChain 的属性中;
(3)分别调用 HandlerExecutionChain 的三个方法,applyPreHandle、applyPostHandle、triggerAfterCompletion;
(4)实现前置拦截/请求提交拦截和请求完成后拦截;
如何自定义拦截器:

4 SpringMVC如何将请求映射定位到方法上面?结合源码阐述?

5 SpringMVC常见注解有哪些?

6 SpringMVC容器和Spring容器的区别?

6 SpringMVC容器和Mybatis区别?

首先你bai要清楚springmvc和mybatis都是干什么的du,springmvc负责的是zhi接受用户请求以及业务的分发和视图的渲染,mybatis只是用来与数据库做交互的,mybatis侧重sql语言的编写,如果你不用spring做粘合剂将这两个框架粘合起来的话,需要做的步骤也很多。

MyBatis就是对JDBC的封装,操作的是数据库连接,执行各种增删改查的语句。主要关注SQL的组装和结果集的封装。
SpringMVC可以是Servlet的封装,甚至说它本质上就是Servlet。主要关注的是接收请求和发送响应。
如果没有他们,直接用Servlet+JDBC一样可以开发,只不过自己要写的简单重复的东西更多了。

7 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决?


MyBatis框架

1 看过MyBatis源码吗,请说说它的工作流程?

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录;

Mybatis底层还是采用原生jdbc来对数据库进行操作的,通过下列几个处理器封装了这些过程:
**SqlSessionFactory(核心类):**每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory;
SqlSession(核心类): SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager。SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。此外SqlSession不是线程安全的,因为每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession;
Executor(核心类): Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statement,跟Simple的唯一区别就是内部缓存statement),SimpleExecutor(默认,每次都会创建新的statement);
MappedStatement(核心类):
StatementHandler:
ParameterHandler:
ResultHandler:
TypeHandler(类型转换):

执行器:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
sql查询处理器:
StatementHandler
参数处理器:
ParameterHandler
结果处理器 :
ResultSetHandler
StatementHandler用通过ParameterHandler与ResultHandler分别进行参数预编译与结果处理;

工作流程:
1:加载配置文件(mybatis-config.xml 、 *…Mapper.xml)并初始化,
将SQL的配置信息加载成为一个个MappedStatement对象(包括了传
入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。
2:接收调用请求(调用Mybatis提供的API即增删改查的方法)并传入参数:
即SQL的ID和传入参数对象
3:处理操作请求,过程:
(1)根据SQL的ID查找对应的MappedStatement对象。
(2)根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。
(3)获取数据库连接,根据得到的最终SQL语句和执行传入参数,到数据库执行,并得到执行结果。
(4)根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。
(5)释放连接资源。
4:返回处理结果将最终的处理结果返回。

优点:
1:半自动化的ORM实现(实体类和SQL语句之间建立映射关系)
2:SQL代码从程序代码中彻底分离,可重用
3:与JDBC相比,减少了50%以上的代码量
4:小巧灵活、简单易学,是最简单的持久化框架
5:提供XML标签,支持编写动态SQL
6:提供映射标签,支持对象与数据库的ORM字段映射

缺点:
1:SQL语句编写工作量大,对开发人员有一定sql技术要求
2:数据库移植性差(不同数据库,sql语句语法有所不同)

2 MyBatis中#和$的区别?
#{parameterName}引用参数的时候,Mybatis会把这个参数认为是一个字符串,并自动加上" ",#{}是经过预编译的,是安全的;
${}是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入。

能使用 #{} 的地方就用 #{}
表名作为变量时,必须使用 ${}

3 MyBatis一级缓存原理以及失效情况?
一级缓存是默认开启的,当创建SqlSession对象时候就开启了;
一级缓存通过简单Map集合来实现,并没有对Map集合的大小,容量进行限制;
一级缓存是一个粗粒度的缓存,没有办法去精确控制缓存中的数据是否过期以及去更新缓存数据;

4 MyBatis二级缓存的使用?
mybatis的二级缓存是基于application为生命周期
默认采用基于PerpetualCache的HashMap存储,其存储作用域为Mapper(Namespace)。
当某一个作用域(二级缓存Namespaces)进行了C/U/D操作后,默认该作用域下所有select中的缓存将被clear。

为什么要避免使用二级缓存:
多个Mapper(Namespace)可能对同一个表有相同的操作,会发送错误(两个命名空间下的数据不一致)

5 MyBatis拦截器原理
mybatis给Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了拦截器功能;

Executor提供了增删改查的接口;
StatementHandler负责处理Mybatis与JDBC之间Statement的交互;
ResultSetHandler负责处理Statement执行后产生的结果集,生成结果列表;
ParameterHandler是Mybatis实现Sql入参设置的对象;

拦截器采用了责任链模式,把请求发送者和请求处理者分开,各司其职;


SpringBoot框架

1 请说说SpringBoot自动装配原理?
其实现自动装配的原理是在@SpringBootApplication —>@EnableAutoConfiguration—>@Import(AutoConfigurationImportSelector.class)这个注解中;
那么自动装配到底加载的是什么类:
在这里插入图片描述
会自动扫描所有项目下FACTORIES_RESOURCE_LOCATION这个路径下的类;

总结: springboot的自动装配就是通过自定义实现ImportSelector接口,从而导致项目启动时会自动将所有项目META-INF/spring.factories路径下的配置类注入到spring容器中,从而实现了自动装配;

2 谈谈你对SpringBoot的理解
spring boot 是微服务框架的起点,他简化了配置过程、部署过程、监控过程;
它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了很多的框架,同时将其他技术同spring结合起来;

使用SpringBoot的好处:
1 提供了各种组件便于功能的开箱即用:围绕着Spring Boot生态圈目前已经涌现出了不计其数的starter,这样我们只需将相应的starter配置项引入到项目中即可很方便地使用对应的功能;
2 自动装配:Spring Boot在启动时会自动探测类路径下的各种类型,实现类型的自动装配,无需开发者再通过XML或是注解进行显式的类型装配了,这一点要拜@EnableAutoConfiguration注解或是更为全面的@SpringBootApplication注解所赐;
3 yml配置的支持:它通过缩进的方式来表示层次化的配置项,相比于传统的properties属性文件来说,其层次感会更好一些;当然,顺便也可以让我们少敲一些字母;

使用SpringBoot的不足:
Spring Boot作为一个微框架,离微服务的实现还是有距离的。
没有提供相应的服务发现和注册的配套功能,自身的acturator所提供的监控功能,也需要与现有的监控对接。没有配套的安全管控方案,对于REST的落地,还需要自行结合实际进行URI的规范化工作;

3 SpringBoot核心配置文件
application.properties 或者 application.yml 所有配置项的集中配置文件;

3 bean的生命周期
从对象的创建到销毁的过程。而Spring中的一个Bean从开始到结束经历很多过程,但总体可以分为六个阶段Bean定义、实例化、属性赋值、初始化、生存期、销毁。

SpringCloud框架

1 请说说SpringCloud?
Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集 成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程 序。

2、 有用过SpringCloud吗,请说说SpringCloud和Dubbo有什么不一样?
Dubbo服务调用方式是RPC,SpringCloud的调用方式是Restful风格的API
注册中心:Dubbo是Zookeeper,SpringCloud是Eureka,也可以是Zookeeper
Dubbo很多服务自己没有实现,通过整合第三方实现功能,SpringCloud有完善的服务体系

3、服务注册和发现是什么意思?Spring Cloud 如何实现?

当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添 加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可 能会产生问题。 Eureka 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 Eureka 服务 器上注册并通过调用 Eureka 服务器完成查找,因此无需处理服务地点的任何更改和处理。

4、负载均衡的意义是什么?
负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,避免单一资源过载。

5、什么是Hystrix?它是如何实现容错的?
Hystrix是一个延迟和容错库,当出现故障不可避免时,停止级联故障在复杂的分布式系统中实现弹性。
容错实现方式:在服务提供方使用Hystrix并定义Fallback方法,当正常服务出
现异常时则会执行Fallback方法。Fallback方法应和公开的服务方法返回相同
数据类型。

6、什么是SpringCloud Bus?
Bus通常和Config一起使用,当配置文件修改时,用到该配置文件的服务需要
重新刷新获取最新配置信息,如果服务太多则太多繁琐。因此可已通过Bus将各
个服务之间连接起来,可以用于广播配置文件的更改和服务的监控和管理。

7、什么是SpringCloud Feign?
Feign是Spring Cloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。

8、SpringCloud熔断器作用?
当一个服务调用另一个服务时,由于网络或自身原因,调用者就会等待被调用者
响应消息,当更多的服务请求该资源时就会产生更多的请求等待,从而造成雪崩
效应。

熔断器完全打开状态:一段时间内,请求达到一定数量就无法调用,而且多次监测没有恢复迹象,熔断器就会完全打开
熔断器半开状态:短时间内有恢复迹象,熔断器会将部分请求发送给该服务,正常调用时熔断器关闭
熔断器关闭状态:当服务一直处于正常状态,就可以正常调用

9、什么是SpringCloud Config
由于分布式系统中配,由于服务数量多,为了方便服务的配置文件统一管理,实时更新,所以需要分布式配置中心服务。
配置文件可以放在本地,也可以放在git仓库。


网络架构问题

详情请参考另一篇文章互联网架构发展及其专业术语

1 (1次)Dubbo,Zookeeper;
Dubbo:
Zookeeper: 详情请参考here
首先在服务启动的时候,将服务提供者信息主动上报到服务注册中心进行服务注册。服务调用者启动的时候,将服务提供或者信息从注册中心下拉倒服务调用者本机缓存。当需要调用服务时,从本地缓存列表中找到服务提供者的地址列表,基于某种负载均衡策略(随机、轮询等)选择一台服务器发起远程调用。ZooKeeper就是实现这些功能的分布式协调服务(服务发现就是将服务提供者信息主动上报到服务注册中心进行服务注册,如将其提供的服务和对应的IP和端口注册到ZooKeeper中,服务调用者启动的时候,将ZooKeeper注册中心信息下拉倒服务调用者本机缓存,到需要用到到某个服务时,通过某种算法,去选择其中一个IP+端口,然后调用。);

2 Dubbo完整的一次调用链路介绍?
Dubbo 服务调用过程
详细Dubbo调用过程源码please 参考 here!

Dubbo调用过程大致可以分为六步:
(1):服务消费方(dubbo-consumer)发布请求

(2):请求(request)编码

(3):服务提供方(dubbo-provider)解码请求

(4):服务提供方调用服务

(5):服务提供方返回调用结果(response)

(6):服务消费方接收调用结果

5 什么是WebService,如何基于WebService开发接口?

6 谈谈项目中分布式事务应用场景?

7 使用Redis如何实现分布式锁?

Redis实现分布式锁请参考here
and Here

其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件:

(1) 互斥性: 在任意时刻,只有一个客户端能持有锁;
(2) 不会发生死锁: 即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁;
(3) 具有容错性: 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁;
(4) 解铃还须系铃人: 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了;

加锁代码:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。

解锁代码:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

可以看到,我们解锁只需要两行代码就搞定了!

Redis的四种模式,单机、主从、哨兵、集群
1、单机:安装一个redis,启动起来,业务调用即可

优点:
部署简单,0成本。
成本低,没有备用节点,不需要其他的开支。
高性能,单机不需要同步数据,数据天然一致性。
缺点:
可靠性保证不是很好,单节点有宕机的风险。
单机高性能受限于CPU的处理能力,redis是单线程的。
单机模式选择需要根据自己的业务场景去选择,如果需要很高的性能、可靠性,单机就不太合适了。

2、主从:是指将一台Redis服务器的数据,复制到其他的Redis服务器。
主从模式在很多系统设计时都会考虑,一个master挂在多个slave节点,当master服务宕机,会选举产生一个新的master节点,从而保证服务的高可用性。
在这里插入图片描述

主从模式的优点:
一旦 主节点宕机,从节点 作为 主节点 的 备份 可以随时顶上来。
扩展 主节点 的 读能力,分担主节点读压力。
高可用基石:除了上述作用以外,主从复制还是哨兵模式和集群模式能够实施的基础,因此说主从复制是Redis高可用的基石。

也有相应的缺点,比如我刚提到的数据冗余问题:
一旦 主节点宕机,从节点 晋升成 主节点,同时需要修改 应用方 的 主节点地址,还需要命令所有 从节点 去 复制 新的主节点,整个过程需要 人工干预。
主节点 的 写能力 受到 单机的限制。
主节点 的 存储能力 受到 单机的限制。

3、哨兵:在Redis 2.8版本开始引入,就有了哨兵这个概念。在复制的基础上,哨兵实现了自动化的故障恢复。

故障转移方式:比如 主节点存活检测、主从运行情况检测、主从切换;

哨兵节点由两部分组成,哨兵节点和数据节点:
哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
数据节点:主节点和从节点都是数据节点。
访问redis集群的数据都是通过哨兵集群的,哨兵监控整个redis集群
在这里插入图片描述
监控原理:每个Sentinel以 每秒钟 一次的频率,向它所有的 主服务器、从服务器以及其他Sentinel实例 发送一个PING 命令。
如果一个 实例(instance)距离最后一次有效回复 PING命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel标记为 主观下线。
如果一个 主服务器被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 该主服务器是否的确进入了 主观下线 状态。
如果一个 主服务器 被标记为 主观下线,并且有 足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的 时间范围 内同意这一判断,那么这个该主服务器被标记为 客观下线。
在一般情况下, 每个 Sentinel 会以每 10秒一次的频率,向它已知的所有 主服务器 和 从服务器 发送 INFO 命令。
当一个 主服务器被 Sentinel标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从10秒一次改为 每秒一次。
Sentinel和其他 Sentinel 协商 主节点的状态,如果 主节点处于 SDOWN`状态,则投票自动选出新的主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制。
当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器 的 客观下线状态就会被移除。当 主服务器 重新向 Sentinel的PING命令返回 有效回复 时,主服务器 的 主观下线状态 就会被移除。

哨兵模式的优缺点

它的优点:
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
主从可以自动切换,系统更健壮,可用性更高。
Sentinel 会不断的检查 主服务器 和 从服务器 是否正常运行。当被监控的某个 Redis 服务器出现问题,Sentinel 通过API脚本向管理员或者其他的应用程序发送通知。

它的缺点:
Redis较难支持在线扩容,对于集群,容量达到上限时在线扩容会变得很复杂。

4、集群模式:解决哨兵与主从没有解决的问题——节点的存储能力是有上限,访问能力是有上限的。
集群模式具有 高可用、可扩展性、分布式、容错等特性。

通过数据分片的方式来进行数据共享问题,同时提供数据复制和故障转移功能。
之前的两种模式数据都是在一个节点上的,单个节点存储是存在上限的。集群模式就是把数据进行分片存储,当一个分片数据达到上限的时候,就分成多个分片。

数据分片怎么分?
集群的键空间被分割为16384个slots(即hash槽),通过hash的方式将数据分到不同的分片上的。

如何做到均匀分片
对于分片间key数量不均匀,导致数据倾斜问题,可考虑以下方案(可能性小):
(1)垂直扩容:扩容单分片内存容量(不推荐)
(2)水平扩容:扩容分片数,以把key打散到不同分片(推荐)

对于某分片存在bigkey,导致数据倾斜问题,可考虑以下方案(可能性大):
(1)垂直扩容:扩容单分片内存容量(不推荐)
(2)对bigkey进行改造,拆分成多个key打散(推荐)

8 请谈谈SSO单点登录原理?

SSO单点登陆详情参考here

SSO: 是目前比较流行的企业业务整合的解决方案之一,同时也是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统;

单点登录解决了什么问题: 解决了用户只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录;

优点:
1)提高用户的效率
用户不再被多次登录困扰,也不需要记住多个 ID 和密码。另外,用户忘记密码并求助于支持人员的情况也会减少;
2)提高开发人员的效率
SSO 为开发人员提供了一个通用的身份验证框架。实际上,如果 SSO 机制是独立的,那么开发人员就完全不需要为身份验证操心。他们可以假设,只要对应用程序的请求附带一个用户名,身份验证就已经完成了;
3)简化管理
如果应用程序加入了单点登录协议,管理用户帐号的负担就会减轻。简化的程度取决于应用程序,因为 SSO 只处理身份验证。所以,应用程序可能仍然需要设置用户的属性(比如访问特权);

缺点:
1)不利于重构
因为涉及到的系统很多,要重构必须要兼容所有的系统,可能很耗时;
2) 无人看守桌面
因为只需要登录一次,所有的授权的应用系统都可以访问,可能导致一些很重要的信息泄露;

如何实现:
Server端: 以server群如何生成、验证ID的方式大致分为两种:
 “共享Cookie”这个就是上面提到的共享session的方式,我倒觉得叫“共享session”来得好一点,本质上cookie只是存储session-id的介质,session-id也可以放在每一次请求的url里。据说这种方式不安全,我没去细究,哪位大神可以推荐下相关的资料,我后期补上。其实也是,毕竟session这项机制一开始就是一个server一个session的,把session拿出来让所有server共享确实有点奇怪。

 SSO-Token方式因为共享session的方式不安全,所以我们不再以session-id作为身份的标识。我们另外生成一种标识,把它取名SSO-Token(或Ticket),这种标识是整个server群唯一的,并且所有server群都能验证这个token,同时能拿到token背后代表的用户的信息。

浏览器端: 单点登录还有非常关键的一步,这一步跟server端验证token的方式无关,用最早的“共享session”的方式还是现在的“token”方式,身份标识到了浏览器端都要面临这样的一个问题:用户登录成功拿到token(或者是session-id)后怎么让浏览器存储和分享到其它域名下?同域名很简单,把token存在cookie里,把cookie的路径设置成顶级域名下,这样所有子域都能读取cookie中的token。这就是共享cookie的方式(这才叫共享Cookie嘛,上面那个应该叫共享session)。比如:谷歌公司,google.com是他的顶级域名,邮箱服务的mail.google.com和地图服务的map.google.com都是它的子域。但是,跨域的时候怎么办?谷歌公司还有一个域名,youtube.com,提供视频服务。

要实现SSO,需要以下主要的功能:
所有应用系统共享一个身份认证系统。
  统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。
所有应用系统能够识别和提取ticket信息;
  要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能;

9 Tomcat如何优化?

10 后台系统怎么防止请求重复提交?

防止请求重复提交详情请参考here

客户端的抖动,快速操作,网络通信或者服务器响应慢,造成服务器重复处理。为了防止重复提交,除了从前端控制,后台也需要控制。因为前端的限制不能解决彻底。接口实现,通常要求幂等性,保证多次重复提交只有一次有效。对于更新操作,达到幂等性很难;

重复提交的后果:
1 用户在界面看到两个一模一样的订单,不知道应该支付哪个;
2 系统出现异常数据,影响正常的校验;

前端常用做法
思路: 进入添加页面时,获取服务器端的token,提交时把token提交过去,判断token是否存在,若存在,则进行后续正常业务逻辑,如不存在,则报错重复提交;

后端常用做法:
(1)token: 访问请求到达服务器,服务器端生成token,分别保存在客户端和服务器。提交请求到达服务器,服务器端校验客户端带来的token与此时保存在服务器的token是否一致,如果一致,就继续操作,删除服务器的token。如果不一致,就不能继续操作,即这个请求是重复请求;
(2)缓存: request进来,没有就先存在缓存中,继续操作业务,最后删除缓存或者缓存设置生命周期。如果存在,就直接对request进行验证,就不能继续操作业务;
(3)索引: 数据库中创建唯一索引,记录每次request请求。添加索引成功,就获取锁,继续操作,最后设置索引失效。添加索引失败,获取锁失败,不能继续操作;
(4)Redis计数器: Redis的计数器是原子操作,不存储请求,又能提升QPS的峰值。每次request请求,若相同请求,计数器+1,否则新建id为key的计数器。如果>1,不能获取锁;如果=1,获取锁,操作,最后删除计数器(删除锁);
(5)Post/Redirect/Get: 提交(Post)后执行页面重定向,成功后转到提交成功页面(Get),整个流程才算结束。当刷新页面,或者浏览器前进和后退,都不会引起Post请求的重复提交。这里可以在head中设置control-cache,保存表单信息。这个方法依赖前端限制比较多;

11 Linux常见命令有哪些?

12 请说说什么是Maven的依赖、继承以及聚合?

13 Git暂存区和工作区的区别?

14 Git如何创建、回退以及撤销版本?

15 常见的设计模式有哪些?

23种设计模式超详细文章
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

**1、简单工厂模式:**定义了一个创建对象的类,由这个类来封装实例化对象的行为;
**2、工厂方法模式:**定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类(其实这个模式的好处就是,如果你现在想增加一个功能,只需做一个实现类就OK了,无需去改动现成的代码。这样做,拓展性较好)。
3、抽象工厂模式:定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类

三种工厂模式的使用选择
简单工厂 : 用来生产同一等级结构中的任意产品。(不支持拓展增加产品)
工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品)
抽象工厂 :用来生产不同产品族的全部产品。(支持拓展增加产品;支持增加产品族)

4、单例模式定义:确保一个类最多只有一个实例,并提供一个全局访问点,单例模式可以分为两种:预加载和懒加载:
预加载
顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。
懒加载
为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

5、适配器模式定义: 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。
主要分为三类:
(1)类的适配器模式:通过多重继承目标接口和被适配者类方式来实现适配
(2)对象的适配器模式:对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承。
(3)接口的适配器模式:当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。
接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可

6、装饰者模式定义:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性。
6.1 装饰者模式结构图与代码示例
1.Component(被装饰对象的基类)
定义一个对象接口,可以给这些对象动态地添加职责。

2.ConcreteComponent(具体被装饰对象)
定义一个对象,可以给这个对象添加一些职责。

3.Decorator(装饰者抽象类)
维持一个指向Component实例的引用,并定义一个与Component接口一致的接口。

4.ConcreteDecorator(具体装饰者)
具体的装饰对象,给内部持有的具体被装饰对象,增加具体的职责。

创建型模式(五种):
(1)工厂方法模式: 定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类;
(2)抽象工厂模式: 定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。
(3)单例模式: 确保一个类最多只有一个实例,并提供一个全局访问点;
(4)建造者模式:
(5)原型模式: 通过复制现有实例来创建新的实例,无需知道相应类的信息;

结构型模式(七种):

(1)适配器模式: 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题;
(2)装饰器模式: 动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性;
(3)代理模式: 代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介;
(4)外观模式: 隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口;
(5)桥接模式: 将抽象部分与它的实现部分分离,使它们都可以独立地变化;
(6)组合模式: 有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性;
(7)享元模式: 通过共享的方式高效的支持大量细粒度的对象;

行为型模式(十一种):
策略模式:
模板方法模式:
观察者模式:
迭代子模式:
责任链模式:
命令模式:
备忘录模式:
状态模式:
访问者模式:
中介者模式:
解释器模式:

16 你是怎么理解的shrio

Shiro是一个强大易用的java安全框架,提供了认证、授权、加密、会话管理、与web集成、缓存等功能,对于任何一个应用程序,都可以提供全面的安全服务,相比其他安全框架,shiro要简单的多;

核心概念:
Subject: 主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如爬虫、机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;

SecurityManager: 才是实际的执行者,为安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是shiro的核心, SecurityManager相当于spring mvc中的dispatcherServlet前端控制器;

Realm: 域,shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源;

优点:
1、 简单的身份验证,支持多种数据源;
2、对角色的简单授权,支持细粒度的授权(方法);
3、支持一级缓存,以提升应用程序的性能;
4、内置基于POJO的企业会话管理,适用于web及非web环境;
5、非常简单的API加密;
6、不跟任何框架绑定,可以独立运行;

17 谈谈nginx在项目中的作用

18 谈谈redis在项目中的作用
详情请戳这里
或者戳这里

19 谈谈你理解的RPC(远程过程调用)
简单的理解是一个节点请求另一个节点提供的服务;
远程过程调用:与本地方法调用不同的是,被调用的方法如果在服务端,执行函数的函数体在远程机器上,因此需要:
(1) 首先客户端需要告诉服务器,需要调用的函数,这里函数和进程ID存在一个映射,客户端远程调用时,需要查一下函数,找到对应的ID,然后执行函数的代码;
(2) 客户端需要把本地参数传给远程函数,本地调用的过程中,直接压栈即可,但是在远程调用过程中不再同一个内存里,无法直接传递函数的参数,因此需要客户端把参数转换成字节流,传给服务端,然后服务端将字节流转换成自身能读取的格式,是一个序列化和反序列化的过程;
(3) 数据准备好了之后,如何进行传输?网络传输层需要把调用的ID和序列化后的参数传给服务端,然后把计算好的结果序列化传给客户端,因此TCP层即可完成上述过程,gRPC中采用的是HTTP2协议;

总结为:

// Client端 
//    Student student = Call(ServerAddr, addAge, student)
1. 将这个调用映射为Call ID2.Call ID,student(params)序列化,以二进制形式打包
3.2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
5. 如果服务器调用成功,那么就将结果反序列化,并赋给student,年龄更新

// Server端
1. 在本地维护一个Call ID到函数指针的映射call_id_map,可以用Map<String, Method> callIdMap
2. 等待服务端请求
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在callIdMap中查找,得到相应的函数指针
5. 将student(params)反序列化后,在本地调用addAge()函数,得到结果
6. 将student结果序列化后通过网络返回给Client

在这里插入图片描述

项目部分

一、请你自我介绍一下
二、请你描述一下你的项目(为谁开发、介绍、功能、你负责的模块等等)
三、谈谈此项目由多少张表组成,你参与了哪写,如何设计
四、此项目是否上线、开发周期、开发人员组成
五、请你谈谈你在这个项目中遇到的问题(解决办法)
六、阅读过哪些源码(并根据你说的源码问一些细节的问题)
七、谈谈XX技术在你项目中是怎么实现的,流程等

首先,XX技术主要功能是…从而达到XX效果;在项目中主要…,其优势在于…能够实现…目的,所以…能够帮助我们在项目上做到…;

八、谈谈XX技术是怎么工作的;

例如RDB:默认 Redis 是会以快照"RDB"的形式将数据持久化到磁盘的一个二进制文件 dump.rdb,当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中,当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write;


扩展内容

一、 数据结构(代码算法参考下一部分)
查找(适用场景,案例)
排序(适用场景,案例)
二、基本算法
三、逻辑算法
四、银行面试题

1、在多线程环境中使用HashMap会有什么问题?在什么情况下使用get()方法会产生无限循环
HashMap本身没有什么问题,有没有问题取决于你是如何使用它的。比如,你在一个线程里初始化了一个HashMap然后在多个其他线程里对其进行读取,这肯定没有任何问题。有个例子就是使用HashMap来存储系统配置项。当有多于一个线程对HashMap进行修改操作的时候才会真正产生问题,比如增加、删除、更新键值对的时候。因为put()操作可以造成重新分配存储大小(re-sizeing)的动作,因此有可能造成无限循环的发生,所以这时需要使用Hashtable或者ConcurrentHashMap,而后者更优。

2、不重写Bean的hashCode()方法是否会对性能带来影响?
这个问题非常好,每个人可能都会有自己的体会。按照我掌握的知识来说,如果一个计算hash的方法写得不好,直接的影响是,当向HashMap中添加元素的时候会更频繁地造成冲突,因此最终增加了耗时。但是自从Java 8开始,这种影响不再像前几个版本那样显著了,因为当冲突的发生超出了一定的限度之后,链表类的实现将会被替换成二叉树(binary tree)实现,这时你仍可以得到O(logN)的开销,优于链表类的O(n)。

3、对于一个不可修改的类,它的每个对象是不是都必须声明成final的?
不尽然,因为你可以通过将成员声明成非final且private,并且不要在除了构造函数的其他地方来修改它。不要为它们提供setter方法,同时不会通过任何函数泄露出对此成员的引用。需要记住的是,把对象声明成final仅仅保证了它不会被重新赋上另外一个值,你仍然可以通过此引用来修改引用对象的属性。这一点是关键,面试官通常喜欢听到你强调这一点。

4、String的substring()方法内部是如何实现的?
又一个Java面试的好问题,你应该答出“substring方法通过原字符串创建了一个新的对象”,否则你的回答肯定是不能令人满意的。这个问题也经常被拿来测试应聘者对于substring()可能带来的内存泄漏风险是否有所了解。直到Java 1.7版本之前,substring会保存一份原字符串的字符数组的引用,这意味着,如果你从1GB大小的字符串里截取了5个字符,而这5个字符也会阻止那1GB内存被回收,因为这个引用是强引用。

5、你在写存储过程或者在Java里调用存储过程的时候如何来处理错误情况?
这是个很棘手的Java面试题,答案也并不固定。我的答案是,写存储过程的时候一旦有操作失败,则一定要返回错误码。但是在调用存储过程的时候出错的话捕捉SQLException却是唯一能做的。

6、Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?
如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。多线程和并发编程中使用 lock 接口的最大优势是它为读和写提供两个单独的锁,可以让你构建高性能数据结构,比如 ConcurrentHashMap 和条件阻塞。这道 Java 线程面试题越来越多见,而且随后的面试题都基于面试者对这道题的回答。我强烈建议在任何 Java 多线程面试前都要多看看有关锁的知识,因为如今电子交易系统的客户端和数据交互中,锁被频繁使用来构建缓存。

7、Executor.submit()和Executor.execute()这两个方法有什么区别?
前者返回一个Future对象,可以通过这个对象来获得工作线程执行的结果。当我们考察异常处理的时候,又会发现另外一个不同。当你使用execute提交的任务抛出异常时,此异常将会交由未捕捉异常处理过程来处理(uncaught exception handler),当你没有显式指定一个异常处理器的话,默认情况下仅仅会通过System.err打印出错误堆栈。当你用submit来提交一个任务的时候,这个任务一旦抛出异常(无论是否是运行时异常),那这个异常是任务返回对象的一部分。对这样一种情形,当你调用Future.get()方法的时候,这个方法会重新抛出这个异常,并且会使用ExecutionException进行包装。

9、你在什么时候会重写hashCode()和equals()方法?
当你需要根据业务逻辑来进行相等性判断、而不是根据对象相等性来判断的时候你就需要重写这两个函数了。例如,两个Employee对象相等的依据是它们拥有相同的emp_id,尽管它们有可能是两个不同的Object对象,并且分别在不同的地方被创建。同时,如果你准备把它们当作HashMap中的key来使用的话,你也必须重写这两个方法。现在,作为Java中equals-hashcode的一个约定,当你重写equals的时候必须也重写hashcode,否则你会打破诸如Set, Map等集合赖以正常工作的约定。你可以看看我的另外一篇博文来理解这两个方法之间的微妙区别与联系。

10、如果不重写hashCode方法会有什么问题?
如果不重写equals方法的话,equals和hashCode之间的约定就会被打破:当通过equals方法返回相等的两个对象,他们的hashCode也必须一样。如果不重写hashCode方法的话,即使是使用equals方法返回值为true的两个对象,当它们插入同一个map的时候,因为hashCode返回不同所以仍然会被插入到两个不同的位置。这样就打破了HashMap的本来目的,因为Map本身不允许存进去两个key相同的值。当使用put方法插入一个的时候,HashMap会先计算对象的hashcode,然后根据它来找到存储位置(bucket),然后遍历此存储位置上所有的Map.Entry对象来查看是否与待插入对象相同。如果没有提供hashCode的话,这些就都做不到了。

11、HashMap在调用get()方法的时候equals()和hashCode()方法都起了什么样的作用?
应聘者应该知道的是,一旦你提到了hashCode()方法,人们很可能要问HashMap是如何使用这个函数的。当你向HashMap插入一个key的时候,首先,这个对象的hashCode()方法会被调用,调用结果用来计算将要存储的位置(bucket)。因为某个位置上可能以链表的方式已经包含了多个Map.Entry对象,所以HashMap会使用equals()方法来将此对象与所有这些Map.Entry所包含的key进行对比,以确定此key对象是否已经存在。

12、在Java中如何避免死锁?
你可以通过打破互相等待的局面来避免死锁。为了达到这一点,你需要在代码中合理地安排获取和释放锁的顺序。如果获得锁的顺序是固定的,并且获得的顺序和释放的顺序刚好相反的话,就不会产生出现死锁的条件了。

13、说说ClassLoader.loadClass()与Class.forName()的区别
ClassLoader.loadClass()与Class.forName()大家都知道是反射用来构造类的方法,但是他们的用法还是有一定区别的。在讲区别之前,我觉得很有不要把类的加载过程在此整理一下。在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

装载:查找和导入类或接口的二进制数据;
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;
解析:将符号引用转成直接引用;
初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
于是乎我们可以开始看2者的区别了。

Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。比较下我们前面准备jvm加载类的知识,我们可以清晰的看到在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。

再看ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容。因此2者的区别就显而易见了。

最后还有必要在此提一下new方法和newInstance方法的区别

newInstance: 弱类型。低效率。只能调用无参构造。
new: 强类型。相对高效。能调用任何public构造。
例如,在JDBC编程中,常看到这样的用法,Class.forName(“com.mysql.jdbc.Driver”),如果换成了 getClass().getClassLoader().loadClass(“com.mysql.jdbc.Driver”),就不行。

全篇总结一些经验:

1.先投一些普通公司,等面出了心得再去投理想的公司;
2.不熟悉的技术不要主动提;
3.对于那种实习期6个月还打8折的公司,除非你没有其他选择了,否则不要去;
4.小公司喜欢在薪水上压你,开的时候适当提高;
5.不要去参加招聘会,纯粹是浪费时间;
6.把面试当作一次技术的交流,不要太在意是否能被录取;
7.公司一般面完就决定是否录取了,让你回去等消息这种情况一般没戏,无论你自己觉得面的有多好;
8.尽量少通过电话面试,效果不好;
9.在面试的日子里,要保持每天学习,无论是学习新东西还是复习旧东西;
10.拿到offer了,问问自己这个公司让自己100%满意了吗,如果不是,请继续努力找更好的;
11.通过面试官可以大概判断这家公司的情况;
12.拉勾投的简历很多会被筛掉,但是拉勾还是面试机会的最主要来源;
13.理想的公司可以多投几次,我有好几次都是第一次投被筛掉,多投几次就过的经验;

每一个你不满意的当下,都有一个你不曾努力的过去

最后送给大家的一句话,面试完以后一定要去总结你不会的问题,去网上找找它的答案,否则你是不会有进步的!!!








(未完待续)欢迎留言提出宝贵意见,将不断完善!所有资料均来自网络,有错误请及时指正,蟹蟹

在这里插入图片描述

注:文章所涉及表情包来自[麦田]的[抱抱]系列,如有侵权,请联系;

  • 5
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值