Java-面试-并发-1

线程与进程

进程:程序的一次执行过程,资源分配的基本单位。

线程:CPU 调度的基本单位。拥有自己的 程序计数器、虚拟机栈、本地方法栈。而对于 堆、字符串常量池 是线程共享的

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。

创建

一般说有三种或者多种创建线程的方式,实际本质上只有一种 new Thread().start() 大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!

Java Runnable 与 Callable 的区别是什么?

RunnableCallable 是 Java 中用于并发编程的两个接口,它们都可以被线程执行,但有以下几个关键区别:

1. 返回结果

  • Runnable: 不返回结果。Runnablerun() 方法是 void,所以它不能返回任何结果。

    1
    2
    3
    4
    @FunctionalInterface
    public interface Runnable {
    void run();
    }
  • Callable: 可以返回结果。Callablecall() 方法返回一个泛型结果,可以用 Future 获取返回值。

    1
    2
    3
    4
    @FunctionalInterface
    public interface Callable<V> {
    V call() throws Exception;
    }

2. 异常处理

  • Runnable: 不能抛出检查异常(Checked Exception),只能通过 try-catch 语句捕获和处理异常。

  • Callable: 可以抛出检查异常,call() 方法声明了 throws Exception,允许抛出异常以便调用者处理。

3. 使用场景

  • Runnable: 适用于不需要返回结果的任务,例如在后台运行一个任务或执行一些更新操作。

  • Callable: 适用于需要返回结果或可能抛出异常的任务,比如计算结果或获取数据。

4. 提交到线程池的处理方式

  • Runnable: 可以通过 ExecutorService.submit(Runnable) 提交,返回一个 Future<?> 对象,但 Future.get() 的结果始终为 null

  • Callable: 通过 ExecutorService.submit(Callable<V>) 提交,返回一个带结果的 Future<V>,可以通过 Future.get() 获取 Callable 返回的结果。

示例代码

1
2
3
4
5
6
7
8
9
10
// 使用 Runnable
Runnable runnableTask = () -> System.out.println("Runnable task running");
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(runnableTask); // 提交任务,无返回值

// 使用 Callable
Callable<String> callableTask = () -> "Callable task result";
Future<String> future = executor.submit(callableTask); // 提交任务,有返回值
System.out.println("Callable result: " + future.get());
executor.shutdown();

总结

  • Runnable:不返回结果、不能抛出检查异常,适用于简单任务。
  • Callable:可返回结果、可以抛出检查异常,适合需要返回值或异常处理的任务。

状态

关于状态,JVM有六种

  1. NEW:创建出来但是还没有调用 start
  2. RUNNABLE:可运行,实际上包含了 运行态与就绪态
  3. BLOCKED: 进入临界区等待锁的释放
  4. WAITING:无限期等待,不能自动恢复,只能被其他线程唤醒
  5. TIME_WAITING:有限期,能自动恢复
  6. TERMINATED:终止

在 Java 中,JVM 对线程状态的区分通过 Thread.State 枚举,其中 BLOCKEDWAITINGTIMED_WAITING 是三种不同的等待状态:

1. BLOCKED(阻塞状态)

  • 含义:线程尝试获取一个由其他线程持有的锁(synchronized 块或方法)时进入此状态。线程在进入临界区前会处于 BLOCKED 状态,直到该锁释放并获取成功后,线程才可以继续执行。
  • 触发条件:一个线程在进入 synchronized 块或方法时,发现锁已被其他线程持有时,进入 BLOCKED 状态。
  • 恢复条件:一旦锁被释放,BLOCKED 状态的线程有机会获取锁并继续执行。

2. WAITING(等待状态)

  • 含义:线程在等待其他线程的通知或中断,不会自动返回。它只能被显式唤醒(例如,通过另一个线程调用 notify()notifyAll() 方法)。
  • 触发条件:以下方法会让线程进入 WAITING 状态:
    • Object.wait() 方法(不带超时参数)
    • Thread.join() 方法(不带超时参数)
    • LockSupport.park() 方法
  • 恢复条件:必须由其他线程调用相应的唤醒方法,如 notify()notifyAll(),或在 join() 时等待的线程结束。

3. TIMED_WAITING(超时等待状态)

  • 含义:线程在等待一段时间后将自动返回,不需要被其他线程显式唤醒。该状态适合有超时时间要求的任务或等待。
  • 触发条件:以下方法会让线程进入 TIMED_WAITING 状态:
    • Thread.sleep(long millis) 方法
    • Object.wait(long timeout) 方法(带超时参数)
    • Thread.join(long millis) 方法(带超时参数)
    • LockSupport.parkNanos(long nanos) 方法
    • LockSupport.parkUntil(long deadline) 方法
  • 恢复条件:时间到达后自动返回到 RUNNABLE 状态,或在到期前被其他线程唤醒(例如通过中断)。

状态转换总结

  • BLOCKED:线程等待锁释放。
  • WAITING:线程等待其他线程的通知或中断,无限期等待。
  • TIMED_WAITING:线程在等待超时后自动返回,无需显式唤醒。

sleep wait

Thread.sleep()object.wait()

共同点:都能暂停线程

不同:

  1. 前者没有释放锁,后者释放锁
  2. 前者是 Thread 的静态本地方法,后者是 Object 类的本地方法。

多线程

并发与并行

并发:同一时间段内

并行:同一时刻

同步与异步

同步:调用后被迫原地等待

异步:调用后继续执行,不用等待

CPU密集型与IO密集型

单核CPU上使用多线程不一定快。

  • CPU 密集型:太多线程会导致频繁的线程上下文切换,增加了开销,降低了效率。
  • IO 密集型:多线程可以利用等待 IO 的空闲时间,提高效率。

死锁

线程持有并等待资源,被无限期阻塞,程序无法正常终止。

死锁的四个条件:

  1. 互斥条件:一个萝卜一个坑,一个资源一个线程
  2. 请求并保持条件:吃着碗里的想着锅里的
  3. 不剥夺条件:谁也不能抢走我碗里的
  4. 循环等待条件:我想要你碗里的,你想要我碗里的

预防

  1. 破坏请求保持:一次性申请资源
  2. 破坏不剥夺:主动释放对方手里的
  3. 破坏循环等待:按顺序申请,相反顺序释放。

避免

资源分配时借助算法堆资源分配进行估算,使其进入安全状态。银行家算法

安全状态:存在一种资源分配顺序,使得系统按序分配后,所有线程都可以顺利满足。

检测

一些Java 工具

消除

虚拟线程

就是JDK中使用多个线程对应于一个操作系统线程。

JDK 的线程更加轻量化。

能够在 IO 密集场景下,提高处理速度。减少县城资源的额创建以及上下文的切换。

JMM JVM内存模型