从 0 到 1 学 Java 多线程:线程是什么?怎么用?如何保证安全?

从 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() 方法执行完毕,或因异常退出,线程生命周期结束。

状态转换图NEWRUNNABLE(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 请求的线程、电商系统中处理支付的线程,都是用户线程 ------ 它们不结束,服务器进程就不会退出。
  • 线程 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 实现了 RunnableFuture 接口);
  • 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(),只是单线程里的普通方法执行。

相关推荐
weixin_425023007 分钟前
Java开发高频实用技巧汇总(List操作/多线程/反射/监控等)
java·windows·list
zh_xuan8 分钟前
kotlin的常见空检查
android·开发语言·kotlin
alonewolf_991 小时前
深入Spring核心原理:从Bean生命周期到AOP动态代理全解析
java·后端·spring
天远Date Lab1 小时前
Python实现用户消费潜力评估:天远个人消费能力等级API对接全攻略
java·大数据·网络·python
Tony Bai8 小时前
高并发后端:坚守 Go,还是拥抱 Rust?
开发语言·后端·golang·rust
wjs20248 小时前
Swift 类型转换
开发语言
没有bug.的程序员8 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
一线大码9 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
秃了也弱了。9 小时前
python实现定时任务:schedule库、APScheduler库
开发语言·python
weixin_440730509 小时前
java数组整理笔记
java·开发语言·笔记