并发基础
并行和并发的区别
- 并行是多核 CPU 上的多任务处理,多个任务在同一时间真正地同时执行。
- 并发是单核 CPU 上的多任务处理,多个任务在同一时间段内交替执行,通过时间片轮转实现交替执行,用于解决 IO 密集型任务的瓶颈。

线程安全
如果一段代码块或者一个方法被多个线程同时执行,还能够正确地处理共享数据,那么这段代码块或者这个方法就是线程安全的。
可以从三个要素来确保线程安全:
- 原子性:一个操作要么完全执行,要么完全不执行,不会出现中间状态。
- 可见性:当一个线程修改了共享变量,其他线程能够立即看到变化。
- 有序性:要确保线程不会因为死锁、饥饿、活锁等问题导致无法继续执行。
进程和线程的区别
进程说简单点就是我们在电脑上启动的一个个应用。它是操作系统分配资源的最小单位
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,
所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

线程的创建方式有哪些?
继承 Thread 类
这是最直接的一种方式,用户自定义类继承 java.lang.Thread 类,重写其 run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用 start()方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}采用继承 Thread 类方式
- 优点: 编写简单,如果需要访问当前线程,无需使用 Thread.currentThread ()方法,直接使用 this,即可获得当前线程
- 缺点:因为线程类已经继承了 Thread 类,所以不能再继承其他的父类
实现 Runnable 接口
如果一个类已经继承了其他类,就不能再继承 Thread 类,此时可以实现 java.lang.Runnable 接口。实现 Runnable 接口需要重写 run()方法,然后将此 Runnable 对象作为参数传递给 Thread 类的构造器,创建 Thread 对象后调用其 start()方法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}采用实现 Runnable 接口方式:
- 优点:线程类只是实现了 Runable 接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用 Thread.currentThread()方法。
实现 Callable 接口与 FutureTask
java.util.concurrent.Callable 接口类似于 Runnable,但 Callable 的 call()方法可以有返回值并且可以抛出异常。要执行 Callable 任务,需将它包装进一个 FutureTask,因为 Thread 类的构造器只接受 Runnable 参数,而 FutureTask 实现了 Runnable 接口。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码,这里返回一个整型结果
return 1;
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
try {
Integer result = futureTask.get(); // 获取线程执行结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}采用实现 Callable 接口方式:
- 缺点:编程稍微复杂,如果需要访问当前线程,必须调用 Thread.currentThread()方法。
- 优点:线程只是实现 Runnable 或实现 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。
使用线程池(Executor 框架)
从 Java 5 开始引入的 java.util.concurrent.ExecutorService 和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过 Executors 类的静态方法创建不同类型的线程池。
class Task implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
for (int i = 0; i < 100; i++) {
executor.submit(new Task()); // 提交任务到线程池执行
}
executor.shutdown(); // 关闭线程池
}采用线程池方式:
- 缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂。
- 优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并且,线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。通过合理配置线程池大小,可以最大化 CPU 利用率和系统吞吐量。
调用 start 方法时会执行 run 方法,那怎么不直接调用 run方法?
调用 start() 会创建一个新的线程,并异步执行 run() 方法中的代码。
直接调用 run() 方法只是一个普通的同步方法调用,所有代码都在当前线程中执行,不会创建新线程。没有新的线程创建,也就达不到多线程并发的目的。
线程有几种状态?
6 种。
new 代表线程被创建但未启动;runnable 代表线程处于就绪或正在运行状态,由操作系统调度;blocked 代表线程被阻塞,等待获取锁;waiting 代表线程等待其他线程的通知或中断;timed_waiting 代表线程会等待一段时间,超时后自动恢复;terminated 代表线程执行完毕,生命周期结束。
三分恶面渣逆袭:Java线程状态变化
也就是说,线程的生命周期可以分为五个主要阶段:新建、就绪、运行、阻塞和终止。线程在运行过程中会根据状态的变化在这些阶段之间切换
sleep和wait的区别
所属类不同
sleep():是Thread类的静态方法,直接通过Thread.sleep(毫秒数)调用。wait():是Object类的成员方法,必须通过对象(通常是锁对象)调用,如obj.wait()。
锁的处理(最关键区别)
sleep():不会释放持有的锁。线程调用sleep后进入休眠状态,但依然霸占着锁,其他线程无法获取该锁。wait():会主动释放持有的锁。线程调用wait后,会释放锁并进入该对象的 “等待池”,其他线程可以获取锁执行,直到被notify()/notifyAll()唤醒。
唤醒方式
sleep():有两种唤醒方式:① 休眠时间到自动唤醒;② 被其他线程调用interrupt()中断(会抛出InterruptedException)。wait():① 被其他线程调用obj.notify()/obj.notifyAll()唤醒;② 等待超时(wait(毫秒数))自动唤醒;③ 被interrupt()中断。
怎么保证线程安全
为了保证线程安全,可以使用 synchronized关键字对方法加锁,对代码块加锁
如果需要保证变量的内存可见性,可以使用volatile关键字