Java多线程全体系教程-第一篇:Java多线程零基础入门·基础篇
核心定位:搞懂「什么是多线程」「为什么要用多线程」「怎么创建线程」「线程生命周期」。
一、先搞懂:进程、线程、多线程核心概念
1.1 什么是进程?
进程是操作系统分配资源的最小单位,是运行中的程序实例。每个进程都有独立的内存空间、文件句柄、系统资源,进程之间相互隔离,互不干扰。
举个例子:我们打开IDEA、浏览器、微信,每一个运行的程序,都是一个独立的进程。
1.2 什么是线程?
线程是进程内的执行单元,是CPU调度和执行的最小单位。一个进程至少包含一个主线程,同一个进程内的所有线程,共享该进程的内存空间、堆内存、静态资源,只有栈内存是线程独立的。
简单理解:进程是一个工厂,线程是工厂里干活的工人,工厂的资源所有工人共享,每个工人有自己独立的工作台。
1.3 什么是多线程?
多线程,就是在同一个进程内,同时运行多个线程,并发执行多个任务。
我们日常写的Java程序,默认只有一个main主线程,所有代码串行执行;引入多线程后,可以让多个任务同时执行,互不阻塞。
1.4 为什么要用多线程?核心作用
-
提高CPU利用率,提升程序执行效率:单核CPU下,线程可以利用IO阻塞的空闲时间执行其他任务;多核CPU下,多线程可以真正实现并行执行,充分利用多核性能。
-
避免阻塞主线程:耗时操作(网络请求、文件读写、数据库操作)放在子线程执行,不会卡住主线程,保证程序响应流畅。
-
实现异步处理:日志记录、消息推送、数据统计等非核心业务,可异步执行,不用等待主线程业务完成。
重要注意事项:多线程不是线程越多越好,线程过多会导致频繁的线程上下文切换,反而降低程序性能;同时多线程会带来线程安全问题,必须合理控制线程数量。
二、Java中创建线程的4种方式(必考+必掌握)
Java中创建线程,本质只有1种底层方式:创建Thread类对象,其余方式都是对Thread的封装。日常开发常用4种写法,按学习顺序排序。
方式1:继承Thread类,重写run()方法
这是最基础的创建方式,自定义类继承Thread类,重写run()方法,run()方法内就是线程要执行的任务。
/**
* 方式1:继承Thread类创建线程
*/
public class ThreadExtendDemo extends Thread {
// 重写run方法,线程执行的核心逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread() 获取当前执行的线程对象
System.out.println(Thread.currentThread().getName() + "正在执行,i=" + i);
}
}
public static void main(String[] args) {
// 1. 创建线程对象
ThreadExtendDemo thread1 = new ThreadExtendDemo();
ThreadExtendDemo thread2 = new ThreadExtendDemo();
// 2. 调用start()方法启动线程(绝对不能直接调用run())
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName() + "主线程执行完毕");
}
}
核心注意事项:启动线程必须调用start\(\)方法,不能直接调用run\(\)方法。直接调用run()只是普通的方法调用,会在主线程串行执行,不会开启新线程;只有start()方法会通知操作系统创建新线程,由CPU调度执行run()方法。
优缺点:
-
优点:写法简单,适合简单场景;
-
缺点:Java是单继承,继承Thread类后,无法再继承其他类,扩展性极差,开发中不推荐使用。
方式2:实现Runnable接口,重写run()方法(推荐)
这是开发中最常用的基础写法,实现Runnable接口,只关注任务逻辑,不继承Thread类,保留类的扩展性。
/**
* 方式2:实现Runnable接口创建线程
*/
public class RunnableImplDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行,i=" + i);
}
}
public static void main(String[] args) {
// 1. 创建任务对象
RunnableImplDemo task = new RunnableImplDemo();
// 2. 创建Thread对象,传入任务对象
Thread thread1 = new Thread(task, "线程1");
Thread thread2 = new Thread(task, "线程2");
// 3. 启动线程
thread1.start();
thread2.start();
}
}
优缺点:
-
优点:接口可以多实现,不影响类继承其他类,任务与线程分离,同一个任务可以被多个线程共享,扩展性强,基础场景首选;
-
缺点:run()方法没有返回值,无法抛出异常,只能内部try-catch处理,不适合需要获取执行结果的场景。
方式3:实现Callable接口,结合FutureTask(有返回值、可抛异常)
解决Runnable接口的痛点:支持线程执行完成后返回结果,支持抛出异常,适合需要获取线程执行结果的场景。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 方式3:实现Callable接口,带返回值的线程
*/
public class CallableImplDemo implements Callable<Integer> {
// 泛型就是线程执行完成后返回的结果类型
@Override
public Integer call() throws Exception {
// 模拟计算任务
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + "计算完成");
// 返回执行结果
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建Callable任务对象
CallableImplDemo callableTask = new CallableImplDemo();
// 2. 创建FutureTask对象,包装Callable任务(FutureTask同时实现了Runnable和Future接口)
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
// 3. 创建Thread对象,传入FutureTask
Thread thread = new Thread(futureTask, "计算线程");
// 4. 启动线程
thread.start();
// 5. 获取线程执行结果(get()方法是阻塞方法,会一直等待线程执行完成)
Integer result = futureTask.get();
System.out.println("线程执行结果:" + result);
}
}
核心注意事项:FutureTask\.get\(\)方法是阻塞方法,调用后主线程会一直等待子线程执行完成,才能继续执行。不要在业务逻辑中间随意调用,避免阻塞主线程。
优缺点:
-
优点:支持返回值、支持抛出异常,功能完善;
-
缺点:写法相对繁琐,手动创建线程无法控制线程数量,高并发场景下禁止手动创建线程。
方式4:线程池创建线程(企业开发唯一规范写法)
以上3种方式,都是手动创建线程,高并发场景下,频繁创建/销毁线程会极大消耗系统资源,引发OOM内存溢出。
企业开发强制要求:所有多线程任务,必须通过线程池执行,统一管理线程生命周期、控制线程数量、复用线程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 方式4:线程池执行线程任务(开发规范)
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定线程数的线程池(基础演示,生产环境禁止使用Executors创建)
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 2. 提交10个任务,线程池复用3个线程执行
for (int i = 1; i <= 10; i++) {
int taskNum = i;
// 提交任务到线程池
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "正在执行任务" + taskNum);
});
}
// 3. 关闭线程池
threadPool.shutdown();
}
}
核心注意事项:生产环境绝对禁止使用Executors工具类创建线程池,Executors创建的线程池默认无界队列、无界线程数,高并发场景下会导致OOM内存溢出,必须手动通过ThreadPoolExecutor创建线程池。
三、线程的生命周期(6种状态,必考)
Java线程的生命周期,定义在Thread类的State枚举中,一共6种状态,线程在整个生命周期中,只会在这6种状态之间切换。
线程6种状态详解
-
新建状态(NEW):创建了Thread对象,还没有调用start()方法,线程未启动。
-
就绪状态(RUNNABLE):调用了start()方法,线程已经被操作系统创建完成,等待CPU分配时间片执行。
-
运行状态(RUNNING):CPU分配时间片,线程开始执行run()方法中的代码。
-
阻塞状态(BLOCKED):线程等待锁资源,进入阻塞状态,拿到锁之后会回到就绪状态。
-
等待状态(WAITING):线程主动调用wait()、join()、LockSupport.park()方法,进入无限等待,需要其他线程唤醒,才能回到就绪状态。
-
超时等待状态(TIMED_WAITING):线程调用sleep(毫秒)、wait(毫秒)、join(毫秒),进入限时等待,时间到了自动唤醒,回到就绪状态。
-
终止状态(TERMINATED):线程执行完run()方法、或者异常终止,线程生命周期结束。
状态切换核心流程
NEW → RUNNABLE → RUNNING → (阻塞/等待/超时等待)→ RUNNABLE → RUNNING → TERMINATED
重要注意事项:线程一旦进入TERMINATED终止状态,生命周期结束,无法再次调用start()启动,重复启动线程会抛出IllegalThreadStateException异常。
四、线程基础常用方法(必掌握)
1. Thread.sleep(long millis):线程休眠
-
作用:让当前线程进入超时等待状态,休眠指定毫秒数,不会释放持有的锁;
-
休眠结束后,线程回到就绪状态,等待CPU调度,不会立即执行;
-
静态方法,谁调用谁休眠。
2. Thread.yield():线程让步
-
作用:让当前线程从运行状态,回到就绪状态,主动让出CPU时间片;
-
不会释放锁,线程回到就绪状态后,依然可以和其他线程竞争CPU资源,可能立即再次运行。
3. join():线程插队
-
作用:在主线程中,调用子线程的join()方法,主线程会进入等待状态,直到子线程执行完毕,主线程才会继续执行;
-
适合场景:主线程需要等待子线程执行完成,获取执行结果后再继续运行。
4. setPriority(int priority):设置线程优先级
-
Java线程优先级范围1~10,默认优先级5;
-
优先级越高,被CPU调度的概率越大,不是优先级高就一定先执行,最终调度由操作系统决定,优先级可靠性不强,不要依赖优先级实现业务逻辑。
5. setDaemon(true):设置守护线程
-
Java线程分为用户线程和守护线程;
-
用户线程:主线程退出,用户线程依然可以执行,直到自己执行完毕;
-
守护线程:当进程内所有用户线程都退出后,守护线程会被强制终止,无论是否执行完毕;
-
典型场景:JVM的垃圾回收线程,就是守护线程。
核心注意事项:设置守护线程,必须在调用start()方法之前设置,启动后设置无效,会抛出异常。
第一篇总结
本篇是Java多线程的入门地基,核心掌握4点:
-
进程与线程的区别,多线程的核心作用;
-
4种创建线程的方式,重点掌握Runnable、Callable、线程池三种写法;
-
线程6种生命周期状态,以及状态切换逻辑;
-
sleep、join、yield、守护线程等基础方法的用法与注意事项。
基础用法完全掌握后,就可以进入第二篇,学习多线程最核心的难点:线程安全、锁机制、线程通信。