多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。在Java中实现多线程的方法有多种,主要包括继承Thread类、实现Runnable接口、实现Callable接口并通过FutureTask包装器来创建Thread线程,以及使用ExecutorService来管理线程。
如果你觉得我分享的内容或者我的努力对你有帮助,或者你只是想表达对我的支持和鼓励,请考虑给我点赞、评论、收藏。您的鼓励是我前进的动力,让我感到非常感激。
文章目录
1 基本概念
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。
1.1 进程与线程
进程: 是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程: 是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的 资源。
虽然系统是把资源分给进程,但是 CPU 很特殊,是被分配到线程的,所以线程是 CPU 分配的基本单位。
二者关系:
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
- 程序计数器: 是一块内存区域,用来记录线程当前要执行的指令地址 。
- 栈: 用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
- 堆: 是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
- 方法区: 则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。
二者区别:
- 进程: 有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
- 线程: 是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
总结:
- 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
1.2 并发与并行
并发: 是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行: 是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于 CPU 的个数,所以一般都称多线程并发编程而不是多线程并行编程。
1.3 并发过程中常见的问题
1、线程安全问题:
多个线程同时操作共享变量 1 时,会出现线程 1 更新共享变量 1 的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。
2、共享内存不可见性问题
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如上图所示)
上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU 的每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 都共享的二级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 缓存或者 CPU 的寄存器
- 线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以加载主内存中 X 的值,假如为 0。然后把 X=0 的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入两级 Cache,并且刷新到主内存。线程 A 操作完毕后,线程 A 所在的 CPU 的两级 Cache 内和主内存里面的 X 的值都是 l。
- 线程 B 获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X=1;到这里一切都是正常的,因为这时候主内存中也是 X=l。然后线程 B 修改 X 的值为 2,并将其存放到线程 2 所在的一级 Cache 和共享二级 Cache 中,最后更新主内存中 X 的值为 2,到这里一切都是好的。
- 线程 A 这次又需要修改 X 的值,获取时一级缓存命中,并且 X=l 这里问题就出现了,明明线程 B 已经把 X 的值修改为 2,为何线程 A 获取的还是 l 呢?这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。
2 多线程的实现方式
2.1 继承Thread类
通过继承Thread类,可以创建一个新的线程。你需要重写Thread类的run方法,然后创建该类的实例并调用start方法来启动线程。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
2.2 实现Runnable接口
通过实现Runnable接口,你可以定义一个线程执行的任务,然后将该任务传递给Thread对象并启动线程。这种方法更加灵活,因为Java单继承的限制使得继承Thread类不够灵活。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
}
}
2.3 实现Callable接口并通过FutureTask包装器来创建Thread线程
Callable接口与Runnable类似,但Callable接口可以返回一个结果并且可以抛出一个异常。通过FutureTask包装Callable对象,可以得到任务执行的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result: " + Thread.currentThread().getName();
}
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread t1 = new Thread(futureTask);
t1.start();
try {
// Get the result of the Callable
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 使用ExecutorService
ExecutorService提供了更高级的线程管理方式,包括线程池、任务提交和结果返回等功能。可以使用Executors工厂类来创建不同类型的ExecutorService。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceRunnable {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable task1 = () -> System.out.println("Runnable Task 1: " + Thread.currentThread().getName());
Runnable task2 = () -> System.out.println("Runnable Task 2: " + Thread.currentThread().getName());
executorService.execute(task1);
executorService.execute(task2);
executorService.shutdown();
}
}
实例: 处理java调用python脚本,输入流缓冲区内存满,未读出时,导致进程无法结束问题。【异步消耗输入流内容】
private static void asyncHandleInputStream(Process proc) {
ExecutorService asyncHandleInputStream = null;
try {
asyncHandleInputStream = new ThreadPoolExecutor(5, 10, 60, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
asyncHandleInputStream.execute(() -> {
String inputInfoLine;
try (BufferedReader inputReader = new BufferedReader(
new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8))) {
while ((inputInfoLine = inputReader.readLine()) != null) {
}
} catch (Exception exp) {
LOGGER.error("handle InputStream error! {}", exp);
}
});
} finally {
if (asyncHandleInputStream != null) {
asyncHandleInputStream.shutdown();
}
}
}
3 不同实现方式对比
方法 | 继承Thread类 | 实现Runnable接口 | 实现Callable接口 + FutureTask | 使用ExecutorService |
---|---|---|---|---|
实现方式 | 继承Thread | 实现Runnable接口 | 实现Callable接口,用FutureTask包装 | 使用ExecutorService管理任务 |
代码复杂度 | 简单 | 简单 | 稍复杂 | 简单且灵活 |
是否支持返回值 | 不支持 | 不支持 | 支持 | 支持(通过Callable) |
是否支持异常抛出 | 不支持 | 不支持 | 支持 | 支持(通过Callable) |
是否受单继承限制 | 是 | 否 | 否 | 否 |
是否适合资源共享 | 否(除非static) | 是 | 是 | 是 |
是否方便线程池管理 | 不方便 | 方便 | 方便 | 非常方便 |
适用场景 | 简单任务 | 简单、共享资源任务 | 需要返回值或处理异常的任务 | 需要高级线程管理的任务 |
4 相关问答
4.1 继承Thread类和实现Runnable接口这两种实现方式区别
- 继承Thread类:好处是: 因为是继承,代码简单,能够直接使用Thread类的方法。确点是: 扩展性比较差,因为继承了Thread类,不能再继承其他的类。
- 实现Runnable接口:好处是: 扩展性比较强。缺点时:代码比较冗余,因为不是继承Thread类,无法直接使用thread中的方法。
4.2 实现Runnable和Callable的区别?
- Runnable接口的run方法没有返回值,不能抛异常;Runnable接口的实现类对象既可以作为参数传递给Thread的构造方法,也可以用线程池submit的参数;
- Callable接口的call方法可以抛异常,有返回值。Callable接口的实现类对象只适应于线程池。