java后端常见面试题

java后端常见面试题

java基础知识

jvm

jvm内存结构

  • 参考: link
  • 参考: link
    • 线程私有内存:
      • 虚拟机栈
      • 程序计数器
      • 本地方法区
    • 线程共享内存:
      • 堆内存
      • 方法区(元空间)
      • 直接内存
    • 虚拟机栈
      • 一个线程拥有一个独立的栈
      • 创建一个方法就创建了一个栈帧
      • 方法的调用也是通过入栈和出栈来完成的
      • 栈帧中保存了方法的局部变量,操作数,动态链接
    • 程序计数器
      • 唯一不会出现内存溢出的区域
      • 保留了当前线程即将执行的指令地址
    • 本地方法区
      • 本地方法区和虚拟机栈相似,不同的是这块区域是用来执行native方法的
    • 堆内存
      • 存放着字符串常量池
      • 通过参数-Xmx,-Xms来设置最大堆内存,最小堆内存
    • 方法区,保存了类加载信息,静态变量,运行时常量池
    • 直接内存,元空间通过nio可以分配直接内存
    • jdk1.8用把方法区移到了直接内存中

GC

垃圾收集算法
  • 堆的分区
    在这里插入图片描述

    • 新生代划分了eden,幸存区,非幸存区
    • 各个区域的占比可以通过jvm参数配置,默认是:8:1:1
    • 这样就相当于在标记-整理算法下,只造成了10%的内存空间浪费
  • 标记-清除算法

    • 扫描标记内存中需要清除的区域,然后清除
    • 使用场景:新生代
    • 优点:只需要清除被标记需要收集的内存,高效
    • 缺点:造成内存碎片
  • 标记-复制算法

    • 划分一块内存空闲区,存放需要保留的对象,然后一次性清除剩下的区域
    • 使用场景:新生代
    • 优点:存活对象少的情况下,可以高效完成复制
    • 缺点:浪费一半的内存空间
  • 标记-整理算法

    • 朝着一个方向不断移动需要保留的内存,同时清除标记内存
    • 使用场景:老年代
    • 优点:不会产生内存碎片,假定需要清除的内存很少
    • 缺点:移动大量内存,开销大
垃圾收集器

参考: link
在这里插入图片描述

  • Serial
    • 算法:标记复制
    • 特点:单线程收集,会造成用户线程暂停工作
    • 使用场景:新生代
  • ParNew
    • 算法:标记复制
    • 特点:多线程收集,同样也会造成用户线程暂停工作
    • 使用场景:新生代
  • ParallelScavenge
    • 算法:标记复制
    • 特点:多线程收集,用户线程不会暂停工作,注重jvm的吞吐量
    • 使用场景:新生代
  • CMS
    • 算法:标记清除
    • 特点:多线程收集,用户线程不会暂停工作,注重垃圾收集时候的停顿时间
    • 使用场景:老年代
  • SerialOld
    • 算法:标记整理
    • 特点:单线程收集,会造成用户线程暂停工作
    • 使用场景:老年代
  • ParallelOld
    • 算法:标记整理
    • 特点:多线程收集,同样也会造成用户线程暂停工作
    • 使用场景:老年代
  • G1
    • 新生代和老年代中都会使用的收集器

jvm优化

  • 通过visualvm在本机查看内存使用是否是锯齿形状
  • 如何设置参数
    • Xmx 堆内存最大值
    • Xms 堆内存初始值

jvm内存溢出如何排查

  • 确定是堆内存溢出还是栈内存溢出
  • 如果是堆内存溢出的话,先增大堆内存大小观察,输出堆内存快照包方便问题排查
  • 如果是栈内存溢出的话,根据日志定位到栈内存溢出的方法,查看是否是局部变量过多导致的

threadlocal

内部实现原理

参考: link
在这里插入图片描述

  • 通过每个线程内部创建一个map来保证线程安全
  • map的生命周期和线程生命周期保持一致
  • map的内部是通过线性探测来解决hash冲突
    • 线性探测
      • 优点:实现简单
      • 缺点:
        • 算法不断寻找下一个元素是否满足条件,实际上是穷举法,性能不高
        • 如果删除一个元素,需要重新hash计算并排列所有元素的位置

为什么会内存泄漏

  • key为弱引用,垃圾回收的时候key被回收,key为null
  • value引用还被持有没有被释放,造成内存泄漏
  • value在下次调用threadLocal.get方法的时候也会被值为null,从而被垃圾收集器收集
  • key为弱引用的目的:避免value被长期使用,保持key-value最新

虚引用,弱引用,软引用,强引用的区别

  • 强引用,通过new创建的对象引用
  • 软引用,垃圾回收时,发现内存不足,软引用指向的对象就会被回收
  • 弱引用,垃圾回收时,不管内存是否充足,都会回收
  • 虚引用,只是为了收到垃圾回收信号

hashmap

实现原理

  • 通过数组加链表的结构来存储数据
  • put操作计算key的hashcode,用hashcode和数组长度进行位与运算
  • 定位到相应的数组元素,如果为null就直接插入,否则插入链表尾部
  • 判断链表长度是否大于8,大于8需要把链表升级为红黑树

扩容为什么是2的n次方

key计算hashcode后需要和数组长度进行位与运算,可以保证数组元素均匀分布

为什么线程不安全

当两个线程同时设置数组元素,恰好key发生哈希碰撞,就会出现value相互覆盖

为什么concurrentHashmap线程安全

  • 当数组元素为null,通过cas的方式设置key-value,通过cas的方式保证线程安全
  • 当数组元素已经有值,通过synchronized关键字锁住当前桶位
  • 相当于只有一个线程能操作当前桶位的链表或者红黑树

synchronized

实现原理

参考: link

锁升级过程

  1. 对象头在无锁的情况下,对象头保存了:
    • hashcode
    • 垃圾回收分代信息
    • 锁的标志位
  2. 偏向锁
    • 在锁没有被占用的时候,获取锁的方式是,通过cas的设置对象头的几个字节保存当前线程id
    • 下次再次获取偏向锁的时候就直接比较线程id是否相等
  3. 轻量级锁
    • 线程检测到锁被占用,会根据以往自旋获取锁的成功率来判断是否进入自旋
    • 自旋的时间和自旋的次数是由jvm自动优化的
    • 自旋可以避免线程挂起和唤醒的开销
    • 因为从线程挂起需要由用户态切换到操作系统的内核态
  4. 重量级锁
    • 线程挂起,等待被唤醒

synchronized和lock的区别

  • 线程持有lock锁和等待lock锁的时间太长,是可以被中断的
  • lock锁可以通过condition来灵活控制线程的状态,比如:在生产者和消费者模型中,生产者可以让消费者处于等待或唤醒状态
  • lock锁实现公平锁,实现原理是:lock锁内部通过有序链表存储了等待获取锁的线程,只有最先请求锁的线程可以得到锁

CAS

  • 三个关键要素:内存值,预期值,新值
  • 只有内存值和预期值相等的情况下才会修改成功,否则一直重试,直到成功为止
  • 使用场景:在资源竞争少的场景使用,假设不存在并发的场景
  • cas会产生ABA问题,不会对执行结果有什么影响,可以忽略
  • 如果一定避免ABA问题,那就直接使用lock锁或者synchronized锁
  • cas广泛使用在各种并发编程中,比如:concurrentHashmap的put一个key的时候

volatile关键字

  • 线程安全指的是:并发场景下保证当前操作的原子性,可见性,有序性
  • volatile可以保证变量操作的可见性,有序性,但是不能保证原子性
  • volatile是通过内存屏障来实现的,内存屏障指令前后的指令不能交换顺序,保证了有序性
  • 写入遇到内存屏障是需要把CPU高速缓存中的数据写入主内存
  • 读取遇到内存屏障需要直接从主内存中读
  • 当两个线程同时对一个volatile修饰,初始值为0变量加1时,有可能变量的最终结果是1,而不是2,所以不能保证对这个变量操作的原子性
  • 使用场景:
    • 变量操作不依赖当前值,比如:布尔类型的标识位
    • 反例:访问量统计

lock

实现原理

  • 当多个线程竞争lock锁时,lock锁通过cas修改同步状态的方式保证线程安全
  • lock锁内部通过双向链表形成一个存储线程引用的队列
  • 公平性:lock锁的公平性就是利用了链表的特性,获取公平锁的线程会判断是否链表中存在排队线程
  • 可重入性:lock锁通过state变量记录了同一个线程获取锁的次数
  • 可中断:lock.lockInterruptibly()
  • lock锁可以通过定义condition进行线程之间的通信
  • condition通过调用线程的wait方法和notify方法控制线程的状态
  • lock还可以满足锁获取超时自动中断的场景,防止想要获取锁的线程长时间等待

读写锁

  • 使用场景:读多写少
  • 实现方式:内部使用整型标识位记录当前锁是否已经写线程获取

生产者和消费者代码实现

class Buffer {
    private  final Lock lock;
    private  final Condition notFull;
    private  final Condition notEmpty;
    private int maxSize;
    private List<Date> storage;
    Buffer(int size){
        //使用锁lock,并且创建两个condition,相当于两个阻塞队列
        lock=new ReentrantLock();
        notFull=lock.newCondition();
        notEmpty=lock.newCondition();
        maxSize=size;
        storage=new LinkedList<>();
    }
    public void put()  {
        lock.lock();
        try {
            while (storage.size() ==maxSize ){//如果队列满了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                notFull.await();//阻塞生产线程
            }
            storage.add(new Date());
            System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
            Thread.sleep(5000);
            notEmpty.signalAll();//唤醒消费线程
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public void take() {
        lock.lock();
        try {
            while (storage.size() ==0 ){//如果队列满了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                notEmpty.await();//阻塞消费线程
            }

            System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
            Date d=((LinkedList<Date>)storage).poll();
            Thread.sleep(5000);
            notFull.signalAll();//唤醒生产线程

        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
}

class Producer implements Runnable{
    private Buffer buffer;
    Producer(Buffer b){
        buffer=b;
    }
    @Override
    public void run() {
        while(true){
            buffer.put();
        }
    }
}
class Consumer implements Runnable{
    private Buffer buffer;
    Consumer(Buffer b){
        buffer=b;
    }
    @Override
    public void run() {
        while(true){
            buffer.take();
        }
    }
}
public class Main {

    public static void main(String[] arg){
        Buffer buffer=new Buffer(10);
        Producer producer=new Producer(buffer);
        Consumer consumer=new Consumer(buffer);
        for(int i=0;i<3;i++){
            new Thread(producer,"producer-"+i).start();
        }
        for(int i=0;i<3;i++){
            new Thread(consumer,"consumer-"+i).start();
        }
    }
}

线程池

线程的状态变化

  • new,ready,running,waitting,blocked,terminated
  • waitting和blocked状态的发生都会导致线程操作由用户态切换到内核态
  • waitting是主动等待
  • blocked是被动等待,比如没有获取到锁的时候处于等待状态

线程池有哪些参数

  • 队列未满时,核心线程数
  • 队列满时,最大线程数
  • 大于核心线程的线程空闲回收时间
  • 空闲回收时间单位
  • 任务队列
  • 任务拒绝策略
    • 直接丢弃
    • 丢弃然后抛出异常
    • 让正在运行的线程处理当前任务
    • 丢弃队列尾部的任务,插入当前任务

如何设置线程池的核心数

  • CPU密集型任务
    • 线程数 = CPU核心数 + 1
  • IO密集型任务
    • 线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)

其他

string的创建和内存分配原理

参考: link

  • 字符串常量池的内存分配是堆内存
  • string.intern方法返回首次出现这个字符串对象的引用

数据库

事务

事务特性

  • 原子性
  • 隔离性
  • 持久性
  • 一致性

事务隔离级别

  • 读未提交
    • 脏读
  • 读已提交
    • 不可重复读
  • 可重复读
    • 幻读
  • 串行执行

索引

mysql数据库引擎

参考: link

索引的实现原理

  • 聚族索引(主键索引)
    • innodb引擎一张表对应一个聚族索引
    • 索引的数据结构是一棵B+树
    • 每个叶子结点对应一个数据页
    • 数据页的一行保存了一条完整的记录
    • 数据页之间通过双向链表连接,方便数据的区间查询
  • 非聚族索引(辅助索引)
    • 开发人员平时建的索引就是辅助索引
    • 辅助索引先查询到记录的主键id,想要获取完整的记录,还需要根据主键id查询主键索引

联合索引失效场景

  • 联合索引其实相当于多层级排序,第一层级排序结束后,继续接下来的排序
  • 联合索引失效,是因为没有遵循最左匹配原则

常见的sql优化

  • 避免全表扫描的情况
    1. 避免 where is null
    2. 避免<>或!=
    3. 避免or
    4. 避免使用like
    5. 慎用in和not in,使用between和exists代替
    6. 避免对字段进行表达式而导致索引失效
  • 使用数据量小的表作为驱动表
  • 使用联合索引,避免回表操作
  • 通过explain查看索引使用情况和扫描的行数

B树和B+树的区别

  • B树每个节点都存储了key和data
  • B+树只有叶子结点存储了key和data,非叶子结点存储的是key
  • B+叶子结点连接成有序链表,方便区间查询
  • B+所有key都会包含到叶子结点,B树的key是分散在所有节点中

数据结构和算法

归并排序

public class MergeSortTest {

    public static void main(String[] args) {
        //测试数据
        int A[] = { 1, 6, 4, 5, 2, 9, 7, 23, 56, 43, 99 };
        // 排序前
        System.out.println("排序前:");
        for (int a : A) {
            System.out.print(a + " ");
        }
        System.out.println();
        // 排序
        mergeSort(A);
        // 排序后
        System.out.println("排序后:");
        for (int a : A) {
            System.out.print(a + " ");
        }
        System.out.println();
    }

    // 排序入口
    public static void mergeSort(int[] A) {
        sort(A, 0, A.length - 1);
    }

    //递归
    public static void sort(int[] A, int start, int end) {
        if (start >= end)
            return;
        // 找出中间索引
        int mid = (start + end) / 2;
        // 对左边数组进行递归
        sort(A, start, mid);
        // 对右边数组进行递归
        sort(A, mid + 1, end);
        // 合并
        merge(A, start, mid, end);

    }

    // 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
    public static void merge(int[] A, int start, int mid, int end) {
        int[] temp = new int[A.length];// 临时数组
        int k = 0;
        int i = start;
        int j = mid + 1;
        while (i <= mid && j <= end) {
            // 从两个数组中取出较小的放入临时数组
            if (A[i] <= A[j]) {
                temp[k++] = A[i++];
            } else {
                temp[k++] = A[j++];
            }
        }
        // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
        while (i <= mid) {
            temp[k++] = A[i++];
        }
        while (j <= end) {
            temp[k++] = A[j++];
        }
        // 将临时数组中的内容拷贝回原数组中 (left-right范围的内容)
        for (int m = 0; m < k; m++) {
            A[m + start] = temp[m];
        }
    }
}

手写二分查找

int binarysearch(int array[], int low, int high, int target) {
    if (low > high) return -1;
    int mid = low + (high - low) / 2;
    if (array[mid] > target)
        return binarysearch(array, low, mid - 1, target);
    if (array[mid] < target)
        return binarysearch(array, mid + 1, high, target);
    return mid;
}

树的遍历

// 前序遍历
public void preOrderTraverse1(TreeNode root) {
        if (root != null) {
            System.out.print(root.val + "->");
            preOrderTraverse1(root.left);
            preOrderTraverse1(root.right);
        }
    }

LRU缓存

public class LRUCache<K, V> {
    private Map<K, V> map;
    private final int cacheSize;

    public LRUCache(int initialCapacity) {
        map = new LinkedHashMap<K, V>(initialCapacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > cacheSize;
            }
        };
        this.cacheSize = initialCapacity;
    }
}

设计模式

单例模式

public class Singleton {

    private volatile static Singleton singleton;

    public static Singleton getSingleton() {
        if (singleton == null) {
            // 这里方法是static的,所以synchronized不能锁住this对象
            // 只能锁住class对象
            synchronized (Singleton.class) {
                if (singleton == null) {
                    /**
                     * 这就是为什么要家volatile关键字原因
                     * 1 防止对象没有初始化完成,其他线程使用未初始化的对象
                     * 2 相当于是禁止了指令重排
                     * 
                     */
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

模版方法模式

策略模式

代理模式

责任链模式

spring

自动装配机制

springCloud的各个组件

springboot实现原理

spring事务

参考: link

  • 事务传播机制其实就是处理调用方法和被调用方法之间的事务关系
  • 如果当前方法和子方法同时被@Transaction修饰,子方法是否需要创建新的事务,如果不创建,是否合并到当前事务
  • 如果只有子方法被@Transaction修饰,那么子方法是否创建新的事务,还是抛出异常
  • 只要能够确定当前子方法是否会创建新的事务,一般就不会错误使用事务传播机制

feign超时时间设置

参考: link

redis

如何删除数据量很大的key

  1. 如果直接删除很大key,比如zset,可能会导致redis计算资源被占用,影响正常业务
  2. 可以批量删除

如何模糊搜索key

  1. 设计key的时候需要做好层次划分
  2. 对于频繁访问的key可以存储在map或zset中

zset跳表

在这里插入图片描述

  1. 空间换时间,时间复杂度logN
  2. 方便zset进行区间查询

红黑树特性

为什么选择跳表而不是红黑树

缓存雪崩,缓存穿透,缓存击穿原因和解决办法

  1. 缓存雪崩,原因:缓存大规模失效,解决办法:设置缓存随机失效时间
  2. 缓存穿透,原因:缓存和数据库都没有需要访问的数据,解决办法:设置key的value设置为null并添加过期时间(或者使用布隆过滤器)
  3. 缓存击穿,原因:热点数据失效,解决办法:热点数据永不过期

redis如何实现分布式锁

  • 切面
  • 自定义注解
  • redis setnx
    • redis单线程保证了命令的原子性
    • 超时时间设置太大,只要正常释放锁就没有问题
    • 超时时间设置太小,会导致想要锁住的代码逻辑没有执行完毕,锁就释放了导致锁失效

mybatis

缓存

  • 装饰器模式
    • 打印缓存日志
    • 缓存线程安全
    • 缓存定时清理
    • 缓存事务封装
  • 二级缓存脏读
    • 更新操纵只能确保单机缓存最新
    • 根据具体业务,设置缓存过期的前提下可以使用缓存
  • 一级缓存在本次session关闭时会清除

插件

  • 通过责任链模式添加插件
  • 责任链中每个处理器又是一个动态生成的代理对象
  • 框架自带的分页是内存分页不能满足需求,可能造成内存溢出
  • 需要使用第三方的分页插件来实现分页

消息中间件

kafka

消息丢失的原因

  • 生产者
    • 原因:
      • 网络波动
      • 发送消息异常
    • 办法:
      • 消息发送确认,失败的话重复发送
        • 确认主节点发接收消息成功,ack=1
        • 确认所有节点接收消息成功,ack=all
  • kafka
    • 原因:kafka内部保存消息异常
    • 办法:运维问题
  • 消费者
    • 原因:消费者处理消息异常
    • 办法:先处理再commit
      • 可能导致消息重复消费,但是可以通过业务逻辑进行判断

Git

git空间

  • 工作区
    • git stash命令存储代码版本的空间
  • 暂存区
    • git commit命令存储代码版本的空间
  • 版本库
    • git push命令存储代码版本的空间

rebase

  • git rebase可以把多次提交合并为一条提交,保持提交记录清晰
  • git rebase合并提交记录最好是代码还没有执行push操作
  • 执行push也可以继续rebase,然后强制push到远程版本库
  • 强制push之后,需要其他合作开发人员执行git pull --rebase才能保证本地代码和远程代码一致,不冲突
  • 不使用这个命令会造成代码冲突,造成不必要的麻烦

项目经验

技术点

雪花算法

参考: link

在这里插入图片描述

  • 如何生成workId
    • 通过redis incr获取原子自增id
    • 保存ip地址和workId的键值对
  • 解决时钟回拨问题
    • 阻塞10毫秒,尝试获取当前时间,如果大于上次生成id成功的时间,则恢复正常
    • 从10位workid,预留3位,如果碰到时钟回拨就+1处理

接口幂等实现

  • 动态代理
    • 切面
  • 自定义注解
    • 对接口代码无侵入
  • redis
    • key为接口名 + 用户id

oauth2协议

参考: link

  • 项目用到的是密码模式

字节码编码

功能模块设计

通过redis zset集合对直播间排序

  1. 技术方案:直播间的关键字段存在了mysql,详细信息存hbase,为了提升接口响应时间,用redis zset集合对直播间排序
  2. 技术细节:
    • zset有序集合存储所以房间id和访问人数(或访问时间)的对应关系
    • 多个set无序集合存储不同分类直播间房间id,比如:体育,游戏,新闻
    • 通过zset和set集合的交集得到所以体育直播间的有序集合
    • 通过zset集合可以对直播间分页获取
    • 其中涉及的zset命令:zinterstore(交集),zunionstore(并集),zrangebyscore limit(分页)
    • 为了保持redis和mysql的数据一致性,定时任务每晚同步mysql的数据到redis

列表多字段排序

  • 层级排序
    • 第一层级,活跃项设置为100,非活跃项设置为200
    • 第二层级,活跃项中个人和公司分别用101和102区分
  • 多字段排序
    • 自定义权重排序
    • 时间排序

抢优惠券秒杀系统设计

  • 前端设置5秒内的随机等待时间,减少服务器压力
  • incr记录优惠券被抢张数
  • zset记录前100个抢到优惠券的用户,根据抢到时间排序
  • 不管是否抢到都把记录加入消息队列,方便后期逻辑处理
  • 使用定时任务读取zset发放优惠券,发放成功id放到set集合
  • 读取zset和set交集,发放优惠券是两个步骤,需要加分布式锁

一个完整的项目开发流程

  • 项目开发流程的每个关键节点都需要关注,从而把控项目进度
  • 关键节点
    • 需求评审
      • 理解需求背后的价值
      • 评估需求时间成本,避免做无用功
    • 需求敲定
    • 技术评审
    • 开发
      • 工作计划
      • 待办事项优先级
      • 自测
    • 提测
    • 预发布
    • 上线
      • 制定上线流程
  1. 为什么重写equals方法,就要重写hashCode方法
    先看看object默认的equals方法

    public boolean equals(Object obj) {
            return (this == obj);
        }
    

    默认的equals直接比较的是内存地址,我们想要的效果是类的某些属性相等,这样就需要重写equals方法
    在hashmap中对象作为key,equals返回ture,hashcode方法也需要返回相同的值,这样才可以找到map正确的位置

  2. final, finally, finalize的区别。
    final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
    finally是异常处理语句结构的一部分,表示总是执行。
    finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集 时的其他资源回收,例如关闭文件等

  3. mybatis原理

  4. feign超时时间设置

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值