一、引言
在Java编程中,多线程是一个重要且复杂的主题。多线程允许程序同时执行多个任务,从而提高了程序的执行效率和响应速度。然而,多线程编程也带来了同步、通信和数据一致性等问题。因此,深入理解Java多线程的知识点对于编写高效、稳定的多线程程序至关重要。本文将详细探讨Java多线程的基本概念、线程创建与启动、线程状态与生命周期、线程同步与通信以及线程池等关键知识点。
二、Java多线程基本概念
进程与线程
进程是系统分配资源的基本单位,它包含了一个程序的执行实例及其拥有的系统资源(如内存、文件、设备等)。线程是CPU调度的基本单位,它是进程中的一个执行单元,负责执行进程中的一段程序。一个进程可以包含多个线程,这些线程共享进程的资源,但拥有独立的执行路径和状态。
并发与并行
并发是指多个任务在同一时间段内交替执行,而并行是指多个任务在同一时刻同时执行。在单核CPU上,多线程程序实际上是并发执行的,因为CPU在同一时刻只能处理一个线程;而在多核CPU上,多线程程序可以并行执行,因为不同的线程可以在不同的核心上同时运行。
三、线程的创建与启动
在Java中,创建和启动线程主要有两种方式:继承Thread类和实现Runnable接口。
1.继承Thread类
通过继承Thread类并重写其run()方法,可以创建自定义的线程类。然后,创建该线程类的实例并调用其start()方法,即可启动线程。start()方法会调用线程的run()方法,从而使线程开始执行。
示例代码:
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
2.实现Runnable接口
通过实现Runnable接口并重写其run()方法,可以创建可运行的线程任务。然后,将该任务作为参数传递给Thread类的构造函数,创建Thread对象并调用其start()方法,即可启动线程。这种方式更加灵活,因为Java不支持多重继承,但可以实现多个接口。此外,将线程任务与代码分离也便于管理和复用。
示例代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread thread = new Thread(task);
thread.start(); // 启动线程
}
}
四、线程状态与生命周期
Java中的线程具有五种状态:新建(NEW)、就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)和终止(TERMINATED)。这些状态反映了线程在其生命周期中的不同阶段。
- 新建状态(NEW):当线程对象被创建但尚未启动时,它处于新建状态。
- 就绪状态(RUNNABLE):当线程启动后,它进入就绪状态,等待CPU的调度执行。
- 阻塞状态(BLOCKED):当线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其他线程持有时,该线程进入阻塞状态。当持有锁的线程释放锁后,该线程将变成就绪状态。
- 等待状态(WAITING):线程通过调用另一个线程的join方法、或者调用LockSupport.park方法、或者等待一个条件变量(Condition)时,该线程进入等待状态。直到其他线程做了某些特定动作(如唤醒该线程),该线程才返回到就绪状态。
- 超时等待状态(TIMED_WAITING):这是线程等待另一个线程执行一个(唤醒)动作的最长时间,或者等待某件事情的发生的最长时间。在指定的等待时间一到或者接收到其他线程的执行动作,该线程就返回就绪状态。
- 终止状态(TERMINATED):表示线程已经执行完毕。
线程的生命周期从创建开始,经历就绪、阻塞、等待等状态,最终结束于终止状态。了解这些状态和状态转换有助于我们更好地控制和管理线程。
五、线程同步与通信
由于多个线程共享进程的资源,因此线程之间需要进行同步和通信以确保数据的一致性和避免竞态条件。Java提供了多种机制来实现线程的同步与通信。
1.synchronized关键字
synchronized关键字是Java中最基本的线程同步机制。它可以用来修饰方法或代码块,以确保同一时刻只有一个线程能够执行被修饰的方法或代码块。synchronized关键字通过获取对象的内部锁来实现同步。
示例代码(synchronized关键字):
public class SynchronizedExample {
private Object lock = new Object();
public void synchronizedMethod() {
synchronized (this) {
// 同步代码块,同一时刻只能有一个线程执行这里的代码
}
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(() -> example.synchronizedMethod());
Thread thread2 = new Thread(() -> example.synchronizedMethod());
thread1.start();
thread2.start();
}
}
在上述示例中,synchronizedMethod
方法使用了synchronized
关键字修饰,确保同一时刻只有一个线程能够执行该方法。此外,synchronized
也可以修饰代码块,通过指定一个锁对象来实现更细粒度的同步。
2.volatile关键字
volatile关键字用于声明变量,它保证了对该变量的修改能够立即被其他线程看到。volatile变量只能确保可见性,不能保证原子性。因此,对于复合操作,仍需要使用其他同步机制。
示例代码(volatile关键字):
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean getFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread thread1 = new Thread(() -> {
example.setFlag(true);
// 其他操作...
});
Thread thread2 = new Thread(() -> {
while (!example.getFlag()) {
// 等待flag变为true
}
// 执行相应操作...
});
thread1.start();
thread2.start();
}
}
在上面的示例中,flag
变量被声明为volatile
,确保当一个线程修改flag
的值时,其他线程能够立即看到修改后的值。
- wait()和notify()方法
wait()
和notify()
方法是Java对象内置的方法,用于实现线程之间的通信。wait()
方法使当前线程等待,直到其他线程调用notify()
或notifyAll()
方法唤醒它。这些方法通常与synchronized
关键字一起使用,因为它们需要在同步代码块或同步方法中调用。
示例代码(wait()和notify()):
public class WaitNotifyExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
while (count == 5) {
try {
lock.wait(); // 当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("Incremented count to: " + count);
lock.notifyAll(); // 唤醒所有等待的线程
}
}
public void decrement() {
synchronized (lock) {
while (count == 0) {
try {
lock.wait(); // 当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("Decremented count to: " + count);
lock.notifyAll(); // 唤醒所有等待的线程
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
example.increment();
}
});
Thread decrementThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
example.decrement();
}
});
incrementThread.start();
decrementThread.start();
}
}
在这个示例中,increment()
和decrement()
方法分别用于增加和减少count
的值。当count
达到5或降至0时,线程会调用wait()
方法进入等待状态,直到其他线程调用notifyAll()
方法唤醒它。通过这种方式,线程之间可以进行协调和通信。
六、线程池
线程池是Java中用于优化线程管理的工具,它允许我们预先创建一组线程并保存在内存中,避免了频繁创建和销毁线程所带来的性能开销。线程池可以复用线程,管理线程的生命周期,并且提供了灵活的调度策略。
Java标准库中的java.util.concurrent
包提供了多种线程池的实现,包括FixedThreadPool
、CachedThreadPool
、ScheduledThreadPool
等。
以下是FixedThreadPool
的一个简单示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池,线程数量为3
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交多个任务到线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
// 模拟任务执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池,不再接受新任务,等待已提交任务执行完毕
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务执行完毕
}
System.out.println("All tasks are done.");
}
}
在上面的代码中,我们首先使用Executors.newFixedThreadPool
方法创建了一个固定大小为3的线程池。然后,我们提交了10个任务到线程池。每个任务都会输出当前任务的ID和正在执行任务的线程名称,并模拟了一个耗时操作。
提交完所有任务后,我们调用executor.shutdown()
方法关闭线程池,这表示线程池不再接受新的任务,但是会继续执行已经提交的任务。我们使用executor.isTerminated()
方法检查线程池中的所有任务是否都执行完毕。
线程池的好处包括:
- 降低资源消耗:通过复用线程,避免了线程的频繁创建和销毁,从而减少了系统开销。
- 提高响应速度:当任务到达时,如果线程池中有空闲线程,可以立即执行,无需等待线程的创建。
- 提高系统的稳定性:通过线程池管理线程的生命周期,可以有效避免大量线程同时执行导致系统资源耗尽的情况。
当谈到线程池时,还有一些重要的概念和细节需要了解。
线程池的主要类型
-
FixedThreadPool:固定大小的线程池。当线程池中的线程数达到核心线程数时,新的任务会在队列中等待,直到有线程空闲出来。如果队列满了,而线程池中的线程数还没有达到最大线程数,则会创建新的线程来执行任务。如果线程数已经达到最大线程数,并且队列也满了,就会根据配置的拒绝策略来处理新提交的任务。
-
CachedThreadPool:可缓存的线程池。当线程池中的线程数超过处理任务所需的线程数时,空闲的线程会在指定的时间内自动销毁,以减少系统开销。如果当前线程池中的线程数小于核心线程数,即使线程池中的其他线程是空闲的,也会创建一个新线程来处理新提交的任务。这种线程池比较适合执行大量的短时任务。
-
ScheduledThreadPool:支持定时及周期性任务执行的线程池。它除了能像FixedThreadPool和CachedThreadPool那样接收任务之外,还可以接收ScheduledFuture<?>类型的任务,表示延迟执行或定期执行的任务。
线程池的拒绝策略
当线程池中的线程都在忙碌,并且工作队列已满时,如果继续提交任务,就需要有一种策略来处理这种情况,这就是拒绝策略。Java提供了四种内置的拒绝策略:
- AbortPolicy:直接抛出RejectedExecutionException异常阻止系统正常运行。
- CallerRunsPolicy:调用execute方法的线程来执行该任务。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
- DiscardPolicy:不处理,直接丢弃掉。
当然,也可以根据需要实现RejectedExecutionHandler
接口,自定义拒绝策略。
线程池的关闭
当不再需要线程池时,应该调用shutdown()
或shutdownNow()
方法来关闭它。shutdown()
方法会启动线程池的关闭序列,线程池不再接受新的任务,但是会等待所有已提交的任务执行完毕。而shutdownNow()
方法会尝试停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
线程池的监控
线程池还提供了一些方法来监控其状态,如getPoolSize()
(返回线程池中的线程数)、getActiveCount()
(返回正在执行任务的线程数)、getQueue()
(返回任务队列)等。这些方法可以帮助我们更好地了解线程池的运行情况,从而进行调优。
线程池的调优
线程池的调优主要涉及到核心线程数、最大线程数、队列容量等参数的设定。这些参数的设定需要根据具体的业务场景和系统资源来决定。一般来说,核心线程数可以设置为系统可以承受的同时处理的任务数,最大线程数可以设置为系统可以承受的最大并发任务数,队列容量则可以根据任务的生成速度和处理速度来设定。