Java中高级面试题及答案【第三部分】

3,320 阅读18分钟

数据库的三大范式

1 、第一范式(1NF)

在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。

所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。

在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式要求数据表中的每一列(每个字段)必须是不可拆分的最小单元。

2、 第二范式(2NF)

第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。

第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系。

3 、第三范式(3NF)

满足第三范式(3NF)必须先满足第二范式(2NF)。第三范式(3NF)要求一个数据库表中不包含其它表中已包含的非主关键字信息。简而言之,第三范式要求表中的每一列只与主键直接相关而不是间接相关,表中的每一列只能依赖于主键。

TCP和UDP的区别及其适用场景

首先说一下什么是TCP和UDP:

TCP是传输控制协议,提供的是面向连接、可靠的字节流服务。

UDP是用户数据报协议,是一个简单的面向数据报的运输层协议。

TCP和UDP的区别:

  • TCP面向连接的运输层协议,UDP无连接

  • TCP是可靠交付,UDP是尽最大努力交付

  • TCP面向字节流,UDP面向报文

  • TCP是点对点连接的,UDP一对一,一对多,多对多都可以

  • TCP适合用于网页,邮件等,UDP适合用于视频,语音广播等

TCP和UDP的适用场景:

整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。

当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,比如视频、广播等,这时就可以使用UDP。

说一下Spring的核心模块

  • Spring Core【核心容器】:核心容器提供了Spring的基本功能。核心容器的核心功能是用IOC容器来管理类的依赖关系。

  • Spring AOP【面向切面】:Spring的AOP模块提供了面向切面编程的支持。SpringAOP采用的是纯Java实现,采用基于代理的AOP实现方案,AOP代理由IOC容器负责生成、管理,依赖关系也一并由IOC容器管理。

  • Spring ORM【对象实体映射】:提供了与多个第三方持久层框架的良好整合。

  • Spring DAO【持久层模块】: Spring进一步简化DAO开发步骤,能以一致的方式使用数据库访问技术,用统一的方式调用事务管理,避免具体的实现侵入业务逻辑层的代码中。

  • Spring Context【应用上下文】:它是一个配置文件,为Spring提供上下文信息,提供了框架式的对象访问方法。

  • Spring Web【Web模块】:提供了基础的针对Web开发的集成特性。

  • Spring MVC【MVC模块】:提供了Web应用的MVC实现。Spring的MVC框架并不是仅仅提供一种传统的实现,它提供了一种清晰的分离模型。

(转发)forward与(重定向)redirect的区别

  • forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。

  • redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL。

  • forward转发页面和转发到的页面可以共享request里面的数据。

  • redirect不能共享数据。

  • redirect不仅可以重定向到当前应用程序的其他资源,还可以重定向到同一个站点上的其他应用程序中的资源,甚至是使用绝对URL重定向到其他站点的资源。

  • forward只能在同一个Web应用程序内的资源之间转发请求。

  • forward是服务器内部的一种操作。

  • redirect是服务器通知客户端,让客户端重新发起请求。

  • forward一般用于用户登陆的时候根据角色转发到相应的模块。

  • redirect一般用于用户注销登陆时返回主页面和跳转到其它的网站等。

  • forward效率高。

  • redirect效率低。

redis常用的五种数据类型

1.String(字符串)

String是简单的 key-value 键值对,value 不仅可以是 String,也可以是数字。它是Redis最基本的数据类型,一个redis中字符串value最多可以是512M。

2.Hash(哈希)

Redis hash 是一个键值对集合,对应Value内部实际就是一个HashMap,Hash特别适合用于存储对象。

3.List(列表)

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。

底层实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

4.Set(集合)

Redis的Set是String类型的无序集合,它的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

5.zset(有序集合)

Redis zset 和 set 一样也是String类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数,用来排序。

多线程中sleep()、 wait()、 yield()和 join()的用法与区别

1.sleep()方法

在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。

sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

2.wait()方法

在其他线程调用对象的notify或notifyAll方法前,当前线程处于等待状态。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。

当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。

唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。

waite()和notify()必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数和non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

3.yield方法

暂停当前正在执行的线程对象。

yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

yield()只能使同优先级或更高优先级的线程有执行的机会。

4.join方法

等待该线程终止。

等待调用join方法的线程结束,再继续执行。如:t.join();它主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。

线程和进程的区别?

进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元。

同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。

进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束。

线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的。

线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源。

线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志。

Array和ArrayList有何区别?什么时候更适合用Array?

存储内容比较:

Array数组可以包含基本类型和对象类型,ArrayList只能包含对象类型。

Array数组存放的一定是同种类型的元素,ArrayList可以存放任意对象(object的子类)。

空间大小比较:

Array的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。

ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。(比较麻烦的地方)。

方法上的比较:

ArrayList作为Array的增强版,在方法上比Array更多样化,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。

适用场景:

如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组里;但是如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进行查找的话,那么,我们就选择ArrayList。

而且还有一个地方是必须知道的,就是如果我们需要对元素进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList就真的不是一个好的选择,因为它的效率很低,我们可以考虑选择LinkedList。

Tomcat服务器优化(内存,并发连接数,缓存)

  • 内存优化:主要是对Tomcat启动参数进行优化,我们可以在Tomcat启动脚本中修改它的最大内存数等等。

  • 线程数优化:Tomcat的并发连接参数,主要在Tomcat配置文件中server.xml中配置,比如修改最小空闲连接线程数,用于提高系统处理性能等等。

  • 优化缓存:打开压缩功能,修改参数,比如压缩的输出内容大小默认为2KB,可以适当的修改。

手写一段单例中的懒汉模式和饿汉模式,并且简单说一下他们的区别

懒汉模式:

public class LazySingleton {
   //懒汉式单例模式
   //在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
   
   
   private static LazySingleton intance = null;
    //静态私用成员,没有初始化
   
   private LazySingleton()
   {
       //私有构造函数
   }
   
   public static synchronized LazySingleton getInstance()    
   //静态,同步,公开访问点
   {
       if(intance == null)
       {
           intance = new LazySingleton();
       }
       return intance;
   }
}

饿汉模式:

public class EagerSingleton {
   //饿汉单例模式
   //类加载较慢,但获取对象的速度快
   
   private static EagerSingleton instance = new EagerSingleton();
 //静态私有成员,已初始化
   
   private EagerSingleton() 
   {
       //私有构造函数
   }
   
   public static EagerSingleton getInstance()    
   //静态,不用同步
   {
       return instance;
   }
}

懒汉模式和饿汉模式的比较:

饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全。饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。

懒汉模式的特点是加载类时比较快,但是在运行时获取对象的速度比较慢,线程不安全, 懒汉式如果在创建实例对象时不加上synchronized则会导致对象的访问不是线程安全的。

抽象类和接口的区别,类可以继承多个类吗,接口可以继承多个接口吗,类可以实现多个接口吗?

  • 抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

  • 抽象类要被子类继承,接口要被类实现。  

  • 接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现

  • 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

  • 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该 类也只能为抽象类。

  • 抽象方法只能申明,不能实现。abstract void abc();不能写成abstract void abc(){}。

  • 抽象类里可以没有抽象方法 。

  • 如果一个类里有抽象方法,那么这个类只能是抽象类 。

  • 抽象方法要被实现,所以不能是静态的,也不能是私有的。

  • 接口可继承接口,并可多继承接口,但类只能单根继承。

Tomcat,Apache,Jboss的区别?

Tomcat是servlet容器,用于解析jsp,servlet。是一个轻量级的高效的容器;缺点是不支持EJB,只能用于Java应用。

Apache是http服务器(web服务器),类似于IIS可以用来建立虚拟站点,编译处理静态页面。支持SSL技术,支持多个虚拟主机等功能。

Jboss是应用服务器,运行EJB的Javaee应用服务器,遵循Javaee规范,能够提供更多平台的支持和更多集成功能,如数据库连接,JCA等。其对servlet的支持是通过集成其他servlet容器来实现的。

说出Servlet的生命周期,并说出Servlet和CGI的区别。

Servlet被服务器实例化后,容器运行其init方法,请求到达时运行其service方法,service方法自动派遣运行与请求对应的doXXX方法(doGet,doPost)等,当服务器决定将实例销毁的时候调用其destroy()方法。

与CGI的区别在于Servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于Servlet。

谈谈你对MVC的理解

MVC是Model—View—Controler的简称。即模型—视图—控制器。MVC是一种设计模式,它强制性的把应用程序的输入、处理和输出分开。

MVC中的模型、视图、控制器它们分别担负着不同的任务。

视图: 视图是用户看到并与之交互的界面。视图向用户显示相关的数据,并接受用户的输入。视图不进行任何业务逻辑处理。

模型: 模型表示业务数据和业务处理,相当于JavaBean。一个模型能为多个视图提供数据。这提高了应用程序的重用性。

控制器: 当用户单击Web页面中的提交按钮时,控制器接受请求并调用相应的模型去处理请求,然后根据处理的结果调用相应的视图来显示处理的结果。

MVC的处理过程:首先控制器接受用户的请求,调用相应的模型来进行业务处理,并返回数据给控制器。控制器调用相应的视图来显示处理的结果。并通过视图呈现给用户。

如何避免浏览器缓存?

  • HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等设置浏览器不用缓存请求。

  • 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的。

  • 经过HTTPS安全加密的请求不能被缓存。

  • HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存 。

如何防止缓存雪崩?

原因:

缓存雪崩可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。

对应解决:

  • 采用加锁计数,或者使用合理的队列数量来避免缓存失效时对数据库造成太大的压力。这种办法虽然能缓解数据库的压力,但是同时又降低了系统的吞吐量。

  • 分析用户行为,尽量让失效时间点均匀分布。避免缓存雪崩的出现。

  • 如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。

数据库会死锁吗,举一个死锁的例子,mysql 怎么解决死锁。

产生死锁的原因主要是:

  • 系统资源不足。

  • 进程运行推进的顺序不合适。

  • 资源分配不当等。

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

解决数据库死锁的方法:

  • 重启数据库。

  • 杀掉抢资源的进程。

Dubbo源码使用了哪些设计模式?

工厂模式:

ExtenstionLoader.getExtenstionLoader(Protocol.class).getAdaptiveExtenstion()

装饰器模式+责任链:

以provider的调用链为例,具体调用链代码是在protocolFilterWrapper的buildInvokeChain完成的,将注解中含有group=provider的Filter实现。

调用顺序:

  1. EchoFilter

  2. ClassLoaderFilter

  3. GenericFilter

  4. ContextFilter

  5. ExceptionFilter

  6. TimeoutFilter

  7. MonitorFilter

  8. TraceFilter

装饰器模式和责任链混合使用,Echo是回声测试请求,ClassLoaderFilter则只是在其主功能上添加了功能。

观察者模式:

provider启动时需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时采用了观察者模式,注册中心每5s定时检查是否有服务更新,有更新则向服务提供者发送1个notify消息后即可运行NotifyListener的notity方法,执行监听器方法。

动态代理模式:

扩展JDK的ExtensionLoaderdeAdaptive实现,根据调用阶段动态参数决定调用哪个类,生成代理类的代码是ExtensionLoader的createAdaptiveExtenstionClassLoader方法。


最后,欢迎关注微信公众号“Java知音”,回复面试题,送你一个大惊喜!