你应该知道的Java面试题解答

http://ifeve.com/java-interview-question/对于这个网站上提出的面试题的解答

Posted by irving.gx on February 21, 2021

前言

这篇博客是对 http://ifeve.com/java-interview-question/ 对于这个网站上提出的面试题的解答

基础题目

1.Java线程的状态

java线程一共有6种状态,分别是

  • 初始状态(NEW)
  • 运行状态(RUNNABLE)
  • 阻塞状态(BLOCKED)
  • 等待(WAITING)
  • 超时等待(TIMED_WAITING)
  • 终止状态(TERMINATED)

其中运行状态包括就绪(READY)以及运行中(RUNNING) 具体每个状态之间的转换关系参见下图 NEW 初始化一个线程的方法可以实现Runnable接口或者继承Thread类,new一个实例之后就创建了一个子线程,进入了初始状态。 READY 这种状态表示线程处于一种可以运行的状态,但是还没有运行。从new状态中调用线程的start方法就进入了ready状态,从上图我们还可以看到从WAITING以及TIMED_WAITING状态可以到READY状态中。Object.notify()这个方法用于唤醒在某个对象上等待的线程。LockSupport.park()这个方法也会使线程进入阻塞状态。park的含义是停车,也就是让一个正在运行的线程停止,阻塞,调用LockSupport.park这个方法之后并不会让线程释放当前拥有的锁。与之对应的是LockSupport.unpark()方法,这个方法不需要获取对象当前的锁就可以调用,这个是一个static的方法,调用后能让线程从park的阻塞状态中恢复到ready状态。下面分析一下如下代码:

public class MHTest {
    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");
    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super(name);
        }
        @Override public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName()+"被中断了");
                }
                System.out.println(Thread.currentThread().getName()+"继续执行");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(1000L);
        t2.start();
        Thread.sleep(3000L);
        t1.interrupt();
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

这个代码开启了两个子线程,t1和t2,t1子线程先start,然后走到LockSupport.park()这行代码就阻塞了,然后t2子线程启动,由于此时t1正在占有对象u的锁,所以子线程t2此时阻塞等待,因为两个线程都处于阻塞状态,所以要通过t1.interrupt()方法来打破阻塞,这个方法实际作用并不是立即中断线程,而是将该线程的这个状态位进行置位,具体什么时候中断交给这个线程本身。t1子线程调用Thread.currentThread().isInterrupted()方法,输出”t1被中断了”,然后是“t1继续执行”,执行完这个方法之后释放锁,子线程t2获取到锁,然后走到LockSupport.park()被阻塞,此时main主线程中调用了LockSupport.unpark(t2),将t2从阻塞状态中解除。打印出“t2继续执行”。所以执行以上代码的输出结果是

in t1
t1被中断了
t1继续执行
in t2
t2继续执行

RUNNING这种状态与READY之间可以互相转化,系统可以将一个线程从READY状态调度为RUNNING状态。Thread.yield()方法可以将一个线程从RUNNING状态转化为READY状态,yield的含义是让出,也就是一个线程将当前的CPU资源让出来,然后让系统进行重新调度。yield转化到这种状态之后系统会从所有的READY状态的线程中选择一个进行执行,也就是说刚才那个让出资源的可能仍然会被选中继续执行,并不表示yield之后,这个线程就不会执行了。

WAITTING进入这种状态的条件是调用了Object.wait()方法或者LockSupport.park(),或者Thread.join()方法。处于这种状态的线程在等待被唤醒,继续执行逻辑或者去争夺锁。与另外一种状态BLOCKED的区别就是BLOCKED是线程在等待获取锁。说一下Thread.join()这个方法,如果在A线程中调用B线程的join方法,那么A线程会在这里阻塞,直到等到B线程执行完毕,A线程才会继续执行。举个例子

public class Employee extends Thread{
      
    private String employeeName;
    
    private long time;

    public Employee(String employeeName,long time){
        this.employeeName = employeeName;
        this.time = time;
    }
    
    @Override
    public void run() {
        try {
            System.out.println(employeeName+ "开始准备");
            Thread.sleep(time);
            System.out.println(employeeName+" 准备完成");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Employee a = new Employee("A", 3000);
        Employee b = new Employee("B", 3000);
        Employee c = new Employee("C", 4000);
        
        b.start();
        c.start();
        
        b.join();
        c.join();
        System.out.println("B,C准备完成");
        a.start();
    }
}

在这个例子当中在主线程中调用子线程b和c的join方法,那么主线程只有当b和c的子线程结束之后才会执行下一步

BLOCKED进入这种状态是线程等待进入到synchronized方法或者synchronized代码块当中。

TIMED_WAITING这种状态是超时等待状态,除了与进入WAITING方法类似的方法之外,还可以通过Thread.sleep(long)方法进入。从这种状态转换的条件除了从WAITING状态中唤醒的那些方法,还有一个方法是超时时间到。

TERMINATED线程执行完成会进入到这个状态

2.进程和线程的区别,进程间如何通讯,线程间如何通讯

进程与线程区别:在操作系统中一个任务就是一个进程,比如运行不同的软件就是开启了不同的进程,进程有自己的独立的地址空间以及内存单元。线程是CPU调度的最小基本单位,同一个进程当中的线程共享内存空间。类似于火车和车厢的关系,火车可以看做是进程,而线程可以看做是火车中的车厢。不同进程之间彼此互不影响,就如同不同火车不会影响彼此,即使一个进程挂了,也不会影响到其他进程。但是如果进程中的线程挂了,那么就会影响到所在的进程。一个进程至少有一个线程,一列火车至少有一个车厢。

进程间通信:由于不同进程间有不同的地址以及内存空间,所以要想实现进程间通信,要在内核开辟一块缓冲区来实现。进程A将数据拷贝到内核缓冲区当中,进程B再从内核缓冲区当中读取数据。

  • 匿名管道通信:匿名管道是一种半双工的方式,只能进行单向通信,只能用于具有血缘关系的进程间通信,比如父子进程。管道实质是内核缓冲区,管道一端的进程顺序读入,另外一端顺序读出。这种通信方式的特点是缓冲区大小有限,而且只能发送无格式的字节流,子进程是由父进程fork创建出来,创建出来的子进程也有自己的地址空间,类似于父进程的一个副本,将父进程的资源全部复制了一遍。
  • 命名管道通信(named pipe):这个是用于没有血缘关系的进程间的通信。与匿名管道的区别是有名管道以文件形式存在于文件系统中。只要可以访问该文件,那么就可以通过命名管道进行通信。
  • 消息队列:消息队列存放于内存中,是消息的链表,允许一个或者多个线程写入以及读取消息,具有一定的格式。主要有POSIX 消息队列以及System V消息队列两种类型的消息类型
  • 信号:这个是linux系统用于进程间通信的一种机制,既可以来自硬件,也可以来自软件,比如按下了ctrl+C进行中断,或者软件调用了kill函数等等
  • 共享内存: 多个进程对同一块缓冲区进行读或者写,由于多个进程共享同一块内存,就需要同步机制来达到线程间的同步以及互斥
  • 信号量:实质是一个计数器,用于多进程对同一块资源进行访问的同步。这里说一下互斥以及同步的区别,互斥指的是对于同一个资源,同一时刻只能有一个访问者,访问是乱序的。同步指的是对于同一个资源按照一定的机制实现有序访问。互斥量只能是0或者1,但是信号量可以为非负整数。
  • 套接字:可用于进程间通信,可以通过网络让不同的计算机实现通信,双向通信。套接字由三个属性决定,域,端口号以及协议。常见的套接字域有两种,一是AF_INET,指的是Internet网络,另外一个是AF_UNIX,表示UNIX文件系统

3.HashMap的数据结构是什么?如何实现的。和HashTable,ConcurrentHashMap的区别

HashMap是通过数组加链表来实现的,会将元素的key通过hash函数映射到对应的位置,如果这个位置已经有元素了,也就是hash碰撞,那么就把元素通过链表的方式存储。java1.7以及1.8之后实现方式是不一样的。1.7之前是用数据加链表的方式来实现,如果遇到hash碰撞采用的是头插法来解决,但是这样会产生逆序且链表死循环的情况。1.8之后如何链表过长采用了红黑树来解决,并且遇到冲突的时候采用的是尾插法。

那么为什么1.7的时候会出现死循环呢,首先出现死循环的方法是hashmap.get()方法,出现的位置是在同一个位置冲突的多个元素,场景是多线程使用hashmap,并且因为map的容量达到了阈值,需要进行扩容。在扩容的时候,线程1冲突位置的指针是1-2-3,线程2由于采用了头插法,同一个位置如果又不巧发生了冲突,那么指针的方向变成了3-2-1,这样在内部就形成了环,当在这个位置寻找一个元素的时候,就会一直在这里循环,出现CPU100%的情况

4.Cookie和Session的区别

cookie与session都是用来跟踪浏览器用户身份的会话方式。cookie数据存在于客户端,session数据保存在服务器端。因为cookie保存在浏览器端,所以不够安全。

5.索引有什么用?如何建索引?

6.ArrayList是如何实现的,ArrayList和LinkedList的区别?ArrayList如何实现扩容。

ArrayList底层是采用数组来实现的,而LinkedList底层是采用链表来实现的。ArrayList每次扩容会申请一块内存区域,将新的数组扩容到当前容量的1.5倍,然后再把原有的元素拷贝到新的数组当中,再将原来数组的指针指向新的数组。

7.equals方法实现

8.面向对象

面向对象是相对于面向过程来说的,面向对象的三个最重要的特性是继承,封装和多态。继承就是一个子类可以拥有父类的属性以及方法,通过继承可以共用类的特性和方法,这样可以避免重复编程。封装是指将属性和方法封装在类的内部,这样能够实现属性以及方法的访问控制。多态指的是同一个类的方法调用,在运行时才能具体决定是由哪个具体的类来进行执行

9.线程状态,BLOCKED和WAITING有什么区别

10.JVM如何加载字节码文件

JVM把编译好的.class字节码文件从硬盘读取到内存的过程叫加载

11.JVM GC,GC算法。

12.什么情况会出现Full GC,什么情况会出现yong GC。

对于新生代的垃圾回收叫minor GC,对于老年代的垃圾回收叫major gc,对于整个堆的垃圾回收叫full gc。出现full gc的情况如下:

  • 年老代连续空间不足。从新生代晋升到年老代的对象大小大于年老代的连续可用内存
  • 调用System.gc(),会建议jvm进行full gc,但是并不是一定会执行
  • 在JDK 1.7之前方法区是用永久代来实现的,永久代中存放的是一些class信息,静态变量,常量等等。永久代被占满的时候会发生full gc
  • 空间分配担保失败。每次晋升对象的平均大小大于老年代的剩余空间。minor gc后存活的对象超过了老年代的剩余空间。

13.JVM内存模型

14.Java运行时数据区

15.事务的实现原理

事务的四种特性。ACID,原子性,一致性,隔离性,持久性。

  • 原子性A: 表示在一个事务当中的语句要么都成功,要么都失败。如何保证原子性的呢,采用Innodb当中的undo log,这个log就是说如果在事务执行的时候发生了错误,那么如果是insert就进行delete操作,如果是update就做相应的update,如果是delete那么就将原来的记录重新insert进来。
  • 一致性C: 就是保证事务从一个正确状态转化到另外一个有效状态。这个一致性是应用层的状态,而其他三种状态都是数据库底层逻辑来保证的。就是说一个事务不会违反数据库对于数据的限制,比如某一个字段不能为空,转账的时候转出和转入账户金额一样等等。
  • 隔离性I:表示不同的事务对同一个数据进行更改的时候不会互相影响。读取同一份数据的时候要么读取到的是另外一个事务更改之前的状态,要么是更改之后的状态,不会读取到另外一个事务更改中间的状态。隔离性是用的数据库的锁来实现的,有隔离锁和排他锁
  • 持久性D:表示事务一旦提交,对于数据库已经做了修改,并不会被回滚,即使出现了断电等故障,对于数据库的修改也会持久化保存。其实也就是说即使发生了故障也要保证事务能够正常提交。持久性是通过redo log来实现的。首先数据库的数据是存储在硬盘当中的,但是如果频繁IO效率会比较低,因此实际上有一个Buffer Pool来缓存数据,当读取数据的时候先从Buffer Pool读取,如果没有的话会从硬盘当中读取,然后放入Buffer Pool;写数据的时候也是先往Buffer Pool写数据,然后再刷入硬盘当中,但是如果在向Buffer Pool写完数据之后,数据库发生了宕机,那么事务就没有正常提交。因此在修改数据的时候除了向Buffer Pool提交数据之外,还会在redo log当中记录操作,当事务提交的时候,会调用fsync接口对redo log进行刷盘,然后再写入Buffer Pool。数据库数据就可以通过redo log进行恢复。这样持久性就可以进行保证。

16.聚簇索引与非聚簇索引的关系

聚簇索引是数据存储与索引放到了一块,找到了索引也就找到了数据。非聚簇索引是将数据存储与索引分开,因此一个表只有一个聚簇索引。innodb采用的是聚簇索引,MyISAM用的是非聚簇索引。在聚簇索引之上建立的索引为辅助索引,比如复合索引,前缀索引,唯一索引等等,辅助索引的叶子节点存储的是主键值。采用聚簇索引查找数据的时候只用查找一次,但是采用辅助索引的话需要二次查找。那么这两种索引有什么特点呢?

17.覆盖索引

如果索引包含所有满足查询需要的数据,那么这种索引成为覆盖索引,也就是不需要回表操作,对于索引覆盖查询,explain之后为using index

18.索引下推

索引下推解决的问题是在索引遍历的时候对索引中包含的字段先做判断,如果不满足条件直接过滤掉,减少回表的次数,出现在mysql 5.6以后

19.MRR优化

MRR全称是Multi-Range Read Optimization,能够将随机IO转化为顺序IO来降低查询的IO开销

20.filesort是什么

filesort是当需要对mysql返回的数据进行排序的时候,对数据进行分块然后采用快排进行排序,然后对不同的分块进行merge sort。有个sort_buffer_size,如果分块的大小超出了这个大小的时候,就会生成一个临时文件,先对每个文件进行排序,然后再进行归并。

21.index_merge技术

在MySQL5.0之前,一个表一次只能使用一个索引,在此之后使用了index_merge的技术,可以一次使用多个索引进行扫描,就是对多个索引分别进行条件扫描,然后将各自的结果进行合并。

22.如何从mysql向es进行数据同步

logstash-input-jdbc这个插件可以实现mysql与es之间的数据同步。logstash不仅可以从mysql到es的同步,而且还可以进行日志文件中日志的收集,消息队列kafka等等同步到es

23.类静态方法加锁与非静态方法加锁会互斥吗?

这个要分情况。首先加入这个类叫Test,有一个静态方法A(),非静态方法B(),静态方法C().实例出了一个x与y两个对象,两个线程t1与t2.

  • x.A(),y.B()不会阻塞,因为一个是类锁,一个是对象锁
  • x.A(),y.A(),阻塞,同一个类锁,x.A(),Test.A()也会阻塞,因为都是同一个类锁
  • x.B(),y.B(),不会阻塞,不同对象的对象锁
  • x.A(),y.B(),不会阻塞,一个是类锁,一个是对象锁
  • x.A(),y.C(),阻塞,同一个类的类锁

24.事务有哪几种隔离级别

READ UNCOMITTED(读未提交),可能会出现一个事务读取到了另外一个事务没有提交的数据,也就是会出现脏读的现象 READ COMITTED(读提交),就是一个事务可以读取另外一个事务已经提交的结果,这个是Oracle以及Sql Server的默认隔离级别。这种隔离级别会出现多次读取可能结果不一样的现象,就是不可重复读的问题。详细说不可重复读问题就是在一个事务当中读取同一个数据,这个数据可能两次读取期间被另外一个事务修改,导致在这同一个事务中读取到的结果不一样,这就是不可重复读的问题。

REPETABLE READ(可重复读),这个是MySQL的默认隔离级别。解决了上面的不可重复读的问题,还是上面的例子,一个事务A对于同一个数据读了两次(历史读),在这当中另外一个事务B新增了一条记录,并且第三个事务C更新了A读到的数据,A如果在事务中再次select刚才的条件的时候,发现没有读到事务B的新增数据以及事务C的更改,这就是可重复读。如果另外一个事务insert了一条语句提交了,原来的事务就发现不能插入一条相同id的数据了,发现数据库多了一条数据。 这种隔离级别会出现幻读,也就是说查到之前没有存在的数据。可重复读会出现幻读的情况,之所以我们没有看到,是因为普通的select是快照读,也就是读的历史版本。

MySQL会有快照读以及当前读两种,读取历史数据叫做快照读,读取数据库当前的版本数据叫做当前读。一般的select叫快照读,select for update以及select lock in share mode是当前读,需要加锁。MySQL为了解决当前读的幻读问题,采用Next-Key锁来解决,这个锁就是行锁与间隙锁的合并,就是将区间锁住,让别的事务无法插入新的记录。 也就是说mysql解决可重复读的实现方式是通过MVCC(多版本并发控制),就是搞一个版本号来实现的。MySQL可以设置事务的隔离级别。 select for update需要数据库引擎是innodb,并且是在事务当中才会生效。for update会对数据库表中的一行记录加上行级排他锁,其他请求如果要请求这行数据都要进行等待,是悲观锁的一种。

SERIALIZABLE(序列化),就是事务是顺序执行的,可以避免上述的问题,不过效率会比较低。

25.两个list,一个存储字符串{a,b,c,d,e,f,g,h},一个存储数字{1,2,3,4},如何用两个线程打印出两个字符,一个数字的形式,也就是ab1cd2ef3gh4这样的形式。

首先第一种方法,用Cyclicbarrier来实现,两个线程轮流打印完一组(ab,1)的时候阻塞,然后从阻塞唤醒的时候将打印数字的线程延迟一会,这样就会先打印出字符串,后打印出数字。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Test {
    public static void printStr(List<String> strList,Integer strIndex){
        System.out.println(strList.get(strIndex));
    }
    public static void printNum(List<Integer> numList,Integer numIndex){
        System.out.println(numList.get(numIndex));
    }
    public static void main(String[] args){
        Test test1 = new Test();
        Test test2 = new Test();
        List<String> strList = Arrays.asList("a","b","c","d","e","f","g","h");
        List<Integer> numList = Arrays.asList(1,2,3,4);
        CyclicBarrier innerBarrier = new CyclicBarrier(2);

        Thread t1 = new Thread(() -> {
            int t1index = 0;
            while (t1index < strList.size()){
                test1.printStr(strList,t1index++);
                test1.printStr(strList,t1index++);
                try {
                    innerBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            int t2index = 0;
            while (t2index < numList.size()) {
                //延迟一会
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test2.printNum(numList, t2index++);
                try {
                    innerBarrier.await();
                    innerBarrier.reset();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();;
        t2.start();
    }
}

第二种方法,采用LockSupport的park()以及unpark()方法,打印完字符串之后让第一个线程park(),等到第二个线程打印完数字的时候再让第一个线程unpark()

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

public class Test {
    public static void printStr(List<String> strList,Integer strIndex){
        System.out.println(strList.get(strIndex));
    }
    public static void printNum(List<Integer> numList,Integer numIndex){
        System.out.println(numList.get(numIndex));
    }
    public static void main(String[] args){
        Test test1 = new Test();
        Test test2 = new Test();

        List<String> strList = Arrays.asList("a","b","c","d","e","f","g","h");
        List<Integer> numList = Arrays.asList(1,2,3,4);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int t1index = 0;
                while (t1index < strList.size()){
                    test1.printStr(strList,t1index++);
                    test1.printStr(strList,t1index++);
                    LockSupport.park();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int t2index = 0;
                while (t2index < numList.size()){
                    test2.printNum(numList,t2index++);
                    LockSupport.park();
                }
            }
        });

        int outerindex = 0;
        t1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while(outerindex < numList.size()){
            LockSupport.unpark(t1);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(t2);
            outerindex++;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

26.数据库事务隔离级别

READ UNCOMITTED(读未提交),可能会出现一个事务读取到了另外一个事务没有提交的数据,也就是会出现脏读的现象 READ COMITTED(读提交),就是一个事务可以读取另外一个事务已经提交的结果,这个是Oracle以及Sql Server的默认隔离级别。这种隔离级别会出现多次读取可能结果不一样的现象,就是不可重复读的问题。详细说不可重复读问题就是在一个事务当中读取同一个数据,这个数据可能两次读取期间被另外一个事务修改,导致在这同一个事务中读取到的结果不一样,这就是不可重复读的问题。 REPETABLE READ(可重复读),这个是MySQL的默认隔离级别。解决了上面的不可重复读的问题,还是上面的例子,一个事务A对于同一个数据读了两次(历史读),在这当中另外一个事务B新增了一条记录,并且第三个事务C更新了A读到的数据,A如果在事务中再次select刚才的条件的时候,发现没有读到事务B的新增数据以及事务C的更改,这就是可重复读。如果另外一个事务insert了一条语句提交了,原来的事务就发现不能插入一条相同id的数据了,发现数据库多了一条数据。

这种隔离级别会出现幻读,也就是说查到之前没有存在的数据。

MySQL会有快照读以及当前读两种,读取历史数据叫做快照读,读取数据库当前的版本数据叫做当前读。一般的select叫快照读,select for update以及select lock in share mode是当前读,需要加锁。MySQL为了解决当前读的幻读问题,采用Next-Key锁来解决,这个锁就是行锁与间隙锁的合并,就是将区间锁住,让别的事务无法插入新的记录

SERIALIZABLE(序列化),就是事务是顺序执行的,可以避免上述的问题,不过效率会比较低。

技术深度

1.有没有看过JDK源码,看过的类实现原理是什么。

2.HTTP协议

3.TCP协议

4.一致性Hash算法

传统hash之后取模的算法的弊端是扩展性和兼容性比较差,如果新增或者删除一个节点的话所有元素都要重新取模分配到不同的节点。一致性hash可以解决这样的问题,把用户数据以及服务器通过hash映射到一个环上面,这个环的范围是从0-2^32-1,将用户数据沿着这个环顺时针走,走的的第一个服务器就是这个用户数据被分配到的服务器。

5.JVM如何加载字节码文件

6.类加载器如何卸载类字节码

只有满足三个条件类字节码才会被卸载

  • 该类的所有对象已经被GC
  • 加载该类的类加载器实例被GC
  • 没有该类对象的引用

一般而言,启动类加载器加载的类不会被卸载,系统类加载器以及标准扩展类加载的类不会被卸载,用户自定义的类加载器加载的类可以被卸载

7.IO和NIO的区别,NIO优点

  • IO是阻塞的。也就是说线程在读取和写入的时候是被一直阻塞的。NIO是非阻塞的,读取数据的话,如果没有可读的数据,也不会一直阻塞,而是会去做别的事情。
  • IO是面向流的,每次会读取流中的数据,直到读取完所有的字节,不能向前或者向后。而NIO是面向缓冲区的
  • NIO可以通过一个单独的线程来监视多个通道

8.Java线程池的实现原理,keepAliveTime等参数的作用。

9.HTTP连接池实现原理

10.数据库连接池实现原理

11.数据库的实现原理

技术框架

1.看过哪些开源框架的源码

caffine cache缓存框架。这种缓存框架性能比较高,使用的是window-TinyLFU算法,这种算法能够克服LRU以及LFU缓存策略的不足之处。LFU是根据一个元素访问频次高低来选择是否缓存还是剔除,不足之处是历史上一个被访问很多次的元素,虽然最近访问次数非常少仍然会被缓存,比如一个过气的电视剧。而并且要对每一个元素都要维护这个元素出现的频率,这样就会有很多存储成本。LRU不会累计历史的访问频次,该算法认为最近被访问的元素有可能还会被访问。LRU的局限是无法通过历史数据来预测未来。这样这两种算法都会使缓存命中率受损。

而caffine采用的Window TinyLfu算法提供了一种近似最优的命中率。首先解决一下LFU的需要大量存储空间来存储元素频率的问题,这里采用了Count Min Sketch方法,就是用一种比较少的存储来统计元素的访问情况。对于一个元素采用多个函数映射到一个数组上,如果有元素访问,那么对应的函数映射到的位置就加一,最后将该元素所有映射的最小值作为访问情况的估计,这样解决了LFU消耗比较多的空间进行存储的问题。另外一个问题就是LFU无法将时间反映到统计的频率上面。针对这个问题,W-TinyLFU算法会对于统计的频率进行衰减,就是当计数的总数达到一个阈值W的时候,就会将所有的频率减半进行衰减,这样过去一个访问量非常多的元素的数据统计会随着时间进行不断衰减。

Spring Boot 2.0之后开始使用caffine替代之前的guava cache来进行缓存

2.为什么要用Redis,Redis有哪些优缺点?Redis如何实现扩容?

  • 优点:redis是内存数据库,读写数据速度快。支持数据的持久化,可以通过RDB和AOF方式同步到硬盘当中。Redis是单线程,不用考虑多线程并发访问加锁同步问题。redis支持多种数据类型,String,list,hash,set,sorted set
    • 缺点:因为redis是内存数据库,所以redis能够存储的容量大小取决于机器的内存大小
    • redis集群扩容:当添加一个新的节点的时候,要给这个节点分配槽位,每个redis集群负责的槽位是0-16383之间,每一个节点存储一部分槽位的数据
    • 如果是字典的扩容的话,采用的渐进式rehash的方式,就是首先为一个新的字典申请空间,h[1],在扩容的时候字典会同时持有h[0]以及h[1]两个hashtable的索引,在开始进行rehash的时候,在进行CRUD操作的时候,除了执行操作,还会将涉及到的key rehash到新的hash表当中,有一个计数器,记录了当前rehash的进程,当h[0]上的数据全部rehash到h[1]时,迁移结束,此时用新的hash表代替旧的hash table

3.Netty是如何使用线程池的,为什么这么使用

4.为什么要使用Spring,Spring的优缺点有哪些

5.Spring的IOC容器初始化流程

  • 首先spring启动
    • 加载配置文件,比如xml,JavaConfig,注解等等
    • 加载完之后,将配置文件转化为统一的resource处理
    • 使用resource解析转化成BeanDefinition,BeanDefinition是对于Bean的描述,存放的是bean的元数据,比如类名,属性,是否是单例类等等
    • Bean进行初始化

6.Spring的IOC容器实现原理,为什么可以通过byName和ByType找到Bean

IOC就是控制反转,就是之前需要new一个实例化对象的过程,交给IOC容器来实现,当需要这个实例的时候IOC容器会将这个实例注入进来,因此IoC又称为依赖注入,IoC的好处是可以使资源统一调配和管理,减少模块之间的耦合。通过byName方法是采用了Java的 反射机制从ioc容器当中查找是否有这个名字的bean

7.Spring AOP实现原理

  • JDK动态代理。但是这种方式只能代理接口,对于没有实现接口的代理类无法采用这种方式。因为Java只支持单继承,在创建代理对象的代理对象默认继承了Proxy类,所以JDK只能通过接口实现动态代理
    • cglib动态代理。在底层修改字节码实现代理

8.消息中间件是如何实现的,技术难点有哪些

系统架构

1.如何搭建一个高可用系统

两级缓存,redis作为一级缓存,caffine本地缓存作为二级缓存

2.哪些设计模式可以增加系统的可扩展性

3.介绍设计模式,如模板模式,命令模式,策略模式,适配器模式、桥接模式、装饰模式,观察者模式,状态模式,访问者模式。

  • 模板模式 就是把通用的方法实现放在父类当中,对于那些不同的实现只是在父类中定义了抽象方法,但是并没有去具体实现,具体实现是在子类当中实现
    • 策略模式 有一个策略接口,不同的策略类实现了这个共同的接口,不同的策略会实现不同的方法。
    • 适配器模式 有类的适配器模式与对象的适配器模式,类的适配器模式是通过继承的方式来调用实际的执行方法的,而对象的适配器模式是采用的委派关系来进行调用,什么是委派关系呢?就是有两个类A和B,调用B的方法其实调用的A的方法,调用者不知道A的存在,也不需要知道A的存在,相当于A把方法调用委派给了B。可以看到类的适配器模式耦合比较严重,所以如果采用适配器模式的话尽量采用对象的适配器模式。
  • 桥接模式 https://www.liaoxuefeng.com/wiki/1252599548343744/1281319266943009 廖雪峰在这个上面解释了一下桥接模式,好处是可以避免继承带来子类爆炸,举了一个例子,一个类A,有两个维度的特性x和y,x和y都可以有多种,如果生成对应子类的话会有很多种组合,那么就可以用桥接模式来实现。但是个人感觉这个例子不好,这种场景只用在类A当中持有x和y两种属性就可以。廖雪峰说这种设计模式用的比较少
  • 装饰模式 这种设计模式就是能够提高设计的可扩展性,如果想对一个类添加特性的话可以采用这种方式

4.抽象能力,怎么提高研发效率。

5.什么是高内聚低耦合,请举例子如何实现

6.什么情况用接口,什么情况用消息

7.如果AB两个系统互相依赖,如何解除依赖

8.如何写一篇设计文档,目录是什么

9.什么场景应该拆分系统,什么场景应该合并系统

10.系统和模块的区别,分别在什么场景下使用

分布式系统

1.分布式事务,两阶段提交。

两阶段提交(2PC,2-phase-commit)实现两阶段提交的有协调者和参与者。在事务开始的时候,协调者会给所有的参与节点发送prepare请求,收到请求之后各个节点写入各自的undo log以及redo log,然后开始执行事务,但是并不提交。在完成这个动作的时候会向事务协调者返回done完成的消息,同时自身阻塞等待。协调者收到的信息有如下几种情况,所有的参与者都回复自己可以正常提交事务,那么协调者就会发送commit的通知,所有参与者就commit提交事务,然后释放自己占有的资源。如果有一个或者多个参与者回复执行失败或者协调者等待超时,那么协调者就会发送rollback请求,然后所有参与者就会rollback请求。 2PC的问题

  • 通信时间会比较长,在整个过程中,所有节点都是出于阻塞状态,效率比较低。
  • 单点问题。此外协调者的作用非常重要,如果协调者挂掉的话,那么整个数据库集群都不能正常运行
  • 数据不一致问题。在第二阶段commit的时候如果一部分参与者收到了commit指令,但是另外一部分参与者没有收到,那么就会出现一部分提交了,但是另外一部分参与者没有提交的情况,就会出现数据不一致的情况。

2.如何实现分布式锁

数据库。基于数据库实现分布式锁,就是创建一个表,如果向数据库插入记录成功就表示成功获取到锁,释放锁就是将这条记录从表中进行删除。

redis.这种方式实现分布式锁,主要用的是setnx命令,set if not exists,如果设置成功,那么表示客户端已经获取到这个锁,否则表示没有获取到。在获取到之后设置expire将这个key在一定时间之后过期。需要注意的是setnx与expire需要是原子操作,要保证原子操作的原因是如果setnx了,但是服务器出现了故障导致没有设置过期时间,就会出现一直无法获取到这个锁的情况。所以要设置为原子操作,设置原子操作的做法是采用lua脚本来实现。或者可以采用set key value (ex seconds) nx命令来实现,这样设置value与过期函数都在一个命令中实现

采用这种方式会有一些问题,比如客户端A的执行时间比较长,expire把这个锁过期释放了。然后另外一个线程B获取到了这个锁,A此时如果执行完毕,想要释放锁,将这个key删除掉,那么释放掉的其实是客户端B的锁,因此在删除之前要判断一下key当中对应的value是否是自己当时在setnx的时候设置的value。此外假如客户端A没有执行完毕,那么其实客户端A与B同时在执行操作,分布式锁并没有发挥作用。针对这种情况可以把锁的过期时间设置的时间长一点,能够保证在过期时间之内能够完成操作。这样做还是有问题的,因为无论设置多长的时间都有可能超时。有一个开源的框架redisson是这么做的,假如设置key的过期时间为30s,那么每隔10s都把这个key的过期时间设置为30s,也就是一直延长,执行这个机制的是watchdog,假如机器宕机的话那么watchdog也就挂了,这个key到了一定时间就会自动释放。

zookeeper.采用这种方式用到了zookeeper当中的临时顺序节点,两个客户端A和B想要对同一个资源进行加锁,如果A抢先一步,那么就会在加锁的node下建立一个带有一个序号的节点,A创建完节点的时候会检查一下这个锁节点下的所有节点,看看自己创建的这个节点是不是排在第一位,如果是的话就加锁成功。 B如果再来的话也会在这个锁节点下创建一个节点,这个节点也带有一个序号,不过序号要比A创建节点的序号要大。此时B也会检查一下自己在该锁节点下创建的节点,自己创建的节点的序号是不是最小的,因为客户端A已经建立了序号较小的一个顺序节点,所以发现自己无法加锁,然后就监听比自己序号小的上一个顺序节点,监听这个节点是否被删除,如果被删除的话那么就表示上一个客户端把锁释放了,然后客户端B就会重新去尝试加锁。

3.如何实现分布式Session

session就是会话,是客户端访问服务器的时候在服务器当中存储的客户端的信息。服务器从cookie当中获取sessionid,然后从服务器中查找对应的session,如果找到就返回session,如果找不到就在服务器中生成session然后存储。因为请求可能打到多台服务器上面,所以要实现分布式session,可以采用的方案是用redis来进行存储,采用redis的hash结构来进行存储

4.如何保证消息的一致性

采用事务消息,这种方法就是为了实现业务事务与发消息两者能够达成一致。不一致的情况如下:本地事务执行成功,但是宕机了,发消息失败。消息已经发出,但是业务执行失败回滚了,下游认为业务执行成功了,这就是不一致的情况。 那么事务消息是什么呢。感觉和两阶段提交的思路非常像,就是先发一个half消息到mq,mq收到之后进行持久化,然后返回一个ack到消息生产者,生产者收到之后开始执行本地事务,执行事务完毕之后会发送执行结果到MQ,MQ根据收到的执行结果选择将half消息删除还是发送给消费者。

5.负载均衡

6.正向代理(客户端代理)和反向代理(服务器端代理)

7.CDN实现原理

8.怎么提升系统的QPS和吞吐量

增大并发数。可以增加tomcat并发线程数,增加数据库的连接数 减少平均响应时间。增加缓存,流量削峰。 扩容集群 数据库数据量比较大的时候进行分库分表。 异步化,比如使用消息来进行异步处理

9.负载均衡的方式

有硬件负载均衡与软件负载均衡,软件负载均衡包括四层负载均衡(tcp)与七层负载均衡(http)

实战能力

1.有没有处理过线上问题?出现内存泄露,CPU利用率标高,应用无响应时如何处理的。

内存泄露是那些已经被分配内存空间的对象无法被回收就会导致内存泄露。一个长生命周期的对象持有一个短生命周期对象的引用就有可能发生内存泄露,这句话是什么意思呢,就比如说一个方法内的局部变量作为对象的属性声明在了对象的属性域当中,但是实际就在一个方法内部被用到,这样这个对象只有等到类的实例 内存泄露产生的原因可能会有如下情况: https://cloud.tencent.com/developer/article/1031919

  • 静态集合类造成内存泄露。就是集合是static

2.开发中有没有遇到什么技术问题?如何解决的

3.如果有几十亿的白名单,每天白天需要高并发查询,晚上需要更新一次,如何设计这个功能。

4.新浪微博是如何实现把微博推给订阅者

5.Google是如何在一秒内把搜索结果返回给用户的。

6.12306网站的订票系统如何实现,如何保证不会票不被超卖。

https://www.infoq.cn/article/12306-core-model-design 这篇文章对于12306的架构描述比较很不粗,可以采用的方式是

  • redis消息队列的方式,利用redis的单线程预减库存,主要涉及到decr的原子操作,当redis里面的数据减到0的时候就拒绝其他请求。
  • redis实现分布式锁来对库存进行加锁,但是这种方式会让其他请求等待
  • 可以采用数据库自身的锁来对数据库进行加锁,比如select for update,在更新之前先对数据库的这行记录进行加锁,这属于一种悲观锁。当然也可以搞一个乐观锁,就是先去查一下数据库的库存,更新的时候只有当库存与查出来的这个库存一样就进行更新

7.如何实现一个秒杀系统,保证只有几位用户能买到某件商品。

后端优化:
+	限流。也就是屏蔽掉无用的流量
+	异步。将同步请求转化为异步请求
+	缓存。将商品信息放在缓存当中,减少数据库查询。
前端优化:
+	限流。通过答题或者验证码来分散用户请求。
+	禁止重复提交。用户发起秒杀之后,需要等待才可以发起另一次请求。
+	动静分离。将前端静态数据缓存到离用户最近的地方,cdn或者用户浏览器当中。

更新库存:
1.采用乐观锁进行更新,更新库存之前检验版本号。
2.采用redis进行分布式限流,将无效的请求过滤掉。采用的方案就是每个请求进来获取一个令牌,也就是进行计数,当没有令牌的时候就直接返回
3.除了限流之外,将商品库存信息缓存到redis当中,这样就不会有大量的请求直接访问数据库。可以在秒杀之前,将redis当中预热一下商品的库存信息。此时就会涉及到缓存与数据库的一致性问题。如果先更新数据库再更新缓存的这种方式就会产生缓存当中是脏数据的问题,比如两个请求,A先更新了数据库,但是还没有来得及更新缓存,然后B进来更新了数据库同时更新了缓存,然后A再更新缓存,那么此时缓存当中就是脏数据。解决这种问题的方式就是更新完数据库,删除缓存,而不是更新。这样当一个请求查不到缓存的时候就会从数据库同步数据到缓存当中。但是这种方式在实际的使用中对于并发的处理效果并不是很好,因为每次都会删缓存,命中率不是很高。

热点数据的发现:
热点数据分为静态热点数据以及动态热点数据。静态热点数据就是秒杀之前可以预测的,比如就几个商品要秒杀。动态热点数据是在秒杀之后才能识别的,具体识别方法是在链路当中进行埋点的上报,识别出达到一定阈值的热点数据。

综上所述,一个完整的秒杀系统的架构是首先通过redis进行限流,如果成功获取到令牌,就去redis查询商品的库存信息,然后将商品的扣减库存请求发送到kafka进行流量的削峰,然后通过乐观锁修改数据库当中的库存
https://gongfukangee.github.io/2019/06/09/SecondsKill/ 这篇博客写的还行,但是感觉不够。

继续说更新库存,更新库存可以选择下单的时候减库存,付款成功之后减库存,以及预扣库存(也就是下单后保留15分钟,如果没有付款就释放库存)。
下单扣减库存的优势是不会出现下了单但是无法付款的情况,劣势是如果出现恶意下单但是不付款就会导致商品卖不出去。
付款扣库存不会出现上述的情况,但是可能会出现下单人数超过真正库存数,因为下单的时候不会扣库存。
预扣库存是两者的折中,但是无法完全解决恶意下单的问题,因为过了15分钟可以再次下单(但是感觉这时候商品都买完了,这种情况比较少)。
避免超卖的方式,超卖就是卖出的商品超过商品的存量,解决超卖可以将扣减商品的操作放在事务当中,如果库存为负就回滚。

高并发读:在读的时候不做影响性能的逻辑,到最后写数据的时候才做严格的校验。可以采用分层校验,就是不同层次尽可以过滤掉无效请求,只在漏斗最末端进行有效处理。

软能力

1.如何学习一项新技术,比如如何学习Java的,重点学习什么

2.有关注哪些新的技术

3.工作任务非常多非常杂时如何处理

4.项目出现延迟如何处理

5.和同事的设计思路不一样怎么处理

6.如何保证开发质量

7.职业规划是什么?短期,长期目标是什么

8.团队的规划是什么

9.能介绍下从工作到现在自己的成长在那里

以上这些问题来自方腾飞在并发编程网上(http://ifeve.com/java-interview-question/)写的一篇博客


如果对你有帮助,请作者喝一杯牛奶吧