后端开发面经

乐信集团

  1. protected关键字修饰的范围?

    答:

    private:只能被自身访问

    default(默认):同包可以访问

    protected:同包和不同包的子类可以访问

    public:可以被所有类访问

  2. final关键字有什么作用?abstract final修饰类的情形是否存在?

    答:

    • final不能被继承,没有子类,final类中的方法默认是final的。
    • final方法不能被子类的方法覆盖,但可以被继承。
    • final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
    • final不能用于修饰构造方法。

    final类型运用于数据:

    • 基本数据类型(int、double、char…)运用final时,使数值恒定不变;
    • 对象引用运用final时,final使得引用恒定不变,引用内部的数据若不是final型,可以进行修改
    • 数组类型运用final时,final使得数组引用恒定不变,数组内部的数据若不是final型,可以进行修改

    final与static:

    • final指明数据为一个常量,恒定无法修改;
    • static指明数据只占用一份存储区域;

    不存在,因为被final修饰的方法意味着该方法不可以被子类重写,这跟abstract的语义相互矛盾。

  3. i++在两个线程分别执行100次,最大值和最小值分别多少?为什么?如何使得i的值为200?

    答:

    i++不是原子操作,也就是说,它不是单独一条指令,而是3条指令:

    1、从内存中把i的值取出来放到CPU的寄存器中

    2、CPU寄存器的值+1

    3、把CPU寄存器的值写回内存

    注:

    注:对于多线程,线程共用一个内存,如果线程A在寄存器执行操作后而没有写入内存,则会切
    换到另一个线程。

    i的值的变化范围为2到200:

    • 结果为2的情况是:

      A线程执行第一次i++后,寄存器的值为1,但是没有刷新到主内存,这时候B线程开始执行第一次i++,寄存器的值也为1,也没有刷新到主内存。后面A线程开始执行完第99次i++操作后,寄存器为99,并且每次都刷新到主内存中去,此时内存的值为99。然后B线程接着执行完第一次i++操作,把1刷新到主内存中去,把99覆盖掉。这时,线程A开始执行自己的第100次i++操作,此时线程A读到内存中的值为1,存到寄存器然后++,此时寄存器的值为2,不刷新到内存里去,内存的值依旧为1。然后,线程B执行完剩余的i++操作,并且每次都刷新值到主内存中去,这时内存的值为100。然后线程A执行完第100次操作的剩余部分,将2刷新到主内存,这时候内存值为2,结束。

    • 结果为200的情况:

      线程A和线程B每次执行完i++操作都写入内存,并且每次操作交替进行。

如何使得i的值为200?

  • 锁机制。给方法添加synchronized关键字或者使用ReentrantLock都可以解决这个问题。这里可以拓展说下synchronized关键字和ReentrantLock的优劣。我认为一般使用synchronized更好,因为JVM团队一直以来都在优先改进这个机制,可以尽早获得更好的性能,并且synchronized对大多数开发人员来说更加熟悉,方便代码的阅读。

    • API层面:对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      private ReentrantLock lock = new ReentrantLock();
      public void run() {
      lock.lock();
      try{
      for(int i=0;i<5;i++){
      System.out.println(Thread.currentThread().getName()+":"+i);
      }
      }finally{
      lock.unlock();
      }
      }
    • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可等待特性对处理执行时间非常长的同步快很有帮助。

    • 可实现公平锁。公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      /**
      * Creates an instance of {@code ReentrantLock} with the
      * given fairness policy.
      *
      * @param fair {@code true} if this lock should use a fair ordering policy
      */
      public ReentrantLock(boolean fair) {
      sync = fair ? new FairSync() : new NonfairSync();
      }
    • ReentrantLock可以绑定多个condition对象,和多个条件关联。synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件。但如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。

  • 使用AtomicInteger原子类。为什么AtomicInteger使用CAS完成?因为传统的锁机制需要陷入内核态,造成上下文切换,但是一般持有锁的时间很短,频繁的陷入内核开销太大,所以随着机器硬件支持CAS后,JAVA推出基于compare and set机制的AtomicInteger,实际上就是一个CPU循环忙等待。因为持有锁时间一般较短,内核开销较大,所以大部分情况CAS比锁性能更优。

    注意:volatile不能解决这个线程安全问题。因为volatile只能保证可见性,不能保证原子性。

    拓展:

    i++和++i的线程安全分为两种情况:

  1. 如果i是局部变量(在方法里定义的),那么是线程安全的。因为局部变量是线程私有的,别的线程访问不到,其实也可以说没有线程安不安全之说,因为别的线程对他造不成影响。
  2. 如果i是全局变量(类的成员变量),那么是线程不安全的。因为如果是全局变量的话,同一进程中的不同线程都有可能访问到。

    如果有大量线程同时执行i++操作,i变量的副本拷贝到每个线程的线程栈,当同时有两个线程栈以上的线程读取线程变量,假如此时是1的话,那么同时执行i++操作,再写入到全局变量,最后两个线程执行完,i会等于3而不会是2,所以,出现不安全性。

  1. 假设有一张数据库表tableA,字段分别为(name,age,city),使用SQl语言求每个城市的平均年龄。

    答:

    1
    select city,avg(age) as '平均年龄' from tableA group by city
  2. 假设有一张数据库表tableA,字段分别为(name,age,city),使用SQL语言求每个城市的第二大年龄是多少。(数据库保证每个城市至少有两条记录以上)

    答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* 分组后并列排名:组内相同数值排名相同*/
    select s.name,s.city,s.age,s.rank FROM
    (
    select tablea.*,
    IF(city = @last_city,
    CASE
    WHEN age = @last_age THEN @rank
    ELSE @rank := @rank+1
    END,
    @rank := 1) as rank,
    @last_city := city,
    @last_age := age
    from tablea, (select @last_city := '', @last_age := '', @rank := 0) r
    order by city,age desc
    )as s where s.rank = 2
  3. 假设有一张数据库表tableA,字段分别为(name,age,city),使用SQl语言求纽约中年龄最大的人的姓名。

    答:

    1
    2
    3
    select name,age from tableA 
    where city = '纽约' and
    age = (select max(age) from tableA where city = '纽约')
  4. 有表Student(Sno,Sname,Sage,Ssex)学生表,表Course(Cno,Cname,Tno)课程表,表SC(Sno,Cno,score)成绩表,表Teacher(Tno,Tname)教师表

    • 统计列印各科成绩,各分数段人数:课程ID,课程名

      1
      2
      3
      4
      5
      6
      select c.Cno,c.Cname,
      count(case when s.score between 85 and 100 then 1 end) as '[100-85分]',
      count(case when s.score between 70 and 85 then 1 end) as '[85-70分]',
      count(case when s.score between 60 and 70 then 1 end) as '[70-60分]',
      count(case when s.score<60 then 1 end) as '[60分以下]' from Course c, SC s where c.Cno = s.Cno
      group by c.Cno,c.Cname
    • 查询两门以上不及格课程的同学的学号及其平均成绩

      1
      2
      3
      4
      5
      6
      select Sno,avg(score) from SC where Sno in (
      select Sno
      from SC
      where score < 60
      group by Sno having count(*) >= 2
      ) group by Sno
  5. Java中的子类是否要实现父类所有的抽象方法?

    答:

    • 子类如果是非抽象类的话,那么一定要实现父类中所有的抽象方法;
    • 子类也是抽象类,那么可以不实现父类中所有的抽象方法,可以实现一部分抽象方法。
  6. Java的抽象类中可以定义final方法吗?Java的抽象类可以被private修饰吗?

    答:

    可以。但是final是不能修饰abstract所修饰的方法的。

    • Java抽象类是内部类时,可以被private修饰。
    • Java抽象类不是内部类时,不可以被private修饰。

中信银行信用卡

  1. Spring Boot和Spring MVC的区别?
  2. 如果想引入一个jar包,如何在Spring MVC中配置xml文件?
  3. 在数据库表中建立一个字段,只有一个字节的长度,用varchar还是char好?为什么?
  4. 多线程实现的几种方式?
  5. 线程的几种状态和切换的方式?
  6. Linux用过吗?了解哪些命令?
  7. 一个http请求比较慢的时候,应该从哪些方面去排查并且优化?
  8. 当并发请求较多时如何进行优化处理?

4399

  1. 什么是守护线程,实现守护线程的基本流程和原理是什么?

  2. HashMap线程不安全的体现?

  3. HashSet底层实现原理

  4. redis数据存储在内存还是磁盘?

  5. Nosql用过哪些?

  6. ConcurrentHashmap加锁体现在哪里?

  7. 数据库分区分表如何实现?

  8. Java Bean和普通的类有什么区别

  9. 如何读取Spring Boot配置文件中的属性?(有哪些注解)

  10. Synchronized关键字底层语义原理

    答:

    之所以这个synchronized关键字会起带同步的作用,就是因为monitor。monitor只是一个简称,通常称为intrinsic lock或者monitor lock

    每个对象都有一个monitor lock与之相关联,如果一个线程需要排他和一致访问对象的某个域,它就必须先获得对象的monitor lock,当它完成任务之后再释放这个锁,让其它线程可以访问到。只要一个线程获得了一个monitor lock,那么其它线程就没办法获取到这个锁。也就是说,当其它线程尝试获得锁的时候就会阻塞。

    Synchronized进行编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1。相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

    在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

    无论方法是正常结束还是异常结束,方法中调用过的每条monitorenter指令都有执行其对应monitorexit指令。

    注意:Synchronized是一种可重入锁。

    synchronized的可重入性

    从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
    for(int j=0;j<1000000;j++){

    //this,当前实例对象锁
    synchronized(this){
    i++;
    increase();//synchronized的可重入性
    }
    }
    }

    public synchronized void increase(){
    j++;
    }


    public static void main(String[] args) throws InterruptedException {
    Thread t1=new Thread(instance);
    Thread t2=new Thread(instance);
    t1.start();t2.start();
    t1.join();t2.join();
    System.out.println(i);
    }
    }

    正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。

    注意:由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

    Java虚拟机对synchronized的优化

    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

    • 自旋锁
    • 锁粗化:
      • 偏向锁
      • 轻量级锁
      • 重量级锁
    • 锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
  11. ReentrantLock和synchronized使用分析?

    答:

    ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。

    1、用法比较

    • Lock使用起来比较灵活,但是必须有释放锁的配合动作
    • Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
    • Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等

    2、特性比较

    • ReentrantLock的优势体现在:
      • 具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
      • 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
      • 超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回

    3、注意事项

    • 在使用ReentrantLock类的时,一定要注意三点:
      • 在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
      • 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
      • ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。
  12. 什么是覆盖索引?覆盖索引的优点?覆盖索引的使用场景?

    答:

    通常大家都会根据查询的where条件来创建合适的索引,不过这只是索引优化的一个方面。索引确实是一种查找数据的高效方式,但是mysql也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果索引的叶子节点中已经包含要查询的数据,那么还有什么必要再回表查询呢?

    如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为“覆盖索引”。

    覆盖索引的优点

    • 索引项通常远小于记录,所以如果只需读取索引,那mysql就会极大的减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也很有帮助,因为索引比数据更小,更容易全部放入内存中
    • 因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少的多。对于某些存储引擎,例如MyISAM,甚至可以通过optimize命令使得索引完全顺序排列,这让简单的范围查询能使用完全顺序的索引访问。
    • 一些存储引擎如MyISAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占用了数据访问中的最大开销的场景。
    • 由于InnoDB的聚簇索引,覆盖索引对InnoDB特别有用。InnoDB的二级索引在叶子节点中保存了数据行的主键值,所以如果二级索引能够覆盖查询,则可以避免对主键索引的二次查询。

    判断标准

    使用explain,可以通过输出的extra列来判断,对于一个索引覆盖查询,显示为using index,MySQL查询优化器在执行查询前会决定是否有索引覆盖查询。

秉持初心,继续向前。
显示 Gitment 评论