在 Java 开发领域,多线程编程是一项极为重要的技能。它不仅能够显著提升程序的性能和响应速度,还能让我们充分利用现代多核处理器的强大计算能力。对于 Java 开发程序员而言,深入理解和熟练掌握 Java 多线程相关知识,是从初级迈向中高级开发的关键一步。接下来,让我们一起深入探索 Java 多线程的世界。
线程基础概念
线程,简单来说,是程序执行流的最小单元。在 Java 中,它是轻量级的进程,每个线程都有自己独立的栈空间,用于存储局部变量、方法调用等信息,但它们共享进程的堆内存,堆内存中存放着对象实例等数据。打个比方,线程就像是一个公司里的员工,每个员工都有自己独立的办公桌(栈空间),方便自己处理工作事务,但大家共享公司的办公场地(堆内存),在这个公共空间里协作完成项目任务。这种共享与独立的特性,使得多线程编程既强大又复杂。
线程创建方式
继承 Thread 类
通过继承 Thread 类,并重写其 run 方法,就可以创建一个自定义线程类。在 run 方法中编写线程执行的具体逻辑,然后创建该子类的实例,并调用 start 方法来启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread created by extending Thread class.");
}
}
// 使用时
MyThread myThread = new MyThread();
myThread.start();
这种方式的优点是代码简单直观,直接继承 Thread 类,方便对线程进行个性化的控制。但缺点也很明显,由于 Java 是单继承语言,一旦继承了 Thread 类,就无法再继承其他类,这在一定程度上限制了类的扩展性。
实现 Runnable 接口
实现 Runnable 接口也是创建线程的常见方式。首先创建一个实现 Runnable 接口的类,并实现其 run 方法,然后将该实现类的实例传递给 Thread 类的构造函数来创建线程。示例代码如下:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("This is a thread created by implementing Runnable interface.");
}
}
// 使用时
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
与继承 Thread 类相比,实现 Runnable 接口的方式更加灵活,因为一个类可以实现多个接口,这样就避免了单继承的限制,使代码的可维护性和扩展性更强。同时,这种方式也更符合面向接口编程的思想,将线程的执行逻辑与线程本身进行了解耦。
实现 Callable 接口
Java 5.0 引入了 Callable 接口,它与 Runnable 接口类似,也是用于创建线程执行的任务。但 Callable 接口的 run 方法可以有返回值,并且可以抛出异常。使用 Callable 接口时,需要借助 FutureTask 类来获取任务的执行结果。示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
// 模拟一些计算任务
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class CallableExample {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("The result is: " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
这种方式在需要获取线程执行结果的场景下非常有用,比如在一些需要异步计算并获取结果的任务中,Callable 接口提供了更强大的功能支持。
线程状态
Java 线程有六种状态:新建(New)、运行(Runnable)、阻塞(Blocked)、等待(Waiting)、计时等待(Timed_Waiting)和终止(Terminated)。如下所示。
新建状态(New)
当使用 new 关键字创建一个线程对象时,线程处于新建状态。此时线程还没有开始执行,只是在内存中分配了相应的资源,包括线程对象和其所属的栈空间等。例如:
Thread thread = new Thread();
这个 thread 对象就处于新建状态,它还没有被启动,不会执行任何实际的任务。
运行状态(Runnable)
当调用线程的 start 方法后,线程进入运行状态。此时线程已经被调度器安排执行,它会在 CPU 上运行其 run 方法中的代码。需要注意的是,Runnable 状态实际上包含了两个子状态:就绪状态和运行中状态。就绪状态表示线程已经准备好执行,但还没有被 CPU 调度执行;而运行中状态则表示线程正在 CPU 上执行。在多线程环境下,由于 CPU 资源有限,多个线程会在就绪状态和运行中状态之间不断切换。
阻塞状态(Blocked)
当线程试图获取一个被其他线程占用的锁,或者在等待 I/O 操作完成时,线程会进入阻塞状态。在阻塞状态下,线程不会占用 CPU 资源,直到引起阻塞的原因消除。例如,当一个线程调用 synchronized 方法或进入 synchronized 代码块时,如果该锁已经被其他线程持有,那么当前线程就会进入阻塞状态,等待锁的释放。
等待状态(Waiting)
线程调用 wait () 方法、join () 方法,或者 LockSupport.park () 方法时,会进入等待状态。在等待状态下,线程会释放所持有的锁(如果有的话),并且不会被 CPU 调度执行,直到其他线程调用 notify ()、notifyAll () 方法或者 unpark () 方法来唤醒它。等待状态常用于线程之间的协作,比如一个线程等待另一个线程完成某个任务后再继续执行。
计时等待状态(Timed_Waiting)
与等待状态类似,计时等待状态也是线程暂停执行的一种状态,但它有一个时间限制。线程调用 sleep (long millis) 方法、wait (long timeout) 方法、join (long millis) 方法或者 LockSupport.parkNanos (long nanos)、LockSupport.parkUntil (long deadline) 方法时,会进入计时等待状态。在指定的时间内,如果没有其他线程唤醒它,线程会自动恢复到运行状态。这种状态在需要控制线程执行时间间隔或者设置等待超时的场景下非常有用。
终止状态(Terminated)
当线程的 run 方法执行完毕,或者因为异常而提前终止时,线程进入终止状态。此时线程已经完成了它的任务,不再需要 CPU 资源,其占用的系统资源(如栈空间等)也会被回收。
线程同步
在多线程环境下,由于多个线程共享同一堆内存,当它们同时访问和修改共享数据时,就可能会出现线程安全问题,导致数据不一致或程序运行结果错误。为了解决这些问题,Java 提供了多种线程同步机制。
synchronized 关键字
synchronized 关键字是 Java 中最基本的线程同步工具,它可以修饰方法或代码块。当一个线程进入被 synchronized 修饰的方法或代码块时,它会自动获取对象的锁(如果是静态方法,则获取类的锁),在方法或代码块执行完毕后,会自动释放锁。这样就保证了同一时刻只有一个线程能够访问被 synchronized 修饰的部分,从而避免了数据竞争和线程安全问题。例如:
class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 线程安全的代码
}
}
上述代码中,synchronizedMethod 方法被 synchronized 关键字修饰,当一个线程调用该方法时,会获取 SynchronizedExample 对象的锁,其他线程如果也想调用该方法,就必须等待锁的释放。
Lock 接口
Java 5.0 引入了
java.util.concurrent.locks.Lock 接口,它提供了比 synchronized 关键字更灵活和强大的线程同步控制。Lock 接口的实现类(如 ReentrantLock)提供了更细粒度的锁控制,例如可以实现公平锁和非公平锁,还可以使用 tryLock () 方法尝试获取锁,如果获取不到锁可以立即返回,而不是像 synchronized 关键字那样一直等待。示例代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockExample {
private Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 线程安全的代码
} finally {
lock.unlock();
}
}
}
在使用 Lock 接口时,需要注意手动释放锁,通常将解锁操作放在 finally 块中,以确保无论代码是否发生异常,锁都能被正确释放。
volatile 关键字
volatile 关键字主要用于解决变量在多线程环境下的可见性问题。当一个变量被声明为 volatile 时,它会保证所有线程对该变量的访问都是直接从主内存中读取,而不是从线程的工作内存中读取,从而避免了缓存不一致的问题。例如:
class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean isFlag() {
return flag;
}
}
在上述代码中,flag 变量被声明为 volatile,当一个线程修改了 flag 的值后,其他线程能够立即看到这个变化,保证了数据的可见性。需要注意的是,volatile 关键字并不能保证原子性,即多个线程对 volatile 变量的复合操作(如自增、自减等)仍然可能会出现线程安全问题。
线程池
在实际应用中,频繁地创建和销毁线程会带来较大的开销,影响程序的性能。为了解决这个问题,Java 提供了线程池的概念。线程池是一种基于池化技术的多线程处理方式,它预先创建一定数量的线程,并将这些线程放在一个线程池中,当有任务需要执行时,从线程池中获取一个空闲线程来执行任务,任务执行完毕后,线程不会被销毁,而是返回线程池等待下一个任务。这样可以大大减少线程创建和销毁的开销,提高程序的性能和响应速度。
Java 提供了多种线程池的实现类,如 ThreadPoolExecutor、FixedThreadPool、CachedThreadPool、SingleThreadExecutor 等。其中,ThreadPoolExecutor 是最基础的线程池实现类,其他几个线程池都是通过对 ThreadPoolExecutor 进行不同的参数配置来实现特定的功能。例如,创建一个固定大小的线程池可以使用 Executors 类的静态方法:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为5的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
final int taskNumber = i;
executorService.submit(() -> {
System.out.println("Task " + taskNumber + " is being executed by " + Thread.currentThread().getName());
// 模拟任务执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
在上述代码中,通过
Executors.newFixedThreadPool (5) 创建了一个固定大小为 5 的线程池,然后向线程池中提交了 10 个任务。由于线程池大小为 5,所以这 10 个任务会分批次执行,每个任务由线程池中的一个线程来执行。当所有任务执行完毕后,调用 executorService.shutdown () 方法关闭线程池。
总结
Java 多线程编程是一个复杂而又强大的领域,掌握好它对于 Java 开发程序员来说至关重要。通过深入理解线程的基础概念、创建方式、状态转换、同步机制、线程池以及多线程的应用场景,我们能够编写出高效、稳定、并发性能强的 Java 程序。在实际开发中,我们需要根据具体的业务需求和场景,合理地运用多线程技术,充分发挥其优势,同时避免出现线程安全问题和性能瓶颈。希望本文能帮助你对 Java 多线程有更深入的理解和掌握,让你在 Java 开发的道路上更上一层楼。