在《Java中级教程》的进阶之路上,多线程与并发编程是一座必须翻越的高峰。随着多核处理器的普及,充分利用硬件资源、提升应用程序性能和响应能力,已成为现代软件开发的基本要求。Java从诞生之初就内置了对多线程的支持,使得开发者能够编写出同时执行多个任务的程序。然而,多线程编程并非易事,它带来了巨大的性能提升潜力的同时,也引入了线程安全、死锁、竞态条件等一系列复杂问题。掌握其核心原理和常用工具,是Java开发者从"会用"到"精通"的标志。
1. 线程的创建与生命周期
在Java中,创建线程主要有三种方式:继承Thread
类、实现Runnable
接口,以及实现Callable
接口。
- 继承
Thread
类 :这是最简单直接的方式。只需创建一个类继承Thread
,并重写其run()
方法,然后在main
方法中创建该类的实例并调用start()
方法即可。但由于Java是单继承,这种方式会限制类的扩展性,不推荐使用。 - 实现
Runnable
接口 :这是更常用、更灵活的方式。创建一个类实现Runnable
接口,实现其run()
方法,然后将该Runnable
实例作为参数传递给Thread
对象的构造函数。这种方式避免了单继承的局限性,更符合面向对象的设计思想(将任务与线程执行机制分离)。
java
复制
typescript
// 实现Runnable接口的任务类
class MyTask implements Runnable {
private String taskName;
public MyTask(String name) { this.taskName = name; }
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(taskName + " 正在执行,计数: " + i);
try {
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask("任务-1"));
Thread t2 = new Thread(new MyTask("任务-2"));
t1.start(); // 启动线程,JVM会调用run()方法
t2.start();
System.out.println("主线程继续执行...");
}
}
- 实现
Callable
接口 :Callable
接口与Runnable
类似,但它的call()
方法可以有返回值,并且可以抛出异常。要执行Callable
任务,通常需要配合FutureTask
使用,FutureTask
包装了Callable
对象,并实现了Future
接口,可以用来获取异步计算的结果。
一个线程从创建到消亡,会经历新建、就绪、运行、阻塞和死亡五种状态。理解这个生命周期对于调试和设计多线程程序至关重要。
2. 线程安全与同步机制
当多个线程同时访问和修改同一个共享资源(如一个对象、一个静态变量)时,就可能出现数据不一致的问题,这就是线程安全问题。例如,两个线程同时对一个计数器进行加一操作,由于操作不是原子性的(包含"读取-修改-写入"三步),最终结果可能不是预期的2,而是1。
Java提供了synchronized
关键字来解决这一问题,它是最基本的同步机制。synchronized
可以修饰方法或代码块,确保在同一时刻,只有一个线程能够执行被它修饰的代码。
java
复制
java
class Counter {
private int count = 0;
// 使用synchronized修饰方法,保证方法的原子性
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join(); // 等待t1线程执行完毕
t2.join(); // 等待t2线程执行完毕
System.out.println("最终计数: " + counter.getCount()); // 输出 2000
}
}
在这个例子中,如果没有synchronized
,count
的最终值几乎肯定会小于2000。加上后,increment()
方法变成了一个原子操作,保证了线程安全。synchronized
的底层实现依赖于对象头中的Monitor(监视器)锁,也称为"内置锁"或"互斥锁"。
3. 高级并发工具包:java.util.concurrent
虽然synchronized
简单易用,但在某些场景下,它的功能有限且性能可能不是最优。Java 5引入了java.util.concurrent
(JUC)包,提供了一套功能强大、性能更优的并发工具。
ReentrantLock
:一个可重入的互斥锁,它比synchronized
提供了更灵活的锁定操作,例如尝试获取锁(tryLock()
)、可中断的锁获取(lockInterruptibly()
)以及公平锁与非公平锁的选择。ExecutorService
(线程池) :频繁地创建和销毁线程会带来巨大的性能开销。线程池通过复用已创建的线程,来降低资源消耗,提高响应速度。ExecutorService
是Java线程池的核心接口,我们通常使用Executors
工厂类来创建不同类型的线程池,如固定大小的线程池(newFixedThreadPool
)、单线程化的线程池(newSingleThreadExecutor
)等。
java
复制
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个固定大小为3的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在处理任务 " + taskId);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池(不再接受新任务,但会完成已提交的任务)
executor.shutdown();
// 等待所有任务完成
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("所有任务执行完毕。");
}
}
- 并发集合 :如
ConcurrentHashMap
、CopyOnWriteArrayList
等。这些集合类通过更精细的锁策略(如分段锁、写时复制)来保证线程安全,其并发性能远高于使用synchronized
包装的普通集合。
Java多线程与并发编程是一个广阔而深邃的领域。从基础的线程创建和synchronized
同步,到高级的JUC工具包,每一步都需要大量的实践和思考。只有真正理解了其背后的原理,并能在实际项目中权衡利弊、做出合理选择,才能说真正掌握了Java并发编程的精髓,从而编写出高效、稳定、可伸缩的并发应用。