多线程快速入门
本知识文档基于 【www.zxit8.com】 0001-多线程快速入门 课程整理,涵盖 Java 多线程从基础到实战的核心知识点。
进程与线程基础概念
什么是进程?
进程(Process) 其实就是一个应用程序,进程是所有线程的集合。每个正在运行的程序就是一个进程,进程拥有独立的资源(内存空间、文件句柄等),进程之间相互隔离。
什么是线程?
线程(Thread) 其实就是一条执行路径。主要包括:
- main 主线程:程序入口,JVM 自动创建
- 子线程:开发者自己创建的线程
- gc 线程:JVM 专门用于垃圾回收的线程
单任务 vs 多任务
| 概念 | 说明 | 类比 |
|---|---|---|
| 单任务 | 一次只做一件事,做完才能做下一件 | 排队买票,一个人买完才到下一个人 |
| 多任务 | 同一时间做多件事 | 边听音乐边写代码 |
多线程的目的:提高 CPU 利用率,让程序”同时”执行多个任务。
CPU 时间片与随机切换
CPU 通过 时间片轮转 在各个线程之间快速切换,切换速度极快,给人”同时运行”的错觉。线程的随机性 是每次执行顺序可能不同,这是多线程编程的难点之一。
三种创建线程的方式
Java 中创建线程主要有三种方式,难度递进。
继承 Thread 类
步骤: 定义一个类,继承 Thread 类,重写 run() 方法,调用 start() 方法启动线程。
class CreateThread extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
CreateThread createThread = new CreateThread();
createThread.start(); // 启动线程,不是调用 run()
}
}启动线程必须调用 start() 方法,而不是 run() 方法。调用 run() 相当于在主线程中执行普通方法。
实现 Runnable 接口
步骤: 定义一个类,实现 Runnable 接口,重写 run() 方法,将该类的实例作为参数传入 Thread 的构造器,调用 start() 方法启动。
class CreateRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
CreateRunnable task = new CreateRunnable();
Thread thread = new Thread(task);
thread.start();
}
}匿名内部类
适用场景:线程任务简单,不需要复用的情况。
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
});
thread.start();
}
}三种方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 继承 Thread | 简单直接 | Java 单继承,无法继承其他类 | 简单的独立线程 |
| 实现 Runnable | 灵活,可继承其他类 | 稍复杂 | 资源共享、推荐使用 |
| 匿名内部类 | 代码简洁 | 无法复用 | 一次性任务 |
线程常用方法
Thread.sleep(毫秒)
让当前线程从运行状态变为休眠(阻塞)状态,时间到期后回到就绪状态。sleep 不会释放锁。
try {
Thread.sleep(1000); // 休眠 1 秒
} catch (InterruptedException e) {
e.printStackTrace();
}Thread.currentThread()
获取当前正在执行的线程对象。
System.out.println("id:" + Thread.currentThread().getId() + "-i:" + i);getId / getName / setName
Thread thread = new Thread(task);
thread.setName("线程①");
System.out.println("id:" + thread.getId() + " name:" + thread.getName());线程 ID 是 JVM 自动分配的、不重复的 long 值。
线程状态流转
新建(New) → 就绪(Runnable) → 运行(Running) → 阻塞(Blocked)
↓ ↓
终止(Dead) 就绪(Runnable)线程安全与同步
线程安全问题
多个线程同时访问共享资源时,由于线程的随机切换和交错执行,导致数据不一致的问题。
经典案例:火车票售票系统
class ThreadTrain1 implements Runnable {
int trainCount = 100; // 共享资源
@Override
public void run() {
while (trainCount > 0) {
show();
}
}
public void show() {
if (trainCount > 0) {
try { Thread.sleep(10); } catch (Exception e) { }
System.out.println("出售第" + (100 - trainCount + 1) + "票");
trainCount--;
}
}
}上述代码中,两个线程同时对 trainCount 进行读写操作,会出现线程安全问题(如:卖出第 0 张票、重复卖同一张票)。
解决方案:synchronized 关键字
synchronized 保证同一时刻只有一个线程执行被修饰的代码块/方法,实现线程同步。
方式一:同步代码块
public void show() {
synchronized (oj) { // 使用任意对象作为锁
if (trainCount > 0) {
try { Thread.sleep(10); } catch (Exception e) { }
System.out.println("出售第" + (100 - trainCount + 1) + "票");
trainCount--;
}
}
}方式二:同步方法
public synchronized void show() { // 默认锁是 this
if (trainCount > 0) {
try { Thread.sleep(10); } catch (Exception e) { }
System.out.println("出售第" + (100 - trainCount + 1) + "票");
trainCount--;
}
}方式三:静态同步方法
public static synchronized void show() { // 锁是 类.class
if (trainCount > 0) {
// ...
}
}三种 synchronized 对比
| 形式 | 锁对象 | 说明 |
|---|---|---|
synchronized(oj) | 任意对象 oj | 粒度最灵活 |
synchronized method | this(当前实例) | 整个方法加锁 |
static synchronized method | 类名.class(类对象) | 类级别锁,所有实例共享 |
死锁
什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,互相持有对方需要的锁,导致所有线程都无法继续执行的状态。
死锁产生的原因
同步中嵌套了同步,即:
线程1 持有锁 A,等待锁 B
线程2 持有锁 B,等待锁 A
↓
互相等待,死锁形成死锁示例
public void run() {
if (flag) {
while (trainCount > 0) {
synchronized (oj) { // 先拿 oj 锁
show(); // 进入 show() 需要 this 锁
}
}
} else {
while (trainCount > 0) {
show(); // 先拿 this 锁
// show() 内需要 oj 锁
}
}
}
public synchronized void show() { // this 锁
synchronized (oj) { // 再拿 oj 锁
// 卖票...
}
}如何避免死锁
- 避免同步嵌套
- 固定锁的获取顺序
- 使用
tryLock()等超时机制
实战:多线程分批发送短信
业务场景
向 140 名学员发送短信通知,要求使用多线程分批发送,提高效率。
核心设计
用户数据 (140条)
↓
ListUtils.splitList(list, pageSize=50)
↓
[第1批: 50条] → UserThread 线程1
[第2批: 50条] → UserThread 线程2
[第3批: 40条] → UserThread 线程3工具类:List 分批切割
public class ListUtils {
static public <T> List<List<T>> splitList(List<T> list, int pageSize) {
int listSize = list.size();
int page = (listSize + (pageSize - 1)) / pageSize;
List<List<T>> listArray = new ArrayList<List<T>>();
for (int i = 0; i < page; i++) {
List<T> subList = new ArrayList<T>();
for (int j = 0; j < listSize; j++) {
int pageIndex = ((j + 1) + (pageSize - 1)) / pageSize;
if (pageIndex == (i + 1)) {
subList.add(list.get(j));
}
}
listArray.add(subList);
}
return listArray;
}
}实体类
public class UserEntity {
private String userId;
private String userName;
// getter / setter
}发送线程
class UserThread extends Thread {
private List<UserEntity> list;
public UserThread(List<UserEntity> list) {
this.list = list;
}
public void run() {
for (UserEntity userEntity : list) {
System.out.println("threadName:" + Thread.currentThread().getName()
+ "-学员编号:" + userEntity.getUserId()
+ "---学员名称:" + userEntity.getUserName());
// 调用发送短信具体代码
}
}
}主程序
public class SendThread {
public static void main(String[] args) {
List<UserEntity> listUserEntity = init();
int userThreadPage = 50;
List<List<UserEntity>> splitUserList = ListUtils.splitList(listUserEntity, userThreadPage);
for (int i = 0; i < splitUserList.size(); i++) {
List<UserEntity> list = splitUserList.get(i);
UserThread userThread = new UserThread(list);
userThread.start();
}
}
}实战总结
| 要点 | 说明 |
|---|---|
| 数据分片 | 将大数据集拆分为小批次 |
| 多线程并行 | 每个批次独立线程处理 |
| 线程安全 | 每个线程只处理自己的数据,无需共享锁 |
| 扩展性 | 可调整 pageSize 控制并发度 |
面试题梳理
基础概念
进程和线程的区别是什么? 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程之间独立,线程共享进程资源。创建/销毁线程的开销远小于进程。
Java 中创建线程有哪几种方式? 继承
Thread类、实现Runnable接口、使用匿名内部类 / Lambda 表达式。start()和run()的区别?start()启动一个新线程,由新线程执行run();run()是普通方法调用,在主线程执行。
线程安全
什么是线程安全问题? 多个线程并发访问共享资源,导致数据不一致。
synchronized关键字的作用? 保证同一时刻只有一个线程执行被修饰的代码,保证可见性和原子性。同步代码块和同步方法的区别? 同步代码块锁对象可自定义,粒度更灵活;同步方法锁对象固定为
this或类.class。静态同步方法的锁是什么?
类名.class(类级别的锁,所有实例共享)。
死锁
什么是死锁?产生条件? 两个线程互相持有对方需要的锁,互相等待。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。
如何避免死锁? 避免锁嵌套,统一锁的获取顺序,使用超时机制(如
Lock.tryLock())。
进阶
sleep()和wait()的区别?sleep()是 Thread 类方法,不释放锁;wait()是 Object 类方法,释放锁,需在同步块中使用。多线程的优缺点? 优点:提高 CPU 利用率、提升响应速度;缺点:增加复杂度、可能出现死锁/线程安全问题。
参考资源:课程来源 [www.zxit8.com] 0001-多线程快速入门,作者余胜军(上海每特教育科技有限公司),JDK 版本 1.8。示例代码可直接运行,建议配合源码目录一起学习。