Java多线程面试题总结(上)

简介: 进程和线程是操作系统管理程序执行的基本单位,二者有明显区别:1. **定义与基本单位**:进程是资源分配的基本单位,拥有独立的内存空间;线程是调度和执行的基本单位,共享所属进程的资源。2. **独立性与资源共享**:进程间相互独立,通信需显式机制;线程共享进程资源,通信更直接快捷。3. **管理与调度**:进程管理复杂,线程管理更灵活。4. **并发与并行**:进程并发执行,提高资源利用率;线程不仅并发还能并行执行,提升执行效率。5. **健壮性**:进程更健壮,一个进程崩溃不影响其他进程;线程崩溃可能导致整个进程崩溃。

一、线程和进程的区别?

进程和线程是操作系统中的两个重要概念,它们之间存在着明显的区别。以下是对进程和线程区别的详细阐述:

1、定义与基本单位

  • 进程(Process):进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。它是程序的一次执行,可以包含多个线程。每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销。
  • 线程(Thread):线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程是处理器(CPU)任务调度和执行的基本单位,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

2、独立性与资源共享

  • 进程:进程是相对独立的,每个进程都有自己的内存空间、文件描述符等资源,进程之间的通信需要通过显式的机制,如管道、消息队列和共享内存等来实现。进程切换开销较大,因为需要保存和恢复整个进程的上下文。
  • 线程:线程是进程的一部分,用于实现并发和并行操作。线程共享进程的地址空间和资源,包括文件描述符和内存。因此,线程之间的通信更加方便和快捷,可以直接读写进程共享的数据。线程切换的开销较小,因为只需要保存和恢复线程的上下文。

3、管理与调度

  • 进程:进程的管理相对复杂,需要操作系统的支持。进程的创建和销毁都需要操作系统进行资源分配和回收。进程的调度由操作系统根据一定的调度算法进行,以确保系统的公平性和效率。
  • 线程:线程的管理更为灵活,线程的创建和销毁相对简单,可以由应用程序自行控制。线程的调度也由操作系统进行,但由于线程共享进程的地址空间,因此线程的调度开销较小。

4、并发与并行

  • 进程:进程可以并发执行,即多个进程可以在同一时间段内交替执行,但每个进程在某一时刻只能占用一个CPU。进程之间的并发执行可以提高系统的资源利用率和吞吐量。
  • 线程:线程不仅可以并发执行,还可以并行执行。在多核或多CPU的计算机上,多个线程可以同时执行,从而提高程序的执行效率。此外,线程还可以用于实现任务的分解和并行处理,以进一步提高程序的性能。

5、健壮性

  • 进程:由于进程之间是相互独立的,一个进程的崩溃通常不会影响到其他进程的运行。因此,多进程系统通常比多线程系统更加健壮。
  • 线程:线程共享进程的地址空间和资源,因此一个线程的崩溃可能会导致整个进程的崩溃。这要求程序员在编写多线程程序时需要更加注意线程之间的同步和互斥问题,以确保程序的稳定性和可靠性。

综上所述,进程和线程在定义、独立性、资源共享、管理与调度、并发与并行以及健壮性等方面都存在明显的区别。了解这些区别对于编写高效、安全和可靠的程序至关重要。

二、什么是多线程切换?

多线程会共同使用一组计算机上的 CPU, 而线程数大于给程序分配的 CPU 数时,为了让各个线程都有执行的机会,就需要轮换使用 CPU,不同的线程切换使用 CPU 时发生的切换过程就是上下文切换。

三、死锁和活锁的区别,死锁和饥饿的区别?

1、死锁

是指两个或两个以上的进程(或线程)在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一段时间内独占资源。
  • 请求和保持条件:一个进程因请求资源而阻塞时,对方获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

2、活锁

任务或执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的 ;处于死锁的实体表现为等待;活锁有可能自行解开,死锁不能。

3、饥饿

一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。Java 中导致饥饿的原因:

  • 高于优先级线程吞噬所有的低优先级线程的 CPU 时间。
  • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续的对该同步块进行访问。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法)因为其他线程总是被持续的获得、唤醒。

四、为什么我们调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?

当调用 run() 方法时会创建新的线程,并且执行在 run() 方法里的代码。但是如果直接调用 run() 方法,它不会创建新的线程也不会执行调用线程的代码,只会把它当作一个普通方法来执行。

五、线程的五种基本状态

1、新建状态(New):当线程对象创建后,即进入了新建状态,如:Thread t = new Thread()

2、就绪状态(Runnable):当调用线程对象的 start() 方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好准备,随时等待 CPU 调度执行,并不是说执行了 start() 方法此线程立即就会被执行。

3、运行状态(Running): 当 CPU 开始调度处于就绪状态的线程时,此时线程才能得以真正的运行,即进入运行状态。注意,就绪状态时进入运行状态的唯一入口,也就是说,线程想要进入运行状态执行,必须处于就绪状态中。

4、阻塞状态(Blocked):处于运行状态的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行。此时进入阻塞状态,直到其进入就绪状态才有机会再次被 CPU 调用进行运行状态。根据阻塞产生的不同原因,阻塞线程又可以分为三种状态:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使本线程进入等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其他线程所占用),它就会进入同步阻塞状态。
  • 其他阻塞:通过调用线程的 sleep() 或者 join() 或发出 I/O 请求时,线程就会进入到阻塞状态。当 sleep() 状态超时时、join() 等待线程终止或者超市、或者 I/O 处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead): 线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。

六、synchronized 的作用?

在 JAVA 中 synchronized 关键字时用来控制线程同步的,就是在多线程环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以加在一段代码上,也可以加在方法上面。

七、线程池

Java 中的线程池是一个非常重要的概念,它用于管理一组工作线程,这些线程可以重复执行提交给它们的任务。使用线程池可以显著降低资源消耗、提高系统响应速度、增强系统稳定性,同时还可以有效地控制系统中并发线程的数量。

Java 中实现线程池主要通过 java.util.concurrent 包下的 ExecutorService 接口,Executors 类提供了几种静态工厂方法来创建不同类型的线程池。

常见的线程池类型

1、FixedThreadPool(固定大小线程池)

  • 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
  • 使用场景:适用于负载比较重的服务器,为了资源的合理利用,限制线程的数量。
  • 示例代码:ExecutorService executor = Executors.newFixedThreadPool(nThreads);

2、CachedThreadPool(可缓存线程池)

  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • 使用场景:适用于执行大量的短时间异步任务。
  • 示例代码:ExecutorService executor = Executors.newCachedThreadPool();

3、SingleThreadExecutor(单线程化的Executor)

  • 创建一个单线程的线程池,它用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • 使用场景:适用于需要顺序地执行各个任务并且不希望多个线程同时访问共享资源的情况。
  • 示例代码:ExecutorService executor = Executors.newSingleThreadExecutor();

4、ScheduledThreadPool(定时/周期任务执行的线程池)

  • 创建一个定长线程池,支持定时及周期性任务执行。
  • 使用场景:需要按照指定时间间隔或延迟后执行任务的场景。
  • 示例代码:ScheduledExecutorService executor = Executors.newScheduledThreadPool(nThreads);

线程池的使用

1、提交任务

  • 通过 execute(Runnable command) 方法提交任务给线程池执行。
  • 对于 ScheduledThreadPool,可以使用 schedule(Runnable command, long delay, TimeUnit unit)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 等方法提交定时或周期任务。

2、关闭线程池

  • 使用 shutdown() 方法来平滑地关闭ExecutorService,不再接受新任务,但是已经提交的任务会继续执行。
  • 使用 shutdownNow() 方法尝试立即停止所有正在执行的任务,停止处理正在等待的任务,并返回等待执行的任务列表。

3、等待任务完成

  • 调用 awaitTermination(long timeout, TimeUnit unit) 可以阻塞当前线程直到所有任务完成或被终止,或者直到超时期满,或者线程被中断,取决于哪个事件先发生。

注意事项

  • 线程池中的线程数量应该根据任务的性质、执行时间、系统资源等因素进行合理设置。
  • 线程池中的任务应该是相互独立的,避免共享数据导致线程安全问题。
  • 线程池不应该被随意创建和销毁,以避免资源浪费。
  • 合理使用线程池的状态和关闭方法,确保线程池资源得到正确释放。

在Java中,线程池的使用是一个从简单到复杂逐步深入的过程。下面我将通过一个从简单到复杂的示例来展示线程池的使用,并且会提供一个涉及对象处理的案例。

简单示例:使用FixedThreadPool

在这个简单的例子中,我们将使用Executors.newFixedThreadPool(int nThreads)来创建一个固定大小的线程池,并提交一些简单的任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleThreadPoolExample {
   

    public static void main(String[] args) {
   
        // 创建一个固定大小为5的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务
        for (int i = 0; i < 10; i++) {
   
            int taskId = i;
            executor.execute(() -> {
   
                System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskId);
            });
        }

        // 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
        executor.shutdown();
    }
}

复杂示例:使用ThreadPoolExecutor处理对象

在这个例子中,我们将使用ThreadPoolExecutor直接构造一个线程池,并处理一些包含对象的任务。我们将创建一个简单的Task类,该类包含执行逻辑和与之关联的对象数据。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

class Task implements Runnable {
   
    private String data;

    public Task(String data) {
   
        this.data = data;
    }

    @Override
    public void run() {
   
        System.out.println(Thread.currentThread().getName() + " 正在处理数据: " + data);
        // 模拟任务执行时间
        try {
   
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
   
            Thread.currentThread().interrupt();
        }
    }
}

public class ComplexThreadPoolExample {
   

    public static void main(String[] args) {
   
        // 使用ThreadPoolExecutor直接创建线程池
        ExecutorService executor = new ThreadPoolExecutor(
                5, // 核心线程数
                10, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程存活时间
                new java.util.concurrent.ArrayBlockingQueue<>(100), // 等待队列
                Executors.defaultThreadFactory(), // 线程工厂
                new java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
        );

        // 提交任务
        for (int i = 0; i < 20; i++) {
   
            executor.execute(new Task("任务" + i));
        }

        // 关闭线程池
        executor.shutdown();
        // 等待所有任务完成
        while (!executor.isTerminated()) {
   
            // 等待线程池中的任务完成
        }
        System.out.println("所有任务执行完成");
    }
}

在这个例子中,Task类是一个实现了Runnable接口的类,它接受一个字符串数据并在其run方法中处理这些数据。我们创建了一个ThreadPoolExecutor实例,它允许我们更细致地控制线程池的行为,包括核心线程数、最大线程数、空闲线程的存活时间、任务队列、线程工厂以及拒绝策略等。

通过提交多个Task实例给线程池,我们展示了如何在线程池中处理包含对象数据的任务。最后,我们等待所有任务完成并打印了一条消息。

这两个示例从简单到复杂地展示了如何在Java中使用线程池,并演示了如何处理包含对象数据的任务。

目录
相关文章
|
2天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
24 6
|
17天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
15天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
17天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
45 14
|
17天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
10天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
10天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
30 3
|
11天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
16天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
54 6
|
15天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
24 2