为什么我们需要关注线程?

为什么我们需要关注线程?

在多核处理器成为主流的今天,我们手中的手机、电脑甚至智能家居设备都拥有多个计算核心。这意味着,如果我们的程序只能在一个核心上运行,就相当于让其他核心"闲置",无法充分发挥硬件性能。想象一下,一个餐厅只有一个服务员,即使厨房有多个厨师,顾客仍然需要排队等待服务——这就是单线程程序的局限性。

并发编程正是为了解决这个问题而生,而线程作为并发编程的基础单元,理解其工作机制对于编写高效、稳定的应用程序至关重要。作为一名Java开发者,我深刻体会到,对线程的深入理解往往区分了初级和高级程序员。在这篇博客中,我将分享我对Java线程的个人理解,从基础概念到底层实现,希望能为你提供有价值的见解。

一、线程与进程:本质区别与内在联系

在深入线程之前,我们需要从根本上理解线程与进程的区别。这个理解不能停留在表面,而要深入到操作系统层面。

进程:独立的王国

进程可以理解为一个独立的"程序王国",每个王国都有自己独立的领土(内存空间)、资源(打开的文件、网络连接等)和法律(安全上下文)。操作系统为每个进程分配独立的虚拟地址空间,这意味着:

进程A无法直接访问进程B的内存数据。

进程崩溃通常不会影响其他进程。

进程间通信需要特殊机制(管道、消息队列、共享内存等)。

线程:王国内的协作团队

线程则是同一个"王国"内的不同"工作团队",它们:

共享王国的资源(内存、文件描述符等)。

各自执行不同的任务,但可以协作完成共同目标。

通信成本极低,因为可以直接访问共享内存。

技术视角的深度理解
从操作系统角度看,进程是资源分配的实体,而线程是CPU调度的实体。当我们在Java中创建线程时,实际上是在用户态创建了一个线程控制块,然后通过系统调用在内核态创建对应的内核线程(在Linux中通过clone系统调用)。这就是为什么线程的创建和销毁比进程轻量得多。

二、Java线程的创建方式:选择背后的思考

1. 继承Thread类:简单但不推荐

class MyThread extends Thread {

@Override

public void run() {

System.out.println("线程执行: " + Thread.currentThread().getName());

}

}

这种方式看似简单,但实际上存在设计上的问题。Java是单继承语言,如果继承了Thread类,就无法继承其他类。这违反了"组合优于继承"的设计原则。此外,从任务执行的角度看,线程的执行体(run方法)和线程本身(Thread类)应该是两个关注点,这种方式将它们耦合在一起。

2. 实现Runnable接口:推荐的标准做法

class MyRunnable implements Runnable {

@Override

public void run() {

System.out.println("线程执行: " + Thread.currentThread().getName());

}

}

为什么这是更好的选择?

符合面向对象设计原则:任务与执行机制分离。

灵活性:可以继承其他类,实现其他接口。

可复用性:同一个Runnable实例可以被多个线程共享执行。

3. 实现Callable接口:需要返回值的场景

class MyCallable implements Callable<String> {

@Override

public String call() throws Exception {

return "线程执行结果: " + Thread.currentThread().getName();

}

}

核心价值:
Callable的出现解决了Runnable无法返回结果和抛出受检异常的问题。FutureTask作为RunnableFuture接口的实现,既可以被Thread执行,又可以通过Future接口获取结果,这种设计体现了接口隔离原则。

4. 线程池方式:生产环境的必然选择

ExecutorService executor = Executors.newFixedThreadPool(5);

Future<String> future = executor.submit(new MyCallable());

为什么线程池如此重要?
直接创建线程的成本很高,包括:

内存分配:每个线程需要分配栈空间(默认512KB-1MB)。

系统调用:需要内核参与线程创建。

资源管理:线程数量无限制增长会导致系统资源耗尽。

线程池通过复用线程、控制并发数量、管理生命周期,解决了这些问题。

三、线程状态与生命周期:状态机的艺术

理解线程的状态转换不仅仅是记住几个状态名称,而是要理解每个状态转换的条件和意义。

状态转换的深度解析

NEW → RUNNABLE:(线程生命开始)
当调用start()方法时,线程从NEW状态进入RUNNABLE状态。这里有个重要细节:start()方法只能调用一次,否则会抛出IllegalThreadStateException。这是因为线程的生命周期是不可逆的。

RUNNABLE → BLOCKED:(锁竞争导致)
这种情况通常发生在 synchronized 同步块上。当线程A持有锁,线程B尝试获取同一个锁时,线程B就会进入BLOCKED状态。这里的关键理解是:BLOCKED状态只与同步的monitor锁相关。

RUNNABLE → WAITING:(主动等待)
有三种方法会导致这种转换:

Object.wait():释放锁并等待,需要其他线程调用notify()/notifyAll()

Thread.join():等待目标线程终止

LockSupport.park():底层并发工具使用

RUNNABLE → TIMED_WAITING:(主动等待)
与WAITING类似,但带有超时时间。这是为了避免永久等待导致的死锁。

实际开发中的意义:
理解这些状态转换对于调试多线程问题至关重要。当线程出现问题时,我们可以通过jstack等工具查看线程状态,快速定位问题原因。

四、线程同步与线程安全:秩序的艺术

可见性、原子性、有序性

在深入同步机制前,必须理解并发编程的三个核心问题:

可见性:一个线程对共享变量的修改,其他线程能够立即看到。由于CPU缓存的存在,线程可能读取到过期的数据。

原子性:一个或多个操作要么全部执行成功,要么全部不执行,不会出现中间状态。

有序性:程序执行的顺序按照代码的先后顺序执行。由于指令重排序的存在,实际执行顺序可能与代码顺序不同。

synchronized的深度理解

public class SynchronizedDemo {

// 实例同步方法:锁是当前对象实例

public synchronized void instanceMethod() {

// 临界区

}

// 静态同步方法:锁是当前类的Class对象

public static synchronized void staticMethod() {

// 临界区

}

// 同步代码块:可以指定任意对象作为锁

public void someMethod() {

synchronized(this) {

// 临界区

}

}

}

synchronized的实现原理:

在字节码层面,通过monitorenter和monitorexit指令实现。

每个对象都有一个monitor(监视器锁)与之关联。

锁具有可重入性:同一个线程可以多次获取同一把锁。

ReentrantLock:更灵活的锁机制

public class ReentrantLockDemo {

private final ReentrantLock lock = new ReentrantLock(true); // 公平锁

public void performTask() {

lock.lock(); // 可以在这里使用lockInterruptibly()支持中断

try {

// 临界区

} finally {

lock.unlock(); // 必须在finally块中释放锁

}

}

}

与synchronized的对比:

特性

synchronized

ReentrantLock

实现机制

JVM内置

JDK实现

锁获取

自动获取释放

手动控制

可中断

不支持

支持

公平性

非公平

可选择公平或非公平

条件变量

单一

多个

volatile关键字:轻量级的同步

public class VolatileExample {

private volatile boolean shutdown = false;

public void shutdown() {

shutdown = true; // 写操作具有原子性和可见性

}

public void doWork() {

while (!shutdown) { // 读操作总能获取最新值

// 执行任务

}

}

}

volatile的语义:

可见性:对volatile变量的写操作会立即刷新到主内存。

有序性:禁止指令重排序(内存屏障)。

不保证原子性:复合操作(如i++)仍然需要同步。

适用场景:

状态标志位。

双重检查锁定模式。

观察者模式中的状态发布。

五、线程间通信:协作的智慧

wait/notify机制:经典的线程协作

public class WaitNotifyDemo {

private boolean condition = false;

public synchronized void waitForCondition() throws InterruptedException {

// 必须使用while循环检查条件,避免虚假唤醒

while (!condition) {

wait(); // 释放锁并等待

}

// 条件满足,执行后续操作

doSomething();

}

public synchronized void signalCondition() {

condition = true;

notifyAll(); // 通知所有等待线程

}

}

wait/notify的使用要点:

必须在同步方法或同步块中调用。

总是使用while循环检查条件,避免虚假唤醒。

优先使用notifyAll()而不是notify(),避免信号丢失。

Condition接口:更精确的线程控制

public class ConditionDemo {

private final Lock lock = new ReentrantLock();

private final Condition condition = lock.newCondition();

private boolean ready = false;

public void await() throws InterruptedException {

lock.lock();

try {

while (!ready) {

condition.await(); // 等待条件

}

} finally {

lock.unlock();

}

}

public void signal() {

lock.lock();

try {

ready = true;

condition.signal(); // 通知等待线程

} finally {

lock.unlock();

}

}

}