java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

注:部分答案引用网络文章

简答题

1、Spring项目启动后的加载流程

(1)使用spring框架的web项目,在tomcat下,是根据web.xml来启动的。web.xml中负责配置启动springmvc和启动spring,其中,在

<servlet>
<servlet-name>springMVC(名字任意)</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>中启动spring mvc并进行资源路径映射

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext.xml</param-value>
</context-param>

中通过ContextLoaderListener启动spring容器,同时自动装配applicationContext.xml中的配置信息。

(2)spring中对一些orm框架的启动,包括Mybatis/hibernate。orm框架的启动基本都是通过sqlsessionFactory bean来启动的,并配置各种bean到ioc容器中。包括datasource等:

<!-- MyBatis配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 显式指定Mapper文件位置 -->
<property name="mapperLocations" value="classpath*:/mybatis/*/*Mapper.xml" />
<!-- mybatis配置文件路径 -->
<property name="configLocation" value="classpath:/mybatis/config.xml" />
</bean>

(3)web应用程序中,spring相当于程序运行的平台,spring对整个程序提高供ioc支持和aop支持。spring提供注解如@service @repository @component将各种类注册到ioc容器中。通过设置scan package的方式,spring在启动时候会扫描包下的所有注解,并将它们注册到ioc容器中。并针对@autowired @resource,将一些bean从ioc容器中获取填充到bean的构造属性中(@autowired @resource只针对类成员变量,不针对方法的局部变量)。spring会根据配置自动扫描包中的注解:

<!-- 启用spring mvc注解 -->
<context:annotation-config />
<context:component-scan base-package="com.kuangchi.*" />

即所有的bean注入都在applicationContext.xml中配置的

注:正是spring的ioc支持了controller层注入service,service注入dao。打通了各层之间的桥梁,省去了原来的new service(),new Dao()的方法。

延伸:(1)spring IOC的好处:1方便测试,被测试类的依赖类可以通过spring IOC注入进来,2 方便维护,只要接口不变,重写实现类,修改配置即可 ,3 默认IOC容器管理的bean都是单例的,不会被回收掉,4 面向接口开发,service里直接是接口就可以了,解耦 , 5 用AOP作增强

(2)spring的面向接口编程:由于依赖接口,可通过依赖注入随时替换dao接口的实现类,而应用程序完全不用了解接口与底层数据库操作交互的细节。

2、SpringMVC请求响应流程

SpringMVC框架是一个基于请求驱动的Web框架,并且使用了‘前端控制器’模型来进行设计,再根据‘请求映射规则’分发给相应的页面控制器进行处理。

整体流程:

java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

()1、  首先用户发送请求到前端控制器,前端控制器根据请求信息(如 URL)来决定选择哪一个页面控制器进行处理并把请求委托给它,即以前的控制器的控制逻辑部分;图中的 1、2 步骤;

()2、  页面控制器接收到请求后,进行功能处理,首先需要收集和绑定请求参数到一个对象,这个对象在 Spring Web MVC 中叫命令对象,并进行验证,然后将命令对象委托给业务对象进行处理;处理完毕后返回一个 ModelAndView(模型数据和逻辑视图名);图中的 3、4、5 步骤;

()3、  前端控制器收回控制权,然后根据返回的逻辑视图名,选择相应的视图进行渲染,并把模型数据传入以便视图渲染;图中的步骤 6、7;

()4、  前端控制器再次收回控制权,将响应返回给用户,图中的步骤 8;至此整个结束。

核心流程:

java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

第一步:发起请求到前端控制器(DispatcherServlet)

第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)

第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略

第四步:前端控制器调用处理器适配器去执行Handler

第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler

第六步:Handler执行完成给适配器返回ModelAndView

第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)

第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可

第九步:视图解析器向前端控制器返回View

第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)

第十一步:前端控制器向用户响应结果

总结 核心开发步骤

()1、  DispatcherServlet 在 web.xml 中的部署描述,从而拦截请求到 Spring Web MVC

()2、  HandlerMapping 的配置,从而将请求映射到处理器

()3、  HandlerAdapter 的配置,从而支持多种类型的处理器

注:处理器映射求和适配器使用纾解的话包含在了注解驱动中,不需要在单独配置

()4、  ViewResolver 的配置,从而将逻辑视图名解析为具体视图技术

()5、  处理器(页面控制器)的配置,从而进行功能处理

View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)

3、Spring事务什么时候会失效(spring事务失效的原因)

事务就是一系列指令的集合,服从ACID 原则

(1)使用了spring+springmvc,则在 <context:component-scan>重复扫描问题可能会引起事务失效

原因是:spring的context是父子容器,会产生冲突。

由servletContextListener产生的是父容器,springmvc产生的是子容器,子容器controller进行扫描装配时,装配了@service注解的实例,而该实例应由父容器进行初始化以保证事务的增强处理,故而此处得到的将是没有经过事务加强处理、没有事务处理能力的原样service

解决方法:

在主容器applicationContext.xml中将controller的注解排除掉:

<context:component-scan base-package="com"

<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />

</context:component-scan

在springMVC配置文件springmvc中将service注解给去掉:

<context:component-scan base-package="com"

    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> 
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" /> 
</context:component-scan>
(2)@Transactional注解开启配置,如果放到DispatcherServlet的配置里,事务也是不起作用的。必须放到listener里加载
(3)@Transactional注解只能应用到public方法上,若在protected、private或者package-visible的方法上使用,不会报错,但事务会失效
(4)@Transactional注解要用在具体的类或方法上,不要用在类所要实现的任何接口上。
原因:在接口上使用@Transactional注解,只能当你设置了基于接口的代理时才生效。因为注解是不能被继承的,这就意味着如果正在使用基于类的代理时,事务的设置将不能被其识别,而且对象不会被事务代理所包装

4、synchronized和volatile关键字 

java中为了保证多线程读写数据时,保证数据的一致性,采用:

(1)同步:如synchronized关键字,或者锁对象

(2)使用volatile关键字:它能使变量在值发生改变时尽快让其他线程知道;

对volatile关键字的解释:volatile关键字只修饰变量。一般对变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存,这个过程中变量的新值对其他线程是不可见的(在多线程问题中这会引发严重问题)。修改volatile修饰的变量的值时,会将其他缓存中存储的修改前的变量清除,然后重新读取,即修改volatile修饰的变量的值时,会直接更新其他缓存中该变量的值(即达到了尽快通知其他线程该变量值改变的目的)。

volatile与synchronized的比较:

(1)volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞;(volatile不会造成线程阻塞而synchronized会)

(2)volatile仅能修饰变量;synchronized能修饰变量和方法(如单例模式);

(3)volatile仅能实现变量的修改可见性;synchronized可以保证变量的修改可见性和原子性(操作步骤要么全部执行,要么都不执行);

volatile失效的情况:1 当一个域的值依赖于它之前的值(n+=1、n++) 2 当某个域的值受到其他域的值的限制(如Range类的lower和upper边界,需要lower<=upper)

使用volatile而不是synchronized唯一安全的情况是只有一个可变的域

5、画出JMM和JVM模型,并添加注释

JMM:java内存模型,规定了线程和内存之间的关系(线程相关)。

系统存在一个主内存,java所有变量都储存在主存中,对所有线程共享。每条线程有自己的工作内存(即本地内存),其中保存的是主存中的变量的拷贝,线程对变量的操作都是在自己的工作内存中进行的,变量传递需要通过主存完成(涉及问题4)

java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

JVM模型:

java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

classLoader:类加载器,将class文件加载到内存(做数据校验、转换解析、初始化数据)

Runtime data area: 运行时数据区,即JVM管理的内存

program counter register:程序计数器,线程私有,指向下一条要执行的指令,(区分“寄存器”,寄存器是执行指令的)

java stack: 栈,线程私有,生命周期与线程相同。每个方法被执行时都会创建一个栈,用于存储局部变量

heap: 堆,线程共享,存储实例对象及数组,是垃圾收集器管理的主要区域

method area:方法区,线程共享的内存区域,存储类信息、常量、静态变量

6、二分查找法的算法实现(java实现)

二分法要求数组是有序的,查找速度快,适用于不经常变动而查找频繁的有序列表

递归实现:

public static int binarySearch(int[] arr,int key,int low,int high){  //方法最后返回的是查找到的位置

   if(key > arr[high] || key < arr[low] || low > high){ return -1;}

   int mid = (low + high)/2;

   if(key < arr[mid]){

     return binarySearch(arr, key, low, mid-1);

    }else if(key > arr[mid]){

      return binarySearch(arr, key, mid+1, high)

    }else{return mid;}

}

非递归实现(用while循环)

public static int commonBinarySearch(int[] arr, int key){

  int low = 0;  //低位

  int high = arr.length-1;  //高位

  int mid = 0;

  if(key > arr[high] || key < arr[low] || low > high){  return -1; }

  while(low <= high){

    mid = (low+high)/2;

    if(arr[mid] > key){

      high = mid - 1;

    }else if(arr[mid] < key){

      low = mid+1;

    }else{ return mid; }

  }

  return -1;  //最后没有找到,返回-1

}

7、有哪些垃圾收集器,及其使用的垃圾回收算法

一、(1)Serial串行垃圾收集器:采用复制算法、使用单线程进行垃圾回收(执行垃圾回收时,冻结所有应用程序线程),不适合服务器环境,适合简单的命令行程序

(2)Parallel并行垃圾收集器:JVM的默认垃圾收集器,采用复制算法、使用多线程进行垃圾回收(执行垃圾回收时,冻结所有的应用程序线程)

(3)CMS并发标记扫描垃圾收集器(concurrent mark sweep):采用“标记-清除”算法、使用多线程扫描堆内存,标记需要清理的实例并清理被标记过的实例。(会出现“碎片”问题)

当标记的引用对象在tenured区域(heap区下的一个区)或者当垃圾回收的时候堆内存的数据被并发的改变,CMS会持有应用程序所有线程。

相比并行垃圾收集器,CMS使用更多的CPU来确保程序的吞吐量;若为了更好的程序性能分配更多的CPU,那么CMS是第一选择。

(4)G1垃圾收集器(比较新,JDK1.7才正式引入):适用于堆内存很大的情况,将堆内存分割多区域,并且并发的对其进行垃圾回收,回收之后对剩余的堆内存空间进行压缩(G1会优先选择第一块垃圾最多的区域)

二、垃圾回收算法:(1)复制算法:把内存空间分成两等分,垃圾回收时,把当前区域中正在使用的对象复制到另一区域(缺点是需要两倍内存空间)

(2)标记-清除算法:从引用根节点开始标记所有被引用的对象,再遍历整个堆,将未标记的对象清除(缺点是要暂停整个应用,产生内存碎片)

(3)标记-整理算法:结合了(1)(2)的优点,从根节点标记所有被引用的对象,再遍历整个堆,清除未标记对象,并把存活的对象按顺序压缩到堆中一块。避免了碎片问题,也避免了两倍空间问题

8、三个线程:T1输出A,T2输出B,T3输出C,如何保证控制台顺序打印A,B,C?

实现线程顺序输出的三种方式:1、使用join方法,等待线程结束在执行其他;2、使用同步synchronized方法;3、使用锁ReentrantLock

1、最简单的实现方式join方法,思路:线程T1打印A,线程T2等待T1结束,再打印B,线程T3等待T2结束,在打印C

主类   public class MainClass{

  public static void main(String[] args){

    ThreadT1 T1= new ThreadT1 ();

    ThreadT2 T2= new ThreadT2(T1);

    ThreadT3 T3= new ThreadT3(T2);

    T1.start();  T2.start();    T3.start();   //此处三个线程启动的顺序可随意,最终都会顺序打印出ABC

  } 

}

线程类    class ThreadT1 extends Thread{

  public void run(){ system.out.println("A") }

}

class  ThreadT2  extends Thread{

  private ThreadT1  t1;

  public  ThreadT2 (ThreadT1   t){ this.t1 = t; }

  public void run(){

    try{

      t1.join();//等待t1线程结束

    }catch(InterruptedExceptionb e){ e.printStackTrace();  }

    system.out.println("B");

  }

}

//线程T3同上实现

2、使用synchronized同步,思路:主类中一个状态变量、三个synchronized修饰的方法分别打印A\B\C,同步锁在唤醒的过程中,会将同一个锁上的线程都唤醒,故方法中的条件判断中使用while循环

主类   public class PrintABC{

  private int status = 1;

  public void printA(){

    synchronized(this){

      while(status!=1){

        try{  this.wait();   }catch(interruptedException e){  e.printStackTrace();  }

      }

      system.out.println("A");   status = 2;   this.notifyAll();     //打印A后,修改状态变量值,唤醒之前被wait的线程

    }

  }

public void printB(){

    synchronized(this){

      while(status!=2){

        try{  this.wait();   }catch(interruptedException e){  e.printStackTrace();  }

      }

      system.out.println("B");   status = 3;   this.notifyAll();     //打印B后,修改状态变量值,唤醒之前被wait的线程

    }

  }

public void printC(){

    synchronized(this){

      while(status!=3){

        try{  this.wait();   }catch(interruptedException e){  e.printStackTrace();  }

      }

      system.out.println("C");   status = 1;   this.notifyAll();     //打印C后,修改状态变量值,唤醒之前被wait的线程

    }

  }

   public static void main(string[] args){

  PrintABC pabc = new PrintABC();

  Thread printA = new Thread(new RunnableA(pabc));

  Thread printB = new Thread(new RunnableB(pabc));

  Thread printC = new Thread(new RunnableC(pabc));

  printA.start();  printB.start();   printC.start();

}

}

线程类   class   RunnableA  implements Runnable{

  private  PrintABC  p;

  public RunnableA (PrintABC  pp){ 

    super();     

     this.p = pp;

  }

  public void run(){

    p.printA();

   }

}

//RunnableB、RunnableC同上实现

3、使用jdk1.5并发包中引入的ReentrantLock,是在方式2的基础上,主类添加ReentrantLock锁,其他不变,比方式2更灵活,也提供了在获取锁时阻塞的办法

import  java.util.concurrent.locks.Condition;

import  java.util.concurrent.locks.ReentrantLock;

public class PrintABC{

  private int status = 1;

  private ReentrantLock lock = new ReentrantLock();

  private Condition ca = lock.newCondition();

  private Condition cb= lock.newCondition();

  private Condition cc = lock.newCondition();

  public void printA(){

    lock.lock();

    try{

      if(status != 1){  ca.await();  }

      system.out.println("A");

      status = 2;

      cb.signal();

    }catch(){ e.printStackTrace();

    }finally{ lock.unlock();

    }

  }  

public void printB(){

    lock.lock();

    try{

      if(status != 2){  cb.await();  }

      system.out.println("B");

      status = 3;

      cc.signal();

    }catch(){  e.printStackTrace();

    }finally{  lock.unlock();

    }

  }

public void printC(){

    lock.lock();

    try{

      if(status != 3){  cc.await();  }

      system.out.println("C");

      status = 1;

      ca.signal();

    }catch(){  e.printStackTrace();

    }finally{  lock.unlock();

    }

  }

  //主方法同方式2

}

//线程类同方式2

上一篇:[转]flume-ng+Kafka+Storm+HDFS 实时系统搭建


下一篇:Mysql增量写入Hdfs(一) --将Mysql数据写入Kafka Topic