2021春招Java面试题大全(精华三)

返回首页

21.什么情况下会出现死锁?如何解除死锁?

当多个线程在执行过程中,进行资源抢占或者线程间通讯时产生的阻塞现象,线程不能向下执行。然后只有在外力作用下才能向下推进的过程就是死锁。比如A线程拿着B线程想要的资源不释放,同时B线程拿着A线程的资源不释放,然后他们互相等待。

产生死锁的原因:系统资源不足,资源分配不当。

产生死锁的四个条件是:

  • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  • 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

只需要破坏其中一种就可以解除死锁。

class MyRunnable implements Runnable {
     
     

    String lockA;
    String lockB;

    public MyRunnable(String lockA, String lockB) {
     
     
        this.lockA = lockA;
        this.lockB = lockB;
    }


    @Override
    public void run() {
     
     
        synchronized (lockA) {
     
     
            System.out.println(Thread.currentThread().getName()+"自己持有" + lockA + "尝试获得" + lockB);
            try {
     
     
                Thread.sleep(1000);
            } catch (InterruptedException e) {
     
     
                e.printStackTrace();
            }

            synchronized (lockB) {
     
     
                System.out.println(Thread.currentThread().getName()+"自己持有" + lockB + "尝试获得" + lockA);
            }
        }
    }
}

public class SiSuo {
     
     
    public static void main(String[] args) {
     
     
        new Thread(new MyRunnable("LockA","LockB"),"A").start();
        new Thread(new MyRunnable("LockB","LockA"),"B").start();
    }
}
怎么定位分析死锁在哪产生的?

解决:

  • jps命令定位进程号(只查看Java的后台进程,类似于linux的ps)
  • jstack找到死锁查看(打印Java异常栈信息)

22.事物的隔离级别有哪些?

Serializable、REPEATABLE READ、READ-COMMITED、READ-UNCOMMITED

脏读:当前事务读到了其他事务修改后但未提交的事务

不可重复读:一个事务读取了一个数据,然后另一个事务修改了这个数据且提交,刚才那个事务再来读这个数据时就会造成两次读取不一致的情况。针对更新

幻读:第一次读一组数据,第二次再读发现有好多行。针对插入

演示流程

  • 设置事务的隔离级别:set session transaction isolation level read committed;

  • 开启事务:start transaction;

  • 用两个事务演示

  • 提交/回滚:commit / rollback

23.Spring事物的传播特性有哪些?

事务传播行为:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播

REQUIRES、REQUIRED_NEW、SUPPORTS、NOT_SUPPORTED、NEVER、NESTED、MANDATORY


  • REQUIRED:必要的,指当前方法必须要在事务下执行,如果当前没有事务,就新建一个事务。这是最常见的选择。

  • REQUIRES_NEW:指当前方法必须要在事务下执行,如果这时开启了一个事务,则将原来的事务挂起来。然后在新建一个事务

    上面两个的区别是:

    如果当前都没有事务,则都开启一个事务(两个的作用一模一样)。

    如果当前有事务,REQUIRES_NEW是把当前事务挂起,然后新开一个事务执行,而REQUIRED直接在原来的事务中执行。

  • SUPPORTS:如果当前有事务,就以事务方式执行,如果当前没有事务,就以非事务方式执行。 相当于随便~

  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

  • MANDATORY:如果当前有事务,就以事务方式执行,如果当前没有事务,就抛出异常。

  • NESTED:如果当前有事务,就以事务方式执行,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。

  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

几种情况:

  • A事务调用B事务,B事务的传播特性为SUPPORTS的,然后B事务中抛出异常,这时A事务会发现这个异常,A方法也会回滚
  • 如果B的传播特性是REQUIRES_NEW,还是不能入库
  • 如果B的传播特性改为NOT_SUPPORTED,依然不能入库
  • 为什么呢,这是因为B确实是将A事务挂起了,在执行自己的这个方法时没有事务,但是不代表我这个方法的异常不能被A捕获到。归根结底,A事务会产生回滚的原因是B事务抛出的方法被A捕获到了,所以他会回滚。

24.Spring的核心内容

Spring的核心就是IoC容器和AOP模块,通过IOC容器来管理bean对象以及他们之间的耦合关系。通过AOP以动态非侵入的方式增强服务。Ioc让相互协作之间的组件保持松散的耦合,而AOP编程允许我们把遍布于应用各层的功能分离出来形成可重入的功能组件。

25.SpringMVC的工作原理

客户端发送一个请求,Tomcat获得这个请求后将其做了一个映射判断(<url-pattern>/</url-pattern>,将所有的请求都交给DisPatcherServlet )传给DispatcherServlet(前端控制器)

DispatcherServlet会根据请求去 HandlerMapping查找Handler(可以根据xml配置、注解@RequestMapping进行查找 )生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet.

DispatcherServlet调用HandlerAdaptor执行对应的处理器Handler(Interceptor/Controller),并将请求传来的参数传给Handler,

Handler(后端控制器)通过Service调用数据库找到对应的信息然后返回给Handler

Handler将ModelAndView返回给HandlerAdaptor

当Handler在完成逻辑处理后,通常会产生一些信息,这些信息就是需要返回给用户并在浏览器上显示的信息,它们被称为模型(Model)。仅仅返回原始的信息时不够的——这些信息需要以用户友好的方式进行格式化,一般会是 HTML,所以,信息需要发送给一个视图(view),通常会是 JSP。

Handler所做的最后一件事就是将模型数据打包,并且表示出用于渲染输出的视图名(即代码中:mav.setViewName(“index”)的index,就是逻辑视图名)(逻辑视图名)。它接下来会将请求连同ModelAndView发送回HandlerAdaptor。

HandlerAdaptor再将ModelAndView返回给DispatcherServlet,

但是DispatcherServlet不会处理这个ModelAndView,所以将其传给ViewResolver进行解析,ViewResolver根据逻辑视图名称解析真正的视图 ,并返回给DispatcherServlet。

这样以来,控制器就不会和特定的视图相耦合,传递给 DispatcherServlet 的视图名并不直接表示某个特定的 JSP。(实际上,它甚至不能确定视图就是 JSP)相反,它传递的仅仅是一个逻辑名称,这个名称将会用来查找产生结果的真正视图(给逻辑视图名拼接前缀和后缀, 进而确定一个 Web 应用中视图资源的物理路径 )

DispatcherServlet进行视图渲染,就是将Model填充到Response中显示在view上。最后在传到前端

26.常见的缓存算法,如何通过LinkedHashMap实现LRU过程

  • FIFO算法:它的思想是先进先出(FIFO,队列),这是最简单、最公平的一种思想,即如果一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。空间满的时候,最先进入的数据会被最早置换(淘汰)掉

  • LFU(Least Frequently Used ,最近不经常使用算法)LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰

  • LRU(The Least Recently Used,最近最少使用算法),也叫最近最久未使用算法,在很多分布式缓存系统(如Redis, Memcached)中都有广泛使用。

  • LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)

public class LRU<k,v> {
     
     

    private static final float DEFAULT_LOAD_FACTORY = 0.75f;//加载因子
    private int MAX_CACHE_SIZE;//最大容量
    LinkedHashMap<k, v> map;

    public LRU(int cacheSize) {
     
     
        this.MAX_CACHE_SIZE = cacheSize;
        int capacity = (int) Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTORY);//ceil向上取整
       /*LinkedHashMap有一个removeEldestEntry(Map.Entry eldest)方法,可以通过重写这个方法来控制缓存元素的删除,
       当缓存满了后,就可以通过返回true删除最久未被使用的元素,达到LRU的要求。
        当put进新的值方法返回true时,便移除该map中最老的键和值。
        注意第三个参数,boolean accessOrder,这个参数默认为false,从它的注释说明来看,
        当参数为true时,按照访问顺序,当参数为false时,按照插入顺序,
        这就为我们实现一个LRU的缓存系统提供了很大的遍历,因为我们要利用这个访问顺序来实现。
         */
        map = new LinkedHashMap<k, v>(capacity, DEFAULT_LOAD_FACTORY, true) {
     
     
            private static final long serialVersionUID = 1L;

            @Override
            protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
     
     
//                如果Map的尺寸大于设定的最大长度,返回true,再新加入对象时删除最老的对象
                return size() > MAX_CACHE_SIZE;
            }
        };
    }

    public synchronized int size() {
     
     
        return map.size();
    }

    public synchronized void put(k key, v value) {
     
     
        map.put(key, value);
    }

    public synchronized v get(k key) {
     
     
        return map.get(key);
    }

    public synchronized void remove(k key) {
     
     
        map.remove(key);
    }

    public synchronized Set<Map.Entry<k, v>> getAll() {
     
     
        return map.entrySet();
    }


//测试
    public static void main(String[] args) {
     
     
        LRU<String,String> lru = new LRU<>(3);
        lru.put("zhang","1");
        lru.put("ao","2");
        System.out.println(lru.getAll());
        lru.get("ao");
        lru.put("qi","3");
        System.out.println(lru.getAll());
        lru.get("zhang");
        lru.put("li","4");
        System.out.println(lru.getAll());

    }
}

27.为什么LRU使用LinkedHashMap实现更加方便

Java在实现LRU算法时采用LinkedHashMap容器。缓存体系必须要有一个key值(因为我们在查询数据的时候肯定要用一个特征值去查询这个缓存数据),因为在查询缓存数据时,肯定要拿个特征值去查这个缓存数据 。

不能用HashMap,因为HashMap底层是无序的,而且存在扩容,扩容一次原来的顺序可能就发生变化了,也不能用TreeMap,因为他会把数据按字典升序排(位置一定会被固定)。

28.什么是指令重排序?

在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置影响,虚拟机会按照自己的规则将程序编写顺序打乱以尽可能充分利用CPU。

什么是指令重排序:

  • 指令重排序就是指的我们的某一行程序比如在代码中是第20行,但是操作系统在读的时候,他就不一定是第20行了。
  • 假设我们源代码写的是123456789,底层为了保证执行的效果性能更好,在执行的时候顺序就不一定是123456789了。

在单线程环境下,我们不用担心指令重排序,处理器在进行重排序时必须要考虑指令之间的数据依赖性。

多线程环境下,线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

----------------------------------------------------------------------------------------------

为了提高性能,编译器和处理器会对指令做重排序

Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现。

29.如何避免指令重排序?

计算机避免指令重排采用的是内存屏障

Java语言避免采用volatile关键字

30.JSP的9大内置对象

  • PageContext:pageContext对象相当于JSP程序中所有对象功能的集成者。
  • Page:page对象代表JSP本身,它可以调用Servlet类所定义的方法。
  • Request:request对象可以获得客户端的输入信息。
  • Response:表示服务器端对客户端的一种回应,主要将JSP 处理数据后的结果传回到客户端。
  • Session:可以用来保存用户信息
  • Config:包括servlet程序初始化时所需要的参数及服务器的有关信息
  • Exception:若要使用exception 对象时,必须在page 指令中设定。
  • out:能把结果输出到网页上。
  • application:用于在多个程序之间保存信息,他的生命周期贯穿服务器的整个运行周期

31.AOP的原理,什么是动态代理,Spring通过哪些过程可以实现AOP过程

AOP的原理就是动态代理,动态代理的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。

动态代理有两种方式:

  • 一种是基于接口的实现动态代理——JDK动态代理
  • 一种是基于类的CGLib。

CGLib创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。(因为JDK动态代理要先执行代理类中某个某个方法,再执行被代理类的某个方法,再执行代理类的某个方法,执行需要多次跳转才能得到结果。需不需要加前后通知是在方法运行起来以后才判断的,所以执行速度慢)。(CGLIB动态代理需要重写被代理类中的所有方法,如果需要增加前置或者后置通知直接在重写的时候写好就行了


Spring可以通过三种方式实现AOP,一种是通过配置文件,需要编写一个类实现MethodBeforeAdvice,然后在xml文件中配置aop:config中的aop:advisor。第二种方式就是不用实现类,通过Aspectj的方式,我们定义一个类,然后把前置通知等写成方法,然后再xml配置文件中配置aop:config的aop:aspect。最后一种是通过注解的方式实现AOP过程。我们只需要在配置文件中启动AOP注解

JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

32.用户在浏览器中输入一行URL地址到收到返回值,这个过程经历了什么

1、首先,在浏览器地址栏中输入url

2、浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。

首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有给本地DNS发送查询请求。DNS的查询方式有两种,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发出查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。

3、在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。

4、浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。

5、握手成功后,浏览器向服务器发送http请求,即请求行,请求头,请求空行,请求体。

6、服务器处理收到的请求,并给浏览器做出响应(即响应行,响应头,响应空行,响应体)。

7、TCP链接关闭

33.什么是反射机制?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,可以构造这个类的对象;对于任意一个对象,都能够调用它的任意一个方法和属性,能够判断这个兑现所属的类;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  • getFields():获取当前运行时类及其父类中声明为public访问权限的属性
  • getDeclaredFields():获取当前运行时类中声明的所有属性(不论是公开的还是私有的)。(不包含父类中声明的属性)
  • 保证当前属性是可访问的:setAccessible(true)

优缺点:

  • 反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力
  • 性能问题,使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。

34.Synchronized锁升级过程和CAS操作

Synchronized的工作方式是让每个对象都关联一个monitor,monitor是真正的锁,但是monitor是操作系统提供的,使用他成本比较高,每次进入synchronized都要获取一个monitor锁。从jdk6以后对synchronized获取锁的方式进行了优化,从直接使用monitor锁改为了可以获得轻量级锁或偏向锁,进行锁升级的过程。


锁升级(结合前面的锁升级过程)

偏向锁:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可。

轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  • cas操作过多会影响cpu资源
  • 轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
  • 获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。

CAS

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

如果线程的期望值和主内存中的真实值相等那么可以更新内存中的值,如果不一样就修改失败,然后重新获得主内存的真实值。


Unsafe是CAS的核心类。它里面所有的方法都是Native修饰的,也就是说Unsafe类中的方法可以直接调用操作系统底层资源执行相应任务。CAS靠Unsafe类来保证他的原子性。CAS是一条CPU的原语指令,不会造成所谓的数据不一致问题。原语指的是由若干条指令组成的,用于完成某个功能的一个过程(判断内存某个位置的值是否是预期值,如果是则更新为新值,如果不是则不更新,然后在尝试修改),并且原语的执行必须是连续的,在执行过程中不允许被中断。

在Unsafe类中有一个getAndAddInt()方法,他实现的过程就是CAS的过程。

在这里插入图片描述


CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。


ABA问题

两个线程:一号线程和二号线程。他们都从主内存中取出值A,然后二号线程把A值改为了B,接着二号线程又把B值改为了A,这时在一号线程开来,主内存中的值没有发生变化,所以他进行CAS就可以成功,但是这时主内存中的值是发生过变化的。

解决:在CAS的基础上加一个版本号,修改了值,版本号就+1,。AtomicStampedReference,待时间戳的原子引用

ABA问题代码

public class Test {
     
     

    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {
     
     
        new Thread(){
     
     
            @Override
            public void run() {
     
     
                atomicReference.compareAndSet(100,101);
                atomicReference.compareAndSet(101,100);
            }
        }.start();


        new Thread() {
     
     
            @Override
            public void run() {
     
     
                try {
     
     
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
     
     
                    e.printStackTrace();
                }
                System.out.println(atomicReference.compareAndSet(100, 2020) +" "+ atomicReference.get());
            }
        }.start();
    }
}

ABA问题解决

public class Test {
     
     

    static AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
     
     
        new Thread(){
     
     
            @Override
            public void run() {
     
     
                try {
     
     
                    Thread.sleep(1000);//暂停一秒保证A B线程或得到的版本号一致
                } catch (InterruptedException e) {
     
     
                    e.printStackTrace();
                }
                atomicReference.compareAndSet(100,101,atomicReference.getStamp(),atomicReference.getStamp()+1);
                atomicReference.compareAndSet(101,100,atomicReference.getStamp(),atomicReference.getStamp()+1);
            }
        }.start();


        new Thread() {
     
     
            @Override
            public void run() {
     
     
                int stamp = atomicReference.getStamp();//获得首次版本号
                try {
     
     
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
     
     
                    e.printStackTrace();
                }
                System.out.println(
                        atomicReference.compareAndSet(100, 2020,stamp,stamp+1)
                                +" "+ atomicReference.getReference());
            }
        }.start();
    }
}
  • synchronized一致性保证,并发性下降。CAS不加锁,保证一致性,但是需要多次比较,循环时间长给CPU带来了很大的开销。
  • 只能保证一个共享变量的原子操作
    • 如果想对某个类进行原子包装,可以使用原子引用AtomicReference<类>,这就跟AtomicInteger的原理是一样的
    • 在这里插入图片描述

CAS的一个实现

原子整型getAndIncrement()之所以可以在多线程的环境下进行i++操作而不加synchronized也能保证线程安全是因为getAndIncrement方法里面调用的是Unsafe类的getAndAddInt()方法。他的底层原理:原子性靠的是Unsafe类,思想靠的是CAS(真实值和期望值相等了修改成功,真实值和期望值不相等了修改失败,重新修改)。

AutomaticInteger,为什么用CAS而不用Synchronized?

Synchronized加锁,同一时间段只能有一个线程访问。一致性得到了保障,但是并发性下降。CAS用的是do-while循环,并没有加锁,既保证了一致性,有提高了并发性

猜你喜欢

转载自blog.csdn.net/ysf15609260848/article/details/114285151