前言
平时上课听多线程总是似懂非懂,听课能听懂,做题写代码就废,而且课本内容太杂、太乱,课后根本不想翻。
所以专门整理这篇极简、干货、复习专用的多线程笔记,只记录:必须掌握、面试常问、开发常用的内容。
以后忘记多线程,直接看这一篇就够,不用翻书、不用到处找资料。
正文
一、什么是线程?(最通俗理解)
1.1 进程和线程的区别(必背)
进程:操作系统资源分配的最小单位。
比如:微信、浏览器、IDE,每一个软件都是一个独立进程,进程之间内存相互隔离、互不干扰。
线程:CPU调度的最小单位,属于进程内部的执行单元。
一个进程里面可以包含多个线程,多个线程共享进程资源(堆内存、全局变量)。
一句话总结:
进程:独立房子
线程:房子里的打工人(多个人同时干活)
1.2 为什么要用多线程?
单线程:代码从上到下串行执行,一个任务卡住,后面全部卡住。
多线程:同一时间执行多个任务。
核心作用:
避免程序阻塞(IO、网络请求时不卡死)
提高 CPU 利用率
提高程序运行效率
GUI 界面不卡顿
二、Java 创建线程的 4 种方式(开发必用)
核心真理:所有线程都是调用 start() 启动,run() 只是普通方法!
2.1 方式一:继承 Thread 类
特点:写法简单,但是不能再继承其他类,实际开发用得少。
缺点:Java 单继承,继承 Thread 后不能再继承其他类。
// 继承Thread class MyThread extends Thread { @Override public void run() { // 线程执行逻辑 System.out.println("线程运行:" + Thread.currentThread().getName()); } } public class Test { public static void main(String[] args) { MyThread t = new MyThread(); t.setName("线程A"); t.start(); // 启动新线程 } }2.2 方式二:实现 Runnable 接口(最常用)
解耦、不占用继承名额,支持 Lambda 简化,日常开发首选。
写法1:标准写法
class MyRunnable implements Runnable { @Override public void run() { System.out.println("runnable线程:" + Thread.currentThread().getName()); } } public class Test { public static void main(String[] args) { Runnable task = new MyRunnable(); Thread t = new Thread(task); t.start(); } }写法2:匿名内部类写法(JDK8 之前没有 Lambda 时主流写法)
不用单独定义一个 MyRunnable 类,代码写在创建处
public class Test { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("匿名内部类线程:" + Thread.currentThread().getName()); } }); t.start(); } }写法 3:Lambda 表达式简化写法(JDK8+,日常开发最常用!)
Runnable 是函数式接口(只有一个抽象方法 run),可以直接用 Lambda 省略一大堆模板代码
格式:() -> { 线程任务代码 };
public class Test { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("Lambda线程:" + Thread.currentThread().getName()); }); t.start(); } }写法 4:任务复用小技巧【高频考点】
同一个 Runnable 任务对象,可以交给多个 Thread 执行!
这是 Runnable 对比继承 Thread 最大优势之一(共享任务资源)
class MyRunnable implements Runnable { private int num = 10; @Override public void run() { while(num > 0){ System.out.println(Thread.currentThread().getName() + " 剩余:" + num--); } } } public class Test { public static void main(String[] args) { // 同一个任务 Runnable task = new MyRunnable(); // 两个线程执行同一个任务 new Thread(task,"窗口1").start(); new Thread(task,"窗口2").start(); } }2.3 方式三:Callable + FutureTask(带返回值)
Runnable 没有返回值、不能抛异常;Callable 可以获取线程执行结果。
适用于:需要拿到线程计算结果的场景。
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test { public static void main(String[] args) throws Exception { Callable<Integer> call = () -> { System.out.println("执行任务"); return 100; // 返回结果 }; FutureTask<Integer> task = new FutureTask<>(call); Thread t = new Thread(task); t.start(); Integer res = task.get(); // 阻塞等待获取结果 System.out.println("结果:" + res); } }2.4 方式四:线程池(生产环境必备)
频繁创建、销毁线程非常耗性能,项目中绝不直接 new Thread,全部用线程池。
作用:线程复用、控制并发数量、防止资源耗尽。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) { // 创建固定大小线程池 ExecutorService pool = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { pool.submit(() -> { System.out.println(Thread.currentThread().getName() + "执行任务"); }); } pool.shutdown(); // 关闭线程池 } }三、线程常用核心方法
1.start():启动线程,向 OS 申请线程资源
2.run():线程业务逻辑,不要手动调用
3.Thread.sleep(long ms)
休眠,不释放锁;抛出 InterruptedException
4.wait() / notify() / notifyAll()(Object 方法)
必须在 synchronized 代码块内;调用后释放锁,进入等待队列
5.join()
等待另一个线程执行完毕,当前线程阻塞
Thread t1 = new Thread(() -> {}); t1.start(); t1.join(); // main线程等t1执行完再继续6.yield() 主动让出 CPU 调度权,但不保证一定切换,大概率同优先级线程竞争
7.interrupt() 中断线程,不是强制杀死;
8.设置中断标记 sleep/wait/join 时被中断,抛出异常并清除中断标志 Thread.currentThread() 获取当前执行线程对象
四、线程五大状态(生命周期)
- 新建 NEW:new Thread (),尚未 start
- 就绪 RUNNABLE:调用 start (),等待 CPU 调度
- 运行 RUNNING:CPU 选中该线程执行 run
- 阻塞 BLOCKED / WAITING / TIMED_WAITING
- BLOCKED:等待 synchronized 锁
- WAITING:wait ()、join () 无限等待
- TIMED_WAITING:sleep (时间)、wait (超时)
- 终止 TERMINATED:run 执行完毕或异常退出
五、核心难点:线程安全
多个线程同时操作共享变量会产生数据错乱
// 不安全示例 class Ticket implements Runnable { private int num = 10; @Override public void run() { while(num > 0) { System.out.println("出票:" + num--); } } }解决手段:
synchronized同步锁(方法锁 / 代码块锁)Lock显式锁(ReentrantLock)- 原子类 AtomicInteger(无锁 CAS)
- 使用不可变对象、ThreadLocal(线程私有数据)
六、常见坑总结
- 多次调用
start()→ 抛出 IllegalThreadStateException - 把
run()当成启动方法,没有开启新线程 - 忘记处理 InterruptedException
- 共享变量不加锁,出现数据竞争
- 大量新建 Thread,不使用线程池,造成频繁 GC、资源耗尽
- 死锁:多个线程互相持有对方需要的锁
七、简单实战示例:多窗口卖票(加锁保证安全)
public class TicketDemo { public static void main(String[] args) { Runnable task = new Runnable() { int ticket = 10; @Override public void run() { while(true) { synchronized (this) { // 同步锁 if (ticket <= 0) break; System.out.println(Thread.currentThread().getName() + "卖出第" + ticket-- + "张票"); } } } }; new Thread(task,"窗口1").start(); new Thread(task,"窗口2").start(); new Thread(task,"窗口3").start(); } }