Java面试宝典-并发编程学习01

Java 并发编程学习

1、创建线程的有哪些方式?

创建线程的方式有以下几种:

  1. 继承Thread类:创建一个类继承Thread类,并重写run()方法,然后通过创建类的实例来创建线程。

  2. 实现Runnable接口:创建一个类实现Runnable接口,并重写run()方法,然后通过创建Runnable对象来创建线程。

  3. 使用Executor框架:通过Executor框架创建线程,可以使用线程池来管理和调度线程的执行。

  4. 使用Callable和Future:使用Callable接口可以在执行完任务后返回结果,使用Future接口可以获取Callable执行的结果。

  5. 使用定时器:使用Timer类可以在指定时间间隔执行任务。

  6. 使用线程池:可以使用线程池来管理和调度线程的执行,可以通过ThreadPoolExecutor类或Executors工具类来创建线程池。

这些方式各有优缺点,选择合适的方式取决于具体的需求和场景。

2、创建线程的常用的三种方式如何选择?

(1)继承Thread类 创建简单,但Java不支持多重继承,如果已经继承了其他类,就无法再继承Thread类。

(2)实现Runnable接口 可以避免单一继承的局限性,因为一个类可以实现多个接口。

(3)通过Callable和Future创建线程 Callable接口有返回值,并且能够抛出异常。 提供了更强大的异步执行机制,能够获得任务执行的结果或取消任务。

因此,如果只是创建简单的线程,可以选择继承Thread类;如果需要实现多接口或者避免单一继承的限制,可以选择实现Runnable接口; 如果需要更复杂的异步执行和结果获取,可以选择实现Callable接口配合FutureTask。

3、线程的run()和start()有什么区别?

线程的run()方法和start()方法的区别如下:

  1. run()方法是线程的主体,包含了线程要执行的代码;而start()方法用于启动线程,创建一个新的线程并使其进入就绪状态,等待系统调度并执行run()方法。

  2. 直接调用run()方法会在当前线程中执行普通的方法调用,没有创建新的线程,只是按照顺序执行方法中的代码。而调用start()方法会创建一个新的线程,并在新线程中执行run()方法中的代码。

  3. 注意:不应该直接调用run()方法来启动线程。应该通过调用start()方法来启动线程,以便系统能够正确地管理线程的生命周期和资源分配。

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

在Java中,Thread类中的start()方法用于启动一个新的线程,并在新线程中调用run()方法。而直接调用run()方法只是普通的方法调用,不会创建新的线程。

当调用start()方法时,实际上会完成以下几个步骤:

  1. 创建一个新的线程。

  2. 调用新线程的run()方法。

而直接调用run()方法只会执行该方法,不会创建新的线程。这样的话,多线程的特性就无法体现出来,只是单纯的顺序执行方法而已。

所以,如果我们希望实现多线程的效果,就应该调用start()方法,让系统自动创建新的线程并调用run()方法。而直接调用run()方法只会在当前线程中顺序执行该方法的代码,不会创建新的线程。

5、servlet是线程安全的吗?

Servlet是线程安全的。Servlet容器在每个请求到达时会创建一个新的线程来处理请求。这意味着每个请求都会有自己的线程来执行,避免了多个请求之间的线程安全问题。但是需要注意的是,Servlet中的成员变量(即类级变量)是共享的,在多线程环境下需要注意对共享数据的访问控制,可以使用synchronized关键字或其他线程同步机制来确保线程安全。

可以采用哪些措施保证servlet的线程安全呢?

① 不在Servlet中使用实例变量来存储状态信息,可以使用局部变量来代替,因为局部变量存储在每个线程自己的栈中,自然是线程安全的。

② 如果必须使用实例变量,可以通过同步机制(如synchronized关键字)来确保同一时间只有一个线程能够访问和修改这些变量。

③ 对于每个线程需要独立存储的数据,可以使用ThreadLocal类来实现线程局部存储,这样每个线程都有自己的数据副本,互不干扰。

6、进程与线程有什么区别?

进程和线程是计算机中两个重要的执行单元,有以下区别:

  1. 进程是指正在运行中的程序,它是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间、文件描述符、堆栈和其他系统资源。一个程序可以包含多个进程,这些进程可以并发执行。

  2. 线程是进程中的一个执行单元,是程序执行的最小单位。一个进程可以拥有多个线程,这些线程共享该进程的地址空间和其他系统资源,但每个线程有自己的堆栈和局部变量。多线程可以实现并发执行,提高程序的响应速度和资源利用率。

  3. 进程之间是独立的,它们之间互相隔离,通信需要通过进程间通信(IPC)机制。线程之间是共享资源的,它们可以直接相互通信。

  4. 创建和销毁进程的开销较大,而创建和销毁线程的开销较小。线程的切换速度较快,因为它们共享了大部分的资源和上下文。

  5. 由于线程共享地址空间,所以线程之间的同步和通信相对容易。而进程之间的通信要复杂一些,需要使用特殊的机制。

总的来说,进程和线程都是用于执行程序的,但进程是资源分配的基本单位,而线程是操作系统调度的基本单位。进程之间是独立的,线程之间是共享资源的。

7、什么是java线程池?

‌**Java线程池**是一种多线程处理形式,它维护着多个线程,这些线程等待监督管理者分配可并发执行的任务。 ‌ 线程池避免了在处理短时间任务时创建与销毁线程的代价,不仅能够保证内核的充分利用,还能防止过分调度‌。

线程池的工作原理是:当有任务到来时,线程池从池中取出一个线程去执行该任务,任务执行结束后,线程被放回池中以备循环使用。这种模式减少了线程创建和销毁的开销,提高了系统的效率和性能‌。

8、创建线程池的几个核心构造参数

在Java中,可以使用Executors工具类来创建不同类型的线程池。线程池的核心构造参数包括:

  1. corePoolSize:线程池中的常驻核心线程数。
  2. maximumPoolSize:线程池允许的最大线程数。
  3. keepAliveTime:非核心线程空闲后的存活时间。
  4. unit:keepAliveTime的时间单位。
  5. workQueue:用来保存等待执行任务的队列。
  6. threadFactory:创建线程的工厂。
  7. handler:拒绝策略。
java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

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

            // 创建单线程化的线程池
            ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

            // 创建固定大小的线程池,且支持定时及周期性任务
            ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

            // 创建自适应大小的线程池
            ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

            // 自定义线程池
            ExecutorService customThreadPool = new ThreadPoolExecutor(
                    5, // corePoolSize
                    10, // maximumPoolSize
                    1, // keepAliveTime
                    TimeUnit.MINUTES, // unit
                    new ArrayBlockingQueue<>(10), // workQueue
                    Executors.defaultThreadFactory(), // threadFactory
                    new ThreadPoolExecutor.AbortPolicy() // handler
            );

            // 关闭线程池
            executorService.shutdown();
            singleThreadExecutor.shutdown();
            scheduledExecutorService.shutdown();
            cachedThreadPool.shutdown();
            customThreadPool.shutdown();
        }
    }

9、线程池的类型都有什么?

Java****中通过 Executors类 提供了四种常见的线程池类型,分别是: newSingleThreadExecutor newFixedThreadPool newScheduledThreadPool newCachedThreadPool

  1. newSingleThreadExecutor‌:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。这种线程池适用于需要顺序执行任务的场景,例如文件I/O操作。
  2. newFixedThreadPool‌:创建一个定长线程池,可控制线程的最大并发数。超出的线程会在队列中等待。这种线程池适用于需要固定数量线程的任务处理,如Web服务器。
  3. newScheduledThreadPool‌:创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。这种线程池适用于需要定时执行任务的场景,如定时清理缓存或定时执行某些周期性任务。
  4. newCachedThreadPool‌:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种线程池适用于执行大量短期异步任务的场景,能够动态地调整线程数量,减少资源浪费。

10、为何阿里巴巴开发手册中不允许使用Executors创建线程池?

‌**阿里巴巴开发手册**中不允许使用 Executors 创建线程池的主要原因 ‌是由于其可能导致资源耗尽、线程数不可控和缺乏灵活性。使用Executors创建线程池隐藏了关键配置参数,限制了开发者对线程池行为的精确控制和优化,可能导致资源使用不当或性能问题。此外,Executors提供的便捷方法通常会使用无界队列,如果任务提交速度超过处理速度,可能会导致内存溢出(OOM)的风险‌。

具体原因包括 ‌:

  1. 资源耗尽风险 ‌:Executors类创建的线程池,如FixedThreadPoolSingleThreadPool,可能会导致请求队列堆积大量任务,最终引发内存溢出(OOM)的风险。特别是CachedThreadPool会无限创建新线程,直到达到系统资源的极限,这可能导致系统负载过高‌。
  2. 不可控制的线程数 ‌:Executors类创建的线程池通常具有固定的参数配置,这限制了对于线程池参数的自定义调整,如核心线程数、最大线程数、任务队列等。这可能导致系统资源被迅速耗尽‌。
  3. 缺乏灵活性 ‌:Executors类提供的线程池通常具有固定的参数配置,这限制了对于线程池参数的自定义调整,如核心线程数、最大线程数、任务队列等。这不利于根据实际需求进行细致的配置和调整‌。
  4. 不够透明 ‌:使用Executors类创建的线程池可能不够透明,无法清晰地了解其内部工作机制和潜在问题,这对于性能调优和故障排查都是不利的‌。

推荐的替代方案 ‌是使用ThreadPoolExecutor进行手动创建。通过直接使用ThreadPoolExecutor来创建线程池,可以让开发者更精细地控制线程池的行为,包括设置合理的线程数量、任务队列长度、拒绝策略等,从而更好地管理系统资源,避免不必要的风险。此外,推荐使用有界队列来控制线程池的任务排队数量,以避免潜在的问题‌。

11、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?

线程池中的线程并不是一开始就随着线程池的启动创建好的。 ‌ 线程池中的线程是在有任务提交时动态创建的。当一个新的任务提交到线程池时,线程池会根据当前的状态和配置来决定是否创建新线程‌。

当一个新的任务提交到线程池时,线程池会按照以下步骤执行:

  1. 如果线程池中的线程数小于corePoolSize,则创建一个新的线程,并将任务交给这个线程执行。
  2. 如果线程池中的线程数已经等于corePoolSize,新的任务会被放入工作队列中。
  3. 如果工作队列已满,则会创建新的线程来执行任务,直到线程数达到maximumPoolSize为止。
  4. 如果线程数已经达到maximumPoolSize并且工作队列已满,则执行拒绝策略,如抛出异常或者丢弃任务‌。

此外,如果在线程池启动时设置了prestartAllCoreThreads为true,则会预先创建corePoolSize个线程并启动它们,这样可以提前消耗一部分资源,但仍然需要根据任务的提交情况动态地创建线程来执行任务‌。

12、线程池的执行过程是什么?

‌**线程池**的执行过程可以分为以下几个主要步骤 ‌:

  1. 提交任务 ‌:当一个新的线程任务被提交到线程池时,线程池会首先检查是否有空闲线程。如果有,则分配一个空闲线程执行该任务;如果没有空闲线程,则进行下一步判断‌。
  2. 核心线程判断 ‌:如果当前运行的线程数小于核心线程数(corePoolSize),线程池会创建一个新的核心线程来执行任务。如果当前运行的线程数已经达到或超过核心线程数,则进行下一步判断‌。
  3. 工作队列判断 ‌:如果当前运行的线程数已经达到核心线程数,线程池会检查工作队列是否已满。如果工作队列未满,任务会被放入队列中等待执行;如果工作队列已满,则进行下一步判断‌。
  4. 非核心线程判断 ‌:如果工作队列已满,线程池会判断当前线程数是否已达到最大线程数(maximumPoolSize)。如果没有达到最大线程数,线程池会创建一个新的非核心线程来执行任务;如果已达到最大线程数,则执行拒绝策略‌。
  5. 拒绝策略 ‌:如果所有线程都在运行且工作队列已满,且线程数已达到最大线程数,线程池会执行拒绝策略(如AbortPolicyCallerRunsPolicy等),具体策略取决于线程池的配置‌。

13、Java 线程池中 submit() 和 execute()方法有什么区别?

‌**Java线程池**中的 submit() execute() 方法主要有以下区别 ‌:

  1. 返回值 ‌:
    • submit()方法可以接受Callable或Runnable类型的任务,并返回一个Future对象。通过这个Future对象,可以获取任务的执行结果或者取消任务执行‌。
    • execute()方法只能接受Runnable类型的任务,并且没有返回值。它直接在调用时执行任务,不会返回任何结果或Future对象‌。
  2. 异常处理 ‌:
    • submit()方法可以捕获任务执行过程中抛出的异常,并通过Future对象的get()方法重新抛出这些异常。这意味着异常可以通过编程方式处理,而不是直接暴露给调用者‌。
    • execute()方法无法直接捕获任务执行中的异常,需要在任务内部进行异常处理。如果任务抛出未检查异常,这些异常可能会导致线程池中的线程被异常终止,从而影响线程池的稳定性‌。
  3. 方法来源和灵活性 ‌:
    • execute()方法是Executor接口中定义的方法,主要用于执行不带返回值的任务。它定义较为简单,适用于只需要执行任务而不需要结果反馈的场景‌。
    • submit()方法是ExecutorService接口中定义的方法,在Executor的基础上增加了任务提交后可以获取任务执行结果的能力。这使得submit()方法更加灵活,适用于需要处理带返回值任务的场景‌。

14、如果你提交任务时,线程池队列已满,这时会发生什么

当你提交任务时,如果线程池队列已满,会发生拒绝策略。

具体来说,线程池会根据其配置的拒绝策略来处理无法执行的新任务。主要有以下几种情况:

  1. 使用无界队列(如 LinkedBlockingQueue ‌:如果线程池使用的是无界队列,任务会继续添加到阻塞队列中等待执行,因为无界队列可以无限存放任务‌。
  2. 使用有界队列(如 ArrayBlockingQueue ‌:如果线程池使用的是有界队列,当队列满时,会根据maximumPoolSize的值增加线程数量。如果增加了线程数量还是处理不过来,队列继续满,则会使用拒绝策略处理新的任务,默认是AbortPolicy‌。

拒绝策略主要有以下几种:

  • AbortPolicy‌:直接抛出RejectedExecutionException异常。
  • CallerRunsPolicy‌:不在新线程中执行任务,而是在调用者的线程中直接执行任务。
  • DiscardPolicy‌:默默丢弃无法处理的任务,不抛出异常。
  • DiscardOldestPolicy‌:丢弃队列最老的任一个任务,并执行当前任务‌

15、说说你对核心线程数的理解?

配置文件中的线程池核心线程数为何配置为

java 复制代码
// 获取CPU的处理器数量

int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。

CPU **密集型,**线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。

IO **密集型,**线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。

在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。

如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。

16、什么是线程组?

线程组在Java中主要用于方便地管理线程,它可以包含多个线程,将它们组织成一个单元,从而更容易进行管理和控制。

在Java中创建线程组时,可以使用ThreadGroup类的构造函数,可以指定线程组的名称和父线程组。默认情况下,所有的线程都属于主线程组。我们可以通过线程对象获取它所属的线程组,也可以通过线程组对象获取它所在组的名字。

线程组可以为其中的所有线程设置共同的属性,如线程优先级、是否守护线程等。

一旦线程加入某个线程组,它将一直属于该线程组,直到线程终止,且不能中途改变所属的线程组。

线程组更多地用于对线程进行分组管理和属性设置,而线程池则专注于提高系统性能,通过重用线程来减少资源消耗,并控制并发。

17、为什么在 Java 中不推荐使用线程组?

线程组中的stop、resume和suspend方法会导致安全问题,如死锁,这些方法已经被官方废弃。

线程组的功能相对有限,它无法在运行时对线程进行高级操作,如方法注入或暂停线程等。

在实际开发中,线程组的机制相对笨重,不便于进行动态调度,这导致代码难以扩展。

对于线程管理,推荐使用如Executor框架这样的现代工具,它们提供了更好的线程管理和资源控制,能够更有效地满足并发编程的需求。

Executor框架中的ThreadPoolExecutor类提供了线程池的实现,可以有效地管理和复用线程,减少系统开销。

18、为什么推荐使用Executor框架?

Executor框架将任务的提交与执行分离,提供了一种更加灵活和可扩展的方式来管理线程。它允许开发者专注于任务的实现逻辑,而不必关心任务的执行细节。

通过Executor框架,可以轻松地将任务异步执行,从而提高程序的效率和响应性。

Future接口允许获取异步任务的执行结果,提供了检查任务是否完成、等待任务完成以及获取任务结果的方法。这使得可以灵活地处理异步计算的结果,包括可能的异常处理。

除了基本的线程池实现,Executor框架还提供了ScheduledThreadPoolExecutor类,支持定时或周期性执行任务。

19、Executor 和 Executors 有什么区别?

Executor是一个接口,定义了一个线程池的核心方法execute(),用于提交任务到线程池中执行。它只包含一个execute(Runnable command)方法,用于执行给定的任务。

Executors 是一个工具类,提供了一些静态工厂方法来创建不同类型的ExecutorService实例。这些方法包括newFixedThreadPool、newCachedThreadPool等,它们提供了对ThreadPoolExecutor的封装,生成ExecutorService的具体实现类。

在实际使用中,通常不需要直接与Executor接口打交道,而是通过Executors类提供的方法来获取一个具体的ExecutorService实例。

20、用户线程与守护线程有什么区别?

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。

用户线程是最常见的线程,比如通过main方法启动,就会创建一个用户线程。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

JVM中的垃圾回收、JIT编译器线程就是最常见的守护线程。

只要有一个用户线程在运行,守护线程就会一直运行。只有所有的用户线程都结束的时候,守护线程才会退出。

编写代码时,也可以通过 thread.setDaemon(true) 指定线程为守护线程。

相关推荐
呆呆小雅几秒前
C#关键字volatile
java·redis·c#
Monly211 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
Ttang233 分钟前
Tomcat原理(6)——tomcat完整实现
java·tomcat
钱多多_qdd14 分钟前
spring cache源码解析(四)——从@EnableCaching开始来阅读源码
java·spring boot·spring
waicsdn_haha16 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
biter008821 分钟前
opencv(15) OpenCV背景减除器(Background Subtractors)学习
人工智能·opencv·学习
Q_192849990626 分钟前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
Code_流苏29 分钟前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
ThisIsClark35 分钟前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
禁默1 小时前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程