多线程快速入门

12 min

本知识文档基于 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 methodthis(当前实例)整个方法加锁
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 控制并发度

面试题梳理

基础概念

  1. 进程和线程的区别是什么? 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。进程之间独立,线程共享进程资源。创建/销毁线程的开销远小于进程。

  2. Java 中创建线程有哪几种方式? 继承 Thread 类、实现 Runnable 接口、使用匿名内部类 / Lambda 表达式。

  3. start()run() 的区别? start() 启动一个新线程,由新线程执行 run()run() 是普通方法调用,在主线程执行。

线程安全

  1. 什么是线程安全问题? 多个线程并发访问共享资源,导致数据不一致。

  2. synchronized 关键字的作用? 保证同一时刻只有一个线程执行被修饰的代码,保证可见性和原子性。

  3. 同步代码块和同步方法的区别? 同步代码块锁对象可自定义,粒度更灵活;同步方法锁对象固定为 this类.class

  4. 静态同步方法的锁是什么? 类名.class(类级别的锁,所有实例共享)。

死锁

  1. 什么是死锁?产生条件? 两个线程互相持有对方需要的锁,互相等待。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。

  2. 如何避免死锁? 避免锁嵌套,统一锁的获取顺序,使用超时机制(如 Lock.tryLock())。

进阶

  1. sleep()wait() 的区别? sleep() 是 Thread 类方法,不释放锁;wait() 是 Object 类方法,释放锁,需在同步块中使用。

  2. 多线程的优缺点? 优点:提高 CPU 利用率、提升响应速度;缺点:增加复杂度、可能出现死锁/线程安全问题。


参考资源:课程来源 [www.zxit8.com] 0001-多线程快速入门,作者余胜军(上海每特教育科技有限公司),JDK 版本 1.8。示例代码可直接运行,建议配合源码目录一起学习。