1. 并行和并发有什么区别?
并行(Parallelism)和并发(Concurrency)在计算机科学中都是关于处理多个任务或操作的重要概念,但它们之间存在显著的差异。以下是并行和并发的主要区别:
并发(Concurrency):
- 任务交错执行:并发指的是两个或多个任务在同一时间段内开始,但它们的执行是交错进行的,即一个任务的执行不会等到另一个任务完成。
- 资源共享:在并发系统中,多个任务共享相同的系统资源(如CPU、内存、I/O设备等)。这通常要求使用某种形式的同步机制(如锁、信号量等)来避免资源竞争和数据不一致。
- 单核或多核处理器:并发可以在单核或多核处理器上实现。在单核处理器上,并发是通过时间片轮转或类似机制实现的,即处理器在多个任务之间快速切换。
并行(Parallelism):
- 任务同时执行:并行是指两个或多个任务在同一时刻点真正同时执行,每个任务都在不同的处理单元(如CPU核心)上运行,互不干扰。
- 无需资源共享:在并行处理中,每个任务通常都有它自己的资源副本(例如,不同的数据分区),因此不需要担心资源竞争。
- 多核处理器:并行处理通常需要多核或多处理器系统来支持,每个核心可以独立地执行一个任务。
总结:
- 执行方式:并发是逻辑上的同时执行(通过任务切换),而并行是物理上的同时执行(通过多处理单元)。
- 资源共享与同步:并发通常涉及资源共享和同步问题,而并行则通常避免了这些问题,因为每个任务都有自己的资源。
- 依赖的硬件:并发可以在单核或多核处理器上实现,而并行则主要依赖多核或多处理器系统。
在实际应用中,并行和并发经常结合使用。例如,在Java中,我们可以使用多线程来实现并发,而在多核处理器上,这些线程可以并行执行。此外,Java 8引入的并行流(Parallel Streams)允许我们以声明式的方式利用多核处理器进行并行处理。然而,要有效地利用并行处理,需要仔细考虑任务的划分、数据分布以及可能的同步问题。
2. 线程和进程的区别?
线程和进程是操作系统中用于实现并发执行的两个基本概念,它们之间存在一些明显的区别。以下是线程和进程之间的主要差异:
-
资源占用:
- 进程:进程是资源分配的基本单位,它拥有独立的内存空间和系统资源。每个进程都有自己独立的代码段、数据段和堆栈段,以及自己的系统资源,如文件描述符、信号处理器等。因此,创建进程通常需要较多的系统开销。
- 线程:线程是CPU调度的基本单位,它共享进程所拥有的资源,包括内存空间、代码段、数据段、打开的文件等。线程只拥有少量的私有数据,如线程ID、程序计数器、寄存器集合和堆栈,因此创建线程的开销通常较小。
-
执行方式:
- 进程:每个进程都有独立的执行路径,操作系统通过调度进程来分配CPU时间片,实现并发执行。进程之间的切换涉及资源的重新分配和状态保存,因此切换开销较大。
- 线程:线程在进程内部共享相同的执行环境,因此线程之间的切换通常更快,因为不涉及资源的重新分配。线程之间可以通过共享内存进行通信,实现协同工作。
-
通信与同步:
- 进程:进程之间的通信通常需要通过进程间通信(IPC)机制,如管道、消息队列、共享内存等,以实现数据的传递和同步。
- 线程:线程之间可以通过共享内存直接通信,这使得线程间的数据交换更加高效。同时,线程同步机制(如互斥锁、条件变量等)用于协调线程间的执行顺序,确保数据的一致性。
-
独立性:
- 进程:进程具有较高的独立性,一个进程的崩溃通常不会影响到其他进程的执行。
- 线程:线程依赖于进程,一个线程的崩溃可能导致整个进程的崩溃。因此,线程之间需要更加谨慎地处理异常和错误。
-
并发性:
- 线程:由于线程之间的切换开销较小,且可以共享进程资源,因此线程通常比进程更适合实现高并发场景。
- 进程:虽然进程也可以实现并发,但由于其资源占用较大和切换开销较高,通常不如线程适用于高并发场景。
3. 守护线程是什么?
守护线程(Daemon Thread)是运行在后台的一种特殊进程,也被称为“服务进程”、“精灵线程”或“后台线程”。它独立于控制终端,并且周期性地执行某种任务或等待处理某些发生的事件。守护线程的主要职责是为其他线程(包括用户自定义线程和主线程)提供服务。
在Java中,守护线程的一个典型例子是垃圾回收线程。只要还有非守护线程在运行,程序就不会终止,守护线程也会持续运行以提供服务。然而,当所有非守护线程执行完毕后,无论有没有守护线程存在,Java虚拟机(JVM)都会自动退出。此时,由于没有了被守护者,守护线程也就没有工作可做,JVM会“杀死”所有守护线程,程序随之终止。
简而言之,守护线程在Java程序中扮演着为其他线程提供服务的角色,但当所有非守护线程结束时,守护线程也会随之结束,不再继续执行。
4. 创建线程的几种方式?
在Java中,创建线程主要有四种方式:
- 继承Thread类创建线程
通过继承Thread类并重写其run()方法,可以创建线程。在启动线程时,需要调用线程的start()方法,而不是直接调用run()方法。因为直接调用run()方法只会执行线程中的代码,而不会创建新的线程。
示例代码:
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
- 实现Runnable接口创建线程
通过实现Runnable接口并重写其run()方法,也可以创建线程。与继承Thread类不同,实现Runnable接口的方式可以将线程的任务与代码分离,使得代码更加灵活和可重用。在创建线程时,需要将实现Runnable接口的对象作为参数传递给Thread类的构造函数,然后调用Thread对象的start()方法启动线程。
示例代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
- 实现Callable接口创建线程(结合FutureTask使用)
Callable接口与Runnable接口类似,但是Callable接口可以有返回值,并且可以声明抛出异常。创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。使用FutureTask对象作为Thread对象的target创建并启动新线程。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
示例代码:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码,可以有返回值
return 123;
}
}
public class Main {
public static void main(String[] args) throws Exception {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
Integer result = futureTask.get(); // 获取返回值
System.out.println(result); // 输出:123
}
}
- 使用线程池创建线程
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory创建一个新线程。通过Executor框架的工具类Executors来实现。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable); // 提交任务到线程池执行
executorService.shutdown(); // 关闭线程池,不再接受新任务,等待已有任务执行完毕
}
}
以上就是在Java中创建线程的四种主要方式。每种方式都有其适用的场景和优缺点,需要根据具体需求进行选择。
5. Runnable 和 Callable 有什么区别?
Java中的Runnable和Callable接口都是用于多线程编程的重要工具,主要区别在于它们的返回值、异常处理、使用场景以及与Executor框架的集成方式。
-
方法定义与返回值:
- Runnable接口定义了一个run()方法,该方法没有返回值(返回类型为void),且不能抛出checked异常。它主要用于那些不需要返回值,且不会抛出checked异常的场景,如简单的打印输出或修改共享变量。
- Callable接口定义了一个call()方法,该方法可以返回一个值,并且这个返回值的类型可以通过泛型进行指定。此外,call()方法还可以抛出checked异常。这使得Callable接口特别适合用于那些需要计算并返回结果的任务。
-
异常处理:
- 对于Runnable的run()方法,如果线程中出现了异常,那么这个异常将会被抛出,并可能导致线程终止。而且,我们无法对run()方法抛出的异常进行任何处理。
- Callable的call()方法则可以抛出异常,因此我们可以对这些异常进行捕获和处理,这使得异常管理更为灵活和强大。
-
使用场景:
- Runnable接口主要用于执行那些不需要返回结果的任务,这些任务往往更关注过程而非结果。
- Callable接口则主要用于执行那些需要计算并返回结果的任务。它使得我们可以直接获取线程执行的结果,而无需通过其他方式进行传递。
-
与Executor框架的集成:
- Callable接口实际上是Executor框架中的功能类,与Runnable接口相比,它提供了更强大的功能。例如,通过Future接口,我们可以获取Callable任务的结果,甚至可以检查任务是否完成或等待任务完成。
6. 线程状态及转换?
Java中的线程状态及其转换主要涉及到六种状态:NEW(新建)、RUNNABLE(就绪/运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(限时等待)和TERMINATED(终止)。
-
NEW(新建):当线程对象被创建,但尚未启动(即没有调用
start()
方法)时,它处于NEW状态。此时,线程只是一个Thread对象,还没有开始执行。 -
RUNNABLE(就绪/运行):一旦线程对象调用了
start()
方法,它就进入RUNNABLE状态。这个状态有两种可能的情况:- 就绪状态:线程已经具备了所有执行所需的资源,等待CPU的调度执行。
- 运行状态:线程正在CPU上执行。
需要注意的是,线程在RUNNABLE状态下可以随时被其他高优先级的线程抢占CPU资源,从而进入就绪状态。
-
BLOCKED(阻塞):当线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其他线程持有时,该线程进入BLOCKED状态。当持有锁的线程释放锁后,BLOCKED状态的线程将有机会重新进入RUNNABLE状态。
-
WAITING(等待):线程通过调用另一个线程的
wait()
方法进入WAITING状态。这个状态表示线程正在无限期地等待另一个线程执行特定的操作(通常是通知或唤醒)。 -
TIMED_WAITING(限时等待):线程通过调用
sleep(long millis)
、wait(long millis)
或某个Thread池对象的await()
方法,带有超时参数的方法,进入TIMED_WAITING状态。与WAITING状态不同,处于TIMED_WAITING状态的线程会在指定的时间后自动醒来,或者在其他线程执行notify()
或notifyAll()
方法时醒来。 -
TERMINATED(终止):当线程执行完毕或因异常退出
run()
方法后,它进入TERMINATED状态。此时,线程的生命周期结束,不能再被启动。
线程状态之间的转换通常是由线程自身的行为(如调用sleep()
、wait()
等方法)或外部操作(如其他线程调用notify()
、notifyAll()
方法)触发的。理解这些状态及其转换过程对于编写高效且健壮的多线程程序至关重要。在实际编程中,需要根据具体需求合理控制线程的状态转换,以避免死锁、活锁等问题。
7. sleep() 和 wait() 的区别?
sleep()
和 wait()
虽然都可以用于控制线程的执行,但它们在锁的释放、唤醒机制和使用场景等方面存在显著的区别。
-
所属类和方法类型:
sleep()
是Thread
类的一个静态方法。wait()
是Object
类的一个实例方法,可以在任何对象上调用。
-
释放锁:
- 当线程调用
sleep()
方法时,它不会释放任何锁。这意味着如果线程持有一个对象的锁并调用sleep()
,其他线程仍然不能访问该对象的同步块或方法,直到原线程醒来并退出同步块或方法。 - 当线程调用
wait()
方法时,它会释放当前持有的对象的锁。这允许其他线程进入该对象的同步块或方法。
- 当线程调用
-
唤醒机制:
sleep()
方法会使线程在一定时间内休眠,时间结束后线程会自动醒来并继续执行。wait()
方法会使线程无限期地等待,直到其他线程调用同一对象的notify()
或notifyAll()
方法。如果没有其他线程调用这些方法,wait()
将会一直阻塞。
-
异常处理:
sleep()
方法在休眠时间结束或被中断时会抛出InterruptedException
。wait()
方法在被唤醒、中断或发生其他 I/O 异常时会抛出InterruptedException
。
-
使用场景:
sleep()
通常用于简单的线程暂停,比如模拟延时操作。wait()
和notify()
/notifyAll()
通常一起使用,用于实现线程间的通信和同步,特别是在生产者-消费者模型中。
-
响应中断:
- 调用
sleep()
的线程可以通过interrupt()
方法被中断,并抛出InterruptedException
。 - 调用
wait()
的线程同样可以通过interrupt()
方法被中断,并抛出InterruptedException
。
- 调用