JAVA多线程及并发面试题
JAVA多线程及并发面试题
线程的基础知识
进程和线程的区别
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。
进程
是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程,是操作系统资源分配的基本单位。进程是一个正在执行的程序的实例,包括程序计数器、寄存器和程序变量的当前值。
线程
进程内的一个执行单元,是CPU调度和任务执行的最小单位。共享所属进程的内存空间和系统资源(如文件描述符),仅独立拥有程序计数器、寄存器和栈等运行时必需资源。
进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
一个进程之内可以分为一到多个线程。
进程强调资源隔离,线程强调执行效率,两者协同实现高效的系统运作。

- 二者对比
- 资源分配:进程拥有独立的内存和资源,线程共享所属进程的资源,减少资源冗余。
- 调度单位:线程作为轻量级执行单元,切换时上下文开销更小,适合并发操作。
- 执行方式:多进程更安全但通信复杂,多线程高效但需处理同步问题。
- 系统开销:进程创建需分配独立资源,耗时多;线程共享资源,创建更快。
并发和并行
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
创建线程的四种方式
共有四种方式可以创建线程,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程
详细创建方式参考下面代码:
继承Thread类
继承 Thread 类并重写 run() 方法,实例化后调用start()方法启动线程
优缺点:
- 代码简单直观,适合简单任务
- 受限于Java单继承机制,线程与任务耦合度高
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}实现runnable接口
实现 Runnable 接口的类并重写 run() 方法,将实例传入Thread构造函数后启动线程
优缺点
- 解耦线程逻辑与任务,支持多接口实现
- 需额外创建Thread对象,无法直接获取返回值
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable() ;
// 创建Thread对象
Thread t1 = new Thread(mr) ;
Thread t2 = new Thread(mr) ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}实现Callable接口
创建类实现Callable接口并重写call()方法,配合FutureTask封装结果,通过Thread或线程池执行
优缺点
支持返回值和异常抛出,适合于异步计算结果
需结合Future阻塞获取结果,代码复杂度增加
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}线程池创建线程
使用线程池可以更好地管理和复用线程,提高线程的执行效率。在 Java 中,可以使用 Executor 框架来创建线程池。
创建一个 ExecutorService 对象,通常使用 Executors 工厂类提供的静态方法创建线程池,创建实现 Runnable 接口或 Callable 接口的任务对象。并将任务对象提交给线程池执行。
当不再需要提交新的任务时,调用线程池的 shutdown() 方法关闭线程池。
注意
调用 shutdown() 方法后线程池不再接收新的任务,但会等待已提交的任务执行完毕。
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
}runnable 和 callable 有什么区别
- Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
线程的 run()和 start()有什么区别?
start(): 会新开一个线程用于执行任务,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,是在当前主线程中执行任务,可以被调用多次。
线程状态及状态变化
线程的六种状态
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待( WAITING )、限时等待(TIMED_WALTING)、终止(TERMINATED)
Java源码
public enum State {
/**
* 尚未启动的线程的线程状态
/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自
* 操作系统的其他资源,例如处理器。
/
RUNNABLE,
/**
* 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调
* 用Object.wait后重新进入同步块/方法。
/
BLOCKED,
/**
* 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
* Object.wait没有超时
* 没有超时的Thread.join
* LockSupport.park
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()
* 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定 * 时等待状态:
* Thread.sleep
* Object.wait超时
* Thread.join超时
* LockSupport.parkNanos
* LockSupport.parkUntil
* </ul>
/
TIMED_WAITING,
/**
* 已终止线程的线程状态。线程已完成执行
*/
TERMINATED;
}- 线程状态的切换

- 新建状态(NEW)
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时未与操作系统底层线程关联
- 可运行状态(RUNNABLE)
- 调用了 start 方法,就会由新建进入可运行,等待CPU时间片调度。此状态包含操作系统层面的“就绪”和“运行”两种子状态。
- 此时与底层线程关联,由操作系统调度执行
- 阻塞状态(BLOCKED)
- 仅在等待获取对象同步锁时进入该状态,若锁被其他线程占用,当前线程会被放入锁池(Lock Pool)。
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
- 等待状态(WAITING)
- 线程主动调用Object.wait()或Thread.join()方法后无限期等待,需其他线程显式唤醒,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
- 限时等待状态(TIMED_WAITING)
- 通过Thread.sleep(long)或Object.wait(timeout)等方式进入有时间限制的等待,超时后自动恢复可运行状态。
- 终止状态(TERMINATED)
- 线程执行完毕run()方法或因异常退出后进入此状态,不可再次启动
状态转换的关键触发条件
从NEW到RUNNABLE:调用start()方法。
从RUNNABLE到BLOCKED:竞争同步锁失败。
从RUNNABLE到WAITING/TIMED_WAITING:执行wait()、join()或sleep()方法。
从所有阻塞状态返回RUNNABLE:获得锁、外部唤醒或超时结束。
线程的顺序执行
新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();
}
}notify()和 notifyAll()
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程
