Java多线程全体系教程-第一篇:Java多线程零基础入门·基础篇

6 阅读9分钟

Java多线程全体系教程-第一篇:Java多线程零基础入门·基础篇

核心定位:搞懂「什么是多线程」「为什么要用多线程」「怎么创建线程」「线程生命周期」。

一、先搞懂:进程、线程、多线程核心概念

1.1 什么是进程?

进程是操作系统分配资源的最小单位,是运行中的程序实例。每个进程都有独立的内存空间、文件句柄、系统资源,进程之间相互隔离,互不干扰。

举个例子:我们打开IDEA、浏览器、微信,每一个运行的程序,都是一个独立的进程。

1.2 什么是线程?

线程是进程内的执行单元,是CPU调度和执行的最小单位。一个进程至少包含一个主线程,同一个进程内的所有线程,共享该进程的内存空间、堆内存、静态资源,只有栈内存是线程独立的。

简单理解:进程是一个工厂,线程是工厂里干活的工人,工厂的资源所有工人共享,每个工人有自己独立的工作台。

1.3 什么是多线程?

多线程,就是在同一个进程内,同时运行多个线程,并发执行多个任务。

我们日常写的Java程序,默认只有一个main主线程,所有代码串行执行;引入多线程后,可以让多个任务同时执行,互不阻塞。

1.4 为什么要用多线程?核心作用

  1. 提高CPU利用率,提升程序执行效率:单核CPU下,线程可以利用IO阻塞的空闲时间执行其他任务;多核CPU下,多线程可以真正实现并行执行,充分利用多核性能。

  2. 避免阻塞主线程:耗时操作(网络请求、文件读写、数据库操作)放在子线程执行,不会卡住主线程,保证程序响应流畅。

  3. 实现异步处理:日志记录、消息推送、数据统计等非核心业务,可异步执行,不用等待主线程业务完成。

重要注意事项:多线程不是线程越多越好,线程过多会导致频繁的线程上下文切换,反而降低程序性能;同时多线程会带来线程安全问题,必须合理控制线程数量。

二、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种状态详解

  1. 新建状态(NEW):创建了Thread对象,还没有调用start()方法,线程未启动。

  2. 就绪状态(RUNNABLE):调用了start()方法,线程已经被操作系统创建完成,等待CPU分配时间片执行。

  3. 运行状态(RUNNING):CPU分配时间片,线程开始执行run()方法中的代码。

  4. 阻塞状态(BLOCKED):线程等待锁资源,进入阻塞状态,拿到锁之后会回到就绪状态。

  5. 等待状态(WAITING):线程主动调用wait()、join()、LockSupport.park()方法,进入无限等待,需要其他线程唤醒,才能回到就绪状态。

  6. 超时等待状态(TIMED_WAITING):线程调用sleep(毫秒)、wait(毫秒)、join(毫秒),进入限时等待,时间到了自动唤醒,回到就绪状态。

  7. 终止状态(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点:

  1. 进程与线程的区别,多线程的核心作用;

  2. 4种创建线程的方式,重点掌握Runnable、Callable、线程池三种写法;

  3. 线程6种生命周期状态,以及状态切换逻辑;

  4. sleep、join、yield、守护线程等基础方法的用法与注意事项。

基础用法完全掌握后,就可以进入第二篇,学习多线程最核心的难点:线程安全、锁机制、线程通信