目录
1. 什么是进程和线程?有什么区别和联系?
2. Java的线程和操作系统的线程有什么区别?
3. 线程的创建方式有哪些?
4. 如何启动和停止线程?
5. Java线程的状态模型(有哪些状态)?
6. 调用 interrupt 是如何让线程抛出异常的?
7. 什么是线程上下文切换?
8. sleep 和 wait的区别是什么?
9. sleep会释放cpu吗?
10. 可以直接调用 Thread 类的 run 方法吗?
11. blocked和waiting有什么区别?
12. wait 状态下的线程如何进行恢复到 running 状态?
13. notify 和 notifyAll 的区别?
14. 并发和并行的区别?
15. 同步和异步的区别?
16. 单核 CPU 支持 Java 多线程吗?
17. 单核 CPU 上运行多个线程效率一定会高吗?
18. 不同的线程之间如何通信?
19. 线程间通信方式有哪些?
20. 使用多线程要注意哪些问题?
21. 什么是线程死锁?
1. 什么是进程和线程?有什么区别和联系?
-
进程:是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间。进程间相互隔离,一个进程崩溃通常不影响其他进程。
-
线程:是进程内的执行单元,是CPU调度的最小单位。同一进程的多个线程共享进程的内存空间和资源(如堆、全局变量),但每个线程有独立的栈和程序计数器。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
2. Java的线程和操作系统的线程有什么区别?
现在的 Java 线程的本质其实就是操作系统的线程。
Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。
3. 线程的创建方式有哪些?
1. 继承Thread类
这是最直接的一种方式,用户自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。
class MyThread extends Thread {@Overridepublic void run() {// 线程执行的代码}
}public static void main(String[] args) {MyThread t = new MyThread();t.start();
}
采用继承Thread类方式
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
2. 实现Runnable接口
如果一个类已经继承了其他类,就不能再继承Thread类,此时可以实现java.lang.Runnable接口。实现 Runnable接口需要重写run()方法,然后将此Runnable对象作为参数传递给 Thread类的构造器,创建Thread对象后调用其start()方法启动线程。
class MyRunnable implements Runnable {@Overridepublic void run() {// 线程执行的代码}
}public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();
}
采用实现 Runnable接口方式:
- 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread( )方法。
3. 实现Callable接口与FutureTask
java.util.concurrent.Callable
接口类似于Runnable
,但Callable
的call()
方法可以有返回值并且可以抛出异常。要执行Callable
任务,需将它包装进一个FutureTask
,因为Thread
类的构造器只接受Runnable
参数,而FutureTask
实现了Runnable
接口。
class MyCallable implements Callable<Integer> {@Overridepublic 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 对象,非常适合多线程处理同一份资源的情形。
4. 使用线程池(Executor框架)
从Java 5开始引入的java.util.concurrent.ExecutorService和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过Executors类的静态方法创建不同类型的线程池。
class Task implements Runnable {@Overridepublic 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利用率和系统吞吐量。
4. 如何启动和停止线程?
启动线程通过 Thread 类的 start()方法:
// 创建两个线程,用 start 启动线程
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
停止线程的方法:
- 异常法停止:线程调用
interrupt()
方法后,在线程的run
方法中判断当前对象的interrupted()
状态,如果是中断状态则抛出异常,达到中断线程的效果。 - 在沉睡中停止:先将线程
sleep
,然后调用interrupt
标记中断状态,interrupt
会将阻塞状态的线程中断。会抛出中断异常,达到停止线程的效果 stop()
暴力停止:线程调用stop()
方法会被暴力停止,方法已弃用,该方法会有不好的后果:强制让线程停止有可能使一些请理性的工作得不到完成。- 使用
return
停止线程:调用interrupt
标记为中断状态后,在run
方法中判断当前线程状态,如果为中断状态则return
,能达到停止线程的效果。
5. Java线程的状态模型(有哪些状态)?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
线程状态 | 解释 |
---|---|
NEW | 尚未启动的线程状态,即线程创建,还未调用start方法 |
RUNNABLE | 就绪状态(调用start,等待调度)+正在运行 |
BLOCKED | 等待监视器锁时,陷入阻塞状态 |
WAITING | 等待状态的线程正在等待另一线程执行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待时间的等待状态 |
TERMINATED | 线程完成执行,终止状态 |
可以调用线程 Thread中的getState()方法获取当前线程的状态。
详细解释
NEW
NEW 是线程的初始状态。当通过new Thread()
创建线程对象后,但尚未调用start()
方法时,线程处于此状态。此时,线程尚未分配系统资源,仅作为Java对象存在。需要强调的是,调用start()
方法会触发线程进入可运行状态。
RUNNABLE
RUNNABLE 表示线程已启动并准备好执行。调用start()
方法后,线程进入此状态。它可能正在运行(占用CPU)或就绪等待CPU调度(例如,在操作系统的就绪队列中)。关键点在于,此状态涵盖了线程的实际执行和就绪等待,不区分两者。如果线程在等待I/O操作(如网络请求),它仍被视为RUNNABLE,因为底层操作系统可能处理了阻塞。
BLOCKED
BLOCKED 发生在线程尝试获取一个监视器锁(monitor lock)时,但该锁已被其他线程持有。典型场景是进入synchronized
块或方法时遇到锁竞争。核心概念是线程在等待锁释放期间被阻塞,无法执行。一旦锁可用,线程会自动转换回RUNNABLE状态。注意,这与等待状态不同,BLOCKED是特定于锁的竞争。
WAITING
WAITING 是线程无限期等待另一个线程执行特定操作的状态。通过调用无参的Object.wait()
、Thread.join()
或LockSupport.park()
方法进入此状态。关键特征是线程不会主动唤醒,必须依赖其他线程的通知(如notify()
或notifyAll()
)才能恢复。这常用于线程间协调,但需小心避免死锁。
TIMED_WAITING
TIMED_WAITING 类似于WAITING,但线程在指定时间内等待。通过调用带超时参数的方法进入,如Thread.sleep(long millis)
、Object.wait(long timeout)
或Thread.join(long millis)
。核心区别在于,线程会在超时后自动返回RUNNABLE状态,无需外部通知。这提供了更可控的等待机制,减少无限等待的风险。
TERMINATED
TERMINATED 是线程的终止状态。当线程的run()
方法正常执行完毕或抛出未捕获异常时,进入此状态。关键点是线程资源已被释放,无法再重启(调用start()
会抛出异常)。此时,线程对象可能仍被引用,但已无执行能力。
6. 调用 interrupt 是如何让线程抛出异常的?
每个线程都有一个与之关联的布尔属性来表示其中断状态,中断状态的初始值为false,当一个线程被其它线程调用 Thread.interrupt()
方法中断时,会根据实际情况做出响应。
- 如果该线程正在执行低级别 的可中断方法(如
Thread.sleep()
、Thread.join()
或Object.wait()
),则会解除阻塞并抛出InterruptedException
异常。 - 否则
Thread.interrupt()
仅设置线程的中断状态,在该被中断的线程中稍后可通过轮询中断状态来决定是否要停止当前正在执行的任务。
7. 什么是线程上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了
sleep()
,wait()
等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
8. sleep 和 wait的区别是什么?
特性 | sleep() | wait() |
---|---|---|
所属类 | Thread类(静态方法) | Object类(实例方法) |
锁释放 | ❌ | ✅ |
使用前提 | 任意位置调用 | 必须在同步块内(持有锁) |
唤醒机制 | 超时自动恢复 | 需 notify()/notifyAll() 或超时 |
设计用途 | 暂停线程执行,不涉及锁协作 | 线程间协调,释放锁让其他线程工作 |
- 所属分类的不同:sleep是 Thread类的静态方法,可以在任何地方直接通过 Thread.sleep()调用,无需依赖对象实例。wait是 Object类的实例方法,这意味着必须通过对象实例来调用。
- 锁释放的情况: Thread.sleep()在调用时,线程会暂停执行指定的时间,但不会释放持有的对象锁。也就是说,在 sleep期间,其他线程无法获得该线程持有的锁。 Object.wait():调用该方法时,线程会释放持有的对象锁,进入等待状态,直到其他线程调用相同对象的 notify()或 notifyAll()方法唤醒它。
- 使用条件:sleep可在任意位置调用,无需事先获取锁。wait必须在同步块或同步方法内调用(即线程需持有该对象的锁),否则抛出 IllegalMonitorStateException。
- 唤醒机制:sleep休眠时间结束后,线程自动恢复到就绪状态,等待CPU调度。wait需要其他线程调用相同对象的 notify()或 notifyAll()方法才能被唤醒。notify()会随机唤醒一个在该对象上等待的线程,而 notifyAll()会唤醒所有在该对象上等待的线程。
9. sleep会释放cpu吗?
是的,调用 Thread.sleep()
时,线程会释放 CPU,但不会释放持有的锁。
当线程调用 sleep()
后,会主动让出 CPU 时间片,进入 TIMED_WAITING
状态。此时操作系统会触发调度,将 CPU 分配给其他处于就绪状态的线程。这样其他线程(无论是需要同一锁的线程还是不相关线程)便有机会执行。
sleep()
不会释放线程已持有的任何锁(如 synchronized
同步代码块或方法中获取的锁)。因此,如果有其他线程试图获取同一把锁,它们仍会被阻塞,直到原线程退出同步代码块。
10. 可以直接调用 Thread 类的 run 方法吗?
new 一个 Thread
,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
11. blocked和waiting有什么区别?
区别如下:
- 触发条件:线程进入
BLOCKED
状态通常是因为试图获取一个对象的锁 (monitor lock) ,但该锁已经被另一个线程持有。这通常发生在尝试进入synchronized
块或方法时,如果锁已被占用,则线程将被阻塞直到锁可用。线程进入WAITING
状态是因为它正在等待另一个线程执行某些操作,例如调用Object.wait()
方法、Thread.join()
方法或LockSupport.park()
方法。在这种状态下,线程将不会消耗 CPU 资源,并且不会参与锁的竞争。
唤醒机制
当一个线程被阻塞等待锁时,一旦锁被释放,线程将有机会重新尝试获取锁。如果锁此时未被其他线程获取,那么线程可以从 BLOCKED
状态变为 RUNNABLE
状态。线程在 WAITING
状态中需要被显式唤醒。例如,如果线程调用了 Object.wait()
,那么它必须等待另一个线程调用同一对象上的 Object.notify()
或 Object.notifyAll()
方法才能被唤醒。
所以,
BLOCKED
和WAITING
两个状态最大的区别有两个:
BLOCKED
是锁竞争失败后被被动触发的状态,WAITING
是人为的主动触发的状态BLCKED
的唤醒是自动触发的,而WAITING
状态是必须要通过特定的方法来主动唤醒
12. wait 状态下的线程如何进行恢复到 running 状态?
线程从等待(WAIT)状态恢复到运行(RUNNING)状态的核心机制是通过外部事件触发或资源可用性变化,比如等待的线程被其他线程对象唤醒,notify() 和 notifyAll()。
synchronized (lock) {// 线程进入等待状态,释放锁lock.wait();
}// 其他线程调用以下代码唤醒等待线程
synchronized (lock) {lock.notify(); // 唤醒单个线程// lock.notifyAll(); // 唤醒所有等待线程
}
13. notify 和 notifyAll 的区别?
同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。
区别在于:
-
notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断。
-
notifyAll:所有线程退出wait 的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁。
notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm。
14. 并发和并行的区别?
- 并发:两个及两个以上的作业在同一 时间段 内执行。
- 并行:两个及两个以上的作业在同一 时刻 执行。
最关键的点是:是否是 同时 执行。
15. 同步和异步的区别?
- 同步:任务按顺序依次执行,必须等待当前操作完成才能继续下一个操作。
- 异步:任务发起后无需等待完成,程序可继续执行其他操作。
16. 单核 CPU 支持 Java 多线程吗?
单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。
17. 单核 CPU 上运行多个线程效率一定会高吗?
单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:
- CPU 密集型:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
- IO 密集型:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
因此,对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。当然,这里的“很多”也要适度,不能超过系统能够承受的上限。
18. 不同的线程之间如何通信?
共享变量是最基本的线程间通信方式。多个线程可以访问和修改同一个共享变量,从而实现信息的传递为了保证线程安全,通常需要使用 synchronized 关键字或 volatile 关键字。
class SharedVariableExample {// 使用 volatile 关键字保证变量的可见性private static volatile boolean flag = false;public static void main(String[] args) {// 生产者线程Thread producer = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}// 修改共享变量flag = true;System.out.println("Producer: Flag is set to true.");});// 消费者线程Thread consumer = new Thread(() -> {while (!flag) {// 等待共享变量被修改}System.out.println("Consumer: Flag is now true.");});producer.start();consumer.start();}
}
代码解释
volatile
关键字确保了flag
变量在多个线程之间的可见性,即一个线程修改了flag
的值,其他线程能立即看到。- 生产者线程在睡眠 2 秒后将
flag
设置为true
,消费者线程在flag
为false
时一直等待,直到flag
变为true
才继续执行。
Object
类中的 wait()
、notify()
和 notifyAll()
方法可以用于线程间的协作。wait()
方法使当前线程进入等待状态,notify()
方法唤醒在此对象监视器上等待的单个线程,notifyAll()
方法唤醒在此对象监视器上等待的所有线程。
class WaitNotifyExample {private static final Object lock = new Object();public static void main(String[] args) {// 生产者线程Thread producer = new Thread(() -> {synchronized (lock) {try {System.out.println("Producer: Producing...");Thread.sleep(2000);System.out.println("Producer: Production finished. Notifying consumer.");// 唤醒等待的线程lock.notify();} catch (InterruptedException e) {e.printStackTrace();}}});// 消费者线程Thread consumer = new Thread(() -> {synchronized (lock) {try {System.out.println("Consumer: Waiting for production to finish.");// 进入等待状态lock.wait();System.out.println("Consumer: Production finished. Consuming...");} catch (InterruptedException e) {e.printStackTrace();}}});consumer.start();producer.start();}
}
代码解释:
lock
是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行相应的操作。- 消费者线程调用
lock.wait()
方法进入等待状态,释放锁;生产者线程执行完生产任务后调用lock.notify()
方法唤醒等待的消费者线程。
java.util.concurrent.locks
包中的 Lock
和 Condition
接口提供了比 synchronized
更灵活的线程间通信方式。 Condition
接口的await()
方法类似于wait()
方法,signal()
方法类似于notify()
方法,signalAll()
方法类似于notifyAll()
方法。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class LockConditionExample {private static final Lock lock = new ReentrantLock();private static final Condition condition = lock.newCondition();public static void main(String[] args) {// 生产者线程Thread producer = new Thread(() -> {lock.lock();try {System.out.println("Producer: Producing...");Thread.sleep(2000);System.out.println("Producer: Production finished. Notifying consumer.");// 唤醒等待的线程condition.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});// 消费者线程Thread consumer = new Thread(() -> {lock.lock();try {System.out.println("Consumer: Waiting for production to finish.");// 进入等待状态condition.await();System.out.println("Consumer: Production finished. Consuming...");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});consumer.start();producer.start();}
}
代码解释:
ReentrantLock
是Lock
接口的一个实现类,condition
是通过lock.newCondition()
方法创建的。- 消费者线程调用
condition.await()
方法进入等待状态,生产者线程执行完生产任务后调用condition.signal()
方法唤醒等待的消费者线程。
java.util.concurrent
包中的 BlockingQueue
接口提供了线程安全的队列操作,当队列满时,插入元素的线程会被阻塞;当队列为空时,获取元素的线程会被阻塞。
队列自动处理线程阻塞与唤醒(如队列空时阻塞消费者,满时阻塞生产者)。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;class BlockingQueueExample {private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);public static void main(String[] args) {// 生产者线程Thread producer = new Thread(() -> {try {System.out.println("Producer: Producing...");queue.put(1);System.out.println("Producer: Production finished.");} catch (InterruptedException e) {e.printStackTrace();}});// 消费者线程Thread consumer = new Thread(() -> {try {System.out.println("Consumer: Waiting for production to finish.");int item = queue.take();System.out.println("Consumer: Consumed item: " + item);} catch (InterruptedException e) {e.printStackTrace();}});consumer.start();producer.start();}
}
代码解释:
- LinkedBlockingQueue 是 BlockingQueue 接口的一个实现类,容量为 1。
- 生产者线程调用 queue.put(1) 方法将元素插入队列,如果队列已满,线程会被阻塞;消费者线程调用 queue.take() 方法从队列中取出元素,如果队列为空,线程会被阻塞。
19. 线程间通信方式有哪些?
1. 共享内存(隐式通信)
线程通过共享变量(如对象成员变量、静态变量)隐式通信。
关键点:必须使用 synchronized
或 volatile
保证可见性与原子性。
示例:volatile
确保变量修改后立即对其他线程可见,synchronized
通过锁机制实现原子操作和内存屏障。
2. wait()
/notify()
(显式同步)
基于 Object
内置锁的经典通信方式:
-
wait()
:释放锁并阻塞线程,需在synchronized
块内调用。 -
notify()
/notifyAll()
:唤醒同一锁上等待的线程(随机一个或全部)。
synchronized(lock) {while(条件不满足) { // 必须用循环检查条件lock.wait(); // 释放锁,线程阻塞}// 执行业务逻辑lock.notifyAll(); // 唤醒其他线程
}
3. Lock
与 Condition
(显式锁)
使用 java.util.concurrent.locks
包下的更灵活机制:
-
Lock
替代synchronized
,提供lock()
/unlock()
显式锁控制。 -
Condition
替代wait()/notify()
,支持多个等待队列:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();lock.lock();
try {while(条件不满足) {condition.await(); // 释放锁并等待}condition.signalAll(); // 唤醒等待线程
} finally {lock.unlock();
}
4. 阻塞队列(BlockingQueue
)
生产者-消费者模型的首选方案:
-
队列自动处理线程阻塞与唤醒(如队列空时阻塞消费者,满时阻塞生产者)。
-
常用实现类:
ArrayBlockingQueue
,LinkedBlockingQueue
。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);// 生产者
queue.put("data"); // 队列满时自动阻塞// 消费者
String data = queue.take(); // 队列空时自动阻塞
5. 同步工具类(CountDownLatch
/CyclicBarrier
)
控制线程执行顺序的高级工具:
-
CountDownLatch
:线程等待指定次数的事件完成(一次性)。
CountDownLatch latch = new CountDownLatch(3);
// 工作线程
latch.countDown(); // 事件完成,计数器减1
// 主线程
latch.await(); // 阻塞直到计数器归零
CyclicBarrier
:线程互相等待到达屏障点(可重复使用)。
CyclicBarrier barrier = new CyclicBarrier(3);
// 工作线程
barrier.await(); // 阻塞直到所有线程到达
20. 使用多线程要注意哪些问题?
要保证多线程的程序是安全,不要出现数据竞争造成的数据混乱的问题。
Java的线程安全在三个方面体现∶
- 原子性:提供互斥访问, 同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和synchronized关键字来确保原子性;
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到, 在Java中使用了synchronized和volatile这两个关键字确保可见性;
- 有序性:一个线程观察其他线程中的指令执行顺序, 由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens - before原则来确保有序性。
21. 什么是线程死锁?
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
产生死锁的四个必要条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。