多线程
2025年11月30日大约 6 分钟
多线程
线程与多线程简介
什么是线程呢,实际上之前我在Python相关内容([Python]进程—函数堵塞的解决方法)的的时候提到过。下面是我当时在网上找到的对线程的解释:
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
那什么是多线程呢:
多线程是指在一个进程中同时运行多个线程,每个线程可以独立执行不同的任务。多线程允许程序在同一时间内处理多个任务,提高了程序的并发性和响应能力。
创建线程
在Java中,创建线程有四种方式:
继承 Thread 类
- 创建一个类继承
Thread类。public class MyThread extends Thread { ...... } - 重写
run()方法,定义线程执行的任务。@Override public void run() { // 线程执行的任务 System.out.println("Thread is running"); } - 创建线程对象并启动线程。
public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程,调用 run() 方法 } }
注意
- 直接调用
方法不会启动新线程,而是作为普通方法调用,因此仍然是单线程执行。run() - 必须使用
start()方法来启动线程。
- 优点:编码简单
- 缺点:Java 不支持多重继承,继承了
Thread类,就不能再继承其他类。
实现 Runnable 接口
- 创建一个类实现
Runnable接口。public class MyRunnable implements Runnable { ...... } - 实现
run()方法,定义线程执行的任务。@Override public void run() { // 线程执行的任务 System.out.println("Runnable is running"); } - 创建
Thread对象,并将Runnable对象作为参数传递,最后调用Thread对象的start()方法启动线程。public class Main { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); // 启动线程,调用 run() 方法 } }
- 优点:可以实现多重继承,因为 Java 支持类实现多个接口。
- 缺点:线程执行完成后,无法直接获取线程的返回值(返回结果)。
使用 Callable 接口和 FutureTask 类
- 创建一个类实现
Callable接口,并指定返回值类型。import java.util.concurrent.Callable; public class MyCallable implements Callable<Integer> { ...... } - 实现
call()方法,定义线程执行的任务,并返回结果。@Override public Integer call() { // 线程执行的任务 return 42; // 返回结果 } - 创建
FutureTask对象,并将Callable对象作为参数传递,然后创建Thread对象并启动线程。import java.util.concurrent.FutureTask; public class Main { public static void main(String[] args) throws Exception { MyCallable myCallable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(myCallable); Thread thread = new Thread(futureTask); thread.start(); // 启动线程,调用 call() 方法 // 获取线程执行结果 Integer result = futureTask.get(); System.out.println("Result: " + result); } }
- FutureTask 的构造器及方法
| 构造器 | 说明 |
|---|---|
| FutureTask(Callable<V> callable) | 创建一个 FutureTask 对象,接受一个 Callable 对象作为参数。 |
| 方法 | 说明 |
|---|---|
| V get() | 获取线程执行的结果,如果线程尚未完成,则阻塞等待直到结果可用。 |
- 优点:可以获取线程执行的返回值。
- 缺点:代码相对复杂。
使用线程池
见线程池
线程安全问题
在多线程环境下,多个线程可能同时修改共享资源,导致数据不一致或程序异常,这就是线程安全问题。
提示
既然有线程安全问题,因此我们就需要解决方法,下面线程同步部分会介绍几种常见的解决方法。
线程同步
- 线程同步的核心思想:让多个线程依次访问共享资源,确保同一时间只有一个线程可以访问共享资源,从而避免数据不一致的问题。
- 线程同步的常见方案:
- 加锁:每次只允许一个线程加锁,加锁后才能访问共享资源,访问完成后自动解锁,其他线程才能继续加锁访问。
同步代码块
- 核心思想: 把访问共享资源的核心代码上锁,确保同一时间只有一个线程可以执行该代码块。
使用 synchronized 关键字定义同步代码块,指定一个锁对象,只有获得该锁的线程才能执行同步代码块。
synchronized (锁对象) {
// 访问共享资源的代码
}注意
对于需要同步的代码块,必须使用同一个锁对象,否则无法实现同步效果。
提示
- 建议使用共享资源作为锁对象,对于实例方法,可以使用
this作为锁对象; - 对于静态方法,可以使用类的字节码(
类名.class)对象作为锁对象。
同步方法
- 核心思想:将整个方法上锁,确保同一时间只有一个线程可以执行该方法。
使用synchronized关键字定义同步方法:
public synchronized void synchronizedMethod() {
// 访问共享资源的代码
}同步方法底层原理
- 同步方法的锁对象由 Java 虚拟机自动管理。
- 对于实例方法,锁对象是当前实例对象(
this)。 - 对于静态方法,锁对象是类的字节码对象(
类名.class)。
lock锁
lock是 Java 提供的一个更灵活的锁机制,位于java.util.concurrent.locks包中。lock是接口,不能直接实例化,需要使用其实现类ReentrantLock。- 使用
lock锁的步骤:- 创建
ReentrantLock对象。 - 在访问共享资源前调用
lock()方法获取锁。 - 在访问共享资源后调用
unlock()方法释放锁。
- 创建
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyClass {
// 使用 final 修饰确保锁对象不可变
private final Lock lock = new ReentrantLock();
public void synchronizedMethod() {
lock.lock(); // 获取锁
try {
// 访问共享资源的代码
} finally { // 写在finally内,确保锁一定会被释放
lock.unlock(); // 释放锁
}
}
}线程池
见线程池。
并发/并行
多线程是怎么执行的
并发和并行同时进行的。
进程
- 进程是计算机中已运行程序的实例,是系统进行资源分配和调度的基本单位。每个进程都有自己的内存空间和系统资源。
- 而线程是属于进程的,一个进程可以运行多个线程。
- 进程中的多个线程实际上是并发和并行执行的。
并发的定义
进程中的线程是由CPU调度的,但是CPU能同时处理的线程数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统每个线程服务,由于CPU的调度速度非常快,所以在宏观上看起来好像是同时进行的,这就是并发。
- 简单来说并发是指在同一时间段内,有多个线程在进行,但并不是同时进行的。操作系统通过时间片轮转等方式,让多个线程交替执行,从而实现并发效果。
并行的定义
并行是指在同一时间点上,有多个线程被CPU调度执行。并行通常需要多核处理器来实现,每个任务可以在不同的核心上同时运行。
- 简单来说并行是指在同一时间点上,有多个任务同时进行。