【JAVA】多线程的入门讲解
一、进程与线程(核心前置概念)
1.1 进程
进程是操作系统资源分配的最小单位,简单来说,我们电脑上独立运行的程序就是进程。比如打开的IDEA、浏览器、微信,每一个程序都对应一个独立进程。
每个进程都有独立的内存空间,进程之间的数据完全隔离,互不干扰,但也正因如此,进程的创建、切换、销毁开销非常大,资源消耗很高。
1.2 线程
线程是CPU调度执行的最小单位,它不能独立存在,必须依附于进程。一个进程可以包含多个线程,同一进程下的所有线程会共享进程的堆内存、方法区资源,只有栈内存是每个线程独有的。
相比于进程,线程轻量化很多,切换和运行的资源开销极小,这也是我们做并发开发、提升程序运行效率优先使用多线程的核心原因。
1.3 核心区别总结
- 资源隔离:进程资源相互独立隔离;线程共享所属进程的全部资源,资源互通。
- 开销成本:进程创建、切换、销毁成本极高;线程是轻量化单元,开销小、效率高。
- 从属关系:一个进程至少自带一个主线程,线程无法脱离进程独立运行。
- 通信方式:进程间通信复杂繁琐;线程可直接共享变量实现通信,简单高效。
1.4 并发与并行
- 并发:主要针对单核CPU,多个线程快速交替抢占CPU执行,宏观上看起来是同时运行,微观上其实是串行执行。
- 并行:针对多核CPU,多个线程在不同CPU核心上真正同时执行,互不抢占、互不干扰。
二、Java多线程的4种实现方式
Java所有线程的底层都依托Thread类实现,日常开发中把它分为无返回值和有返回值两大类。这里直白说结论:实际项目中,优先用Runnable和线程池,另外两种方式基本只用于学习和面试。
2.1 方式一:继承Thread类
自定义类继承Thread类,重写run()方法编写线程的执行逻辑,最终调用start()方法启动线程。
重点避坑:直接调用run()只是普通的方法调用,不会开启新线程,必须调用start()才会创建并启动线程!
// 自定义线程类
public class MyThread extends Thread {
// 重写run方法:线程执行的核心逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程执行:" + Thread.currentThread().getName() + " 次数:" + i);
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.setName("自定义线程-1");
thread.start(); // 启动线程,进入就绪状态
}
}
简单总结下优缺点:
- ✅ 优点:代码写法简单,入门最容易理解
- ❌ 致命缺点:Java是单继承,一旦继承Thread类,就不能再继承其他类,项目扩展性极差,基本不用于实际开发
2.2 方式二:实现Runnable接口
实现Runnable接口,重写run()方法封装任务逻辑,再把任务交给Thread类执行。这种方式最大的好处就是任务和线程彻底解耦,也是开发中最常用的基础写法。
// 实现Runnable接口定义任务
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable线程执行:" + Thread.currentThread().getName() + " 次数:" + i);
}
}
public static void main(String[] args) {
// 1. 传统方式
MyRunnable runnable = new MyRunnable();
new Thread(runnable, "自定义线程-2").start();
// 2. Java8 Lambda简化写法(推荐实战使用)
new Thread(() -> {
System.out.println("Lambda简化多线程执行");
}, "Lambda线程").start();
}
}
优缺点总结:
- ✅ 优点:规避了单继承限制、任务可复用、支持Java8 Lambda简化写法,灵活性很高
- ❌ 缺点:线程执行完没有返回值,无法主动获取执行结果
2.3 方式三:实现Callable接口
Callable是JDK1.5推出的接口,专门弥补Runnable的短板,支持线程返回结果、可以抛出异常,一般配合FutureTask使用,适合需要获取线程执行结果的场景。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 泛型指定返回值类型
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
System.out.println("Callable线程执行完毕");
return sum; // 返回计算结果
}
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask, "有返回值线程").start();
// 获取线程返回结果(阻塞等待线程执行完成)
Integer result = futureTask.get();
System.out.println("线程执行结果:" + result);
}
}
优缺点总结:
- ✅ 优点:有返回值、可捕获异常、场景适配性更强
- ❌ 缺点:代码写法繁琐,且get()获取结果时会阻塞主线程
2.4 方式四:线程池创建
如果频繁手动创建、销毁线程,会极大浪费系统资源、降低程序性能。所以实际项目中,我们几乎不会手动new Thread,而是用线程池。它可以复用线程、控制并发数量、减少资源损耗,是企业开发的标准写法。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建固定大小线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 提交10个任务,线程池自动调度执行
for (int i = 1; i <= 10; i++) {
int taskNum = i;
threadPool.execute(() -> {
System.out.println("线程:" + Thread.currentThread().getName() + " 执行任务:" + taskNum);
});
}
// 关闭线程池
threadPool.shutdown();
}
}
三、线程的五大生命周期
Java线程从创建到销毁,整个过程分为5种状态,由JVM自动调度切换,也是面试常问的知识点,下面通俗讲清楚每种状态:
3.1 五大状态详解
- NEW(新建):仅仅创建了线程对象,还没有调用start()方法,只是完成初始化,没有CPU执行资格。
- RUNNABLE(就绪/运行):调用start()后进入就绪状态,抢到CPU时间片就会切换为运行状态,开始执行任务。
- BLOCKED(阻塞):线程抢不到同步锁资源,被迫暂停执行,处于阻塞等待状态。
- WAITING(无限等待):调用wait()、join()等方法,线程无限等待,必须等待其他线程主动唤醒。
- TIMED_WAITING(限时等待):调用sleep()、带超时的wait()方法,等待指定时间后会自动唤醒,无需手动触发。
- TERMINATED(终止):线程任务执行完毕,或者中途异常终止,生命周期彻底结束。
3.2 核心状态流转逻辑
整体流转逻辑非常好记:新建线程 → 调用start()进入就绪 → 抢到CPU资源开始运行 → 遇到阻塞/等待逻辑暂停 → 被唤醒/超时后重回就绪 → 任务执行完毕,线程终止。
四、线程核心常用方法
4.1 常用方法汇总
- start():正式启动线程,进入就绪状态,一个线程只能调用一次,多次调用会报错。
- run():线程的任务逻辑载体,由JVM自动调用,我们业务代码中禁止手动调用。
- sleep(long time):让线程限时休眠,不会释放锁资源,只是单纯暂停执行。
- wait():让线程无限等待,会主动释放锁,必须在同步代码块/方法中使用,需要notify()唤醒。
- notify()/notifyAll():唤醒单个/所有处于等待状态的线程。
- join():让主线程阻塞,等待子线程执行完成后,主线程再继续运行。
- yield():线程礼让,主动让出CPU时间片,重新回到就绪状态,重新参与抢占。
4.2 wait和sleep的核心区别
- 所属类不同:sleep()是Thread类的静态方法;wait()是Object类的通用方法。
- 锁机制不同:sleep()只休眠不释放锁;wait()会释放当前持有的锁。
- 使用场景不同:sleep()可以在任意场景使用;wait()只能在同步环境中使用。
- 唤醒方式不同:sleep()超时自动唤醒;wait()必须手动通过notify()/notifyAll()唤醒。
五、线程安全问题及解决方案
5.1 线程安全问题产生原因
简单说:多线程场景下,多个线程同时读写操作同一个共享变量,加上CPU抢占式执行的特性,很容易出现数据覆盖、数据不一致的问题,这种现象就是竞态条件,也是线程不安全的根源。
5.2 三大解决方案
1)synchronized(隐式锁)
synchronized是JVM层面的隐式锁,无需手动加锁解锁,系统自动帮我们完成,使用简单,适合并发量不大的场景。分为同步方法和同步代码块,优先推荐粒度更细的同步代码块。
// 同步代码块
public class SyncDemo {
private static int count = 0;
private static final Object LOCK = new Object();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (LOCK) {
count++;
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (LOCK) {
count++;
}
}
}).start();
// 等待线程执行完毕
Thread.sleep(1000);
System.out.println("最终结果:" + count); // 正确结果2000
}
}
2)Lock显式锁(ReentrantLock)
JDK提供的手动锁,需要自己控制加锁、解锁时机,灵活性远高于synchronized。支持尝试获取锁、超时锁、可中断锁等特性,适合高并发业务场景。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private static int count = 0;
// 可重入锁
private static final Lock LOCK = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
LOCK.lock(); // 加锁
try {
count++;
} finally {
LOCK.unlock(); // 最终解锁,避免死锁
}
}
}).start();
}
}
3)volatile关键字
volatile是轻量级同步关键字,只保证变量的内存可见性和禁止指令重排序,不保证原子性。一般用来做状态标记,不能解决i++这类复合操作的线程安全问题,千万别拿来替代锁!
六、线程池核心原理
6.1 为什么要用线程池?
- 节省资源:复用已有线程,避免频繁创建、销毁线程带来的性能损耗。
- 响应更快:任务到来直接复用线程,无需等待线程创建,提升接口响应速度。
- 统一管控:可以精准控制并发数、任务队列、拒绝策略,避免线程无限创建导致服务宕机。
6.2 线程池七大核心参数
ThreadPoolExecutor 手动创建线程池,一共是7个参数,下面逐个通俗讲解:
-
- corePoolSize(核心线程数):线程池的常驻线程,就算空闲也不会被回收,是线程池的基础工作线程。
-
- maximumPoolSize(最大线程数):线程池能容纳的最大线程总量。核心线程全忙、队列满了之后,才会新建非核心线程,最多不超过这个数值。
-
- keepAliveTime(空闲超时时间):专门针对非核心线程,空闲时间超过这个值,就会被自动回收,节省资源。
-
- unit(时间单位):搭配keepAliveTime使用,支持秒、毫秒、分钟等时间单位。
-
- workQueue(阻塞任务队列):核心线程全忙时,新任务会先存入这个队列排队,队列满了才会新建线程。常用队列有ArrayBlockingQueue、LinkedBlockingQueue等。
-
- threadFactory(线程工厂):用来统一创建线程,我们可以自定义线程名称、优先级,方便线上日志排查问题。
-
- handler(拒绝策略):当核心线程满、队列满、线程数达到最大值,线程池彻底满载,就会触发拒绝策略处理新任务。
6.3 线程池完整执行流程
我用最直白的逻辑梳理线程池执行顺序,很好记:
- 新任务提交,先判断核心线程数是否未满,有空余核心线程就直接新建线程执行任务;
- 如果核心线程全部在工作,就判断阻塞队列是否有空位,有空位就把任务存入队列排队;
- 如果队列也满了,就判断当前线程数是否达到最大值,没达到就新建非核心线程处理任务;
- 线程数、队列全部爆满,直接触发拒绝策略拦截新任务。
七、文末总结
多线程并发是Java进阶的核心难点,吃透基础之后,大家可以继续深入学习JMM内存模型、AQS原理、锁优化、并发容器等高阶知识,循序渐进吃透整个Java并发体系!
更多推荐


所有评论(0)