从 0 到 1 学 Java 多线程:线程是什么?怎么用?如何保证安全?
一、介绍
在 Java 中,线程(Thread) 是程序执行的最小单元,是操作系统调度的基本单位。一个进程(Process)可以包含多个线程,这些线程共享进程的内存空间(如堆、方法区),但拥有独立的程序计数器(PC)、虚拟机栈和本地方法栈,因此线程切换的开销远小于进程。
二、概念
1. 线程的状态(生命周期)
Java 线程的生命周期由 6 种状态组成(定义在 Thread.State 枚举中),状态转换是线程的核心逻辑:
| 状态 | 说明 |
|---|---|
NEW(新建) |
线程对象已创建,但未调用 start() 方法(未启动)。 |
RUNNABLE(可运行) |
调用 start() 后,线程进入该状态(包含「就绪」和「运行中」两种子状态):- 就绪:等待 CPU 调度;- 运行中:CPU 正在执行线程的 run() 方法。 |
BLOCKED(阻塞) |
线程因竞争锁失败(如 synchronized 未获取到锁)而暂停执行。 |
WAITING(等待) |
线程通过 wait()、join()、LockSupport.park() 等方法主动放弃 CPU,需被其他线程唤醒(无超时)。 |
TIMED_WAITING(超时等待) |
线程通过 sleep(long)、wait(long)、join(long) 等方法放弃 CPU,等待指定时间后自动唤醒。 |
TERMINATED(终止) |
线程的 run() 方法执行完毕,或因异常退出,线程生命周期结束。 |
状态转换图 :NEW → RUNNABLE(start()) → BLOCKED/WAITING/TIMED_WAITING(触发条件) → RUNNABLE(唤醒 / 超时) → TERMINATED(执行完毕)
2. 线程的属性
-
优先级 :Java 线程优先级范围为
1~10(默认5),优先级越高,CPU 调度概率越高(但不保证,依赖操作系统),通过setPriority(int)/getPriority()设置获取。 -
守护线程(Daemon Thread) :为用户线程服务的线程(如垃圾回收线程),当所有用户线程结束,守护线程会自动终止。通过
setDaemon(true)设置(需在start()前调用),isDaemon()判断。 -
用户线程(User Thread,也叫非守护线程):是 Java 中默认的线程类型,也是程序执行核心业务逻辑的核心载体。
例子:
- Java 程序启动时的
main线程就是典型的用户线程,它会执行核心入口逻辑,还能创建其他用户线程(如处理数据的子线程); - 服务器中处理 HTTP 请求的线程、电商系统中处理支付的线程,都是用户线程 ------ 它们不结束,服务器进程就不会退出。
- Java 程序启动时的
-
线程 ID / 名称 :每个线程有唯一 ID(
getId())和名称(默认Thread-xxx,可通过构造函数Thread(String name)自定义)。
二、线程的创建方式
Java 提供 3 种核心创建线程的方式,核心是定义「线程要执行的任务」(即 run() 方法逻辑):
1. 继承 Thread 类
- 自定义类继承
Thread,重写run()方法(封装任务); - 创建线程对象,调用
start()方法启动线程(不能直接调用run(),否则只是普通方法调用,不会启动新线程)。
java
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务
System.out.println(Thread.currentThread().getName() + " 执行任务");
}
}
// 测试
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.setName("自定义线程");
thread.start(); // 启动线程,触发 run() 执行
}
}
缺点 :Java 单继承限制,自定义类继承 Thread 后无法再继承其他类。
2. 实现 Runnable 接口
- 自定义类实现
Runnable接口,重写run()方法(封装任务); - 创建
Runnable实现类对象,作为参数传入Thread构造函数,调用start()启动。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行任务");
}
}
// 测试
public class RunnableDemo {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "Runnable线程");
thread.start();
}
}
优点 :避免单继承限制,可同时实现多个接口;任务与线程分离,更灵活(多个线程可共享一个 Runnable 对象)。
简化写法(Lambda 表达式,Java 8+) :Runnable 是函数式接口(仅一个抽象方法),可直接用 Lambda 简化:
java
Thread thread = new Thread(() -> {
System.out.println("Lambda 线程执行任务");
}, "Lambda线程");
thread.start();
3. 实现 Callable 接口(带返回值 / 可抛异常)
Runnable 无法返回结果、无法抛出受检异常,Callable 弥补了这一缺陷:
- 实现
Callable<V>接口(V是返回值类型),重写call()方法(任务逻辑,可返回值、抛异常); - 通过
FutureTask<V>包装Callable对象(FutureTask实现了Runnable和Future接口); - 将
FutureTask传入Thread构造函数,启动线程; - 调用
FutureTask.get()获取返回值(会阻塞当前线程,直到任务执行完毕)。
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 模拟耗时计算
Thread.sleep(1000);
return 100; // 返回结果
}
}
// 测试
public class CallableDemo {
public static void main(String[] args) throws Exception {
Callable<Integer> callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask, "Callable线程").start();
// 获取返回值(阻塞等待任务完成)
Integer result = futureTask.get();
System.out.println("线程返回结果:" + result); // 输出 100
}
}
三、线程的核心方法
1. 线程启动与终止
start():启动线程,将线程状态从NEW转为RUNNABLE,由 JVM 调用run()方法(不可重复调用 ,重复调用会抛IllegalThreadStateException)。run():封装线程任务,直接调用无意义(普通方法),需通过start()间接触发。stop():强制终止线程,可能导致资源泄漏(如锁未释放),不推荐使用。- 优雅终止线程:通过标志位控制(如
volatile boolean flag),线程在run()中循环判断标志位,外部修改标志位让线程退出。
java
class StopThread implements Runnable {
private volatile boolean flag = true; // volatile 保证可见性
@Override
public void run() {
while (flag) {
System.out.println("线程运行中...");
}
System.out.println("线程优雅终止");
}
public void stop() {
this.flag = false;
}
}
2. 线程阻塞与唤醒
sleep(long millis):让线程进入TIMED_WAITING状态,暂停指定时间(毫秒),不会释放锁 (如synchronized锁),时间到后自动唤醒。wait()/wait(long):线程必须在synchronized代码块 / 方法中调用(持有锁时),调用后释放锁,进入WAITING/TIMED_WAITING状态;需通过其他线程调用notify()/notifyAll()唤醒(唤醒后需重新竞争锁)。notify()/notifyAll():同样需在synchronized中调用,notify()随机唤醒一个等待线程,notifyAll()唤醒所有等待线程。join()/join(long):让当前线程等待目标线程执行完毕(或超时),例如主线程调用thread.join(),则主线程阻塞,直到thread执行完。
java
// join() 示例
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程:" + i);
}
});
thread.start();
thread.join(); // 主线程等待子线程执行完毕
System.out.println("主线程继续执行");
}
}
3. 线程调度相关
yield():线程主动放弃 CPU 使用权,从「运行中」回到「就绪」状态,重新参与 CPU 竞争(不保证一定能放弃,依赖操作系统)。setPriority(int):设置线程优先级(1~10),优先级高的线程获取 CPU 的概率更高(非绝对)。
四、线程安全问题
1. 什么是线程安全?
当多个线程同时操作共享资源(如共享变量、文件、数据库连接)时,若未做同步控制,可能导致数据不一致(如超卖、计数错误),这就是线程不安全。
示例(线程不安全):
java
// 共享变量 count,多个线程同时自增
class UnsafeThread implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++; // 非原子操作(读取→修改→写入)
}
}
public int getCount() {
return count;
}
}
// 测试:2 个线程各自增 1000 次,预期结果 2000,实际可能小于 2000
public class UnsafeDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeThread unsafe = new UnsafeThread();
new Thread(unsafe).start();
new Thread(unsafe).start();
Thread.sleep(1000);
System.out.println(unsafe.getCount()); // 结果可能是 1500、1800 等
}
}
2. 线程安全解决方案
核心思路:对共享资源的操作进行同步,保证同一时间只有一个线程能执行关键代码。
synchronized关键字 :可修饰方法、代码块,通过「锁机制」保证原子性、可见性、有序性(底层是 JVM 层面的监视器锁monitor)。
java
// 修饰代码块(推荐,锁粒度更小)
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (this) { // 锁对象(需是多个线程共享的对象)
count++;
}
}
}
// 修饰方法(锁对象是 this,静态方法锁是类对象)
public synchronized void increment() {
count++;
}
java.util.concurrent.locks锁 :JDK 1.5+ 提供的显式锁(如ReentrantLock可重⼊锁:⼀种同步机制,允许 同个线程多次获取同个锁),功能比synchronized更灵活(可中断、超时获取锁、公平锁等)。
java
import java.util.concurrent.locks.ReentrantLock;
class SafeThread implements Runnable {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // 显式锁
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 解锁(必须在 finally 中,避免锁泄漏)
}
}
}
}
-
原子类 :
java.util.concurrent.atomic包下的类(如AtomicInteger),通过 CAS(Compare and Swap)机制实现无锁原子操作,效率高于锁。CAS 是一个 CPU 级别的原子指令(不可中断,要么全执行要么全不执行),核心逻辑是:
要修改一个共享变量时,先比较变量的 "当前值" 是否等于 "预期值"(即我上次读取的值);如果相等,就把变量更新为 "目标值";如果不相等,说明变量被其他线程修改过,当前线程不做操作(或重试),全程不阻塞。
java
class AtomicThread implements Runnable {
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet(); // 原子自增
}
}
}
五、线程池(ExecutorService)
频繁创建 / 销毁线程会消耗大量资源,线程池 是线程的「复用池」,提前创建一定数量的线程,任务提交时直接复用线程,避免频繁创建销毁,提高效率。
Java 通过 java.util.concurrent.Executors 提供线程池工厂方法,核心接口是 ExecutorService:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小的线程池(3 个线程)
ExecutorService executor = Executors.newFixedThreadPool(3);
// 2. 提交任务(Runnable 或 Callable)
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务 " + taskId);
});
}
// 3. 关闭线程池(不再接受新任务,等待已提交任务执行完毕)
executor.shutdown();
}
}
常用线程池类型:
newFixedThreadPool(n):固定大小线程池,核心线程数 = 最大线程数 = n。newCachedThreadPool():缓存线程池,核心线程数 0,最大线程数 Integer.MAX_VALUE,空闲线程 60s 后销毁。newSingleThreadExecutor():单线程池,只有 1 个线程,任务按顺序执行。newScheduledThreadPool(n):定时 / 周期性线程池,用于执行定时任务。
线程池的作用 :
复用线程、控制并发数、降低线程生命周期开销,从而提升系统性能与稳定性。
六、总结
- 线程是程序执行的最小单元,共享进程资源,独立拥有栈和程序计数器。
- 线程创建有 3 种方式:继承
Thread、实现Runnable(无返回值)、实现Callable(有返回值)。 - 线程状态有 6 种,核心是
RUNNABLE与其他状态的转换(依赖start()、sleep()、wait()、锁竞争等)。 - 线程安全的核心是同步共享资源操作,常用方案:
synchronized、显式锁、原子类。 - 线程池是线程复用的核心机制,避免频繁创建销毁线程,提高系统性能。
7、补充:
1.线程与进程的关系:
一个进程可以包含多个线程,线程是进程的最小执行单元,也可以这样理解,进程是容器,线程是容器内的执行者。
举个例子:
-
进程:好比一家「公司」,公司拥有独立的资源(办公场地、设备、资金),是操作系统资源分配的基本单位(操作系统给进程分配内存、CPU 时间片、文件句柄等)。
-
线程:好比公司里的「员工」,员工共享公司的资源(用同一间办公室、同一台设备),但各自独立执行任务(做不同的工作),是操作系统调度的基本单位(CPU 直接调度线程执行)。
进程(资源容器)
├── 核心属性:独立地址空间、堆、文件句柄、进程ID
├── 包含:1个或多个线程(主线程+子线程)
├── 生命周期:进程终止 → 所有线程终止
└── 线程(执行单元)
├── 核心属性:私有栈、PC、线程ID
├── 特性:共享进程资源、调度开销小、通信简单
└── 生命周期:线程终止 → 不影响其他线程(除非破坏进程资源)
进程是「资源的集合」,线程是「执行的载体」;进程负责申请资源,线程负责使用资源执行任务。
2.为什么调用start()方法来启动线程,而不是直接调用run()?
start():告诉 JVM "我要启动一个新线程",JVM 会分配线程资源(私有栈、PC 计数器)、将线程状态转为RUNNABLE,等待 CPU 调度后 由 JVM 间接调用run()------ 这才是 "启动新线程"。run():仅封装了线程要执行的任务逻辑(比如循环、计算),直接调用就是普通的 "方法调用",会在 当前线程(如主线程) 中同步执行,不会创建任何新线程。
start() 负责 "创建并启动新线程",是 JVM 层面的线程初始化操作;run() 负责 "存放线程要做的事",是普通的任务逻辑方法 ------ 只有通过 start() 间接调用 run(),才是多线程;直接调用 run(),只是单线程里的普通方法执行。