在Java编程中,多线程是实现高效并发处理、提升程序性能的关键技术之一。无论是在服务器端应对高并发请求,还是在客户端应用中实现流畅的用户交互体验,多线程都发挥着不可替代的作用。本文将从基础出发,系统讲解线程的创建方法、生命周期管理,并结合实际案例掌握核心API的使用,帮助读者完成Java多线程从入门到实践的跨越。
1. 多线程的核心价值:为何要使用它?
在传统的单线程程序中,任务只能按顺序逐一执行。当遇到耗时操作(如网络通信或文件读写)时,CPU会进入等待状态,无法进行其他计算,导致资源利用率低下。而多线程通过支持“并发执行”,使得CPU可以在一个线程等待IO期间,切换至另一个就绪线程继续工作,从而显著提高系统的整体效率和响应能力。
举个形象的例子:如果把单线程比作一位厨师需要独自完成采购、清洗、烹饪全过程,在烧水等待的过程中他只能干等;而多线程则像是一个团队协作的厨房,不同厨师分别负责不同环节,即使某个步骤在等待,其他人仍可继续作业,大大缩短了整体耗时。
Java中的多线程机制基于JVM对操作系统原生线程的封装,开发者无需直接操作底层系统接口,即可通过高级API实现复杂的并发逻辑,简化了开发难度。
2. 创建线程的三种主要方式及其对比分析
Java提供了三种常用的线程创建手段:继承Thread类、实现Runnable接口以及实现Callable接口。每种方式都有其适用场景与优缺点,下面依次介绍并进行比较。
(1)继承Thread类的方式
Thread类是Java中表示线程的基本类,通过继承该类并重写其run()方法,可以定义线程的具体行为。启动线程必须调用start()方法,而不是直接调用run()方法——因为只有start()才会通知JVM将线程加入调度队列,由系统分配时间片后自动执行run()中的代码。
start()
run()
// 继承Thread类创建线程
class MyThread extends Thread {
// 重写run()方法,定义线程执行逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + (i+1) + "次");
try {
// 模拟任务耗时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadCreateDemo {
public static void main(String[] args) {
// 创建线程实例
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 设置线程名称
thread1.setName("线程A");
thread2.setName("线程B");
// 启动线程
thread1.start();
thread2.start();
}
}
运行结果中,“线程A”和“线程B”的输出呈现交错状态,体现了并发执行的特点。但这种方式存在明显局限性:由于Java不支持多重继承,一旦类继承了Thread,就无法再继承其他父类,降低了代码的扩展性和灵活性。
(2)实现Runnable接口
Runnable接口是Java中用于定义线程任务的标准接口,内部仅包含一个run()方法。通过实现该接口来封装任务逻辑,再将其传递给Thread构造函数,即可创建独立线程。这种方法避免了单继承带来的限制,更符合“任务与执行分离”的设计思想,因此在实际项目中更为推荐。
run()
// 实现Runnable接口定义任务
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行第" + (i+1) + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class RunnableCreateDemo {
public static void main(String[] args) {
// 创建任务实例
MyRunnable task = new MyRunnable();
// 将任务传入Thread,创建线程
Thread thread1 = new Thread(task, "线程C");
Thread thread2 = new Thread(task, "线程D");
// 启动线程
thread1.start();
thread2.start();
}
}
此方式将任务逻辑与线程对象解耦,便于任务的复用和线程池的管理,是目前主流的线程创建方式之一。
3. 方式三:实现Callable接口(带返回值)
为解决Runnable接口无法获取执行结果的问题,Java 5引入了Callable接口。该接口的 call() 方法不仅能定义任务逻辑,还支持返回执行结果,并允许抛出异常。通过与FutureTask类(实现了Future和Runnable接口)结合使用,可以成功获取线程执行后的返回值。
示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 实现Callable接口,指定返回值类型为Integer
class MyCallable implements Callable<Integer> {
private int num;
public MyCallable(int num) {
this.num = num;
}
// call()方法:包含任务逻辑并返回计算结果
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= num; i++) {
sum += i;
Thread.sleep(50);
}
System.out.println(Thread.currentThread().getName() + ":计算完成,结果为" + sum);
return sum;
}
}
// 测试类
public class CallableCreateDemo {
public static void main(String[] args) throws Exception {
// 创建两个Callable任务实例
MyCallable task1 = new MyCallable(100);
MyCallable task2 = new MyCallable(50);
// 将任务包装成FutureTask,以便获取返回值
FutureTask<Integer> futureTask1 = new FutureTask<>(task1);
FutureTask<Integer> futureTask2 = new FutureTask<>(task2);
// 将FutureTask作为参数传入Thread并启动
new Thread(futureTask1, "计算线程1").start();
new Thread(futureTask2, "计算线程2").start();
// 获取线程执行结果(get()会阻塞当前线程,直到目标线程完成)
int result1 = futureTask1.get();
int result2 = futureTask2.get();
System.out.println("主线程:线程1结果=" + result1 + ",线程2结果=" + result2);
}
}
call()
运行时,主线程会在调用 futureTask1.get() 和 futureTask2.get() 时被阻塞,直至两个子线程完成计算并返回结果。这种机制适用于需要获取异步执行结果的场景,例如并行数据处理、批量计算等。
get()
该方式的优势在于能够获得任务执行的返回值,并且能捕获检查型异常,提升了线程编程的灵活性和实用性。但其缺点是实现相对复杂,且 get() 方法具有阻塞性,若未合理控制可能影响性能。
thread2.start();
}
}
此方法实现了任务逻辑与线程管理的解耦,同一个任务对象可被多个线程共享执行。然而,由于 run() 方法无返回值,无法获取线程执行后的结果。
run()
三、线程生命周期:掌握线程的“生老病死”
线程从创建到终止的全过程被称为线程的生命周期。在Java中,线程的生命周期由JVM管理,共包含六个核心状态,开发者可通过API间接控制状态之间的转换。深入理解这些状态及其流转关系,有助于正确编写多线程程序,避免出现死锁、线程泄漏等问题。
1. 六个核心状态及状态转换
根据Java官方文档,线程的状态由 Thread.State 枚举类定义,具体包括以下六种:
新建状态(NEW)
当通过 new Thread() 创建线程实例后,尚未调用 start() 方法之前,线程处于新建状态。此时线程已存在于内存中,但还未被JVM调度执行。
就绪状态(RUNNABLE)
调用 start() 方法后,线程进入就绪状态。此时线程已被加入就绪队列,等待CPU分配时间片。一旦获取时间片,即转入运行状态。
运行状态(RUNNING)
线程获得CPU资源后,开始执行 run() 方法中的代码逻辑,处于实际运行中。这是线程真正执行任务的阶段。
阻塞状态(BLOCKED)
Thread.State
三种创建方式对比总结
| 创建方式 | 优点 | 缺点 | 适用场景 |
|------------------------|----------------------------------------|----------------------------------------|--------------------------------------|
| 继承Thread类 | 使用简单,可通过this直接操作线程 | 受限于单继承,任务与线程紧密耦合 | 简单应用场景,无需继承其他父类 |
| 实现Runnable接口 | 避免单继承限制,实现任务与线程的解耦 | 无法返回结果,不能抛出检查型异常 | 大多数并发场景,推荐优先使用 |
| 实现Callable接口 | 支持返回值,可抛出异常,功能更强大 | 实现较复杂,get()方法可能造成阻塞 | 需要获取线程执行结果的场景 |
线程在运行过程中可能由于某些条件未满足而暂时放弃CPU的使用权,进入暂停执行的状态。常见的触发场景包括:等待获取同步锁(synchronized)、调用wait()方法后尚未被唤醒等。处于该状态的线程不会参与CPU调度,只有当特定条件达成时,才会重新回到就绪状态,等待调度执行。
等待状态(WAITING):线程进入一种无限期的等待模式,必须由其他线程显式地唤醒(例如通过调用notify()或notifyAll()方法)。当线程调用Object.wait()、Thread.join()等方法时,便会进入此状态。
超时等待状态(TIMED_WAITING):与WAITING状态类似,但设置了最大等待时限。一旦超时时间到达,即使没有被主动唤醒,线程也会自动恢复到就绪状态。常见于调用Thread.sleep(long)、Object.wait(long)等带有时间参数的方法。
终止状态(TERMINATED):当线程完成run()方法的执行,或因未捕获异常而退出run()方法时,即进入终止状态。此时线程生命周期彻底结束,无法再次启动。
关键注意事项
- 一旦线程进入终止状态(TERMINATED),若尝试再次调用start()方法,将抛出IllegalThreadStateException异常。
- sleep()方法在休眠期间不会释放已持有的同步锁;而wait()方法则会主动释放锁,这是两者的重要区别之一。
- 阻塞状态(BLOCKED)特指线程因竞争同步锁失败而被挂起的情况;而等待状态(WAITING/TIMED_WAITING)涵盖范围更广,表示线程正在等待某个外部事件的发生。
核心API实战:从基础到进阶
Thread类及相关接口提供了丰富的多线程控制手段,熟练掌握这些API是实现高效并发编程的基础。以下按功能分类,结合实际代码示例进行讲解。
线程控制相关API(启动、休眠、中断)
(1)start() 与 run()
如前所述,
start()
是唯一合法启动新线程的方式,它会将线程注册进JVM的调度系统中,使其具备被调度执行的资格。而
run()
方法仅封装了线程需要执行的任务逻辑。如果直接调用run(),并不会创建新线程,而是以当前线程同步方式执行任务体,失去了多线程的意义。
(2)Thread.sleep(long millis)
使当前线程进入指定毫秒数的休眠,状态变为TIMED_WAITING。在此期间,线程让出CPU资源,但不会释放任何已持有的同步锁。常用于模拟耗时操作、限流控制或协调线程执行节奏。
public class SleepDemo {
public static void main(String[] args) {
new Thread(() -> {
long start = System.currentTimeMillis();
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
long end = System.currentTimeMillis();
System.out.println("线程休眠时间:" + (end - start) + "ms");
}).start();
}
}
(3)线程中断机制相关API
Java不支持强制终止线程的操作,而是采用协作式中断机制,依赖中断标志位来通知线程应停止运行。主要涉及以下三个方法:
void interrupt()
:设置目标线程的中断状态为true,并不会立即打断其运行流程。boolean isInterrupted()
:查询指定线程是否已被标记为中断,调用后不会清除该标志。static boolean interrupted()
:检测当前线程是否被中断,并在调用后清除中断状态(具有副作用)。
特别地,当线程正处于sleep()、wait()等阻塞操作中时,若收到中断请求,会立即抛出InterruptedException异常,同时中断标志会被自动清除。
因此,在编写可中断的长任务时,建议循环检查中断状态,及时响应中断信号并安全退出。
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread taskThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程正在执行任务...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("线程被中断,准备退出");
Thread.currentThread().interrupt(); // 重新设置中断状态
}
}
System.out.println("线程退出");
});
taskThread.start();
Thread.sleep(2000); // 主线程等待2秒后发起中断
taskThread.interrupt();
}
}
taskThread.interrupt(); } }线程同步与通信的实现机制
1. synchronized关键字的应用
synchronized 是 Java 提供的一种内置锁机制,主要用于控制多个线程对共享资源的并发访问。它能够修饰实例方法、静态方法或代码块,确保在任意时刻最多只有一个线程可以执行被其保护的代码段。 以下是一个使用 synchronized 修饰方法的示例: // 共享资源类 class ShareResource { private int count = 0; // 同步方法,保证操作的原子性 public synchronized void increment() { count++; System.out.println(Thread.currentThread().getName() + ":count=" + count); } } public class SynchronizedDemo { public static void main(String[] args) { ShareResource resource = new ShareResource(); // 创建三个线程同时调用 increment 方法 for (int i = 0; i < 3; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { resource.increment(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }, "线程" + (i+1)).start(); } } } 运行结果中,count 值会按顺序递增,不会出现重复或跳号的情况。这说明 synchronized 成功保障了 increment 方法的原子性和可见性,避免了多线程环境下的数据竞争问题。2. wait() 与 notify() 的线程通信机制
Object 类中的 wait()、notify() 和 notifyAll() 是实现线程间协作的关键方法。这些方法必须在 synchronized 修饰的代码块或方法中调用,否则会抛出 IllegalMonitorStateException 异常。:调用该方法会使当前线程释放对象锁,并进入 WAITING 状态,直到其他线程在同一对象上调用 notify() 或 notifyAll() 将其唤醒。wait():唤醒在该对象监视器上等待的一个线程(由JVM调度决定具体哪一个)。notify():唤醒所有在该对象监视器上等待的线程,它们将参与锁的竞争。 这些方法常用于实现经典的“生产者-消费者”模型,通过条件等待和通知机制协调线程行为。notifyAll()实战案例:生产者-消费者模型
通过 wait() 和 notify() 实现一个简单的单生产者-单消费者模型,利用共享缓冲区进行数据传递。 // 共享缓冲区类 class Buffer { private int data; private boolean isEmpty = true; // 表示缓冲区当前是否为空 // 生产者调用此方法写入数据 public synchronized void produce(int data) { while (!isEmpty) { try { wait(); // 若缓冲区非空,生产者等待 } catch (InterruptedException e) { e.printStackTrace(); } } this.data = data; isEmpty = false; System.out.println("生产者生产:" + data); notify(); // 通知正在等待的消费者线程 } // 消费者调用此方法读取数据 public synchronized int consume() { while (isEmpty) { try { wait(); // 若缓冲区为空,消费者等待 } catch (InterruptedException e) { e.printStackTrace(); } } int result = this.data; isEmpty = true; System.out.println("消费者消费:" + result); notify(); // 通知正在等待的生产者线程 return result; } } // 生产者线程类 class Producer extends Thread { private Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 1; i <= 5; i++) { buffer.produce(i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 消费者线程类 class Consumer extends Thread { private Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 0; i < 5; i++) { buffer.consume(); try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 测试主类 public class ProducerConsumerDemo { public static void main(String[] args) { Buffer buffer = new Buffer(); new Producer(buffer).start(); new Consumer(buffer).start(); } }
public void run() {
for (int i = 1; i <= 5; i++) {
buffer.consume();
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ProducerConsumerDemo {
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Producer(buffer).start();
new Consumer(buffer).start();
}
}
3. 线程属性相关API
void setName(String name)/String getName()
用于设置或获取线程的名称,有助于在调试过程中识别线程以及输出日志信息。
void setPriority(int newPriority)/int getPriority()
用于设定或读取线程的优先级,取值范围为1到10,默认值为5。优先级较高的线程更有可能获得CPU时间片,但具体执行顺序仍由操作系统调度决定,并不保证绝对优先运行。
static Thread currentThread()
该方法返回当前正在执行的线程对象实例,常用于获取当前线程的引用以进行操作或状态判断。
static void yield()
调用此方法会使当前线程主动释放CPU资源,从运行状态转为就绪状态,从而允许其他同优先级的线程有机会被调度执行。
五、总结与进阶学习方向
本文从多线程的核心作用入手,全面介绍了三种创建线程的方式及其对比分析,深入探讨了线程的六种基本状态及状态间的转换机制,并通过实际代码示例演示了线程控制、同步与通信等关键API的应用场景。掌握上述知识点后,已具备开展Java多线程开发的基本能力。
然而,多线程技术的学习远不止于此,后续可进一步深入以下重点方向:
JUC并发工具包
例如 ThreadPoolExecutor(线程池)、CountDownLatch(倒计时闭锁)、CyclicBarrier(循环栅栏)等高级工具类,广泛应用于企业级高并发系统中,能显著提升程序的性能和可维护性。
锁机制的深化理解
包括 ReentrantLock(可重入锁)和 ReadWriteLock(读写锁),相较于 synchronized 关键字,它们提供了更高的灵活性,支持公平锁策略、可中断等待、超时尝试加锁等功能。
并发安全问题探究
涉及死锁的成因与预防、线程安全集合(如 ConcurrentHashMap)的使用场景,以及 volatile 关键字在内存可见性和禁止指令重排序方面的语义特性。
线程池工作原理
理解线程池的核心参数配置、任务调度流程、线程复用机制以及拒绝策略的选择,是实现高性能服务端应用的关键环节。
多线程编程不仅要求熟练运用各类API,更需深入理解其背后的运行机制与并发设计思想。建议结合实际项目多加实践,并利用JConsole、VisualVM等监控工具观察线程行为,逐步积累并发编程的实战经验。


雷达卡


京公网安备 11010802022788号







