Java面试题总结汇总

1.自我介绍+项目介绍

2.基础知识

1、String类中常用的方法

split():把字符串分割成字符串数组

indexOf():从指定字符提取索引位置

trim():去除字符串两端空格

replace():替换

hashCode():返回此字符串的哈希码

subString():截取字符串

equals():比较

length():获取字符串的长度

valueOf():转换为字符串

concat():将指定字符串连接到此字符串的结尾

compareTo():用来比较两个字符串的字典顺序

compareToIgnoreCase():不考虑大小写,按字典顺序比较两个字符串

contains(): 检查一个字符串中是否包含想要查找的值

2、重载重写的区别

重载: 发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,与方法的修饰符和返回值无关

重写: 发生在父子类中,方法名、参数列表都必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,

3、int、Integer自动拆箱、装箱介绍

装箱就是自动将基本数据类型转换为包装类型;(ValueOf)

拆箱就是自动将包装类型转换为基本数据类型;(xxxValue)

在定义变量的时候,比如Integer num = 1;就会自动装箱成Integer对象操作,int num2 = num;就会进行自动拆箱操作

在比较的时候,也会发生拆箱和装箱操作

  • 无论如何,Integer与new Integer不会相等。不会经历拆箱过程

  • 两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false

    首先这是JDK在1.5版本中添加的一项新特性,把-128~127的数字缓存起来了,用于提升性能和节省内存。所以这个范围内的自动装箱(相当于调用valueOf(int i)方法)的数字都会从缓存中获取,返回同一个数字;在Integer源码中可以看到,在其中有一个成员内部类,将 -128 到 127 之间的每个数字先提前封装成Integer对象,并且缓存在一个静态数组Integer cache[]里面,将int类型的数据赋值给Integer引用的时候,会发生自动装箱,这时不是将每个int值都包装成Integer对象,而是先判断当前需要包装的int值在不在IntegerCache缓存的数组中,如果有直接将缓存数组中的对象的地址赋值给程序中的引用;但是超过这个范围,则需要重新申请空间封装对象,所以是先从缓存中获取,获取不到再new一个然后返回

  • 两个都是new出来的,都为false(new出来的都会在堆中分配新的空间,是不同的对象)

  • int和Integer或者new Integer比较,都为true,因为会把Integer自动拆箱为int再去比

4、值传递和引用传递

从这两个定义可以知道,就是这两种行为发生在传递的过程中

值传递呢就是传递的过程中,传递的是值,对值操作之后,不会影响原有变量的值

引用传递就是传递的过程中,传递的是引用,操作引用之后,会影响原有变量的值

  • 在传递的过程中,如果传递的是基本数据类型以及String,那么都是值传递,不会改变原有变量
  • 在传递的过程中,如果传递的是对象,如果修改了属性的值,那么会直接改动原有对象,会影响外面的值,如果没有修改对应的属性值,那么原有对象不受任何影响。

5、==和equals的区别

== 的话它的作用是判断两个对象的地址是不是相等。就是判断两个对象是不是同一个对象。但是如果有基本数据类型参与比较,无论是基本数据类型相互比较,还是基本数据和他们的封装类比较,都比较的是值,引用数据类型之间 == 比较的是内存地址。
equals的话 , 它的作用也是判断两个对象是否相等。但它一般有两种使用情况,一种呢是这个类没有重写equals() 方法,则通过 equals() 比较该类的两个对象时,等价于通过 ==比较这两个对象。
另一种呢就是类重写了 equals() 方法,重写了之后就按照重写的逻辑来判断了。一般,我们都覆盖 equals() 方法来比较两个对象的内容相等; 若它们的内容相等,就认为两个对象是相等的。

6、String 和 StringBuffer,StringBuilder 的区别是什么

从可变性来说呢,String底层呢其实就是个char数组,使用final修饰了,所以是不可变的,StringBuilder 与 StringBuffer是可变的字符串

从安全上来说,String 中的对象是不可变的,也就可以理解为常量,线程安全。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是线程不安全的。

从性能上来说,每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。StirngBuilder的效率会高一些,而StringBuffer的底层加了同步的关键字,性能会有所下降

所以呢,一般我们操作少量的字符串的时候用String ,在单线程环境下操作大量数据时使用StringBuilder,在多线程操作大量数据使用StringBuffer

7、final、finally、finalize的区别

final是一个安全修饰符,就是用final修饰的类不能被继承,用final声明的方法不能被重写,使用final声明的变量就相当于常量,不能被修改。

finally是在异常里经常用到的,就是try和cach里的代码执行完以后,必须要执行的方法,我们经常在finally里写一些关闭资源的方法,关闭IO流什么的,就算是try和catch里有return代码,也会执行finally里的内容的,除非在try catch里遇到System.exit代码,整个主线程就停掉了,才不会执行finally里的代码。

finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以被垃圾回收。

8、接口和抽象类的区别

  • 接口的方法默认是 public,所有方法在接口中不能有实现,不过在Java 8 开始接口方法可以有默认实现,抽象类可以有非抽象的方法

  • 接口中的实例变量默认是 final 类型的,而抽象类中则不一定

  • 一个类可以实现多个接口,但最多只能实现一个抽象类

  • 一个类实现接口的话要实现接口的所有方法,而抽象类不一定

  • 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象,从设计层面来 说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

9、Java的基本数据类型

基本数据类型有8中,分别是byte、short、int、long float、double、char、boolean,对应基本类型,都会有一个封装类。

10、jdk1.8的新特性

这块的话,我先说一些我们项目中经常会用到的吧

首先呢就是lamda表达式这块,遍历集合以及定义匿名函数,简直是非常方便

还有就是switch中的变量可以是string类型了,之前只能是基本数据类型

还有就是stream流式编程,这个的话可以让我们用strem的方式可以非常方便的对集合里的数据操作

还有就是新的时间类,LocalDate、LocalTime、LocalDateTime这几个类,让我们操作日期的时候非常方便,既可以自定义日期,还可以对年月日时分秒随时进行加减,以及快速格式化和强转等

还有就是其他一些我从别人的博客里看到的,我做的这些项目中没遇到过,就像Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字就行了,给我的感觉就是,我们在接口里定义一些初始化方法很方便了,不用在每个实现类里都实现一遍了,也是非常方便的

11、private、protected、public和default的区别

public关键字

具有最大的访问权限,可以访问任何一个在classpath下的类、接口、异常等。它往往用于对外的情况,也就是对象或类对外的一种接口的形式。

protected关键字

主要的作用就是用来保护子类的。它的含义在于子类可以用它修饰的成员,其他的不可以,它相当于传递给子类的一种继承的东西

default关键字

有时候也称为friendly,它是针对本包访问而设计的,任何处于本包下的类、接口、异常等,都可以相互访问,即使是父类没有用protected修饰的成员也可以。

private关键字

访问权限仅限于类的内部,是一种封装的体现,例如,大多数成员变量都是修饰符为private的,它们不希望被其他任何外部的类访问。

总结 :

public:可以被所有其他类所访问

private:只能被自己访问和修改

protected:自身、子类及同一个包中类可以访问

default:同一包中的类可以访问,声明时没有加修饰符,认为是friendly。

3、Java里常见的数据结构都有哪些以及特征

  • 数组

​ 数组是最常用的数据结构,数组的特点是长度固定,可以用下标索引,并且所有的 元素的类型都是一致的。

  • 列表

​ 列表和数组很相似,只不过它的大小可以改变。列表一般都是通过一个固定大小的 数组来实现的,并且会在需要的时候自动调整大小。列表里可以包含重复的元素。

  • 集合

​ 集合和列表很相似,不过它不能放重复的元素

  • 堆栈

​ 堆栈只允许对最后插入的元素进行操作,也就是先进后出。如果你移除了栈顶的元素,那么你可以操作倒数第二个元素,依次类推。这里边常用的方法有peek()返回不删除栈顶元素,push()向栈里添加元素, pop()返回并删除栈顶元素。

  • 队列

​ 队列和堆栈有些相似,不同之处在于在队列里第一个插入的元素也是第一个被删除 的元素,就是先进先出。常用的方法有peek()返回头部元素,offer()向尾部添加元素, poll() 移除并返回头部元素

  • 链表

​ 链表是一种由多个节点组成的数据结构,并且每个节点包含有数据以及指向下一个节点的引用,在双向链表里,还会有一个指向前一个节点的引用。例如,可以用单向链表和 双向链表来实现堆栈和队列,因为链表的两端都是可以进行插入和删除的动作的。当然,也 会有在链表的中间频繁插入和删除节点的场景。

4.集合

1、Java中集合的结构对比

Java面试题总结汇总_第1张图片

2、HashMap

我先说一下我理解的HashMap吧,HashMap是根据key的 hashCode 值存储数据的,大多数情况下可以直接定位到它的值,因而具有比较快的访问速度,但遍历的顺序却是不确定的。然后就是 HashMap 最多只允许一条记录的key为 null,但是允许多条记录的value是为 null的。

从源码里面可以看到,它里面是设定了一个默认初始容量为16,其实就是一个数组的长度,待会儿我讲它底层数据结构的时候再介绍这个吧,然后还有一个值为0.75的负载因子,这个就是说当HashMap的容量超过当前容量的0.75倍之后,16的0.75倍就是12哦,超过这个容量之后呢就会调用里面的resize方法将它扩容为原来容量的2倍,就是说原来是16嘛,那扩容之后就变成了32咯。

然后它的底层的实现的话,在jdk1.8之前,底层是通过数组+链表实现的,当创建一个HashMap时其实是会先创建一个数组,当我们去用put方法存数据时,会先根据key的hashcode值计算出hash值( int hash = hash(key.hashCode()); ),然后用这个哈希值确定在数组中的位置,然后再把对应的value值放进去,如果这个位置本来没放东西的话,就会直接放进去,如果之前就有,就需要根据equals方法判断key 是否和传入的 key 相等,如果相等就进行覆盖,并且返回原来的值,意思就是说key还是原来的key,但是原来的值被覆盖为一个新的值。但是如果equals方法返回的是false,就是说两个key不一样,但是hash是值一样的,那就产生了哈希冲突,也就是我们说的哈希碰撞哦,这个时候就会形成一个单链表,它会把hash值相同,key不同的元素以Entry链(Entry就是存一个个key-value的对象哦)的方式存放在链表中,这样的话就解决了hash冲突,这种方法我记得好像是叫做分离链表法,然后还一个就是当用get方法取值时,会先根据key的hashcode值计算出hash值,确定位置,再根据equals方法从该位置上的链表中取出对应的value值;(冲突后,取值要遍历链表)

但是在jdk1.8之后,HashMap 进行了一些修改,最大的不同就是利用了红黑树,也就是说从原来的数组+链表变成了数组+链表+ 红黑树。因为在jdk1.7的时候,它产生哈希冲突后形成的链表的长度不固定,所以如果key的hashcode重复之后,那么对应的链表的数据的长度就无法控制了,链表太长的话,那get数据的时间复杂度就比较大了,所以为了提高性能,就加入了红黑树,我记得源码里面是设置了一个阈值8,如果链表的长度超过8之后,不是直接把链表转换为红黑树,而是先会对数组的长度进行判断,如果当前数组的长度小于 64,那么会选择先进行数组的扩容,但要是数组长度达到64以上,就会将链表转换为红黑树,然后通过维持红黑树的平衡来来解决这个问题。

HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。

补充:HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

Java面试题总结汇总_第2张图片

3、ConcurrentHashMap

也可以被问成:线程安全的HashMap类有哪些,ConcurrentHashMap如何保证线程安全

ConcurrentHashMap是线程安全的HashMap,内部采用了的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,默认有16 个 Segment,所以理论上,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。

4、 HashMap 和 Hashtable 区别

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本上都经过synchronized 修饰了。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 源码中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

5、HashMap 和 HashSet 区别

HashSet的话,它也是非线程安全的,从HashSet源码里面可以看到,HashSet 底层是基于 HashMap实现的,也就是说,实际上HashSet就是一个HashMap 的实例。HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()是 HashSet 必须自己实现之外,其他方法都是直接调用 HashMap 中的方法。存储元素的话,从源码里面可以看到,它的内部其实就是使用HashMap的key来存储元素的,而由于HashMap的key是无序的(why?因为key的位置是根据算出来的哈希值来存放的呀,而hash值无序),所以HashSet也是无序的;由此也可以得出,HashSet允许有一个null元素,因为HashMap可以有一个key为空嘛,但是HashSet是没有get()方法的,也就是说HashSet不能通过get()方法来获取值。

(2)HashSet是无序的,因为HashMap的key是无序的;

HashMap HashSet
实现了 Map 接口 实现 Set 接口
存储键值对 仅存储对象
调用 put()向 map 中添加元素 调用 add()方法向 Set 中添加元素(其实add里面也是在调put方法)
HashMap 使用键(Key)计算 Hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性,

5、多线程

1、创建线程的方式

首先呢,Thread 类本质上是实现了 Runnable 接口,代表一个线程的实例。所以我们可以编写一个类,继承Thread类,或者直接实现Runnable接口,然后再重写下run方法就行了。启动线程的方式就是调用类里边的 start方法。start()方法是一个 native 方法,它的作用就是启动线程,线程会去执行 run()方法中的代码。

还有就是实现 Callable 接口,这个接口相当于是Runnable接口的增强版,他的执行代码的方法不是run方法了,是call方法,这个call方法可以有返回值, 我们可以创建一个 FutureTask 类的实例对象,通过他的get()方法得到执行结果,不过这里定的执行结果需要跟FutureTask的泛型一致才行,并且call方法还可以抛出异常,通过这些,我们就能很明确的知道线程内部的执行状态

还有就是通过线程池来实现,线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。 一般创建线程池的话,都是使用个的Executors 类中提供的创建线程池的静态方法。他可以创建4种线程池,有

  1. FixedThreadPool,创建固定大小的线程池,比如线程池容量是10,最多可以同时执行10个线程。

  2. CachedThreadPool,创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于JVM能够创建的最大线程大小,当然线程池里的线程是可以复用的,但是如果在高并发的情况下,这个线程池在会导致运行时内存溢出问题

  3. ScheduledThreadPool,创建一个定时执行的线程池,里边提供了两个方法,FixRate和fixDelay,fixRate就是以固定时间周期执行任务,不管上一个线程是否执行完,fixDelay的话就是以固定的延迟执行任务,就是在上一个任务执行完成之后,延迟一定时间执行。

  4. SingleThreadExecutor,创建一个单线程的线程池,这个线程池同时只能执行一个线程,可以保证线程按顺序执行,保证数据安全。

2、线程的状态

Java面试题总结汇总_第3张图片

1、新建(new):线程对象被创建后就进入了新建状态。如:Thread thread = new Thread();

2、就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而启动该线程。如:thread.start(); 处于就绪状态的线程随时可能被CPU调度执行。

3、运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权限,暂时停止运行。直到线程进入就绪状态,才有机会进入运行状态。阻塞的三种情况:

1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。

2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。

3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead):线程执行完了或因异常退出了run()方法,该线程结束生命周期。

3、线程都有哪些方法

线程里的方法有很多,我给您说说我们常用的方法吧

wait方法呢就是线程等待,调用这个方法之后,线程进入 waiting 状态,只有等待通知notify或者notifyAll才会继续执行,这个时候 会释放对象的锁。因此呢,wait 方法一般用在同步方法或同步代码块中。

sleep线程睡眠,让当前线程休眠,此时线程处于阻塞的状态,当前线程进入timed-waiting状态

yield线程让步,我看源代码的意思是释放CPU资源,让其他线程抢夺CPU资源,但是释放之后又瞬间和其他线程一起抢夺CPU的执行权,实际用的时候,大概就是让当前线程先暂停,让其他线程先运行

join线程插队,我理解的呢就是让当前线程先运行,其他线程先等待,等运行完再执行其他线程,比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

interrupt线程中断,这个就是中断当前线程的意思

notify唤醒线程,notifyAll唤醒所有线程

4、sleep和wait的区别

我之前写代码的时候也一直会用到这两个方法,我总结的区别大致分了三个方面

首先就是sleep不会释放锁,而wait会释放锁

接着呢就是sleep不会解除cpu占用,wait会释放cpu资源

然后还有就是sleep会导致线程阻塞,时间到了之后,线程继续向下执行,但是wait必须配合notify或者notifyAll来唤醒

5、介绍下你理解的线程中的锁

我理解的锁的话,就是在多个线程同时访问一个数据的时候,为了保证数据的安全性,我们需要对数据操作的代码进行加锁处理,一般来说这个锁需要是对所有线程是一致的,一般可以用静态变量来作为锁,这个锁用synchronized关键字来包裹着,当这段代码块执行完之后,释放锁,然后其他线程获取到这个锁之后,才能执行这段代码,通过锁的机制很好的保护了多线程下的数据安全

但是在用锁的时候,如果使用不当的话会导致死锁的问题,就是A线程等待B释放锁,B线程同时在等待A释放锁,这样的话就会导致两个线程相互等待,造成死锁,所以在使用的时候尽量避免多线程之间相互依赖锁的情况发生

还有对锁的分类的话,分为乐观锁和悲观锁

乐观锁的话就是比较乐观,每次去拿数据的时候,认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制或者CAS 算法实现。乐观锁在读操作比较多的场景比较适用,这样可以提高吞吐量,就像数据库提供的write_condition机制,其实都是乐观锁

悲观锁的话就是每次去拿数据的时候,都认为别人会修改数据,这个时候就会加上锁,这就导致其他线程想拿数据的话,就会阻塞,直到这个线程修改完成才会释放锁,让其他线程获取数据。在数据库里的行级锁、表级锁都是在操作之前就先锁住数据再操作数据 ,都属于悲观锁。Java中的 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现

注意:这些线程安全类底层实现使用一种称为CAS的算法,(Compare And Swap)比较交换。其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,也就是说CAS是靠硬件实现的,从而在硬件层面提升效率。
乐观锁:总是认为是线程安全的,不怕别的线程修改变量,如果修改了我就再重新尝试。
悲观锁:总是认为线程不安全,不管什么情况都进行加锁,要是获取锁失败,就阻塞。

6、死锁

死锁的根本原因:是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;

一般造成死锁必须同时满足如下4个条件:
  1,互斥条件:线程使用的资源必须至少有一个是不能共享的;
  2,请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源;
  3,非剥夺条件:分配资源不能从相应的线程中被强制剥夺;
  4,循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程。
因为要产生死锁,这4个条件必须同时满足,所以要防止死锁的话,只需要破坏其中一个条件即可

7、线程池的原理

我之前看过线程池相关的源码,线程池主要由4个部分组成,

  • 线程池管理器:用于创建并管理线程池

  • 工作线程:线程池中的线程

  • 任务接口:每个任务必须实现的接口,用于工作线程调度其运行

  • 任务队列:用于存放待处理的任务,提供一种缓冲机制

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

8、线程池的核心参数都有哪些

  • corePoolSize(核心线程数)

    (1)核心线程会一直存在,即使没有任务执行;

    (2)当线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数;

    (3)设置 allowCoreThreadTimeout=true(默认 false)时,核心线程会超时关闭。

  • queueCapacity(任务队列容量)

    (1)也叫阻塞队列,当核心线程都在运行,此时再有任务进来,会进入任务队列,排队等待线程执行。

  • maxPoolSize(最大线程数)

    (1)线程池里允许存在的最大线程数量;

  • keepAliveTime(线程空闲时间)

    (1)当线程空闲时间达到 keepAliveTime 时,线程会退出,直到线程数等于核心线程数

6、JVM

1、介绍下JVM

JVM主要包括:类加载器(class loa der )、执行引擎(exection engine)、本地接口(native interface)、运行时数据区(Runtimedata area)

类加载器:加载类文件到堆内存。它只管加载,只要符合文件结构就加载,至于能否运行,那就由执行引擎负责了。

执行引擎:负责解释命令,然后交给电脑的操作系统执行,也就是将字节码指令解释为本地机器指令,让电脑的操作系统能够看懂。(后端编译。生成字节码的编译成为前端编译)

本地接口:它的作用是融合不同的语言为java所用,比如说windows是用c和c++实现的嘛,那它就提供一个java和c、c++接口。

JVM的运行时数据区分为五个区域:堆、虚拟机栈、本地方法栈、方法区、程序计数器。其中虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区,JVM不同区域的占用内存大小不同,一般情况下堆内存最大,程序计数器较小。

程序计数器:它记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都是依赖这个计数器。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,那这个计数器的值为空

java虚拟机栈:每个方法执行的时候都会创建一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口。每一个方法从调用直到执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。

本地方法栈:与虚拟机栈很类似,区别是一个是执行Java方法,一个是执行本地方法。在 HotSpot 虚拟机中本地方法栈和 Java 虚拟机栈合二为一了。

:Java堆是Java虚拟机所管理的内存最大的一块,被所有线程共享的一块内存区域,在虚拟机启动的时候就创建了。这个内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器(GC)管理的主要区域,所以也被叫做“GC堆”。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

在这里插入图片描述

上图所示的 eden区、s0区、s1区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 - XX:MaxTenuringThreshold 来设置。还有一点就是如果在堆内存不足,并且也无法及时扩展的时候,会抛出OutOfMemoryError异常。

方法区:用于存储已被Java虚拟机加载的类信息、常量、静态变量、以及编译器编译后的代码等数据。

2、介绍下内存泄漏和内存溢出

1、内存泄漏叫memory leak:是指程序在申请内存后,无法释放已申请的内存空间,也就是说你申请了内存资源,用完之后没有释放这块内存,导致虚拟机不能再次使用该内存,这个时候这段内存就泄露了,因为申请者不用了,但又不能被虚拟机分配给别人用。一次内存泄漏似乎不会有太大的影响,但内存泄漏堆积后的后果就是内存溢出了。
2、内存溢出叫out of memory :指程序申请内存时,没有足够的内存给申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,就是所谓的内存溢出。

Java内存泄漏的根本原因是,长生命周期的引用指向了短生命周期的对象,导致内存无法被回收,我给您说几个具体的场景吧

  • 静态集合类造成的内存泄漏
static List list = new ArrayList(10);
public void method(){
  for (int i = 1; i<100; i++){
    Object o = new Object();
    list.add(o);
    o = null;
  }
}

循环申请Object 对象,并将所申请的对象放入一个ArrayList 中,如果仅仅释放引用本身(o=null),那么ArrayList 仍然引用该对象,所以这个对象对GC 来说是不可回收的,就会导致内存泄漏。因此,如果对象加入到ArrayList 后,还必须从ArrayList 中删除,最简单的方法就是将ArrayList对象设置为null

  • 拦截器中导致内存泄漏

在很多拦截器中,比如总是会是使用threadlocal存储一些线程变量,如果在方法请求完成时,没有将threadlocal中的变量释放,那么也会导致内存泄漏

  • 各种连接导致的内存泄漏

比如数据库连接,网络连接(socket)和io连接,除非显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的

下面我再给您说说内存溢出的情况吧(下接第三题)

3、列举一些会导致内存溢出的类型都有哪些,分别怎么造成的

  • 第一种OutOfMemoryError: PermGen space

发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Permanent Generation space有关。解决这类问题有以下两种办法:

1、增加java虚拟机中的PermSize和MaxPermSize参数的大小,其中PermSize是初始永久保存区域大小,MaxPermSize是最大永久保存区域大小。比如说针对tomcat,在catalina.sh 文件中增加这两个参数的配置就行了(一系列环境变量名说明结束处(大约在70行左右) 增加一行:JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m")。如果是windows服务器还可以在系统环境变量中设置。
2、清理应用程序中web-inf/lib下的jar,如果tomcat部署了多个应用,很多应用都使用了相同的jar,可以将共同的jar移到 tomcat共同的lib下,减少类的重复加载。这种方法是网上有人推荐过,我没试过,但感觉减少不了太大的空间,最靠谱的还是第一种方法。

  • 第二种OutOfMemoryError: Java heap space

发生这种问题的原因是java虚拟机创建的对象太多,在进行垃圾回收之前,虚拟机分配的到堆内存空间已经用满了,与堆空间大小有关。解决这类问题有两种思路:

1、检查程序,看是否有死循环或不必要地重复创建大量对象。找到原因后,修改程序和算法。
2、增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的值。(如:set JAVA_OPTS= -Xms256m -Xmx1024m)

  • 第三种OutOfMemoryError:unable to create new native thread

在java应用中,有时候会出现这样的错误。这种错误是因为给JVM分配了过多内存导致的,比如超过可用内存的一半,就会导致这种问题。在线程个数很多的情况下, 你分配给JVM的内存越多,那么,这种错误发生的可能性就越大。

比如说系统可用内存一共2G,这里假设分配1.5G给JVM,那么还余下500M可用内存。这 500M内存中的一部分必须用于系统文件加载,那么真正剩下的也许只有400M,但是关键是,当你使用Java创建一个线程,在JVM的内 存里也会创建一个Thread对象,但是同时也会在操作系统里创建一个真正的物理线程,操作系统会在剩下的400兆内存里创建这个物理 线程,而不是在JVM的1.5G的内存堆里创建。在jdk1.4里头,默认的栈大小是256KB,但是在jdk1.5之后,默认的栈大小为1M每个线程, 因此,在余下400M的可用内存里边我们最多也只能创建400个可用线程。

这种情况的话,要想创建更多的线程,你必须减少分配给JVM的最大内存,或者增加系统的内存

4、详细介绍下JVM堆中的内存模型

我先给您介绍下jdk1.7中的堆的情况吧

  • Young 年轻代

年轻代区域被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候,GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,仍然存活于Survivor的对象被移动到老年代。

  • Tenured 年老代

老年带主要保存生命周期长的对象,一般是一些老的对象,当一些对象在年轻代复制转移一定的次数以后,对象就会被转移到老年代,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间

  • Perm 永久代

永久代主要保存 class、method、filed对象,这部分的空间如果使用不当,就会造成内存溢出,比如一次性加载了很多的类,或者一个tomcat下部署了几十个应用,不过在涉及到热部署的服务器的时候,有时候会遇到 java.lang.OutOfMemoryError:PermGenSpace的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在永久代中,不过这种情况下,一般重新启动服务器可以解决问题

  • Virtual虚拟区

最大内存和初始内存的差值,就是虚拟区

而在jdk1.8中,最大的变化就是用元数据空间替代了永久代,这块所占用的内存空间不是在虚拟机内部的,而是在本地内存空间中,我看了下官网的解释,是因为了后续要融合两个JVM的版本,因为一个版本中没有设计永久代这个概念,另外一方面就是在我们现实使用中,由于永久代内存经常不够用或发生内存泄露,因此将永久代废弃,而改用元数据空间,改为了使用本地内存空间

5、垃圾回收

1、怎么判断对象是否可以被回收

一般有两种方法来判断,一种是引用计数器算法,还有就是可达性分析算法

  • 引用计数器算法

给对象添加一个引用计数器,每次引用这个对象时计数器加一,引用失效时减一,计数器等于0时就是不会再次使用的。看似非常简单的一个算法,实际上在大部分Java虚拟机中并没有采用这种算法,因为它会带来一个致命的问题——对象循环引用。比如说对象A指向B,对象B反过来又指向A,此时它们的引用计数器都不为0,但它们俩实际上已经没有意义,因为没有任何地方指向它们。

  • 可达性分析算法

是一种类似于二叉树的实现,可以有效地避免对象循环引用的情况,整个对象实例以一个树的形式呈现,这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

Java面试题总结汇总_第4张图片

2、垃圾回收算法(五种)

标记-清除算法

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

Java面试题总结汇总_第5张图片

复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

Java面试题总结汇总_第6张图片

标记-整理算法

根据老年代的特点特意出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

Java面试题总结汇总_第7张图片

分代收集算法(Java堆采用)

主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 90% 的新生代空间。新生代垃圾回收采用复制算法,清理的频率比较高。如果新生代在若干次清理中依然存活,则移入老年代,有的内存占用比较大的直接进入老年代。老年代使用标记清理算法,清理的频率比较低。

分区算法

这种方法将整个空间划分成连续的不同的小区间,每个区间都独立使用,独立回收,好处是可以控制一次回收多少个小区间。

6、类加载

类加载的过程

  • 加载:根据查找路径找到相应的 class 文件然后导入;

  • 检查:检查加载的 class 文件的正确性;

  • 准备:给类中的静态变量分配内存空间;

  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而直接引用是直接指向内存中的地址;

  • 初始化:对静态变量和静态代码块执行初始化工作。

说到这儿我再说下new一个对象之后,类实例化的顺序吧

  • 首先是父类的静态变量和静态代码块(看两者的书写顺序);

  • 第二执行子类的静态变量和静态代码块(看两者的书写顺序);

  • 第三执行父类的成员变量赋值

  • 第四执行父类的普通代码块

  • 第五执行父类的构造方法()

  • 第六执行子类的普通代码块

  • 第七执行子类的构造方法();

也就是说虽然客户端代码是new 的构造方法,但是构造方法确实是在整个实例创建中的最后一个调用

类加载器的加载顺序

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。(rt.jar是JAVA基础类库,也就是Java doc里面看到的所有的类的class文件)

  2. ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。(加载扩展的jar包)

  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

    4.Custom ClassLoader(自定义的类加载器)

Java面试题总结汇总_第8张图片

双亲委派机制

我了解过双亲委派模型的源码,它里面的实现代码还算比较简单,逻辑也比较清晰,总结下来说就是当一个类收到了加载请求时,它不会先自己去尝试加载,而是先委派给父类去完成,在类加载的时候,首先会把这个请求委派给它父类加载器的 loadClass()(源码里面的方法) 处理,进而判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,这样一级一级向上检查,因此所有的类加载请求最终都应该是传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。

然后这样做的好处的话,比如说加载位于rt.jar包中的类时不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样就保证了使用不同的类加载器得到的都是同一个结果,我觉得主要就是保证了Java程序的稳定运行哦,可以避免类的重复加载,因为JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个叫做 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类(那就搞不清楚到底是jdk类的还是自定义的类)。其实这个也是一个隔离的作用,避免了我们自己写的代码影响到JDK的代码。

比如我现在要来一个

public class String(){
    public static void main(){sout;}
}

这种时候,我们的代码肯定会报错,因为在加载的时候其实是找到了rt.jar中的String.class,然后发现这也没有main方法

如果我们不想用双亲委派模型怎么办?

为了避免双亲委派机制,我们可以自己定义一个类加载器,然后重载 loadClass() 即可。

如何自定义类加载器?

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader

说一下JVM调优的工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;

  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

  • jstat:jstat 命令可以查看堆内存各部分的使用量,以及加载类的数量

  • jmap:是用于查看指定Java进程的堆内存使用情况

7、反射

1、介绍下反射

简单的说吧,我理解的就是,在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象,都能够调用它的任意一个方法;这相当于是一种动态获取信息和动态调用对象方法的功能,也就是我们说的反射机制。

在java 里面的话,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。反射的作用其实就是:在运行时能够判断任意一个对象所属的类, 还有就是在运行时构造任意一个类的对象,呃。。。。我们常用的Spring框架其实也是利用Java反射这一块架构的,还有就是在运行时判断任意一个类所具有的成员变量和方法,还能在运行的时候调用任一对象的方法,还有在运行时创建新类对象。

一般都是使用Class clazz=Class.forName("类的全路径")这个方法,获取到class,获取到了之后,可以获取到类中所有的method方法,和所有的属性,调用Method的invoke方法就可以执行该方法,但是如果是私有方法的话,必须通过getDeclaredMethod获取,还需要调用方法的setAccessible设置为true才可以执行。

8、Spring

1、介绍一下spring

关于Spring的话,我们平时做项目一直都在用,它里面主要的就三点,也就是它的核心思想哦,就是IOC控制反转,DI依赖注入和AOP面向切面编程。

我先来说下IOC吧,IOC的话就是spring里的控制反转,它把类的控制权呢交给spring来管理,我们在使用的时候,在spring的配置文件中,配置好bean标签,以及类的全路径,如果有参数,然后再配置好相应的参数。这样的话,spring就会给我们通过反射的机制实例化这个类,同时放到spring容器当中去。

然后我们在使用的时候,需要结合DI依赖注入去使用,把我们想使用的类注入到需要的地方就可以,依赖注入的方式有构造器注入、getset注入还有注解注入。一般我接触的比较多的是使用@autowired或者@Resource注解的方式注入。

然后就是AOP面向切面编程,我觉得他的最大的特点,也可以说是一个很大的优点吧,就是它可以在不改变源代码的情况下对代码的功能进行增强。一般的话在配置文件中配置好切点,然后去实现切面的逻辑就可以实现代码的增强,然后这个代码增强,包括在切点的执行前,执行中,执行后都可以进行增强逻辑处理,同时的话不用去改变源代码。

这块我们项目中一般用于权限认证、日志、事务处理这几个地方。

2、AOP的实现原理

这块的话,我之前看过spring的源码,底层其实就是动态代理来实现的,所谓的动态代理呢就是说 AOP 框架不会去修改字节码,而是每次运行的时候会在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了 目标对象的全部方法,并且在特定的切点做了增强处理,同时回调原对象的方法。

然后的话。。。。Spring AOP 中的动态代理我记得是主要有两种方式,一种是JDK 动态代理,还有呢就是CGLIB 动态代理:

  • JDK 动态代理的话它是只提供接口的代理,不支持类的代理。然后核心是InvocationHandler 接口和 Proxy 类,这个InvocationHandler 通过 invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务联系 在一起;然后Proxy 利用InvocationHandler 动态创建一个符合接口的的实例,生成目标类的代理对象。

  • 如果代理类没有实现 InvocationHandler 接口的话,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB的话(Code Generation Library),就是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并且覆盖其中特定方法并添加增强代码,从而实现 AOP。由于CGLIB 是通过继承的方式做的动态代理嘛,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。

    ​ ( 不过在我们的业务场景中没有代理过final的类,基本上都代理的controller层实现权限以及日志,还有就是service层实现事务统一管理)

3、详细介绍下IOC容器

Spring 提供了两种 IoC 容器,分别为 BeanFactory 和 ApplicationContext

BeanFactory 是基础类型的 IoC 容器,提供了完整的 IoC 服务支持。简单来说,BeanFactory 就是一个管理 Bean 的工厂,它主要负责初始化各种 Bean,并调用它们的生命周期方法。

ApplicationContext 是 BeanFactory 的子接口,也被称为应用上下文。它不仅提供了 BeanFactory 的所有功能,还添加了对 i18n(国际化)、资源访问、事件传播等方面的良好支持。

他俩的主要区别在于,如果 Bean 的某一个属性没有注入,则使用 BeanFacotry 加载后,在第一次调用 getBean() 方法时会抛出异常,但是呢ApplicationContext 会在初始化时自检,这样有利于检查所依赖的属性是否注入。

因此,在实际开发中,通常都选择使用 ApplicationContext

4、@Autowired@Resource的区别

@Autowired 默认是按照类型注入的,如果这个类型没找到,会根据名称去注入,如果在用的时候需要指定名称,可以加注解@Qualifier("指定名称的类")

@Resource注解也可以从容器中注入bean,默认是按照名称注入的,如果这个名称的没找到,就会按照类型去找,也可以在注解里直接指定名称@Resource(name="类的名称")

5、springbean的生命周期

生命周期这块的话无非就是从创建到销毁的过程哦

spring容器可以管理 singleton 作用域 Bean 的生命周期,在singleton 作用域下,Spring 能够准确地知道这个 Bean 什么时候被创建,什么时候初始化完成,以及什么时候被销毁。

但是对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪它的生命周期。每次客户端请求 prototype 作用域的 Bean 时,Spring 容器都会创建一个新的实例,并且不会管那些被配置成 prototype 作用域的 Bean 的生命周期。

整体来说的话springbean就4个步骤哦:实例化bean,属性赋值,初始化bean,销毁bean

  • 首先就是实例化bean,容器通过获取BeanDefinition对象中的信息进行实例化

  • 然后呢就是属性赋值,利用依赖注入完成 Bean 中所有属性值的配置注入

  • 接着就是初始化bean,如果在配置文件中通过 init-method 属性指定了初始化方法,那么就调用这个初始化方法。

  • 最后就是销毁bean,和init-method一样,通过给destroy-method指定函数,就可以在bean销毁前执行指定的逻辑

6、springbean的作用域

Spring 容器中的 bean 可以分为 5 个范围:

(1)singleton:单例模式,使用 singleton 定义的 Bean 在 Spring 容器中只有一个实例,这也是 Bean 默认的作用域。 controller、service、dao层基本都是singleton的。

(2)prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 Bean 时,容器都将创建一个新的 Bean 实例。

(3)request:在一次 HTTP 请求中,容器会返回该这个Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,并且这个作用域的话只在当前 HTTP Request 内有效。

(4)session:在一次 HTTP Session 中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,这个作用域的话只是在当前 HTTP Session 内有效。

(5)global-session:就是全局作用域哦,在一个全局的 HTTP Session 中,容器会返回该这个Bean 的同一个实例。

7、事务的传播特性

解读:事务的传播特性发生在事务方法与非事物方法之间相互调用的时候,在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务

属性名称 描 述
PROPAGATION_REQUIRED required 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将创建新事务,默认就是这个
PROPAGATION_SUPPORTS supports 支持当前事务。如果 A 方法已经在事务中,则 B 事务将直接使用。否则将以非事务状态执行
PROPAGATION_MANDATORY mandatory 支持当前事务。如果 A 方法没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW requires_new 将创建新的事务,如果 A 方法已经在事务中,则将 A 事务挂起
PROPAGATION_NOT_SUPPORTED not_supported 不支持当前事务,总是以非事务状态执行。如果 A 方法已经在事务中,则将其挂起
PROPAGATION_NEVER never 不支持当前事务,如果 A 方法在事务中,则抛出异常
PROPAGATION.NESTED nested 嵌套事务,底层将使用 Savepoint 形成嵌套事务

8、事务的隔离级别

Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过 binlog实现的。隔离级别有四种

  • Read uncommitted (读未提交):读未提交,允许另外一个事务可以看到这个事务未提交的数据,最低级别,任何情况都无法保证。

  • Read committed (读已提交):保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新,可避免脏读的发生。

  • Repeatable read (可重复读):保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新,可避免脏读、不可重复读的发生。

  • Serializable (串行化):一个事务在执行的过程中完全看不到其他事务对数据库所做的更新,可避免脏读、不可重复读、幻读的发生。

9、spring中都用了哪些设计模式

(1)工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;

(2)单例模式:Bean默认为单例模式。

(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;

(4)模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。

(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。

10、spring中如何处理bean在线程并发时线程安全问题

在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域,因为 Spring 对一些 Bean 中非线程安全状态采用 ThreadLocal 进行处理,解决线程安全问题。

ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而 ThreadLocal 采用了“空间换时间”的方式。

ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对这个变量进行同步了。ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。

11、spring框架有哪些主要模块?

Spring有七大功能模块,分别是Spring Core,AOP,ORM,DAO,MVC,WEB,Context。
1,Spring Core
Core模块是Spring的核心类库,Spring的所有功能都依赖于该类库,Core主要实现IOC功能,Sprign的所有功能都是借助IOC实现的。
2,AOP
AOP模块是Spring的AOP库,提供了AOP(拦截器)机制,并提供常用的拦截器,供用户自定义和配置。
3,ORM
Spring 的ORM(ObjectRelationalMapping:对象关系映射)模块提供对常用的ORM框架的管理和辅助支持,Spring支持常用的Hibernate,ibtas,jdao等框架的支持,Spring本身并不对ORM进行实现,仅对常见的ORM框架进行封装,并对其进行管理
4,DAO模块
Spring 提供对JDBC的支持,对JDBC进行封装,允许JDBC使用Spring资源,并能统一管理JDBC事物,并不对JDBC进行实现。(执行sql语句)
5,WEB模块
WEB模块提供对常见框架如Struts1,WEBWORK(Struts 2),JSF的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器。
6,Context模块
Context模块提供框架式的Bean访问方式,其他程序可以通过Context访问Spring的Bean资源,相当于资源注入。
7,MVC模块
WEB MVC模块为Spring提供了一套轻量级的MVC实现,在Spring的开发中,我们既可以用Struts也可以用Spring自己的MVC框架,相对于Struts,Spring自己的MVC框架更加简洁和方便。

9、SpringMVC

1、介绍一下springMVC

springmvc是一个视图层框架,通过MVC模型让我们很方便的接收和处理请求和响应。我给你说说他里边的几个核心组件吧

它的核心控制器是DispatcherServlet,他的作用是接收用户请求,然后给用户反馈结果。它的作用相当于一个转发器或中央处理器,控制整个流程的执行,对各个组件进行统一调度,以降低组件之间的耦合性,有利于组件之间的拓展

接着就是处理器映射器(HandlerMapping):他的作用是根据请求的URL路径,通过注解或者XML配置,寻找匹配的处理器信息

还有就是处理器适配器(HandlerAdapter):他的作用是根据映射器处理器找到的处理器信息,按照特定执行链路规则执行相关的处理器,返回ModelAndView

最后是视图解析器(ViewResolver):他就是进行解析操作,通过ModelAndView对象中的View信息将逻辑视图名解析成真正的视图View返回给用户

接下来我给你说下springmvc的执行流程吧

2、springMVC的执行流程

Java面试题总结汇总_第9张图片

(1)用户发送请求至前端控制器 DispatcherServlet;

(2) DispatcherServlet 收到请求后,调用 HandlerMapping 处理器映射器,请求获取 Handle;

(3)处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;

(4)DispatcherServlet 调用 HandlerAdapter 处理器适配器;

(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);

(6)Handler 执行完成返回 ModelAndView;

(7) HandlerAdapter 将 Handler 执 行 结 果 ModelAndView 返 回 给DispatcherServlet;

(8)DispatcherServlet 将 ModelAndView 传给 ViewResolver 视图解析器进行解析;

(9)ViewResolver 解析后返回具体 View;

(10)DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)

(11)DispatcherServlet 响应用户。

3、springMVC接收前台参数的几种方式

  • 1、如果传递参数的时候,通过ur1拼接的方式,直接拿对象接收即可,或者string、 int

  • 2、如果传递参数的时候,传到后台的是js对象,那么必须使用对象接收,并且加@requestBody

  • 3、get的请求方式,所有的参数接收都使用普通对象或者string、int

  • 4、在用form表单提交的时候,所有的参数接收都使用普通对象或者string、int

4、springMVC中的常用注解

@RequestMapping:指定类或者方法的请求路径,可以使用method字段指定请求方式

@GetMapping、@PostMapping:规定了请求方式的方法的请求路径

@RequestParam:接收单一参数的

@PathVariable:用于从路径中接收参数的

@CookieValue:用于从cookie中接收参数的

@RequestBody:用于接收js对象的,将js对象转换为Java对象

@ResponseBody:返回json格式数据

@RestController:用在类上,等于@Controller+@ResourceBody两个注解的和,一般在前后端分离的项目中只写接口时经常使用,标明整个类都返回json格式的数据。

5、spring如何整合springMVC

简单的说 springMVC在ssm中整合 就是 在 web.xml 里边配置springMVC的核心控制器:DispatcherServlet; 它就是对指定后缀进行拦截;然后在springMVC.xml里边配置扫描器,可以扫描到带@controller注解的这些类,现在用springMVC都是基与注解式开发, 像@service,@Repository @Requestmapping,@responsebody 啦这些注解标签 等等 都是开发时用的,每个注解标签都有自己的作用;它还配置一个视图解析器,主要就是对处理之后的跳转进行统一配置,有页面的路径前缀和文件后缀 ,如果有上传相关的设置,还需要配置上multpart的一些配置,比如单个文件最大的大小,以及最大请求的大小,大致就是这些

10、MyBatis

1、#{}和${}的区别是什么?

  • ${}是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如${driver}会被静态替换为com.mysql.jdbc.Driver
  • #{}是 sql 的参数占位符,Mybatis 会将 sql 中的#{}替换为?号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的?号占位符设置参数值,比如 ps.setInt(0, parameterValue),#{item.name} 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 param.getItem().getName()

2、 Xml 映射文件中,除了常见的 select、insert、updae、delete 标签之外,还有哪些标签?

还有很多其他的标签,,加上动态 sql 的 9 个标签,trim、where、set、foreach、if、choose、when、otherwise、bind等,其中为 sql 片段标签,通过标签引入 sql 片段,为不支持自增的主键生成策略标签。

3、 最佳实践中,通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口,就是人们常说的 Mapper接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中MappedStatement的 id 值,接口方法内的参数,就是传递给 sql 的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为com.mybatis3.mappers.StudentDao下面id = findStudentByIdMappedStatement。在 Mybatis 中,每一个