Java 并发编程学习
1、创建线程的有哪些方式?
创建线程的方式有以下几种:
-
继承Thread类:创建一个类继承Thread类,并重写run()方法,然后通过创建类的实例来创建线程。
-
实现Runnable接口:创建一个类实现Runnable接口,并重写run()方法,然后通过创建Runnable对象来创建线程。
-
使用Executor框架:通过Executor框架创建线程,可以使用线程池来管理和调度线程的执行。
-
使用Callable和Future:使用Callable接口可以在执行完任务后返回结果,使用Future接口可以获取Callable执行的结果。
-
使用定时器:使用Timer类可以在指定时间间隔执行任务。
-
使用线程池:可以使用线程池来管理和调度线程的执行,可以通过ThreadPoolExecutor类或Executors工具类来创建线程池。
这些方式各有优缺点,选择合适的方式取决于具体的需求和场景。
2、创建线程的常用的三种方式如何选择?
(1)继承Thread类 创建简单,但Java不支持多重继承,如果已经继承了其他类,就无法再继承Thread类。
(2)实现Runnable接口 可以避免单一继承的局限性,因为一个类可以实现多个接口。
(3)通过Callable和Future创建线程 Callable接口有返回值,并且能够抛出异常。 提供了更强大的异步执行机制,能够获得任务执行的结果或取消任务。
因此,如果只是创建简单的线程,可以选择继承Thread类;如果需要实现多接口或者避免单一继承的限制,可以选择实现Runnable接口; 如果需要更复杂的异步执行和结果获取,可以选择实现Callable接口配合FutureTask。
3、线程的run()和start()有什么区别?
线程的run()方法和start()方法的区别如下:
-
run()方法是线程的主体,包含了线程要执行的代码;而start()方法用于启动线程,创建一个新的线程并使其进入就绪状态,等待系统调度并执行run()方法。
-
直接调用run()方法会在当前线程中执行普通的方法调用,没有创建新的线程,只是按照顺序执行方法中的代码。而调用start()方法会创建一个新的线程,并在新线程中执行run()方法中的代码。
-
注意:不应该直接调用run()方法来启动线程。应该通过调用start()方法来启动线程,以便系统能够正确地管理线程的生命周期和资源分配。
4、为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方 法?
在Java中,Thread类中的start()方法用于启动一个新的线程,并在新线程中调用run()方法。而直接调用run()方法只是普通的方法调用,不会创建新的线程。
当调用start()方法时,实际上会完成以下几个步骤:
-
创建一个新的线程。
-
调用新线程的run()方法。
而直接调用run()方法只会执行该方法,不会创建新的线程。这样的话,多线程的特性就无法体现出来,只是单纯的顺序执行方法而已。
所以,如果我们希望实现多线程的效果,就应该调用start()方法,让系统自动创建新的线程并调用run()方法。而直接调用run()方法只会在当前线程中顺序执行该方法的代码,不会创建新的线程。
5、servlet是线程安全的吗?
Servlet是线程安全的。Servlet容器在每个请求到达时会创建一个新的线程来处理请求。这意味着每个请求都会有自己的线程来执行,避免了多个请求之间的线程安全问题。但是需要注意的是,Servlet中的成员变量(即类级变量)是共享的,在多线程环境下需要注意对共享数据的访问控制,可以使用synchronized关键字或其他线程同步机制来确保线程安全。
可以采用哪些措施保证servlet的线程安全呢?
① 不在Servlet中使用实例变量来存储状态信息,可以使用局部变量来代替,因为局部变量存储在每个线程自己的栈中,自然是线程安全的。
② 如果必须使用实例变量,可以通过同步机制(如synchronized关键字)来确保同一时间只有一个线程能够访问和修改这些变量。
③ 对于每个线程需要独立存储的数据,可以使用ThreadLocal类来实现线程局部存储,这样每个线程都有自己的数据副本,互不干扰。
6、进程与线程有什么区别?
进程和线程是计算机中两个重要的执行单元,有以下区别:
-
进程是指正在运行中的程序,它是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间、文件描述符、堆栈和其他系统资源。一个程序可以包含多个进程,这些进程可以并发执行。
-
线程是进程中的一个执行单元,是程序执行的最小单位。一个进程可以拥有多个线程,这些线程共享该进程的地址空间和其他系统资源,但每个线程有自己的堆栈和局部变量。多线程可以实现并发执行,提高程序的响应速度和资源利用率。
-
进程之间是独立的,它们之间互相隔离,通信需要通过进程间通信(IPC)机制。线程之间是共享资源的,它们可以直接相互通信。
-
创建和销毁进程的开销较大,而创建和销毁线程的开销较小。线程的切换速度较快,因为它们共享了大部分的资源和上下文。
-
由于线程共享地址空间,所以线程之间的同步和通信相对容易。而进程之间的通信要复杂一些,需要使用特殊的机制。
总的来说,进程和线程都是用于执行程序的,但进程是资源分配的基本单位,而线程是操作系统调度的基本单位。进程之间是独立的,线程之间是共享资源的。
7、什么是java线程池?
**Java线程池**是一种多线程处理形式,它维护着多个线程,这些线程等待监督管理者分配可并发执行的任务。 线程池避免了在处理短时间任务时创建与销毁线程的代价,不仅能够保证内核的充分利用,还能防止过分调度。
线程池的工作原理是:当有任务到来时,线程池从池中取出一个线程去执行该任务,任务执行结束后,线程被放回池中以备循环使用。这种模式减少了线程创建和销毁的开销,提高了系统的效率和性能。
8、创建线程池的几个核心构造参数
在Java中,可以使用Executors工具类来创建不同类型的线程池。线程池的核心构造参数包括:
- corePoolSize:线程池中的常驻核心线程数。
- maximumPoolSize:线程池允许的最大线程数。
- keepAliveTime:非核心线程空闲后的存活时间。
- unit:keepAliveTime的时间单位。
- workQueue:用来保存等待执行任务的队列。
- threadFactory:创建线程的工厂。
- 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 。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。这种线程池适用于需要顺序执行任务的场景,例如文件I/O操作。
- newFixedThreadPool:创建一个定长线程池,可控制线程的最大并发数。超出的线程会在队列中等待。这种线程池适用于需要固定数量线程的任务处理,如Web服务器。
- newScheduledThreadPool:创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。这种线程池适用于需要定时执行任务的场景,如定时清理缓存或定时执行某些周期性任务。
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种线程池适用于执行大量短期异步任务的场景,能够动态地调整线程数量,减少资源浪费。
10、为何阿里巴巴开发手册中不允许使用Executors创建线程池?
**阿里巴巴开发手册**中不允许使用 Executors 创建线程池的主要原因 是由于其可能导致资源耗尽、线程数不可控和缺乏灵活性。使用Executors创建线程池隐藏了关键配置参数,限制了开发者对线程池行为的精确控制和优化,可能导致资源使用不当或性能问题。此外,Executors提供的便捷方法通常会使用无界队列,如果任务提交速度超过处理速度,可能会导致内存溢出(OOM)的风险。
具体原因包括 :
- 资源耗尽风险 :Executors类创建的线程池,如FixedThreadPool和SingleThreadPool,可能会导致请求队列堆积大量任务,最终引发内存溢出(OOM)的风险。特别是CachedThreadPool会无限创建新线程,直到达到系统资源的极限,这可能导致系统负载过高。
- 不可控制的线程数 :Executors类创建的线程池通常具有固定的参数配置,这限制了对于线程池参数的自定义调整,如核心线程数、最大线程数、任务队列等。这可能导致系统资源被迅速耗尽。
- 缺乏灵活性 :Executors类提供的线程池通常具有固定的参数配置,这限制了对于线程池参数的自定义调整,如核心线程数、最大线程数、任务队列等。这不利于根据实际需求进行细致的配置和调整。
- 不够透明 :使用Executors类创建的线程池可能不够透明,无法清晰地了解其内部工作机制和潜在问题,这对于性能调优和故障排查都是不利的。
推荐的替代方案 是使用ThreadPoolExecutor进行手动创建。通过直接使用ThreadPoolExecutor来创建线程池,可以让开发者更精细地控制线程池的行为,包括设置合理的线程数量、任务队列长度、拒绝策略等,从而更好地管理系统资源,避免不必要的风险。此外,推荐使用有界队列来控制线程池的任务排队数量,以避免潜在的问题。
11、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
线程池中的线程并不是一开始就随着线程池的启动创建好的。 线程池中的线程是在有任务提交时动态创建的。当一个新的任务提交到线程池时,线程池会根据当前的状态和配置来决定是否创建新线程。
当一个新的任务提交到线程池时,线程池会按照以下步骤执行:
- 如果线程池中的线程数小于corePoolSize,则创建一个新的线程,并将任务交给这个线程执行。
- 如果线程池中的线程数已经等于corePoolSize,新的任务会被放入工作队列中。
- 如果工作队列已满,则会创建新的线程来执行任务,直到线程数达到maximumPoolSize为止。
- 如果线程数已经达到maximumPoolSize并且工作队列已满,则执行拒绝策略,如抛出异常或者丢弃任务。
此外,如果在线程池启动时设置了prestartAllCoreThreads为true,则会预先创建corePoolSize个线程并启动它们,这样可以提前消耗一部分资源,但仍然需要根据任务的提交情况动态地创建线程来执行任务。
12、线程池的执行过程是什么?
**线程池**的执行过程可以分为以下几个主要步骤 :
- 提交任务 :当一个新的线程任务被提交到线程池时,线程池会首先检查是否有空闲线程。如果有,则分配一个空闲线程执行该任务;如果没有空闲线程,则进行下一步判断。
- 核心线程判断 :如果当前运行的线程数小于核心线程数(corePoolSize),线程池会创建一个新的核心线程来执行任务。如果当前运行的线程数已经达到或超过核心线程数,则进行下一步判断。
- 工作队列判断 :如果当前运行的线程数已经达到核心线程数,线程池会检查工作队列是否已满。如果工作队列未满,任务会被放入队列中等待执行;如果工作队列已满,则进行下一步判断。
- 非核心线程判断 :如果工作队列已满,线程池会判断当前线程数是否已达到最大线程数(maximumPoolSize)。如果没有达到最大线程数,线程池会创建一个新的非核心线程来执行任务;如果已达到最大线程数,则执行拒绝策略。
- 拒绝策略 :如果所有线程都在运行且工作队列已满,且线程数已达到最大线程数,线程池会执行拒绝策略(如AbortPolicy、CallerRunsPolicy等),具体策略取决于线程池的配置。
13、Java 线程池中 submit() 和 execute()方法有什么区别?
**Java线程池**中的 submit() 和 execute() 方法主要有以下区别 :
- 返回值 :
- submit()方法可以接受Callable或Runnable类型的任务,并返回一个Future对象。通过这个Future对象,可以获取任务的执行结果或者取消任务执行。
- execute()方法只能接受Runnable类型的任务,并且没有返回值。它直接在调用时执行任务,不会返回任何结果或Future对象。
- 异常处理 :
- submit()方法可以捕获任务执行过程中抛出的异常,并通过Future对象的get()方法重新抛出这些异常。这意味着异常可以通过编程方式处理,而不是直接暴露给调用者。
- execute()方法无法直接捕获任务执行中的异常,需要在任务内部进行异常处理。如果任务抛出未检查异常,这些异常可能会导致线程池中的线程被异常终止,从而影响线程池的稳定性。
- 方法来源和灵活性 :
- execute()方法是Executor接口中定义的方法,主要用于执行不带返回值的任务。它定义较为简单,适用于只需要执行任务而不需要结果反馈的场景。
- submit()方法是ExecutorService接口中定义的方法,在Executor的基础上增加了任务提交后可以获取任务执行结果的能力。这使得submit()方法更加灵活,适用于需要处理带返回值任务的场景。
14、如果你提交任务时,线程池队列已满,这时会发生什么
当你提交任务时,如果线程池队列已满,会发生拒绝策略。
具体来说,线程池会根据其配置的拒绝策略来处理无法执行的新任务。主要有以下几种情况:
- 使用无界队列(如 LinkedBlockingQueue ) :如果线程池使用的是无界队列,任务会继续添加到阻塞队列中等待执行,因为无界队列可以无限存放任务。
- 使用有界队列(如 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) 指定线程为守护线程。