每天一套面试题Day2-华为高频
1.String、StringBuffer、Stringbuilder有什么区别?
StringBuffer。所有公共方法都有synchronized关键字修饰 由于同步机制,性能比StringBuilder稍慢。 StringBuilder的方法与StringBuffer完全一致,只是没有synchronized修饰。单线程用StringBuilder,多线程用StringBuffer String不可变,每次修改生成新对象;StringBuffer和StringBuilder可变。
// 不可变对象
String str = "Hello";
str = str + " World"; // 实际上创建了新的String对象
StringBuffer线程安全但性能较低,StringBuilder非线程安全但效率更高。单线程用StringBuilder,多线程用StringBuffer。
2.线程的创建方式
2.1 继承Thread
定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法 创建MyThread类的对象 调用线程对象的start()方法启动线程(启动后还是执行run方法的) 注意:启动线程必须是调用start方法,不是调用run方法。 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。 只有调用start方法才是启动一个新的线程执行。
public class ThreadDemo1 {
    // main方法本身是由一条主线程负责推荐执行的。
    public static void main(String[] args) {
        // 目标:认识多线程,掌握创建线程的方式一:继承Thread类来实现
        // 4、创建线程类的对象:代表线程。
        Thread t1 = new MyThread();
        // 5、调用start方法,启动线程。还是调用run方法执行的
        t1.start(); // 启动线程,让线程执行run方法
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {
    // 2、重写Thread类的run方法
    @Override
    public void run() {
        // 3、在run方法中编写线程的任务代码(线程要干的活儿)
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}
main也是一个线程,所以现在其实已经有了两条线程,是多线程。 出现结果:交替出现主线程和子线程的结果。
2.2实现Runnable接口喂给Thread构造器
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法 创建MyRunnable任务对象 把MyRunnable任务对象交给Thread处理。
调用线程对象的start()方法启动线程。
public class ThreadDemo2 {
    public static void main(String[] args) {
        // 目标:掌握多线程的创建方式二:实现Runnable接口来创建。
        // 3、创建线程任务类的对象代表一个线程任务。
        Runnable r = new MyRunnable();
        // 4、把线程任务对象交给一个线程对象来处理
        Thread t1 = new Thread(r); // public Thread(Runnable r)
//        Thread t1 = new Thread(r, "1号子线程"); // public Thread(Runnable r,String name)
        // 5、启动线程
        t1.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
// 1、定义一个线程任务类实现Runnable接口
class MyRunnable implements Runnable {
    // 2、重写run方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}
简化写法用匿名内部类(把接口的那个实现类用匿名内部类的写法)
public class ThreadDemo2_2 {
    public static void main(String[] args) {
        // 目标:掌握多线程的创建方式二:使用Runnable接口的匿名内部类来创建
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        Thread t1 = new Thread(r); // public Thread(Runnable r)
        t1.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();
        new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
2.3 利用Callable接口,FutureTask (本质同二,多了拿结果功能)
其实本质上还是把Runnable喂给thread,只是子类而已。
public class ThreadDemo3 {
    public static void main(String[] args) {
        // 目标:掌握多线程的创建方式三:实现Callable接口,方式三的优势:可以获取线程执行完毕后的结果的。
        // 3、创建一个Callable接口的实现类对象。
        Callable<String> c1 = new MyCallable(100);
        // 4、把Callable对象封装成一个真正的线程任务对象FutureTask对象。
        /**
         * 未来任务对象的作用?
         *    a、本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。
         *    b、可以获取线程执行完毕后的结果。
         */
        FutureTask<String> f1 = new FutureTask<>(c1); // public FutureTask(Callable<V> callable)
        // 5、把FutureTask对象作为参数传递给Thread线程对象。
        Thread t1 = new Thread(f1);
        // 6、启动线程。
        t1.start();
        Callable<String> c2 = new MyCallable(50);
        FutureTask<String> f2 = new FutureTask<>(c2); // public FutureTask(Callable<V> callable)
        Thread t2 = new Thread(f2);
        t2.start();
        // 获取线程执行完毕后返回的结果
        try {
            // 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
            System.out.println(f1.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            // 如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
            System.out.println(f2.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 1、定义一个实现类实现Callable接口
class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    // 2、实现call方法,定义线程执行体
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程计算1-" + n + "的和是:"  + sum;
    }
}
2.4 用线程池
3如何创建线程池(ExecutorService),线程池的参数
3.1 用ThreadPoolExecutor(更推荐)
3.2 用Executors线程池的工具类调用方法返回
4.HashMap底层原理和扩容机制
HashMap底层采用数组 + 链表(JDK7) → 数组 + 链表/红黑树(JDK8 引入,当链表长度 ≥8 且数组长度 ≥64 时,链表转换为红黑树),通过二次哈希,哈希算法确定元素存储位置。默认初始容量16,负载因子0.75,当元素数量超过(容量×负载因子)时触发扩容。扩容时创建双倍容量新数组,通过高位运算重新计算节点位置(JDK8优化为无需重新计算而是与原数组大小相与,因为是原来的两倍,如果为0不动,如果不为0,那么就放在原序号+原数组大小位置上),原数据通过尾插法(避免JDK7以及之前多线程的死循环)迁移到新数组。链表长度超过8且数组长度≥64时会转为红黑树,提升查询效率。
5.ArrayList和LinkedList的区别
ArrayList底层是数组实现的,数组是一组连续的内存单元,读取快(使用索引),插入删除慢 LinkedList底层基于双向链表,增删相对数组快。对首元素,尾元素(双向链表)进行增删改查的速度极快。
6.JVM内存模型
7.AOP
AOP(面向切面编程)的核心思想是解耦和分离关注点(一个程序分解为不同的部分,每个部分解决一个特定的问题(即一个关注点)。 AOP的核心概念:
连接点:JoinPoint,可以被AOP控制的方法。
通知:Advice,定义了在何时执行什么共性功能的代码块
切入点:PointCut,匹配连接点的条件
切面:Aspect,封装横切逻辑的模块,比如日志切面、事务切面
AOP的典型应用场景:日志记录。事务管理(@Transactional)。
Spring AOP通过动态代理实现,分为 JDK动态代理(目标类实现了接口,代理类实现与目标类相同的接口),CGLIB动态代理(目标类未实现接口, Code Generation Library,代码生成库,生成目标类的子类作为代理类。)
8.Redis的数据类型
Bitmap(位图)类型非常适合二值状态统计的场景,很省空间。比如签到统计,判断用户登陆态。 HyperLogLog 提供不精确的去重计数。,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。传统统计UV的问题:需要记录每个用户的ID,用户量巨大时内存消耗大。UV(Unique Visitor)即独立访客数,是指一段时间内访问网站的不同用户的数量
9.Java中的深拷贝,浅拷贝和引用拷贝
引用拷贝:复制对象引用地址,新旧变量指向同一对象。 浅拷贝:创建新对象,复制基本类型值,引用类型仍指向原对象。 深拷贝:完全复制对象及关联的所有子对象,新旧对象完全独立。
实现方式:浅拷贝常用clone()方法(需重写),深拷贝需递归复制或序列化实现。 核心区别:深拷贝隔离数据修改,浅拷贝和引用拷贝存在数据关联性。
变量 person   ───┐
                 │
变量 refCopy ───┼──→ { name: 'Alice', address: ● } ─→ { city: 'Beijing', ... }
                 │
变量 shallowCopy ───→ { name: 'Alice', address: ● } ─┘
                    (这是新对象,但address指向同一地址)
class Person implements Cloneable {
    String name;        // 基本类型
    Address address;    // 引用类型
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // 关键:调用父类(就是Object)clone方法
    }
}
class Address {
    String city;
}
public static void main(String[] args) throws Exception {
    // 创建原对象
    Person original = new Person();
    original.name = "张三";
    original.address = new Address();
    original.address.city = "北京";
    
    // 浅拷贝
    Person copy = (Person) original.clone();
    
    // 测试1:修改基本类型 - 独立
    copy.name = "李四";
    System.out.println(original.name); // 输出:张三(原对象不变)
    
    // 测试2:修改引用类型 - 共享
    copy.address.city = "上海";
    System.out.println(original.address.city); // 输出:上海(原对象被影响!)
}
10.TCP的三次握手
三次 = 双向确认 + 合并包 SYN同步序列号这个动作,实际上是告诉对方自己的序列化从哪里开始。为1代表这个包是一个连接建立请求。seq里面是自己的的序列化开始序号。 ACK=1 就是告诉对方:我包里的确认号ack是有效的,请按这个号来确认。
客户端 → 服务器: SYN=1, seq=x (客户端自己的初始序列号)
服务器 → 客户端: SYN=1, ACK=1, seq=y (服务器的初始序列号), ack=x+1
客户端 → 服务器: ACK=1, seq=x+1, ack=y+1
建立连接的过程其实就是确认双方都能收能发,并且知道对方的初始序列号的过程。中间的一次担当了两个功能而已,让客户端知道服务器能收也能发。
11.什么是进程和线程?进程和线程的区别?
进程是操作系统中资源分配(内存资源,I/O资源等)的基本单位,线程是CPU调度的基本单位。进程共享进程的资源,但是有自己独立的栈空间。 线程切换的开销相对于进程来说小一些。
12.SQL常见调优办法
来自牛客,MYSQL八股待系统学
SQL 调优常见方法包括索引优化、查询重构、使用缓存、减少锁定和优化数据库配置等。
13.事务隔离级别
来自牛客,MYSQL八股待系统学
MySQL 提供四种事务隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read,MySQL 默认) 和 串行化(Serializable),它们从低到高依次增强数据一致性,但并发性能递减。
#面试真题# 查看11道真题和解析
查看11道真题和解析