什么是线程
线程(英语:thread)是进程的基本组成单位,一个进程可以包含多个线程,每个线程可以并行执行不同的任务。
线程与进程的区别
- 根本区别:进程是操作系统(OS)分配资源的基本单位,而线程是处理器(CPU)任务调度和执行的最基本单位。也就是说,进程是程序的一次执行过程,它占有独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。而线程是进程中的一个执行流程,不同的线程共享同一份进程的地址空间。
- 资源管理:同一进程内的不同线程共享该进程的资源,例如内存、I/O、CPU等。因此线程之间的通信更容易,编程也更简单。然而,因为多个线程共享同一进程的资源,所以不利于资源的管理和保护。相反,进程之间的资源是独立的,能很好地进行资源管理和保护。
- 开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器。
- 健壮性:多进程因为资源完全独立,一个进程的崩溃不会影响到其他进程。而在多线程环境中,一个线程的崩溃可能会影响到整个进程中的其他线程。
线程创建常用的四种方式
1)继承 Thread类 创建线程
步骤:
- 创建一个类,继承自
Thread类。 - 重写
run()方法,将需要执行的任务放在run()方法中。 - 创建该类的实例对象。
- 调用
start()方法启动线程
class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2)实现 Runnable 接口创建线程
步骤:
- 创建一个类,实现
Runnable接口。 - 重写
run()方法,将需要执行的任务放在run()方法中。 - 创建该类的实例对象。
- 使用
Thread类的构造方法创建线程对象,并将Runnable实例作为参数传入。 - 调用
start()方法启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
3)使用 Callable 和 Future 创建线程
步骤:
- 创建一个实现
Callable接口的类,重写call()方法,将需要执行的任务放在call()方法中。 - 创建该类的实例对象。
- 使用
ExecutorService的submit()方法提交任务,返回一个Future对象。 - 调用
Future对象的get()方法获取任务执行结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "MyCallable is running";
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyCallable myCallable = new MyCallable();
Future<String> future = executorService.submit(myCallable);
try {
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
4)使用线程池,例如用 Executor 框架
步骤:
- 创建一个实现
Runnable接口的任务类。 - 使用
Executors工厂方法创建一个固定大小的线程池。例如,创建一个包含5个线程的线程池:。 - 通过
executor.execute(new MyTask())将任务提交给线程池执行。 - 当所有任务都提交后,调用
shutdown()方法来关闭线程池。
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务正在执行:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Executor executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(new MyTask());
}
executor.shutdown();
}
}
什么是 Executor框架
Executor框架在Java 5之后引入,最大优点是将任务的提交和执行解耦。
Executor框架主要由三部分组成:任务、任务的执行和异步计算的结果。
- 任务:这是工作单元,包括
Runnable接口和Callable接口,它们是被执行任务需要实现的接口。分别代表了"可以运行的任务"和"可以返回结果的任务"。 - 任务的执行:这是把任务分派给多个线程的执行机制,核心接口为
Executor,以及ExecutorService接口。 - 异步计算的结果:包括
Future接口和实现了Future接口的FutureTask类。通过Future对象,我们能获取到异步任务执行的结果。
ExecutorService接口有两个关键实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。
什么是线程池? 使用它有哪些好处?
线程池是一种用于管理和复用线程的机制,它允许在应用程序中创建一组可用线程,并在需要执行任务时将任务分配给这些线程。
线程池的主要目的是优化线程的创建、销毁和管理,以提高多线程应用程序的性能和效率。
可以把线程池理解为公司(或者管理机制),线程理解为员工,任务理解为公司的工作
具体的好处如下
- 降低线程创建和销毁的开销:线程的创建和销毁是开销较大的操作,线程池可以重复利用线程,减少这些开销。 (理解为公司不会每个任务都去招新人、用完再开除)
- 控制并发度:通过配置线程池的大小,可以限制并发执行的任务数量,防止系统过载。
- 统一管理和监控:线程池提供了统一的管理和监控接口,方便对线程的状态和执行情况进行监控和调整。
- 避免资源竞争:在多线程环境中执行任务,可能会导致资源竞争和线程冲突,线程池可以帮助避免这些问题。
如何自定义线程池
可以用 ThreadPoolExecutor类 自定义线程池,它有多个构造方法来创建线程池,用该类很容易实现自定义的线程池。
其中,自定义线程池的核心参数如下:
- 核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用
corePoolSize方法来设置。一般情况下,可以根据系统的资源情况和任务的特性来设置合适的值。 - 最大线程数 (maximumPoolSize): 线程池中允许存在的最大线程数。可以使用
maximumPoolSize方法设置。如果所有线程都处于活动状态,而此时又有新的任务提交,线程池会创建新的线程,直到达到最大线数。 - 空闲线程存活时间 (keepAliveTime) : 当线程池中的线程数量超过核心线程数时,如果这些线程在一定时内没有执行任务,则这些线程会被销毁。可以使用
keepAliveTime和TimeUnit方法来设置。 - 阻塞队列 (workQueue): 用于存放等待执行的任务的阻塞队列。可以根据任务的特性选择不同类型的队列,如
LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界阻塞队列,即LinkedBlockingQueue,但也可以根据需要设置有界队列。 - 线程工厂 (threadFactory): 用于创建线程的工厂。可以通过实现
ThreadFactory接口自定义线程的创建逻辑。 - 拒绝策略 (rejectedExecutionHandler) : 当线程池无法接受新的任务时,会根据设置的拒绝策略进行处理。常见的拒绝策略有
AbortPolicy、DiscardPolicy、DiscardOldestPolicy 和 CallerRunsPolicy。