Java并发编程面试题

一、线程基础

1. 创建线程的几种方式?

答案

  1. 自定义类来继承 Thread 类,重写 run() 方法。

  2. 实现 Runnable 接口 :重写 run(),将实例传给 Thread 对象。

  3. 实现 Callable 接口 :有返回值,配合 FutureTask

    java 复制代码
    class MyCallable implements Callable<String> {
        public String call() throws Exception { return "result"; }
    }
    FutureTask<String> ft = new FutureTask<>(new MyCallable());
    new Thread(ft).start();
    String result = ft.get();
  4. 使用线程池 (推荐):通过 Executors 工厂类或 ThreadPoolExecutor 创建。

2. 线程生命周期

在 Java 中,线程的生命周期由 Thread.State 枚举定义了 6 种状态,分别是:

  1. NEW(新建)
    线程对象被创建后(比如 new Thread()),但还没有调用 start() 方法。此时线程还未获得执行资源。
  2. RUNNABLE(可运行)
    调用了 start() 后,线程处于 RUNNABLE 状态。在 JVM 层面,它可能是正在运行,也可能在等待操作系统分配时间片(也就是传统意义上的 READY 和 RUNNING 的合并状态)。
  3. BLOCKED(阻塞)
    线程等待进入 synchronized 代码块或方法时,因为锁被其他线程持有而被阻塞。当锁释放后,它会重新回到 RUNNABLE 状态参与竞争。
  4. WAITING(无限等待)
    线程调用了 Object.wait()(不设超时)、Thread.join()(不设超时)或 LockSupport.park() 后,会进入 WAITING 状态。它需要被其他线程显式唤醒(如 notify/notifyAll,或等待的线程结束)。
  5. TIMED_WAITING(计时等待)
    类似 WAITING,但带有超时时间。例如 Thread.sleep(time)wait(timeout)join(timeout)LockSupport.parkNanos() 等。时间到期或被唤醒后,会返回 RUNNABLE
  6. TERMINATED(终止)
    线程的 run() 方法正常执行完毕,或者因异常而结束。一旦终止,就不能再通过 start() 重新启动。
关键状态转换
  • NEW → RUNNABLE :调用 start()
  • RUNNABLE → WAITINGwait()(需先获得锁)、join()LockSupport.park()
  • RUNNABLE → TIMED_WAITINGsleep(time)wait(time)join(time)parkNanos()
  • RUNNABLE → BLOCKED :尝试进入 synchronized 块但锁被占用
  • WAITING/TIMED_WAITING → RUNNABLE :被 notify/notifyAll 唤醒、超时到期、等待的线程结束
  • BLOCKED → RUNNABLE:锁被释放,线程获得锁
  • RUNNABLE → TERMINATEDrun() 结束或抛出未捕获异常

注意:sleep()yield() 不会释放锁,而 wait() 会释放锁并等待队列。

3. yield、join、wait、sleep 方法

Thread.yield() 是一个静态方法,提示线程调度器当前线程愿意主动让出 CPU 时间片,让同级或更高级的线程先执行。它不会释放锁,也不会改变线程状态(仍为 RUNNABLE)。实际中由于平台依赖性强,效果不确定,很少用于生产代码,一般用 sleep(0)LockSupport 替代。

join()Thread 类的一个实例方法,作用是让当前线程阻塞等待目标线程执行完毕。它的底层实现依赖 wait/notify:调用 join() 时,如果目标线程还活着,当前线程会在目标线程对象上调用 wait() 进入阻塞,直到目标线程结束后 JVM 自动调用 notifyAll() 将其唤醒。join() 可以带超时参数,响应中断,并且在等待期间只会释放目标线程对象的锁,不会释放其他锁。

yield() 不同,join() 会导致线程进入 WAITING/TIMED_WAITING 状态,而 yield() 保持在 RUNNABLE 状态

wait()Object 类的实例方法,必须在 synchronized 代码块内调用,否则会抛异常。调用 wait() 会使当前线程进入 WAITING 状态,并释放该对象的锁 ,让其他线程有机会进入同步块。当其他线程调用同一对象的 notify()notifyAll() 时,被唤醒的线程会重新竞争锁,拿到锁后从 wait() 返回继续执行。为了避免虚假唤醒,必须使用 while 循环而不是 if 来检查条件。与 sleep() 不同,wait() 会释放锁,而 sleep() 不会。wait() 是实现生产者-消费者模式的基础。

sleep()Thread 的一个静态方法,让当前线程暂停执行指定的时间,进入 TIMED_WAITING 状态。它不会释放任何锁 ,因此如果线程持有锁,其他线程即使进入同步块也得继续等待。sleep() 可以响应中断,抛出 InterruptedException。与 wait() 不同,wait() 需要配合 synchronized 使用且会释放锁;与 yield() 不同,yield() 只是让出CPU但状态仍是 RUNNABLE,而 sleep() 确确实实让线程休眠一段至少给定的时间。实际开发中,常用 sleep() 做简单延时或降低轮询频率,但精确任务调度应使用定时器或线程池。

特性 wait() sleep() join() yield()
所属类 Object Thread Thread Thread
调用前提 必须在同步块中 任意位置 任意位置 任意位置
释放锁 释放对象锁 不释放 不释放(只释放目标线程的锁,但目标线程已经结束) 不释放
线程状态 WAITING / TIMED_WAITING TIMED_WAITING WAITING / TIMED_WAITING RUNNABLE
唤醒方式 notify/notifyAll 或超时 超时或中断 目标线程结束或超时 不可主动唤醒(仅CPU重新竞争)
典型用途 线程间协作(等待条件) 暂停执行一段时间 等待另一个线程结束 提示调度器让出CPU

4. wait() 后线程什么时候被唤醒?notify 和 notifyAll 的区别?唤醒后需要重新获取 monitor 吗?

调用 wait() 后,线程进入等待状态,可以被 notifynotifyAll、超时或中断唤醒。

notify 只唤醒一个等待线程,notifyAll 唤醒所有等待线程,推荐使用 notifyAll 以避免信号丢失。

无论哪种唤醒,线程都必须重新获取 monitor(锁) 才能从 wait() 返回继续执行。在获取锁之前,线程处于 BLOCKED 状态

5. 用两个线程交替 1-100

java 复制代码
public class AlternatePrint {

    private static final Object lock = new Object();
    private static int num = 1;
    private static final int MAX = 100;

    public static void main(String[] args) {
        Thread oddThread = new Thread(() -> {
            while (num <= MAX) {
                synchronized (lock) {
                    if (num % 2 == 1 && num <= MAX) {   // 奇数
                        System.out.println(Thread.currentThread().getName() + " -> " + num);
                        num++;
                        lock.notify();   // 唤醒另一个线程
                    } else {
                        try {
                            lock.wait(); // 不是自己的轮次,等待
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            }
        }, "奇数线程");

        Thread evenThread = new Thread(() -> {
            while (num <= MAX) {
                synchronized (lock) {
                    if (num % 2 == 0 && num <= MAX) {   // 偶数
                        System.out.println(Thread.currentThread().getName() + " -> " + num);
                        num++;
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            }
        }, "偶数线程");

        oddThread.start();
        evenThread.start();
    }
}

二、锁与同步

1. volatile 和 synchronized 的区别?

答案

  • volatile:轻量级,只能修饰变量,保证可见性和有序性(禁止指令重排),不保证原子性。
  • synchronized:重量级,可修饰方法/代码块,保证原子性、可见性、有序性。JDK6后已优化(锁升级)。
  • 适用场景:volatile适合单写多读的状态标志;复杂操作(如i++)必须用synchronized或Atomic类

2. synchronized 的锁升级过程?

答案(JDK 1.6 优化后):

  • 无锁:对象刚创建,没有线程竞争。
  • 偏向锁:第一个线程获取锁时,会在对象头中记录线程 ID,无需 CAS。适用于同一线程反复获取同一锁的场景。
  • 轻量级锁:当另一个线程来竞争时,偏向锁升级为轻量级锁。线程通过 CAS 尝试获取锁,失败则自旋。
  • 重量级锁:自旋超过一定次数(JDK 1.6 自适应自旋)或并发激烈时,升级为重量级锁,最终依赖操作系统的互斥锁来保证正确性。

3. synchronized 和 Lock 的区别?

核心维度 synchronized LockReentrantLock
类型 Java 内置关键字 接口
使用方式 自动获取/释放锁 手动 lock() / unlock()(必须 finally 释放)
可中断性 不可中断,一直阻塞 可中断(lockInterruptibly()
超时尝试 不支持 支持 tryLock(timeout)
公平锁 只能是非公平 可构造公平锁(new ReentrantLock(true)),默认非公平
条件变量 通过 wait()/notify()/notifyAll() 实现,每个对象只有一个等待队列,无法指定唤醒哪类线程 可通过 newCondition() 创建多个 Condition,实现分组等待与精确唤醒

简单场景用 synchronized,需要高级特性时用 Lock

4. volatile 原理

可见性原理volatile 修饰的变量,写操作会立即刷新到主内存 ,读操作会从主内存重新读取 ,不使用工作内存副本。因此一个线程对 volatile 变量的修改,对其他线程立即可见。

禁止指令重排序原理 :通过内存屏障 实现,写操作前后插入屏障(StoreStore 前,StoreLoad 后),读操作后插入屏障(LoadLoadLoadStore),确保 volatile 变量前后的代码不会被重排序越过该变量,从而保证有序性。

5. 什么是 CAS?有什么问题?

CAS 是乐观锁 思想的一种典型实现。它假设并发冲突很少发生,因此在操作时不加锁,而是在提交更新时检查是否有其他线程修改过数据。如果更新失败,线程通常会通过自旋(循环重试)的方式再次尝试,直到成功为止。

  • CAS是一种原子操作,包含三个操作数:内存位置 V、期望值 A、新值 B。只有当 V 的值等于 A 时,才将 V 更新为 B,否则什么都不做。整个过程是原子的。
  • 问题
    1. ABA 问题 :值从 A 变成 B 又变回 A,CAS 会认为没有变化。解决方案 :为变量添加一个版本号时间戳
    2. 循环时间长开销大:自旋 CAS 长时间不成功会消耗 CPU。
    3. 只能保证单个变量的原子操作:多个变量需要锁。

示例AtomicIntegerincrementAndGet() 内部就是 CAS 自旋。

6. 什么是 AQS(AbstractQueuedSynchronizer)?

答案

AQS 是 java.util.concurrent 包中构建锁和同步器的框架 ,它采用了模板方法模式 。内部维护了一个 volatile int state(表示同步状态)和一个 FIFO 双向等待队列 (CLH 锁变体)。

工作方式

  • 子类通过实现 tryAcquire() / tryRelease()(独占模式)或 tryAcquireShared() / tryReleaseShared()(共享模式)来定义如何修改 state
  • 获取同步状态失败时,AQS 将当前线程封装成 Node 节点加入队列,并通过 LockSupport.park() 阻塞;成功时则尝试唤醒队列中的后继节点。
    常见子类ReentrantLock(独占可重入)、Semaphoreˈseməfɔː®(共享信号量)、CountDownLatch(共享倒计数)、ReentrantReadWriteLock(内部用两个 AQS 分别管理读锁和写锁)。
    一句话总结AQS 把复杂的同步逻辑抽象为"状态 + 队列 ",子类只需实现 tryAcquiretryRelease 等方法。

7. a++ 是线程安全的吗?

a++ 不是线程安全的。因为它不是一个原子操作,包含了读取、加一、写入三个步骤。多线程并发执行时,可能出现线程A 读取后还未写入,线程B 也读取了相同的旧值,导致最终结果比预期少一次自增。要保证安全,可以使用 synchronizedReentrantLock,或者更推荐使用 AtomicInteger(呃涛米克) 的 incrementAndGet() 方法,它基于 CAS 实现,无锁且高效。


三、线程池

1. 线程池的核心参数及工作流程?

核心参数ThreadPoolExecutor 构造方法):

  • corePoolSize:核心线程数,即使空闲也会保留。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:非核心线程空闲存活时间。
  • unit:时间单位。
  • workQueue:阻塞队列(如 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue)。
    • ArrayBlockingQueue:有界队列,推荐使用,可防止任务无限堆积导致内存溢出。
    • LinkedBlockingQueue:默认是无界队列,任务可能无限堆积,有内存溢出风险。
    • SynchronousQueue:不存储元素的同步队列,任务必须直接交付给线程,否则就创建新线程。
  • threadFactory:线程工厂,用于创建新线程。
  • handler:拒绝策略(当队列满且线程数达到最大时触发)。

🔄 核心工作流程

工作流程

  1. 提交任务时,如果运行的线程数 < corePoolSize,直接创建新线程执行。
  2. 如果 ≥ corePoolSize,尝试将任务放入 workQueue
  3. 如果队列已满,且线程数 < maximumPoolSize,创建非核心线程执行。
  4. 如果线程数 ≥ maximumPoolSize,执行拒绝策略。

拒绝策略

  • AbortPolicy:抛异常(默认)。
  • DiscardPolicy:静默丢弃。
  • CallerRunsPolicy:由调用者线程执行。
  • DiscardOldestPolicy:丢弃队列头任务,重试提交。

2. new ThreadPoolExecutor 的核心线程数怎么计算得到的?

"关于 ThreadPoolExecutor 核心线程数的设置,在实际工作中,我通常会先分析业务的任务类型,主要分为 CPU 密集型I/O 密集型 两种场景来评估:

1. 如果是 CPU 密集型任务 (比如复杂的数学运算、加密解密、图像处理等):

这类任务会一直消耗 CPU 资源。如果线程数过多,频繁的线程上下文切换反而会浪费 CPU 性能。

  • 计算公式: 核心线程数 = CPU 核心数 + 1
  • 补充说明: 多加的 1 个线程,是为了在某个线程因为偶尔的页缺失或异常阻塞时,CPU 依然能保持忙碌状态,不浪费计算资源。

2. 如果是 I/O 密集型任务 (比如数据库查询、远程 RPC/HTTP 调用、文件读写等):

这类任务大部分时间线程都在等待 I/O 响应,处于阻塞状态,不占用 CPU。因此我们可以配置更多的线程,让 CPU 在等待期间去处理其他任务。

  • 经验公式: 在快速评估时,通常会设置为 CPU 核心数 * 2

  • 科学公式:如果想要更精准的配置,我会使用《Java并发编程实战》中的经典公式:

    复制代码
    最佳线程数 = N_cpu * U_cpu * (1 + W / C)
    • N_cpu:CPU 核心数(通过 Runtime.getRuntime().availableProcessors() 获取)。
    • U_cpu:期望的 CPU 利用率(通常设为 0.7~0.9,预留资源给系统)。
    • W / C:线程等待时间(Wait)与计算时间(Compute)的比率。
    • 举个例子: 假设 8 核 CPU,目标利用率 80%,任务的等待时间是计算时间的 4 倍,那么核心线程数就是 8 * 0.8 * (1 + 4) = 32 个。

3. 实战中的最终落地:

当然,无论是经验公式还是科学公式,计算出来的都只是理论基准值 。在实际生产环境中,我一定会结合线上压测 (比如用 JMeter)和实时监控(观察 CPU 利用率、任务排队积压情况、接口响应时间等指标)来进行动态微调,最终找到最契合当前业务场景的'黄金参数'。"

3. 为什么不建议使用 Executors 工具类去创建线程池?

这是因为 Executors 提供的快捷方法隐藏了线程池的关键配置细节,在真实的高并发生产环境中,极易引发内存溢出(OOM)系统资源耗尽等严重问题。具体可以分为以下两种场景来理解:

1. 固定线程池的隐患(newFixedThreadPoolnewSingleThreadExecutor

这两个方法底层默认使用的是无界队列 LinkedBlockingQueue(容量默认为 Integer.MAX_VALUE)。

  • 风险: 当任务提交的速度远大于线程处理的速度时(例如大促期间订单暴增,但下游短信接口变慢),任务会源源不断地堆积在队列中。由于队列几乎无界,最终会耗尽堆内存,导致系统抛出 OutOfMemoryError(OOM),甚至引发系统雪崩。

2. 缓存线程池的隐患(newCachedThreadPool

这个方法的最大线程数被设置为了 Integer.MAX_VALUE

  • 风险: 当线上出现突发的高并发流量时,线程池会无限制地创建新线程来应对任务。这不仅会瞬间耗尽系统的 CPU 资源(导致频繁的上下文切换),还会因为每个线程默认占用 1MB 栈内存而迅速耗尽服务器内存,同样会引发 OOM 或系统假死。

一句话总结

Executors 工具类默认允许无界队列或无限创建线程,在高并发场景下极易导致内存溢出(OOM)或系统资源耗尽,因此生产环境必须使用 ThreadPoolExecutor 手动创建线程池以实现可控的并发管理。

4. 线程池中 submit() 和 execute() 方法有什么区别?

答案

特性 execute() submit()
所属接口 Executor ExecutorService
参数类型 只支持Runnable 支持 RunnableCallable
返回值 无返回值 (void) 返回 Future 对象
异常处理 异常直接抛出,主线程无法捕获 异常被封装在 Future 中,调用 get() 时抛出

四、并发工具与通信

1. CountDownLatch 和 CyclicBarrier 的区别是什么?

CountDownLatch 是一个一次性的倒计时门闩(shuān),核心是一对多的单向等待。适用场景是一个主线程等待多个子线程完成任务。

CyclicBarrierˈsaɪklɪk ˈbæriər)是一个可循环使用的栅栏,核心是多对多的相互等待,所有线程都到达同一个公共屏障点(调用 await())后才能一起继续执行。适合多阶段的任务。

java 复制代码
// CountDownLatch: 主线程等待 3 个子线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // do work
        latch.countDown();
    }).start();
}
latch.await(); // 主线程等待

// CyclicBarrier: 3 个线程互相等待,同时开始下一阶段
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("都到了,出发!"));
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            barrier.await(); // 等待其他线程
            // 同时执行后续操作
        } catch (Exception e) {}
    }).start();
}

2. Semaphore 的原理和使用场景是什么?

Semaphore ˈseməfɔː®基于 AQS 维护许可计数器,用于控制同时访问某资源的线程数量(如限流、连接池管理)。

java 复制代码
// 最多允许 10 个线程同时执行
Semaphore semaphore = new Semaphore(10);
semaphore.acquire();
try {
    // 访问资源
} finally {
    semaphore.release();
}

3. ReadWriteLock 读写锁的原理?读写锁有哪些适用场景?

ReadWriteLock 的核心原理是读写分离 ,通过维护一对锁来实现读读不互斥,读写、写写互斥,从而大幅提升'读多写少'场景下的并发性能。

以 Java 的 ReentrantReadWriteLock 为例,它底层基于 AQS 实现。为了在 int 类型的 state 变量中同时记录读锁和写锁的状态,它采用了**'按位切割'**的设计:高 16 位用于记录读锁的获取次数(共享计数),低 16 位用于记录写锁的重入次数(独占计数)

此外,它还有一个重要特性是支持锁降级 (写锁可以降级为读锁),但不支持锁升级(读锁不能升级为写锁,否则会死锁)。在实际应用中,它默认是非公平锁以保证高吞吐,但也支持配置为公平锁来避免写线程饥饿。

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LocalCache<K, V> {
    // 共享的缓存数据存储
    private final Map<K, V> cache = new HashMap<>();
    // 创建读写锁实例
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 获取读锁和写锁
    private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    /**
     * 获取缓存数据(读操作)
     * 多个线程可以同时调用此方法,互不阻塞
     */
    public V get(K key) {
        readLock.lock(); // 1. 加读锁
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取 key: " + key);
            // 模拟读取耗时
            Thread.sleep(100); 
            return cache.get(key);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            readLock.unlock(); // 2. 必须在 finally 中释放读锁
        }
    }

    /**
     * 写入/更新缓存数据(写操作)
     * 同一时刻只能有一个线程执行此方法,且会阻塞所有读操作
     */
    public void put(K key, V value) {
        writeLock.lock(); // 1. 加写锁(独占)
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入 key: " + key);
            // 模拟写入耗时
            Thread.sleep(500); 
            cache.put(key, value);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            writeLock.unlock(); // 2. 必须在 finally 中释放写锁
        }
    }

}

4. 什么是 Future?CompletableFuture 相比 Future 有什么改进?

Future 代表异步结果,但只能阻塞获取且难以组合;CompletableFuture 提供了非阻塞的回调链、异常处理、多任务组合等函数式能力,是 Java 异步编程的进化版本

java 复制代码
// 非阻塞的回调链
CompletableFuture.supplyAsync(() -> "hello")
    .thenApply(s -> s + " world")
    .thenAccept(System.out::println);  // 异步打印 "hello world"
// 异常处理
CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("出错了");
    return "ok";
}).exceptionally(ex -> "默认值: " + ex.getMessage())
  .thenAccept(System.out::println);
// 任务组合
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 2);
f1.thenCombine(f2, (a, b) -> a + b)
  .thenAccept(System.out::println); // 输出 3

五、高级特性

1. ThreadLocal 的原理及内存泄漏问题?

答案

  • 原理 :每个 Thread 内部有一个 ThreadLocalMap(类似 HashMap),key 是 ThreadLocal 对象的弱引用,value 是存储的值。
    • 当调用 set() 时,根据当前线程找到其 map,将 value 存入。
    • get() 同理。
  • 内存泄漏 :由于 key 是弱引用,当 ThreadLocal 对象没有强引用时,下一次 GC 就会回收 key,导致 map 中出现了 key 为 null 的 entry,但 value 仍然存在(强引用链:Thread → ThreadLocalMap → Entry → value)。如果线程长期运行(如线程池),这些 value 永远无法被访问,导致内存泄漏。
  • 解决方法 :使用完 ThreadLocal 后,显式调用 remove() 方法删除 entry。
  • 正确使用 :通常将 ThreadLocal 声明为 private static,使得多个实例共享同一个 ThreadLocal 对象(避免重复创建),并且记得在 finally 块中 remove()

2. 什么是 happens-before 原则?列举几个常见的 happens-before 规则。

happens-before 是 Java 内存模型(JMM)中判断数据是否存在竞争、线程是否安全的核心依据。如果操作A happens-before 操作B,则A的结果对B可见。常见规则包括:

  • 程序次序规则:单线程内代码顺序执行。
  • volatile规则 :线程 A 写了一个 volatile 变量,那么在线程 A 写操作之后任何线程 B 再去读这个变量,一定能读到线程 A 写入的最新值。
  • 锁规则:一个线程在释放锁之前对共享变量的所有修改,对于后续获取到同一把锁的线程来说都是可见的。

3. 并发编程的三大特性是什么?分别如何保证?

1、可见性:一个线程修改了共享变量,其他线程能立即看到。

  • volatile :被 volatile 修饰的变量,在修改后会强制刷新到主内存,其他线程需要从主内存读取。
  • synchronized / Lock:遵循 Happens-Before 原则,线程在释放锁之前,必须把共享变量的最新值刷新到主内存;获取锁时,会清空工作内存,从主内存重新加载最新值。
  • final :被 final 修饰的字段,在构造器初始化完成后,对其他线程立即可见。

2、原子性:操作不可被中断,要么全做要么全不做。

  • synchronized / Lock
  • JUC 原子类(如 AtomicInteger:它基于 CAS 无锁机制来保证单个变量操作的原子性
  • 注意:volatile不能保证原子性

3、有序性:禁止指令重排序,保证程序执行顺序。

  • volatile:通过插入内存屏障(Memory Barrier),禁止特定类型的指令重排序(例如禁止将 volatile 写操作之前的普通写操作重排到其后)。
  • synchronized / Lock:同样通过内存屏障,保证同一时刻只有一个线程执行,从而间接保证了代码块内操作的有序性。
  • Happens-Before 原则:JMM 定义的一系列先行发生规则(如程序次序规则、锁规则、volatile 规则等),从规范层面保证了操作的有序性。
特性 核心问题 常用保证机制
可见性 线程读取到过期的旧值 volatilesynchronizedLockfinal
原子性 操作被线程切换打断 synchronizedLockJUC原子类
有序性 编译器/CPU 指令重排 volatilesynchronizedLock

4. 什么是指令重排?为什么要进行指令重排?如何禁止指令重排?

指令重排 是指编译器和处理器为了优化性能,在不改变单线程程序语义 的前提下,对指令的执行顺序进行调整的行为。目的是提升性能

禁止方法 :使用 volatilesynchronizedLockfinal 或显式内存屏障

5. 线程中断机制:interrupt()、isInterrupted() 的区别?

1. interrupt() -- 设置中断标志
  • 作用 :将目标线程的中断标志设置为 true
  • 特殊行为 :如果目标线程正处于 wait()sleep()join()可中断阻塞 状态,则会抛出 InterruptedException(同时清除中断标志,因为异常被抛出后标志已无意义)。
  • 示例thread.interrupt();
2. isInterrupted() -- 仅测试中断标志(不清除)
  • 作用 :检查目标线程的中断标志是否为 true不修改标志
  • 调用方式 :实例方法,通过线程对象调用,如 thread.isInterrupted()
  • 返回值 :若中断标志为 true 则返回 true,否则 false

interrupt() 设置中断标志;isInterrupted() 检查标志(不清除);

优雅停掉代码的实现方式

java 复制代码
// 1. 在循环中,推荐使用 isInterrupted() 持续检查当前线程是否被中断
while (!Thread.currentThread().isInterrupted()) {
    try {
        // 模拟耗时或阻塞任务
        Thread.sleep(1000); 
    } catch (InterruptedException e) {
        // 2. 捕获到异常后,JVM 已经把标志位清除了。
        // 如果你想让外层的 while 循环也能感知到中断并退出,
        // 必须在这里手动再次调用 interrupt() 把标志位"补"回去!
        Thread.currentThread().interrupt(); 
        break; // 退出循环
    }
}

6. 什么是守护线程?设置守护线程需要注意什么?

守护线程(Daemon Thread)是一种后台线程,它的存在依赖于用户线程(非守护线程)。当 JVM 中所有用户线程都结束后,JVM 会立即退出,不会等待守护线程执行完毕。典型例子:JVM 的垃圾回收线程

注意事项:

必须在 start() 前调用 setDaemon(true)ˈdiːmən,且不能持有必须要释放的临界资源。


六、问题排查

1. 什么是死锁?如何避免?

答案

  • 死锁:两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。
  • 产生条件 (缺一不可):
    1. 互斥:资源不能被共享。
    2. 持有并等待:线程持有至少一个资源,并等待其他资源。
    3. 不可剥夺:资源不能被强制抢占。
    4. 循环等待:线程间形成等待环。
  • 避免方法
    • 破坏"持有并等待":一次性申请所有资源。
    • 破坏"不可剥夺":获取不到所需资源时,释放已占有的资源。
    • 破坏"循环等待":按固定顺序申请资源。
    • 使用 tryLock(ReentrantLock)超时放弃。

2. 如何排查死锁?有哪些工具或命令?

排查死锁(如果 CPU 不高但系统挂起)

步骤一:首先用 ps -ef jps找到PID

步骤二:执行jstack命令导出线程堆栈

bash 复制代码
jstack -l PID > thread_dump.txt

步骤三:分析线程堆栈,例如

复制代码
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8a1c0a6a00 (object 0x00000007d5a8a5a0, a java.lang.Object)
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f8a1c0a6a80 (object 0x00000007d5a8a5b0, a java.lang.Object)
  which is held by "Thread-1"
相关推荐
码不停蹄的玄黓1 小时前
JDK 自带四大命令行工具:jstat、jstack、jmap、jhat 详解
java·开发语言
ch.ju1 小时前
Java程序设计(第3版)第四章——set方法为属性赋值
java·开发语言
创业之路&下一个五年1 小时前
JS编程范式 \& 面向对象范式
开发语言·前端·javascript
代码中介商1 小时前
C++11移动语义:右值引用与高效资源转移
开发语言·c++
Hello:CodeWorld2 小时前
深入浅出 C++:静态多态与动态多态的业务应用场景与源码级实战
开发语言·c++·架构
星恒随风2 小时前
C++入门(一):第一个 C++ 程序、命名空间、输入输出和缺省参数
开发语言·c++·笔记·学习
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第94题】【Mysql篇】第24题:什么是单路排序?什么是双路排序??
java·开发语言·数据库·mysql·面试·排序算法
我是一颗柠檬2 小时前
【Java项目技术亮点】多级缓存一致性方案:Canal+MQ实现数据库与缓存的最终一致
java·数据库·spring·缓存·kafka·rocketmq
于先生吖2 小时前
Java分账体系设计,网约车行程计费与到店线下结账一体化后端开发实战
java·开发语言