Java多线程:核心技术与实战指南
目录
- 🚀前言
- 🤔什么是多线程?
- 💻创建线程
- 💯创建方法一:继承Thread类
- 💯创建方法二:实现Runnable接口
- 💯创建方法三:实现Callable接口
- 💯三种方法对比
- 🦜Thread的常用方法
- ⚙️线程安全与线程同步
- 💯先搞懂:什么是线程安全?(附比喻)
- 💯线程同步:给多线程定“排队规则”
- 🎯方式1:同步代码块(synchronized块)
- 🎯方式2:同步方法(synchronized方法)
- 🎯方式3:Lock锁(显式锁)
- 💯三种方式对比总结
- 💯同步的“代价”
- 🌟线程池
- 💯认识线程池
- 💯创建线程池
- 💯处理Runnable任务
- 💯处理Callable任务
- 💯通过Executors创建线程池
- 💯线程数配置公式
- ✍️并发与并行
- 🌰 并发执行(单核CPU场景)
- 🌰 并行执行(多核CPU场景)
🚀前言
大家好!我是 EnigmaCoder。
- 在Java编程中,“多线程”是一个高频出现的概念,也是处理并发任务的核心技术。如果你想理解程序如何“同时”处理多个任务(比如一边下载文件一边刷新界面),那多线程就是绕不开的知识点。今天我们就从多线程的定义、创建线程、线程安全与同步、线程池等多个维度,聊聊Java中的多线程。
🤔什么是多线程?
定义:多线程(Multithreading
)是指在一个程序(进程)中,同时运行多个独立的执行单元(线程),这些线程共享程序的内存资源(如变量、方法),但拥有各自的执行路径。
- 简单说,线程是进程(一个正在运行的程序,比如你的Java程序、浏览器)中的“小任务”。一个进程至少有一个线程(称为“主线程”),而多线程就是给一个进程“拆分”出多个并行的小任务,让它们协同完成工作。
- 举个生活例子:进程像一家餐厅,主线程是餐厅的“基础运营”(开门、开灯);多线程就像餐厅里同时工作的服务员、厨师、收银员——他们共享餐厅的资源(食材、餐具),但各自执行不同的任务,最终共同完成“服务顾客”的目标。
要理解多线程的价值,先得搞清楚它和“多进程”的区别:
- 多进程:多个独立的程序同时运行(比如同时开着微信和浏览器),进程间内存不共享,通信成本高(需要通过网络或文件等方式)。
- 多线程:同一个程序内的多个任务,共享内存(变量、对象等),通信成本低,且创建/切换线程的资源消耗远低于进程。
优点:
-
避免阻塞,提升用户体验
比如一个Java桌面程序,如果用单线程(只有主线程),当执行一个耗时操作(如下载大文件)时,主线程会被“卡住”,界面会变成“无响应”状态。而多线程可以把下载任务交给“子线程”,主线程继续处理界面刷新,用户完全感知不到卡顿。 -
利用多核CPU,提高效率
现代CPU都是多核的,单线程只能用一个核心,多线程可以让不同线程跑在不同核心上,真正实现“并行计算”。比如处理大量数据时,多线程拆分任务后,效率可能提升数倍。 -
简化复杂任务的拆分
有些任务天然适合拆分(比如同时处理100个用户的请求),多线程可以让每个请求对应一个线程,逻辑更清晰,无需手动协调任务顺序。
缺点:
- 线程安全问题:多个线程共享资源时,可能出现“抢资源”的情况。比如两个线程同时修改一个变量,可能导致结果错乱(专业称“竞态条件”)。
- 复杂度提升:需要处理线程间的协调(如等待、唤醒),调试难度也更高(线程执行顺序不确定)。
💻创建线程
💯创建方法一:继承Thread类
实现步骤:
- 创建自定义线程类:继承
java.lang.Thread
类,并重写run()
方法 - 实例化线程:创建该自定义线程类的对象
- 启动线程:调用线程对象的
start()
方法
代码示例:
public class ThreadDemo1 {public static void main(String[] args) {Thread t1 =new MyThread();t1.start();for(int i=0;i<5;i++){System.out.println("主线程输出:"+i);}}
}
class MyThread extends Thread {@Overridepublic void run (){for(int i=0;i<5;i++){System.out.println("子线程输出:"+i);}}
}
注意事项:
- 调用
start()
方法后,JVM
会自动执行run()
方法中的逻辑 - 只有调用
start()
方法才是启动一个新的线程执行,直接调用run()
方法将不会创建新线程,此时相当于还是单线程执行。 - 不要把主线程任务放在子线程之前,否则一定是主线程先跑完再跑完子线程。
优缺点:
- 优点:编码简单。
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
💯创建方法二:实现Runnable接口
实现步骤:
- 创建自定义线程类
MyRunnable
,实现Runnable
接口并重写其run()
方法 - 实例化
MyRunnable
任务对象 - 将任务对象作为参数传递给
Thread
类构造函数 - 调用
Thread
实例的start()
方法启动新线程
代码示例:
public class ThreadDemo2 {public static void main(String[] args) {Runnable r =new MyRunnable();Thread t1 = new Thread(r);t1.start();for(int i=0;i<5;i++){System.out.println("主线程输出:"+i);}}
}
class MyRunnable implements Runnable {@Overridepublic void run (){for(int i=0;i<5;i++){System.out.println("子线程输出:"+i);}}
}
优缺点:
- 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强。
- 缺点:需要多一个Runnable对象。
💯创建方法三:实现Callable接口
实现步骤:
-
创建任务对象:通过定义一个实现
Callable
接口的类,重写其call方法来封装业务逻辑和返回数据。然后将Callable
对象包装成FutureTask
线程任务对象。 -
提交任务:将创建的
FutureTask
对象传递给Thread
对象进行执行。 -
启动线程:调用
Thread
对象的start
方法启动线程执行任务。 -
获取结果:待线程执行完成后,通过调用
FutureTask
的get
方法获取任务执行结果。
代码示例
public class Test {public static void main(String[] args) {Callable<String> c1=new MyCallable(100);FutureTask<String> f1 =new FutureTask<>(c1);Thread t1 = new Thread(f1);t1.start();Callable<String> c2=new MyCallable(50);FutureTask<String> f2 =new FutureTask<>(c2);Thread t2 = new Thread(f2);t2.start();try {System.out.println(f1.get());} catch (Exception e) {e.printStackTrace();}try {System.out.println(f2.get());}catch(Exception e){e.printStackTrace();}}
}class MyCallable implements Callable<String> {private int n;public MyCallable(int n){this.n=n;}public String call() throws Exception{int sum=0;for(int i=1;i<=n;i++){sum+=i;}return "子线程计算1-"+n+"的和是:"+sum;}}
注意事项:
- 如果主线程发现某个线程还没有执行完毕,会让出CPU,等这个线程执行完毕后,再向下执行。
FutureTask的API
FutureTask提供的构造器 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象 |
FutureTask提供的方法 | 说明 |
---|---|
public V get() throws Exception | 获取线程执行call方法返回的结果 |
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后获取线程执行的结果。
- 缺点:编码会更加复杂。
💯三种方法对比
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
🦜Thread的常用方法
- Thread 常用方法
方法签名 | 说明 |
---|---|
public void run() | 线程执行的任务逻辑(需重写,定义线程要做的事) |
public void start() | 启动线程(JVM 会自动调用 run 方法,真正开启新线程执行) |
public String getName() | 获取线程名称(默认格式:Thread-索引 ,如 Thread-0 ) |
public void setName(String name) | 设置线程名称(自定义线程标识) |
public static Thread currentThread() | 获取当前执行的线程对象(区分主线程/子线程) |
public static void sleep(long time) | 让当前线程休眠 time 毫秒(休眠后自动继续执行) |
public final void join() | 让当前线程等待调用者执行完毕(如主线程等子线程,需处理中断异常) |
- Thread 常见构造器
构造器签名 | 说明 |
---|---|
public Thread(String name) | 直接创建线程并指定名称(适合继承 Thread 类的场景) |
public Thread(Runnable target) | 封装 Runnable 对象为线程(解耦任务与线程,推荐使用) |
public Thread(Runnable target, String name) | 封装 Runnable 对象并指定线程名称(灵活场景) |
- 综合示例:Thread 方法+构造器全场景演示
// 1. 定义 Runnable 任务(解耦线程逻辑)
class MyRunnable implements Runnable {@Overridepublic void run() {// 获取当前线程信息Thread current = Thread.currentThread();System.out.println(current.getName() + " 执行 Runnable 任务");try {// 休眠 2 秒(模拟耗时操作)Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(current.getName() + " 休眠结束,任务完成");}
}// 2. 继承 Thread 类(直接定义线程逻辑)
class MyThread extends Thread {public MyThread(String name) {super(name); // 调用父类构造器设置线程名称}@Overridepublic void run() {Thread current = Thread.currentThread();System.out.println(current.getName() + " 执行 Thread 子类任务");try {Thread.sleep(1000); // 休眠 1 秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println(current.getName() + " 休眠结束");}
}// 3. 主类:演示所有方法+构造器
public class ThreadDemo {public static void main(String[] args) {// ========== 构造器 1:Thread(String name) —— 继承 Thread 类 ==========MyThread thread1 = new MyThread("子类线程");// ========== 构造器 2:Thread(Runnable target) —— 封装 Runnable ==========Thread thread2 = new Thread(new MyRunnable()); // 名称默认:Thread-1// ========== 构造器 3:Thread(Runnable target, String name) —— 自定义名称 ==========Thread thread3 = new Thread(new MyRunnable(), "命名任务线程");// ========== 启动线程(必须用 start(),直接调 run() 是普通方法!) ==========System.out.println("=== 启动线程 ===");thread1.start();thread2.start();thread3.start();// ========== 主线程操作:getName()/setName() ==========Thread mainThread = Thread.currentThread();System.out.println("主线程原名称:" + mainThread.getName()); // 默认:mainmainThread.setName("自定义主线程");System.out.println("主线程新名称:" + mainThread.getName());// ========== 演示 join():主线程等待 thread1 完成 ==========try {System.out.println("主线程等待「子类线程」完成...");thread1.join(); // 主线程进入等待System.out.println("「子类线程」已完成,主线程继续");} catch (InterruptedException e) {e.printStackTrace();}// ========== 演示 sleep():主线程休眠 3 秒 ==========try {System.out.println("主线程开始休眠 3 秒");Thread.sleep(3000);System.out.println("主线程休眠结束");} catch (InterruptedException e) {e.printStackTrace();}// ========== 对比:直接调用 run()(非线程启动!) ==========System.out.println("=== 直接调用 run()(无新线程)===");MyRunnable runnable = new MyRunnable();runnable.run(); // 在主线程中执行,不会开启新线程}
}
- 示例运行效果 :
- 线程启动:
thread1
(子类线程)、thread2
(默认名)、thread3
(命名任务线程)通过start()
启动,各自开启新线程执行run()
。
- 方法调用:
getName()
/setName()
:主线程名称从main
改为自定义主线程
。join()
:主线程等待thread1
完成后再继续。sleep()
:主线程休眠 3 秒,模拟耗时操作。
- 关键对比:
start()
启动新线程,run()
直接调用仅为普通方法(无新线程)。
- 小结
场景 | 正确用法 | 常见误区 |
---|---|---|
启动线程 | thread.start() | 直接调用 thread.run() (无新线程) |
定义线程逻辑 | 实现 Runnable (解耦优先) | 过度使用继承 Thread (单继承限制) |
线程命名 | 构造器指定或 setName() | 依赖默认名称(不利于调试) |
线程等待 | thread.join() | 忽略中断异常处理 |
⚙️线程安全与线程同步
上面我们聊了多线程的基础,知道它能让程序“一心多用”。但如果多个线程同时抢着用同一个资源,就可能出乱子——这就是“线程安全”问题。而“线程同步”就是给多线程定规矩,让它们有序访问资源。下面咱们用生活化的例子,聊聊这两个概念和三种同步方式。
💯先搞懂:什么是线程安全?(附比喻)
线程安全:多个线程同时操作共享资源时,无论线程执行顺序如何,最终结果都和“单线程执行”的结果一致,就叫线程安全。反之,结果错乱就是“线程不安全”。
举个最直观的例子:
假设你和3
个朋友(4
个线程)一起抢10
张演唱会门票(共享资源),每个人都在同时喊“我要1
张”。如果没有规则,可能出现:
- 最后统计时,明明只有10张票,却被抢走了
11
张(超卖); - 或者有人喊了“要
1
张”,但票没减少(漏卖)。
这就是典型的“线程不安全”——共享资源被多线程乱抢,结果错乱。
为什么会这样?
因为线程操作资源的过程(比如“判断剩余票数→减少1张”)不是“一步完成”的,而是分成几步(读、改、写)。多个线程可能在“读”之后、“写”之前插队,导致数据错乱。
💯线程同步:给多线程定“排队规则”
线程同步的核心是:让多个线程“有序访问”共享资源,避免同时操作。就像给抢票的人定规则:“每次只能一个人去查票、买票,其他人排队等”。
同步的本质是“加锁”:把共享资源的操作过程“锁住”,一个线程操作时,其他线程必须等它完成并“解锁”后才能继续。
接下来讲三种最常用的同步方式:
🎯方式1:同步代码块(synchronized块)
定义:用synchronized(锁对象)
包裹需要同步的代码,只有拿到“锁对象”的线程才能执行块内代码,执行完自动释放锁。
synchronized(同步锁){访问共享资源的核心代码
}
比喻:
就像食堂打饭,勺子(锁对象)只有一个。大家想打饭(执行代码块),必须先拿到勺子(获得锁),打完饭(执行完)把勺子放回(释放锁),下一个人才能拿。
代码示例:解决抢票问题
public class Ticket implements Runnable {private int ticketCount = 10; // 共享的10张票private Object lock = new Object(); // 锁对象(任意对象都可)@Overridepublic void run() {while (true) {// 同步代码块:锁住"查票+卖票"的核心操作synchronized (lock) { if (ticketCount > 0) {// 模拟网络延迟(放大线程安全问题)try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--ticketCount));} else {break;}}}}public static void main(String[] args) {Ticket ticket = new Ticket();// 4个线程(4个人)抢票new Thread(ticket, "线程1").start();new Thread(ticket, "线程2").start();new Thread(ticket, "线程3").start();new Thread(ticket, "线程4").start();}
}
说明:
lock
是锁对象,必须是多个线程“共享”的同一个对象(否则锁不住)。- 同步代码块只锁“必要的代码”(查票+卖票),范围越小,效率越高(别把整个循环锁住,不然和单线程没区别)。
注意事项:
- 对于实例方法建议使用
this
作为锁对象。 - 对于静态方法建议使用字节码(
类名.class
)对象作为锁对象。
🎯方式2:同步方法(synchronized方法)
定义:在方法声明处加synchronized
关键字,整个方法成为同步方法。
- 非静态同步方法:锁对象是
this
(当前对象)。 - 静态同步方法:锁对象是当前类的
Class
对象(类名.class)。
修饰符 synchronized 返回值类型 方法名称(形参列表){操作共享资源的代码
}
比喻:
就像公共电话亭,电话亭(同步方法)本身就是“锁”。一个人进去打电话(执行方法),会把门反锁(获得锁),打完电话出来(方法结束)才开锁,下一个人才能进。
代码示例:用同步方法解决抢票问题
public class Ticket implements Runnable {private int ticketCount = 10;// 同步方法:整个方法被锁住,锁对象是this(当前Ticket对象)private synchronized void sellTicket() {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--ticketCount));}}@Overridepublic void run() {while (ticketCount > 0) {sellTicket(); // 调用同步方法}}public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket, "线程1").start();new Thread(ticket, "线程2").start();new Thread(ticket, "线程3").start();new Thread(ticket, "线程4").start();}
}
说明:
同步方法比同步代码块更简洁,直接把整个方法设为同步。但要注意:如果方法里有不需要同步的代码,会降低效率(相当于整个电话亭都排队,哪怕只是进去拿个东西)。
🎯方式3:Lock锁(显式锁)
定义:JDK 5后新增的java.util.concurrent.locks.Lock
接口(常用实现类ReentrantLock
),需手动调用lock()
加锁、unlock()
释放锁,更灵活。
比喻:
就像租共享单车,你需要手动扫码开锁(lock()
),用完后手动关锁(unlock()
)。比同步代码块/方法更灵活(比如可以中途解锁)。
代码示例:用Lock解决抢票问题
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Ticket implements Runnable {private int ticketCount = 10;// 创建Lock锁对象private Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock(); // 加锁try {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--ticketCount));} else {break;}} finally {lock.unlock(); // 释放锁(必须放finally里,确保一定释放)}}}public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket, "线程1").start();new Thread(ticket, "线程2").start();new Thread(ticket, "线程3").start();new Thread(ticket, "线程4").start();}
}
说明:
lock()
和unlock()
必须成对出现,unlock()
放finally
里,避免线程异常时锁没释放,导致死锁。- 比
synchronized
更灵活:支持尝试获取锁(tryLock()
)、可中断锁等,适合复杂场景。
💯三种方式对比总结
方式 | 语法 | 锁释放 | 灵活性 | 适用场景 |
---|---|---|---|---|
同步代码块 | synchronized(锁对象) | 自动释放 | 中等(指定锁) | 部分代码需要同步时 |
同步方法 | synchronized 修饰方法 | 自动释放 | 低(锁固定) | 整个方法需要同步时 |
Lock锁 | lock() +unlock() | 手动释放 | 高(灵活控制) | 复杂同步场景(如尝试锁、超时) |
💯同步的“代价”
同步能解决线程安全问题,但也有成本:
- 线程需要排队等待锁,会降低并发效率(就像大家都排队打饭,速度肯定比各打各的慢)。
- 过度同步可能导致“死锁”(比如线程A拿着锁1等锁2,线程B拿着锁2等锁1,互相卡死)。
所以,同步不是“越多越好”,而是“按需使用”:只给真正需要同步的代码加锁,平衡安全性和效率。
🌟线程池
在多线程编程里,线程池 是提升效率的关键武器。它像一家“劳务公司”——提前养一批线程(工人)待命,任务(活)来了直接分配,避免频繁招人(创建线程)、裁人(销毁线程)的资源浪费。
💯认识线程池
- 生活场景类比
假设你开了家餐厅:
- 不用线程池:顾客点餐时现招服务员(创建线程),点完餐辞退(销毁线程)。高峰期频繁招人→效率低、资源浪费。
- 用线程池:提前培训3个固定服务员(核心线程),再备2个临时工(最大线程扩展),顾客排队(任务队列)。任务来了直接分配,用完回池待命→效率翻倍!
- 技术核心价值
线程池通过 “线程复用、队列缓冲、拒绝策略” 解决三大问题:
问题 | 线程池如何解决? |
---|---|
线程创建销毁开销大 | 提前创建线程,复用现有线程 |
线程数失控(OOM) | 限制最大线程数,任务排队缓冲 |
任务突发无预案 | 配置拒绝策略(任务爆仓时如何处理) |
💯创建线程池
线程池的“灵魂类”是 ThreadPoolExecutor
,像给“劳务公司”定规则:招多少固定工人、最多扩多少临时工、任务咋排队…
- 构造参数解析(类比餐厅管理)
创建线程池时,需设置7个核心参数,每个参数对应餐厅运营规则:
参数名 | 作用(技术解释) | 餐厅类比 |
---|---|---|
corePoolSize | 核心线程数(一直保留的线程) | 固定员工数(3个长期服务员) |
maximumPoolSize | 最大线程数(核心+临时工总数) | 最多雇5人(3固定+2临时) |
keepAliveTime | 临时工空闲超时时间 | 临时工没事做→30秒后辞退 |
TimeUnit | 时间单位(秒/分等) | 时间标准(秒) |
workQueue | 任务队列(存放待处理任务) | 顾客排队区(最多排3人) |
threadFactory | 线程工厂(如何创建线程) | 招聘流程(统一培训工人) |
RejectedExecutionHandler | 拒绝策略(任务满时如何处理新任务) | 排队满了→拒绝新顾客 |
- 任务拒绝策略:四种应急预案
当线程池忙到极限(核心线程+临时工都在干活,队列也排满)时,新任务怎么处理?这就需要 拒绝策略——相当于“餐厅排队满了,如何应对新顾客”。
策略类 | 说明(餐厅类比) | 代码示例效果 |
---|---|---|
AbortPolicy (默认) | 直接拒绝,抛 RejectedExecutionException 异常 | 新顾客被拒,餐厅抛“无法接待”异常 |
DiscardPolicy | 默默丢弃任务,不抛异常 | 新顾客被无视,餐厅继续忙 |
DiscardOldestPolicy | 丢弃队列中最久的任务,把新任务加入队列 | 赶走最早排队的顾客,让新顾客进队 |
CallerRunsPolicy | 让提交任务的线程(主线程)自己执行任务 | 老板亲自帮新顾客点餐(主线程执行) |
- 代码示例:拒绝策略实战(处理Runnable任务)
模拟“餐厅忙到爆”场景:核心3线程、队列3任务、最多5线程→当提交第9个任务时触发拒绝策略。
import java.util.concurrent.*;public class RejectPolicyDemo {public static void main(String[] args) {// 测试四种拒绝策略(解开注释切换)testRejectPolicy(new ThreadPoolExecutor.AbortPolicy());// testRejectPolicy(new ThreadPoolExecutor.DiscardPolicy());// testRejectPolicy(new ThreadPoolExecutor.DiscardOldestPolicy());// testRejectPolicy(new ThreadPoolExecutor.CallerRunsPolicy());}private static void testRejectPolicy(RejectedExecutionHandler policy) {System.out.println("===== 测试策略:" + policy.getClass().getSimpleName() + " =====");ThreadPoolExecutor pool = new ThreadPoolExecutor(3, // 核心3人5, // 最多5人30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), // 队列最多3个任务Executors.defaultThreadFactory(),policy // 设置拒绝策略);// 提交9个任务(3核心+3队列+2临时=8 → 第9个触发拒绝)for (int i = 1; i <= 9; i++) {final int taskId = i;try {pool.execute(() -> { // 执行Runnable任务System.out.println(Thread.currentThread().getName() + " 处理任务:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}});} catch (RejectedExecutionException e) {System.out.println("任务 " + taskId + " 被拒绝!策略:" + policy.getClass().getSimpleName());}}pool.shutdown();}
}
- 任务执行流程
提交任务时,线程池按以下逻辑处理(对应餐厅流程):
- 核心线程先干活:3个核心线程空闲→直接分配任务。
- 核心忙→任务排队:核心线程占满→任务进队列(最多3个)。
- 队列满→招临时工:队列排满→创建临时线程(最多扩到5个)。
- 全忙+队列满→触发拒绝策略:临时工也占满→按配置的策略拒绝新任务。
💯处理Runnable任务
Runnable
是“无返回值任务”的代表,用 execute()
方法提交给线程池。
- 代码示例:提交Runnable任务
// 定义一个Runnable任务(像“点餐任务”)
class OrderTask implements Runnable {private int taskId;public OrderTask(int taskId) { this.taskId = taskId; }@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 处理订单:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}}
}// 提交任务到线程池
public static void main(String[] args) {ThreadPoolExecutor pool = ...; // 同前创建的线程池(可配置拒绝策略)for (int i = 1; i <= 5; i++) {pool.execute(new OrderTask(i)); // 执行Runnable任务}pool.shutdown();
}
- 方法总结
方法 | 作用 | 适用任务类型 |
---|---|---|
execute(Runnable) | 提交无返回值任务 | Runnable |
💯处理Callable任务
Callable
是“有返回值任务”的代表,用 submit()
提交,通过 Future
获取结果。
- 代码示例:计算1~100的和(带返回值)
import java.util.concurrent.*;public class CallableDemo {public static void main(String[] args) throws Exception {// 1. 创建线程池(复用之前的配置,可含拒绝策略)ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());// 2. 定义Callable任务(计算1~100的和)Callable<Integer> sumTask = () -> {int sum = 0;for (int i = 1; i <= 100; i++) sum += i;return sum; // 返回结果};// 3. 提交任务,获取Future(结果的“凭证”)Future<Integer> future = pool.submit(sumTask);// 4. 获取结果(任务未完成时,get()会阻塞等待)System.out.println("计算结果:" + future.get()); // 输出5050// 5. 关闭线程池pool.shutdown();}
}
- 方法总结
方法 | 作用 | 适用任务类型 |
---|---|---|
submit(Callable<T>) | 提交有返回值任务,返回 Future<T> | Callable |
Future.get() | 获取任务结果(阻塞等待或超时等待) | - |
💯通过Executors创建线程池
Executors
是线程池“工具类”,像“快捷模板”,但不适合生产环境(阿里开发手册强制禁用!)。
- 常用快捷方法
方法名 | 特点(技术解释) | 餐厅类比 | 潜在风险 |
---|---|---|---|
newFixedThreadPool(3) | 固定3个核心线程,队列无界 | 固定3个服务员,排队不限 | 任务堆积→内存溢出(OOM) |
newSingleThreadExecutor() | 单线程,队列无界 | 只有1个服务员,排队不限 | 同上(队列无界) |
newCachedThreadPool() | 线程数弹性扩容(最多 Integer.MAX_VALUE ) | 无限招临时工 | 线程数爆炸→OOM |
- 代码示例:FixedThreadPool
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;public class ExecutorsDemo {public static void main(String[] args) {// 快捷创建:固定3个线程的线程池ExecutorService pool = Executors.newFixedThreadPool(3);// 提交任务(用法和 `ThreadPoolExecutor` 一致)for (int i = 1; i <= 5; i++) {final int taskId = i;pool.execute(() -> {System.out.println(Thread.currentThread().getName() + " 处理任务:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}});}pool.shutdown();}
}
- 为什么“不推荐”?
看 阿里巴巴Java开发手册 强制要求:
线程池不允许用
Executors
创建,必须用ThreadPoolExecutor
!
原因:
FixedThreadPool
/SingleThreadExecutor
:队列是Integer.MAX_VALUE
(无限排队),任务堆积会撑爆内存(OOM)。CachedThreadPool
:线程数上限是Integer.MAX_VALUE
(无限招人),线程太多也会OOM。
💯线程数配置公式
线程池的核心/最大线程数配置,必须根据任务类型调整,否则会严重影响效率。先理解两种任务类型:
- 任务类型定义
类型 | 特点(餐厅类比) | 示例任务 |
---|---|---|
CPU密集型 | 任务疯狂“占用CPU”(如计算、加密),线程几乎不空闲 | 复杂数学运算、图片压缩 |
IO密集型 | 任务大部分时间“等IO”(如读写文件、网络请求),CPU空闲 | 数据库查询、文件上传、接口调用 |
- 配置公式(基于CPU核心数
N
)
线程池的核心思路:让CPU尽可能不空闲,同时避免线程过多导致切换开销。
任务类型 | 配置公式 | 原理说明 |
---|---|---|
CPU密集型 | 核心线程数 = N + 1 | 避免线程等待时CPU完全空闲,+1应对偶尔阻塞 |
IO密集型 | 核心线程数 = N * 2 (或 N * 5 等) | 利用IO等待的空闲时间,多线程并行处理任务 |
- 代码示例:根据任务类型配置线程池
import java.util.concurrent.*;public class TaskTypeConfig {public static void main(String[] args) {int cpuCore = Runtime.getRuntime().availableProcessors(); // 获取CPU核心数System.out.println("CPU核心数:" + cpuCore);// 1. CPU密集型任务:核心数 = cpuCore + 1ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(cpuCore + 1, cpuCore + 1, // 最大线程数同核心(CPU没空,无需临时工)30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));// 2. IO密集型任务:核心数 = cpuCore * 2ThreadPoolExecutor ioPool = new ThreadPoolExecutor(cpuCore * 2, cpuCore * 4, // 最大线程数适当扩展(应对突发IO任务)30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));// 提交任务(按类型分配)cpuPool.execute(() -> heavyCalculation()); // CPU密集型ioPool.execute(() -> fetchData()); // IO密集型}// CPU密集型任务:疯狂计算private static void heavyCalculation() {long result = 0;for (long i = 0; i < 1000000000L; i++) {result += i;}System.out.println("计算结果:" + result);}// IO密集型任务:模拟网络请求private static void fetchData() {try {Thread.sleep(1000); // 模拟IO等待System.out.println("网络数据获取完成");} catch (InterruptedException e) {}}
}
- 小结:线程池配置实战指南
场景 | 任务类型 | 核心线程数公式 | 拒绝策略建议 | 典型案例 |
---|---|---|---|---|
计算密集(如加密) | CPU密集型 | CPU核心数 + 1 | AbortPolicy (抛异常提醒) | 大数据排序、视频编码 |
网络/IO 密集(如接口调用) | IO密集型 | CPU核心数 * 2 | CallerRunsPolicy (降级执行) | 微服务调用、文件上传 |
✍️并发与并行
在多线程的世界里,并发和并行是经常被混淆的概念,但它们的本质区别直接影响程序的效率。
1.定义:宏观与微观的区别
简单说,两者的核心差异在于 “任务是否真正同时执行”,用表格对比更清晰:
概念 | 技术解释(CPU视角) | 生活化比喻 |
---|---|---|
并发 | 多个任务交替执行(单核CPU也能实现) | 餐厅1个服务员交替招呼3桌客人 |
并行 | 多个任务同时执行(需要多核CPU支持) | 餐厅3个服务员同时招呼3桌客人 |
- 直观示例:处理任务的两种方式
假设你需要完成3件事:
- 任务A:煮咖啡(需5分钟,大部分时间等水开,CPU空闲)
- 任务B:煎鸡蛋(需3分钟,全程占用CPU,不能停)
- 任务C:烤面包(需4分钟,全程占用CPU,不能停)
🌰 并发执行(单核CPU场景)
像1个服务员处理3桌客人:
- 启动任务A(放水煮咖啡)→ 水开前CPU空闲,切换到任务B煎鸡蛋(3分钟全程占用CPU)→
- 任务B完成后,切换到任务C烤面包(4分钟全程占用CPU)→
- 最后回到任务A,咖啡煮好收尾。
特点:
- 宏观上:3个任务“同时进行”(你感觉在同步推进);
- 微观上:CPU核心在任务间快速交替切换,实际同一时间只做一件事。
🌰 并行执行(多核CPU场景)
像3个服务员同时开工:
- 服务员1负责任务A(煮咖啡,等水开时CPU自动空闲)→
- 服务员2负责任务B(煎鸡蛋,全程占用1个CPU核心)→
- 服务员3负责任务C(烤面包,全程占用另1个CPU核心)。
特点:
- 宏观+微观上:3个任务真正同时执行,效率翻倍(前提是CPU有多个核心)。
- Java多线程中的体现
- 单核CPU:无论创建多少线程,线程池里的任务都只能并发执行(线程交替占用唯一的CPU核心)。
- 多核CPU:线程池中的多个线程可以并行执行(不同核心同时跑任务),剩余线程继续并发交替(充分利用多核+等待时间)。
- 小结
理解这两个概念,才能设计出高效的多线程方案:
维度 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
执行本质 | 交替执行(宏观同时,微观交替) | 同时执行(宏观+微观都同时) |
CPU依赖 | 单核即可实现 | 必须多核支持 |
典型场景 | IO密集型任务(利用等待时间) | CPU密集型任务(榨干多核性能) |
实战逻辑:
- 处理网络请求、文件读写等IO密集型任务→ 用并发让线程交替利用等待时间(线程池配多线程,哪怕单核也能高效);
- 处理加密计算、大数据排序等CPU密集型任务→ 用并行让多核同时开工(线程池核心数贴近CPU核心数,避免切换开销)。
这就是线程池配置要区分任务类型的底层逻辑——并发与并行的协同,才是多线程的最大价值!