预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法老苗一对一私教学员评价blog
blog
  • Java工程师高频面试题

    • 课程介绍
    • Java基础常见面试题
    • Java并发编程面试题
    • 数据库常见面试题
    • Redis 常见面试题
    • Java 框架常见面试题
    • Linux NG ES MQ SEATA 常见面试题
    • Java常见算法面试题
    • Java AI相关面试题
    • Java 工程师面试最常考的 80 道高频题
    • Java面试实录







----- 到底线了 -----

×

欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。

苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗

Java并发编程面试题 ​

通过统计我们发现并发编程涉及到的面试题仅次于Mysql数据库,但是并发编程在我们平时日常的开发中使用却不是很频繁,所以也成为了现在区分程序员水平的一个较好的手段。并且其实部分面试官也没有很丰富的多线程开发经验,所以这部分内容答好对于整体面试而言加分很大

多线程的使用(难度★★) ​

如何创建一个新线程 ​

这个问题不仅要记住实现的几个方式,还要根据代码理解其中的区别,最好把代码都自己写一遍

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 匿名内部类
  • 使用线程池
  • 使用JDK8的CompletableFuture

继承Thread类

面试官有可能会追问run和start的区别

Java
public class MyThread extends Thread{
    @Override
    public void run(){//线程启动后执行的代码逻辑
        System.out.println("线程执行");
    }
    
    public static void main(String[] args){
        MyThread1 t1 = new MyThread();//创建线程对象,注意此时还没有启动线程
        t1.start();//start方法启动线程,注意run和start的区别
    }
}
1
2
3
4
5
6
7
8
9
10
11

实现Runnable接口

本质上来说和Thread没区别,只不过接口可以实现多个,类只能继承一个

Java
public class MyRunnable implements Runnable{
    @Override
    public void run(){//线程启动后执行的代码逻辑
        System.out.println("线程执行");
    }
    
    public static void main(String[] args){
        MyRunnable mr1 = new MyRunnable();//创建Runnable对象
        Thread t1 = new Thread(mr1);//把runnable对象当作参数传递进Thread构造器中
        t1.start();//start方法启动线程,注意run和start的区别
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

实现Callable接口

和Runnable区别很大,具体区别都在下面代码中,同学们自行理解或者上网搜索,因为这个地方面试官有可能会追问两者区别

Java
public class MyCallable implements Callable<String>{
    @Override
    public String call(){//这里和runnable不一样,变成了call方法,并且有返回值,返回值类型就是接口的泛型
        System.out.println("start");
        return "ok";
    }
    
    public static void main(String[] args){
        MyCallable mc1 = new MyCallable();//创建callable对象
        //利用FutureTask获取线程call方法的执行结果
        FutureTask<Stringft = new FutureTask<String>(mc1);//将callable对象做为参数传递到FutureTask中
        Thread t1 = new Thread(ft);//把runnable对象当作参数传递进Thread构造器中
        t1.start();//start方法启动线程,注意run和start的区别
        String result = ft.get();//获取线程执行的结果
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

匿名内部类

本质上和runnable没区别,只不过换成了匿名内部类的方式和lambda的写法,省去了自己定义一个实现runnable接口的类

Java

public class TestThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -{ //利用jdk8的lambda表达式,本质上是个匿名内部类
            System.out.println("start");//相当于在这里重写了run方法
        });
        t1.start();//启动线程
    }
}
1
2
3
4
5
6
7
8
9

使用线程池

创建线程池的方式有很多,这里只是随便列其中一种,我们实际不推荐使用Executors的方式来创建线程池,下个问题会提到

Java
public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("start");
    }

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);//创建线程池
        pool.submit(new MyExecutors());//提交任务
        pool.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

使用JDK8的CompletableFuture

Java
public class MyExecutors {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //还是利用匿名内部类的方式开启一个线程,并且使用JDK自带线程池
        CompletableFuture.runAsync(() -{
            System.out.println("start");
        });
        //使用自己的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
                10,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100));
        CompletableFuture.runAsync(() -{
            System.out.println("start");
        },executor);//参数多了一个executor
        //通过supplyAsync类似之前的callable,通过future获取线程执行结果
        CompletableFuture<Stringfuture = CompletableFuture.supplyAsync(() -{
            System.out.println("start");
            return "ok";
        });
        String result = future.get();//获取执行结果
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

线程的状态流转 ​

还可能问成线程的工作原理,线程有哪些状态,TIMED_WAITING和WAITING的区别等

首先参考JDK中定义的线程状态的枚举类

Java
public enum State {
    //尚未启动的线程的线程状态    
    NEW,
    //可运行线程的线程状态。    
    RUNNABLE,
    //线程阻塞等待监视器锁的线程状态。    
    BLOCKED,     
    //等待线程的线程状态    
    WAITING,
    //具有指定等待时间的等待线程的线程状态    
    TIMED_WAITING,
     //已终止线程的线程状态。线程已完成执行    
     TERMINATED;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

线程状态流转图

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
  • 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
  • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
  • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

这里面试官可能会追问wait()和sleep()的区别,notify()和notifyAll()的区别,同学们自行掌握

如何创建线程池 ​

  • new一个ThreadPoolExecutor(JDK提供)
  • new一个ThreadPoolTaskExecutor(Spring框架提供) 这两个区别在于ThreadPoolTaskExecutor用起来更方便,不需要在手动指定,具体细节同学们自行搜索
  • 使用Executors(不推荐),阿里开发手册禁止使用此方式 FixedThreadPool和SingleThreadPool的队列长度是Integer.MAX_VALUE,可能引起OOMCachedThreadPool的允许创建线程的数量是Integer.MAX_VALUE,可能引起OOM

线程池的核心参数 ​

没啥说的,硬背吧

corePoolSize 核心线程数目 maximumPoolSize最大线程数目 = (核心线程+救急线程的最大数目) keepAliveTime生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放 unit时间单位 - 救急线程的生存时间单位,如秒、毫秒等 workQueue- 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 threadFactory线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等 handler拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 一般会追问怎么确定核心线程数: 先说结论:这个数字没法直接给出一个完美结果,可以先根据下面结论给出一个数字,然后再通过压测工具来测试最优结果,具体要根据任务的执行时间以及任务 简单计算:IO密集型:文件读写、数据库读写、网络读写---》2N+1(N为服务器CPU核数) CPU密集型:计算性质、代码内操作的算法、大批量循环---》N+1(N为服务器CPU核数)

线程池的工作原理 ​

关键在于核心线程数满了的情况下是先进队列

线程池工作原理图

Spring中的Bean对象是线程安全的吗 ​

先说结论,Bean是否线程安全和Spring框架没有直接关系,取决于我们怎么用这个bean对象 首先Spring中的bean有默认是单例的,可以通过@Scope("prototype")注解来设置成多例 由于多例Bean每次都会创建一个新对象,所以不存在线程并发的问题,也就是线程安全的 单例Bean分为无状态和有状态 无状态的单例bean简单来说就是支持对里面成员变量进行查询操作,那就不存在线程安全问题 有状态的单例bean就是需要对bean中的成员变量进行修改,如果没有做一些线程安全的策略(比如使用AtomicInteger或者锁之类的策略),那就会存在线程安全问题

多个线程如何控制线程的执行顺序 ​

使用join()方法解决

Java

Thread t1 = new Thread(() ->
        System.out.println("t1线程执行")
);
Thread t2 = new Thread(() -{
    try {
        t1.join();//加入线程1,等t1执行完后才能继续执行
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("t2线程执行");
}
);
Thread t3 = new Thread(() -{
    try {
        t2.join();//加入线程2,等t2执行完后才能继续执行
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("t3线程执行");
}
);
t1.start();
t2.start();
t3.start();
//结果就是先t1,再t2,再t3
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

JDK21的虚拟线程 ​

对比传统线程 ​

  • 传统线程:由操作系统管理,每个线程需要分配较大的栈空间(通常 1MB),线程切换涉及用户态和内核态切换,开销较高。
  • 虚拟线程:由 JVM 层面管理,栈空间动态分配,仅需几 KB,切换是在用户态完成,性能显著提升。

虚拟线程和传统线程的关系 ​

  • 虚拟线程是JVM管理的轻量级线程,和传统线程存在多对一的关系。即多个虚拟线程共享一个传统的平台线程
  • 资源消耗比传统线程小很多,一个JVM实例允许创建百万级别的虚拟线程

调度机制 ​

虚拟线程的调度完全由JVM内部负责,JVM维护了一个任务队列,包含所有待执行的虚拟线程,JVM检测到虚拟线程发生阻塞时,将其从载体线程(传统线程)接触挂载,从而提高并发性能和资源利用率

适用场景 ​

  • IO密集型任务,比如处理网络请求,文件读写等
  • 高并发的服务器应用
  • 传统同步代码的异步化

总结 ​

对于一些IO密集型的任务,可以用一个传统线程创建多个虚拟线程来提高资源利用率,因为对于IO密集型的任务相对来说CPU是有空闲时间的,所以就可以在任务阻塞的时候去做另外一个任务。但是如果是CPU密集,虚拟线程的提升就不大了

线程安全的集合和工具(难度★★★★) ​

ConcurrentHashMap ​

最重要的线程安全集合,通常伴随着HashMap的问题出现

例如:HashMap是线程安全的吗?---》不是

那什么是线程安全的Map呢?---》ConcurrentHashMap

那这个是怎么保证线程安全(底层原理)---》如下

先说底层结构,这里不再赘述 JDK1.7底层采用分段的数组+链表实现 JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。 先说1.7的结构如下图,底层使用Segment分段锁的机制来进行线程安全控制

当同时put一个元素的时候,会先根据key的hash找到对应的段segment,而每个segment里又有数组+链表的结构 然后使用ReentrantLock判断是否能拿到锁,同时利用CAS来不断尝试获取锁,等拿到锁之后在进行元素的添加

再说JDK1.8的结构,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表 采用 CAS + Synchronized来保证并发安全进行实现 CAS控制数组节点的添加 synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升 具体如下图

CopyOnWriteArrayList ​

线程安全的List实现类

首先CopyOnWriteArrayList是JDK中JUC包里提供了使用CopyOnWrite机制实现的并发容器CopyOnWriteArrayList、CopyOnWriteArraySet,其底层是利用CopyOnWrite实现的。 CopyOnWriteArrayList利用了"不变性"原理,因为容器每次修改都是创建新副本,所以对于旧容器是不可变的,也是线程安全的,无需进一步的同步操作。 可以对 CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,也不会有修改。CopyOnWriteArrayList的所有修改操作(add,set等)都是通过创建底层数组的新副本来实现的,所以 CopyOnWrite容器也是一种读写分离的思想体现,读和写使用不同的容器。 我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

Java
//JDK 11源码
public boolean add(E e) { //新增方法
    synchronized (lock) { //JDK11优化成了synchronized上锁
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}
//JDK8源码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();//JDK8还是用的ReentrantLock
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
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

CopyOnWriteArraySet ​

线程安全的Set实现类

底层和CopyOnWriteArrayList一样,就是多了个去重校验

Java
//JDK源码
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();//构造函数就是内部直接new了个CopyOnWriteArrayList
}
1
2
3
4

CountDownLatch ​

常用于多线程执行某些任务时,需要等待这些任务都执行完毕之后,再接着往下执行主流程

例如:我有一个下单的操作,这里我使用线程1去扣减库存、线程2去计算订单优惠价、线程3计算下单,三个线程并行执行,主线程要等这三个线程都执行完毕之后,再去执行新增订单的操作,就可以使用这个工具类

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完之后再执行

具体用法如下:

Java
public static void main(String[] args) throws InterruptedException {
    int count = 10;//初始10个线程
    CountDownLatch latch = new CountDownLatch(count);//设置计数器为10
    for (int i = 0; i < count; i++) {
        Thread t = new Thread(() -{
            System.out.println("start");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("over");
            latch.countDown();//执行完毕之后相当于减1
        });
        t.start();
    }

    latch.await();//会等待count减到0,也就是所有线程执行完
    System.out.println("all over");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Semaphore ​

在Java中,信号量Semaphore是一种同步辅助工具,用于控制多个线程对共享资源的访问。它是一种多线程同步机制,可以用来控制同时访问某种特定资源的线程数量,以此来实现对资源的有限访问。 简单来说就是我想控制同时执行的线程数量的时候,可以使用信号量 例如:我自己实现了一个连接池,此时我想限制最大的连接数量为10,就可以使用Semaphore

Java
Semaphore semaphore = new Semaphore(3);//定义3个指标
for (int i = 0; i < 10; i++) {//开启10个线程
    new Thread(() -{
        try {
            semaphore.acquire();//获取许可,拿到才能执行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            System.out.println("开始");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("结束");
        } finally {
            //最后要释放许可,相当于释放锁
            semaphore.release();
        }
    }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

synchronized ​

还会追问synchronized底层原理和锁升级,第三篇解答,先把普通用法搞明白

在Java中,synchronized是一个关键字,它可以用来同步线程对共享资源的访问。synchronized关键字可以用于方法或者代码块,还可以加在类上(比较冷门,用于控制构造函数的同步),以确保在同一时刻,只有一个线程能够执行特定段的代码。 synchronized是可重入锁!这个经常问到,什么是可重入,简单来说就是拿到这把锁之后继续尝试上锁,如果可以就是可重入,反之就是不可重入 举例:synchronized代码块中再次嵌套synchronized代码块,不会出现自己等自己的情况。说明可重入

Java
public class MySync {
    private static int count = 100;
    private static int money = 0;
    private static final Object LOCK = new Object();//随便定一个对象当锁对象
    public static synchronized void getTicket()  {//在方法上使用,那这个方法相当于串行(一个一个来)
        count--;
    }

    public static void getMoney() {
        synchronized (LOCK){//注意这里必须锁的是同一个对象才可以,如果对象不同则无法做到同步
            money += 100;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(MySync::getTicket).start();
            new Thread(MySync::getMoney).start();
        }
        Thread.sleep(5000);
        System.out.println("count+"+count+",money:"+money);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

ReentrantLock ​

经常会被问起和synchronized的区别

Java中的ReentrantLock是Java并发API中提供的一个可重入的互斥锁。ReentrantLock类位于JUC包中,它是实现线程同步的一个高级选项,相比于使用synchronized关键字,ReentrantLock提供了更丰富的特性和灵活性。 底层基于AQS实现 其中关键方法就是lock(),tryLock(),unlock() 面试官可能会追问AQS,下面第三篇有解答

Java

public class MyLock {
    private static int money = 0;
    private static final Lock myLock = new ReentrantLock();


    public static void getMoney()  {
        boolean result = myLock.tryLock();//尝试获取锁,拿不到就直接返回false
        //boolean result = myLock.tryLock(3, TimeUnit.SECONDS);//尝试获取锁,会尝试等3秒
        //myLock.lock(); lock方法没有返回值,会阻塞等待
        if(result){
            //拿到锁
            myLock.unlock();//做完业务要释放锁
        }else{
            //没拿到锁
            System.out.println("没抢到");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(MyLock::getMoney).start();
        }
        Thread.sleep(5000);
        System.out.println("money:"+money);
    }
}
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

AtomicInteger ​

经常会被问到的就是多线程操作int i++怎么处理

原子类,顾名思义其本身具备原子性,是线程安全的 除了AtomicInteger还有很多原子类,比如AtomicLong、AtomicLongArray等等,同学们自行查看即可 底层基于CAS(compare-and-set)技术来实现,CAS是一种思想,简单来说就是先比较再设置,修改的时候去判断一下这个值有没有被别人修改过,如果修改过就放弃这次操作

Java
public class MyAtomic {
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(() -{
                atomicInteger.addAndGet(1);//加1操作
            }).start();
        }
        Thread.sleep(1000L);
        System.out.println("atomicInteger = " + atomicInteger);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

volatile ​

不好理解就记结论

先说结论,volatile是个关键字,用于修饰变量,它有两个作用

  1. 保证线程间的可见性 用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
  2. 禁止进行指令重排序 指令重排:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

举例说明可见性案例:

Java
public class VisibilityExample {
    private static boolean running = true; // ⚠️ 没有 volatile

    public static void main(String[] args) throws InterruptedException {
        // 启动一个工作线程
        Thread worker = new Thread(() -{
            int count = 0;
            while (running) { // 循环检查 running
                count++;
            }
            System.out.println("线程结束,计数: " + count);
        });

        worker.start();

        Thread.sleep(1000); // 主线程睡1秒

        running = false; // 主线程设置 running = false,期望工作线程停止
        System.out.println("主线程已设置 running = false");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可能的结果(在多核 CPU + JIT 优化环境下):

  • 程序一直运行,永不退出!
  • 即使主线程已经把 running 设为 false,工作线程仍然看不到这个变化,继续死循环。

为什么会这样?—— 可见性问题

  1. 缓存不一致:

    • 主线程和工作线程可能运行在不同的 CPU 核心上;
    • 每个核心有自己的高速缓存(L1/L2 Cache);
    • 工作线程可能把 running 的值 缓存在自己核心的寄存器或缓存中,不再去主内存读取。
  2. JIT 编译优化:

    • HotSpot JVM 的 JIT 编译器发现 running 在当前线程内从未被修改;
    • 于是把它提升为常量或只读一次,优化成类似:
Java
boolean cached = running;
while (cached) { ... } // 永远不会重新读取 running
1
2

举例说明volatile的防指令重排序

❓问题在哪?
instance = new Singleton(); 看起来是一行代码,但在 JVM 字节码层面,它实际上包含三个步骤:

  1. 分配内存空间(为 Singleton 对象分配内存);
  2. 调用构造方法(初始化对象);
  3. 将 instance 引用指向分配的内存地址(此时 instance != null)。

🔥指令重排序的危险
在没有 volatile的情况下,JVM 或 CPU 可能对步骤 2 和 3 进行重排序,变成:

  1. 分配内存;
  2. 将instance 指向内存地址(此时对象还没初始化!);
  3. 调用构造方法(初始化对象)。 这在单线程下没问题,但在多线程环境下会出大问题!
    🧨并发场景下的灾难
    假设有两个线程 A 和 B 同时调用 getInstance():
    线程 A 执行到 new Singleton(),由于重排序,先执行了步骤 2(instance 指向内存),但还没初始化对象;
    线程 B 进入 getInstance(),看到 instance != null(因为步骤 2 已完成),直接返回一个未初始化的对象!
    线程 B 使用这个半构造对象 → 空指针异常或不可预知行为!

ThreadLocal ​

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。

ThreadLocal同时实现了线程内的资源共享 我们通常用于登录用户的信息传递,例如服务端接收到一个正常请求后,在拦截器中获取这次请求的用户信息如用户id,然后将ID放入ThreadLocal中,在后续的service中我们就可以直接从ThreadLocal取出该用户的id 根据源码我们可以看到,其本质就是里面维护了一个ThreadLocalMap,key是ThreadLocal对象引用,value是存储的值,比如用户ID之类的

追问:内存泄漏问题

首先Java里有四种引用类型:强引用、弱引用、软引用、虚引用我们平时自己new的对象都是强引用,而在ThreadLocalMap的Entry中的key采用的是**弱引用。

设置为弱引用最根本的原因就是防止内存泄露;**把 key 设置成为弱引用,就是在下一次的 GC 的时候,将弱引用指向的对象回收。在多线程中,假设多个线程使用同一个 ThreadLocal类型的变量,也就是每个线程的 ThreadLocalMap的其中一个 Entry 中的 key 使用的是同一个 ThreadLocal类型变量的地址。 举例:三个线程中的每个线程的 ThreadLocalMap的其中一个 Entry 中的 key 使用的是同一个 ThreadLocal类型变量的地址。都指向了 ThreadLocal1; 此时假设是强引用:多个线程依赖同一个 ThreadLocal1,此时 线程 1 的 ThreadLocal1使用结束了想要释放内存,但是由于是强引用(因为还有其他线程还在指向 ThreadLocal1),这就导致了线程1 的持有 ThreadLocal1的 Entry占有的内存无法释放,导致了内存泄露,使用弱引用的时候,这种问题就可以解决。 但是,key是弱引用,value是强引用,还是会出现内存泄漏的问题,因为会出现key被回收,而value还存在且回收不掉,所以我们每次在用完ThreadLocal之后,手动去调用remove()方法

CAS ​

在 Java 中,CAS(Compare And Swap,比较并交换)是一种乐观锁机制,用于实现多线程环境下的无锁并发控制。它是 Java 并发包(如java.util.concurrent)中许多原子类(如AtomicInteger、AtomicReference)的核心实现原理。

CAS 的核心思想 ​

CAS 操作包含三个关键参数:

  • 内存地址 V:要操作的变量在内存中的地址
  • 预期值 A:线程认为变量当前应该有的值
  • 新值 B:线程希望将变量修改为的值 CAS 的执行逻辑是:当且仅当内存地址 V 中的值等于预期值 A 时,才将该值更新为新值 B;否则不做任何操作。整个过程是原子性的,由 CPU 底层指令直接支持(如 x86 的cmpxchg指令),无需使用互斥锁。

Java 中 CAS 的实现原理 ​

以AtomicInteger的incrementAndGet()方法(自增操作)为例,其底层实现依赖于 CAS:

  1. Unsafe 类:Java 通过sun.misc.Unsafe类直接调用底层 CAS 指令。该类提供了compareAndSwapInt()等 native 方法,直接操作内存地址。
  2. 循环重试机制:当 CAS 操作失败(即内存中的值与预期值不符)时,会不断重试直到成功,这一过程称为 "自旋"。

CAS 的优势与问题 ​

优势: ​

  1. 无锁开销:避免了传统锁机制的上下文切换和调度开销,在低并发场景下性能更优。
  2. 粒度更细:可以精确控制到变量级别的原子操作,而不是代码块。
  3. 天然死锁免疫:不需要获取和释放锁,不存在死锁风险。

问题: ​

  1. ABA 问题:
  • 当变量从 A 变为 B 再变回 A 时,CAS 会误认为值未被修改,可能导致错误。
  • 解决方案:使用版本号机制(如AtomicStampedReference),每次更新都增加版本号。
  1. 自旋开销:
  • 高并发场景下,CAS 可能频繁失败并自旋重试,导致 CPU 占用过高。
  • 优化方案:自适应自旋(根据前一次自旋成功与否调整重试次数)。
  1. 只能保证单个变量的原子操作:
  • CAS 无法直接实现多个变量的原子操作(如同时修改两个变量)。
  • 解决方案:可以将多个变量封装成一个对象,通过AtomicReference操作对象引用。

高并发及线程安全问题(难度★★★★★) ​

Java中如何上锁 ​

使用synchronized或者Lock 具体用法参考上面 区别参考下面

synchronized和lock的区别 ​

synchronized是关键字,直接加在方法或者代码块,对对象进行上锁,没有超时机制 Lock是个接口,需要先创建出锁对象比如ReentrantLock,然后调用lock()上锁或unlock()解锁,可设置超时 两个的实现原理也不同,synchronized基于monitor监视器,Lock基于AQS

synchronized底层原理了解过吗 ​

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor 在monitor内部有三个属性,分别是owner、entrylist、waitset 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

synchronized锁升级了解过吗 ​

Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。 具体流程和实现细节大家自行搜索视频或者面试专题 这里只说结论 一个线程持有时,先使用偏向锁 有不同线程交替使用,升级成轻量级锁 有锁竞争升级成重量级锁

AQS了解过吗? ​

全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架 常见实现类有

  • ReentrantLock阻塞式锁
  • Semaphore信号量
  • CountDownLatch倒计时锁 AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程 在AQS内部还有一个属性state(volatile修饰),这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源 在对state修改的时候使用的CAS操作,保证多个线程修改的情况下原子性
  • /**
    Inserts node into queue, initializing if necessary. See picture above.
    @param node the node to insert
    @return node's predecessor
    */
    private Node enq(Node node) {
    for (;😉 {
    Node oldTail = tail;
    if (oldTail != null) {
    node.setPrevRelaxed(oldTail);
    if (compareAndSetTail(oldTail, node)) {//CAS操作
    oldTail.next = node;
    return oldTail;
    }
    } else {
    initializeSyncQueue();
    }
    }
    }

AQS和Synchronized的区别 ​

synchronizedAQS
关键字,c++ 语言实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
悲观锁,手动开启和关闭锁竞争激烈的情况下,提供了多种解决方案

怎么排查死锁问题 ​

大概率会先问什么是死锁,然后再问怎么排查

这里建议同学们提前准备好一个场景,结合你简历项目比如业务A拿到锁1之后,又需要加锁2,然后业务B也需要加锁。。。之类的具体场景,是在编不出来就网上搜 "记录一次排查死锁的经历",把别人的东西看懂了,那就是你自己的

死锁: ​

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。就好比两个人在一条狭窄的走廊上迎面走来,都不愿意退让,结果谁也无法通过,双方就陷入了僵持状态。在计算机系统中,当一个进程占有一部分资源并等待另一个进程占有的资源,而另一个进程也在等待这个进程占有的资源时,就会发生死锁,这会导致系统无法继续正常运行,影响程序的执行效率。

避免死锁的方法: ​

常用套路: 破坏互斥条件、避免嵌套锁、保证锁的顺序、使用锁有超时时间通俗的说就是确保拿资源的顺序大家都一样,别同时拿太多资源,拿不到资源就等一等再试试,如果还是不行就放手,过会儿再来。简单来说,就是有顺序、不贪心、懂放手、会等待。

如何排查思索: ​

我们可以使用jdk自带的工具:JPS和 JStackJPS:输出JVM中运行的进程状态信息 JStack:查看java进程内线程的堆栈信息 具体结合下图 注意:这两个命令的死锁排查的是代码中的死锁场景,而实际开发过程中,我们更多可能会遇到的是代码里的锁和Mysql数据库的锁相互死锁,这种死锁排查就要结合数据库的信息来综合判断了

场景题 ​

你在项目中有用过多线程吗?怎么使用的? ​

结合自己项目具体情况,比如天机学堂里的批量生成兑换码,这个提前准备好比如四方保险中,一些第三方的接口通过线程池来异步执行比如分片上传的时候,通过线程池+CountDownLatch批处理执行 初始化了一个线程池,然后多线程生成 但是注意在SpringBoot框架中,我们是采取的@EnableAsync注解先开启异步 然后写一个配置类ExecutorConfiguration,在这个配置类里使用@Bean注解 返回一个Executor线程池对象,然后在需要多线程执行的方法上加入@Async注解 注解里填的就是Bean的名字,不填就用Spring默认线程池

在项目中有没有遇到过并发安全问题? ​

这个问题也适用于,你在哪里用过锁

常出现于抢红包,抢优惠券之类的高并发接口,结合自己实际项目回答 库存的扣减解决方案有很多种 这里拿天机举例 数据库中有个字段issue_num就是优惠券发放数量,每个用户抢到就会把这个字段+1 原始sql:update coupon set issue_num = issue_num + 1 where id = ? 使用乐观锁 update coupon set issue_num = issue_num + 1 where id = ? and issue_num < total_num ,其中total_num就是总的发放数量,由于update语句本身mysql就会加锁,所以多了个issue_num<total_num的条件之后,就不是每个人都能修改成功,我们根据实际修改的数据条数的这个结果来判断,也就是返回值判断是否成功即可 还可以使用Redis的incrby命令,例如我们定义一个key的value是100,然后通过incrby每次-1得到扣减之后的结果,根据结果>0判断是否扣减成功,由于redis是单线程执行,所以也不会出现并发安全问题 如果是新增操作,比如用户领完券要往数据库中新增一条记录 这里我们可以采取悲观锁的思想,使用基于redis的分布式锁,本质上是redis的set nx命令,但是自己写的分布式锁会出现一些极端问题,比如锁超时、超时误删等问题 所以我们通常会使用redisson提供的分布式锁,底层原理参考Redis篇面试题

现在我有一个ArrayList如果要多线程操作,怎么保证不出问题? ​

使用CopyOnWriteArrayList,Set同理 或者

我现在主线程里创建了多个线程,如何保证这些线程按顺序执行? ​

顺序执行可以使用join方法

Java
public static void main(String[] args) {
    Thread t1 = new Thread(() -{
        System.out.println("t1 start");
    });

    Thread t2 = new Thread(() -{
        try {
            t1.join();//加入t1线程,等待执行完之后再执行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("t2 start");
    });

    Thread t3 = new Thread(() -{
        try {
            t2.join();//加入t2线程,等待执行完之后再执行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("t3 start");
    });
    t1.start();
    t2.start();
    t3.start();
}
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

追问:如何保证这些线程执行完之后再接着走主流程? ​

利用CountDownLatch,具体用法参考上面

我现在有个ArrayList要多线程操作,怎么处理 ​

使用CopyOnWriteArrayList,追问底层原理就参考上面线程安全工具篇

如何控制我一个方法同时只有三个线程在同时执行 ​

使用Semaphore信号量,参考上面线程安全线程安全工具篇

多线程操作int i++,会出现问题吗? ​

有问题,因为++操作不具备原子性,可以使用AtomicInteger或者synchronized上锁 追问:如果对int i修饰volatile,能解决多线程操作i++的问题吗,不能

一个类上里面两个方法一个静态一个非静态都用synchronized修饰,多线程创建类的实例请求静态锁会阻塞吗,请求普通方法会阻塞吗 ​

不废话,大家自己写代码实验,其实也是问synchronized修饰静态方法和非静态方法的区别 下面代码t1和t2之间不会阻塞(不是同一个对象) t3和t4之间会出现阻塞

Java
log.info("case1-------");
//两个实例之间是否会阻塞
MySync2 obj1 = new MySync2();
Thread t1 = new Thread(()->{
    try {
        obj1.method();//先拿到锁
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});
MySync2 obj2 = new MySync2();
Thread t2 = new Thread(()->{
    try {
        obj2.method();//先拿到锁
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});
t1.start();
t2.start();
Thread.sleep(4000L);
log.info("case2---------------");
//实例方法和静态方法会被阻塞吗?
Thread t3 = new Thread(()->{
    try {
        MySync2.sMethod();//会被阻塞吗
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});
Thread t4 = new Thread(()->{
    try {
        MySync2.sMethod();//会被阻塞吗
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});
t3.start();
t4.start();
Thread.sleep(4000);
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
30
31
32
33
34
35
36
37
38
39
40

了解过ThreadLocal吗?什么场景下会产生内存泄漏? ​

注意:内存泄漏并不是一定会出现,而是线程池场景有可能会出现,因为线程池场景线程并不会被销毁,就导致ThreadLocal一直存储新的值且Map里的value回收不掉 但是如果我们自己new一个线程然后把线程都销毁掉,那自然里面的ThreadLocalMap也没了,也就不会出现内存泄漏

同一个线程内共享变量用什么?如果是父子线程想要共享变量用什么? ​

ThreadLocal一个线程内共享 InheritableThreadLocal父子线程内共享

两个线程,一个打印hello,一个打印world,循环10次,怎么让他们依次打印出10次hello world ​

Java
public static void main(String[] args) {
    Object lock = new Object();//使用一个对象当锁
    Thread thread1 = new Thread(() -{
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                System.out.print("hello ");
                lock.notify();//唤醒等待的线程
                if (i < 9) {
                    try {
                        lock.wait();//进入wait状态
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    });

    Thread thread2 = new Thread(() -{
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                System.out.println("world");
                lock.notify();
                if (i < 9) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    });

    thread1.start();//先启动thread1
    thread2.start();
}
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
30
31
32
33
34
35
36
37

现在有三个线程,分别会打印A、B、C,这三个线程同时启动,让他们交替打印A、B、C,并循环100次 ​

java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class PrintABC {
    private static final int PRINT_COUNT = 100; // 每个字母打印的次数

    private ReentrantLock lock = new ReentrantLock();//锁
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    // 当前状态,用于控制哪个线程可以打印
    private volatile int state = 0; // 0 -A, 1 -B, 2 -C

    public void printA(int i) {
        lock.lock();
        try {
            while (state != 0) {
                conditionA.await(); // 不是A的状态就等待
            }
            System.out.print("A ");
            state = 1;
            conditionB.signal(); // 唤醒B线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void printB(int i) {
        lock.lock();
        try {
            while (state != 1) {
                conditionB.await(); // 不是B的状态就等待
            }
            System.out.print("B ");
            state = 2;
            conditionC.signal(); // 唤醒C线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void printC(int i) {
        lock.lock();
        try {
            while (state != 2) {
                conditionC.await(); // 不是C的状态就等待
            }
            System.out.print("C ");
            state = 0;
            conditionA.signal(); // 唤醒A线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintABC printABC = new PrintABC();

        Thread threadA = new Thread(() -{
            for (int i = 0; i < PRINT_COUNT; i++) {
                printABC.printA(i);
            }
        }, "Thread-A");

        Thread threadB = new Thread(() -{
            for (int i = 0; i < PRINT_COUNT; i++) {
                printABC.printB(i);
            }
        }, "Thread-B");

        Thread threadC = new Thread(() -{
            for (int i = 0; i < PRINT_COUNT; i++) {
                printABC.printC(i);
            }
        }, "Thread-C");

        // 启动线程
        threadA.start();
        threadB.start();
        threadC.start();

        // 等待所有线程结束
        try {
            threadA.join();
            threadB.join();
            threadC.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

Condition 是 Java 中用于管理线程间通信的接口,它与 ReentrantLock 一起使用,提供了比传统的 Object 监视器方法(如 wait, notify 和 notifyAll)更强大的功能。Condition 可以让你更加灵活地控制线程间的协作。

← Java基础常见面试题数据库常见面试题 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系方式
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节