Java多线程:核心技术与实战指南

目录

🚀前言

大家好!我是 EnigmaCoder

  • 在Java编程中,"多线程"是一个高频出现的概念,也是处理并发任务的核心技术。如果你想理解程序如何"同时"处理多个任务(比如一边下载文件一边刷新界面),那多线程就是绕不开的知识点。今天我们就从多线程的定义、创建线程、线程安全与同步、线程池等多个维度,聊聊Java中的多线程。

🤔什么是多线程?

定义 :多线程(Multithreading)是指在一个程序(进程)中,同时运行多个独立的执行单元(线程),这些线程共享程序的内存资源(如变量、方法),但拥有各自的执行路径。

  • 简单说,线程是进程(一个正在运行的程序,比如你的Java程序、浏览器)中的"小任务"。一个进程至少有一个线程(称为"主线程"),而多线程就是给一个进程"拆分"出多个并行的小任务,让它们协同完成工作。
  • 举个生活例子:进程像一家餐厅,主线程是餐厅的"基础运营"(开门、开灯);多线程就像餐厅里同时工作的服务员、厨师、收银员------他们共享餐厅的资源(食材、餐具),但各自执行不同的任务,最终共同完成"服务顾客"的目标。

要理解多线程的价值,先得搞清楚它和"多进程"的区别

  • 多进程:多个独立的程序同时运行(比如同时开着微信和浏览器),进程间内存不共享,通信成本高(需要通过网络或文件等方式)。
  • 多线程:同一个程序内的多个任务,共享内存(变量、对象等),通信成本低,且创建/切换线程的资源消耗远低于进程。

优点

  1. 避免阻塞,提升用户体验

    比如一个Java桌面程序,如果用单线程(只有主线程),当执行一个耗时操作(如下载大文件)时,主线程会被"卡住",界面会变成"无响应"状态。而多线程可以把下载任务交给"子线程",主线程继续处理界面刷新,用户完全感知不到卡顿。

  2. 利用多核CPU,提高效率

    现代CPU都是多核的,单线程只能用一个核心,多线程可以让不同线程跑在不同核心上,真正实现"并行计算"。比如处理大量数据时,多线程拆分任务后,效率可能提升数倍。

  3. 简化复杂任务的拆分

    有些任务天然适合拆分(比如同时处理100个用户的请求),多线程可以让每个请求对应一个线程,逻辑更清晰,无需手动协调任务顺序。

缺点

  • 线程安全问题:多个线程共享资源时,可能出现"抢资源"的情况。比如两个线程同时修改一个变量,可能导致结果错乱(专业称"竞态条件")。
  • 复杂度提升:需要处理线程间的协调(如等待、唤醒),调试难度也更高(线程执行顺序不确定)。

💻创建线程

💯创建方法一:继承Thread类

实现步骤

  1. 创建自定义线程类:继承java.lang.Thread类,并重写run()方法
  2. 实例化线程:创建该自定义线程类的对象
  3. 启动线程:调用线程对象的start()方法

代码示例

java 复制代码
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 {
    @Override
    public void run (){
        for(int i=0;i<5;i++){
            System.out.println("子线程输出:"+i);
        }
    }
}

注意事项

  • 调用start()方法后,JVM会自动执行run()方法中的逻辑
  • 只有调用start()方法才是启动一个新的线程执行,直接调用run()方法将不会创建新线程,此时相当于还是单线程执行。
  • 不要把主线程任务放在子线程之前,否则一定是主线程先跑完再跑完子线程。

优缺点:

  • 优点:编码简单。
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

💯创建方法二:实现Runnable接口

实现步骤

  1. 创建自定义线程类MyRunnable,实现Runnable接口并重写其run()方法
  2. 实例化MyRunnable任务对象
  3. 将任务对象作为参数传递给Thread类构造函数
  4. 调用Thread实例的start()方法启动新线程

代码示例

java 复制代码
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 {
    @Override
    public void run (){
        for(int i=0;i<5;i++){
            System.out.println("子线程输出:"+i);
        }
    }
}

优缺点:

  • 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。

💯创建方法三:实现Callable接口

实现步骤

  1. 创建任务对象:通过定义一个实现Callable接口的类,重写其call方法来封装业务逻辑和返回数据。然后将Callable对象包装成FutureTask线程任务对象。

  2. 提交任务:将创建的FutureTask对象传递给Thread对象进行执行。

  3. 启动线程:调用Thread对象的start方法启动线程执行任务。

  4. 获取结果:待线程执行完成后,通过调用FutureTaskget方法获取任务执行结果。

代码示例

java 复制代码
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的常用方法

  1. 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() 当前线程等待调用者执行完毕(如主线程等子线程,需处理中断异常)
  1. Thread 常见构造器
构造器签名 说明
public Thread(String name) 直接创建线程并指定名称 (适合继承 Thread 类的场景)
public Thread(Runnable target) 封装 Runnable 对象为线程(解耦任务与线程,推荐使用)
public Thread(Runnable target, String name) 封装 Runnable 对象并指定线程名称(灵活场景)
  1. 综合示例:Thread 方法+构造器全场景演示
java 复制代码
// 1. 定义 Runnable 任务(解耦线程逻辑)
class MyRunnable implements Runnable {
    @Override
    public 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); // 调用父类构造器设置线程名称
    }
    
    @Override
    public 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()); // 默认:main
        mainThread.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(); // 在主线程中执行,不会开启新线程
    }
}
  1. 示例运行效果 :
  • 线程启动
    • thread1(子类线程)、thread2(默认名)、thread3(命名任务线程)通过 start() 启动,各自开启新线程执行 run()
  • 方法调用
    • getName()/setName():主线程名称从 main 改为 自定义主线程
    • join():主线程等待 thread1 完成后再继续。
    • sleep():主线程休眠 3 秒,模拟耗时操作。
  • 关键对比
    • start() 启动新线程,run() 直接调用仅为普通方法(无新线程)。
  1. 小结
场景 正确用法 常见误区
启动线程 thread.start() 直接调用 thread.run()(无新线程)
定义线程逻辑 实现 Runnable(解耦优先) 过度使用继承 Thread(单继承限制)
线程命名 构造器指定或 setName() 依赖默认名称(不利于调试)
线程等待 thread.join() 忽略中断异常处理

⚙️线程安全与线程同步

上面我们聊了多线程的基础,知道它能让程序"一心多用"。但如果多个线程同时抢着用同一个资源,就可能出乱子------这就是"线程安全"问题。而"线程同步"就是给多线程定规矩,让它们有序访问资源。下面咱们用生活化的例子,聊聊这两个概念和三种同步方式。

💯先搞懂:什么是线程安全?(附比喻)

线程安全:多个线程同时操作共享资源时,无论线程执行顺序如何,最终结果都和"单线程执行"的结果一致,就叫线程安全。反之,结果错乱就是"线程不安全"。

举个最直观的例子

假设你和3个朋友(4个线程)一起抢10张演唱会门票(共享资源),每个人都在同时喊"我要1张"。如果没有规则,可能出现:

  • 最后统计时,明明只有10张票,却被抢走了11张(超卖);
  • 或者有人喊了"要1张",但票没减少(漏卖)。

这就是典型的"线程不安全"------共享资源被多线程乱抢,结果错乱。

为什么会这样?

因为线程操作资源的过程(比如"判断剩余票数→减少1张")不是"一步完成"的,而是分成几步(读、改、写)。多个线程可能在"读"之后、"写"之前插队,导致数据错乱。

💯线程同步:给多线程定"排队规则"

线程同步的核心是:让多个线程"有序访问"共享资源,避免同时操作。就像给抢票的人定规则:"每次只能一个人去查票、买票,其他人排队等"。

同步的本质是"加锁":把共享资源的操作过程"锁住",一个线程操作时,其他线程必须等它完成并"解锁"后才能继续。

接下来讲三种最常用的同步方式

🎯方式1:同步代码块(synchronized块)

定义 :用synchronized(锁对象)包裹需要同步的代码,只有拿到"锁对象"的线程才能执行块内代码,执行完自动释放锁。

java 复制代码
synchronized(同步锁){
    访问共享资源的核心代码
}

比喻

就像食堂打饭,勺子(锁对象)只有一个。大家想打饭(执行代码块),必须先拿到勺子(获得锁),打完饭(执行完)把勺子放回(释放锁),下一个人才能拿。

代码示例:解决抢票问题

java 复制代码
public class Ticket implements Runnable {
    private int ticketCount = 10; // 共享的10张票
    private Object lock = new Object(); // 锁对象(任意对象都可)

    @Override
    public 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)。
java 复制代码
修饰符 synchronized 返回值类型 方法名称(形参列表){
      操作共享资源的代码
}

比喻

就像公共电话亭,电话亭(同步方法)本身就是"锁"。一个人进去打电话(执行方法),会把门反锁(获得锁),打完电话出来(方法结束)才开锁,下一个人才能进。

代码示例:用同步方法解决抢票问题

java 复制代码
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));
        }
    }

    @Override
    public 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解决抢票问题

java 复制代码
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();

    @Override
    public 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,互相卡死)。

所以,同步不是"越多越好",而是"按需使用":只给真正需要同步的代码加锁,平衡安全性和效率。

🌟线程池

在多线程编程里,线程池 是提升效率的关键武器。它像一家"劳务公司"------提前养一批线程(工人)待命,任务(活)来了直接分配,避免频繁招人(创建线程)、裁人(销毁线程)的资源浪费。

💯认识线程池

  1. 生活场景类比

假设你开了家餐厅:

  • 不用线程池:顾客点餐时现招服务员(创建线程),点完餐辞退(销毁线程)。高峰期频繁招人→效率低、资源浪费。
  • 用线程池:提前培训3个固定服务员(核心线程),再备2个临时工(最大线程扩展),顾客排队(任务队列)。任务来了直接分配,用完回池待命→效率翻倍!
  1. 技术核心价值

线程池通过 "线程复用、队列缓冲、拒绝策略" 解决三大问题:

问题 线程池如何解决?
线程创建销毁开销大 提前创建线程,复用现有线程
线程数失控(OOM) 限制最大线程数,任务排队缓冲
任务突发无预案 配置拒绝策略(任务爆仓时如何处理)

💯创建线程池

线程池的"灵魂类"是 ThreadPoolExecutor,像给"劳务公司"定规则:招多少固定工人、最多扩多少临时工、任务咋排队...

  1. 构造参数解析(类比餐厅管理)

创建线程池时,需设置7个核心参数,每个参数对应餐厅运营规则:

参数名 作用(技术解释) 餐厅类比
corePoolSize 核心线程数(一直保留的线程) 固定员工数(3个长期服务员)
maximumPoolSize 最大线程数(核心+临时工总数) 最多雇5人(3固定+2临时)
keepAliveTime 临时工空闲超时时间 临时工没事做→30秒后辞退
TimeUnit 时间单位(秒/分等) 时间标准(秒)
workQueue 任务队列(存放待处理任务) 顾客排队区(最多排3人)
threadFactory 线程工厂(如何创建线程) 招聘流程(统一培训工人)
RejectedExecutionHandler 拒绝策略(任务满时如何处理新任务) 排队满了→拒绝新顾客
  1. 任务拒绝策略:四种应急预案

当线程池忙到极限(核心线程+临时工都在干活,队列也排满)时,新任务怎么处理?这就需要 拒绝策略------相当于"餐厅排队满了,如何应对新顾客"。

策略类 说明(餐厅类比) 代码示例效果
AbortPolicy(默认) 直接拒绝,抛 RejectedExecutionException 异常 新顾客被拒,餐厅抛"无法接待"异常
DiscardPolicy 默默丢弃任务,不抛异常 新顾客被无视,餐厅继续忙
DiscardOldestPolicy 丢弃队列中最久的任务,把新任务加入队列 赶走最早排队的顾客,让新顾客进队
CallerRunsPolicy 让提交任务的线程(主线程)自己执行任务 老板亲自帮新顾客点餐(主线程执行)
  1. 代码示例:拒绝策略实战(处理Runnable任务)

模拟"餐厅忙到爆"场景:核心3线程、队列3任务、最多5线程→当提交第9个任务时触发拒绝策略。

java 复制代码
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();
    }
}
  1. 任务执行流程
    提交任务时,线程池按以下逻辑处理(对应餐厅流程):
  • 核心线程先干活:3个核心线程空闲→直接分配任务。
  • 核心忙→任务排队:核心线程占满→任务进队列(最多3个)。
  • 队列满→招临时工:队列排满→创建临时线程(最多扩到5个)。
  • 全忙+队列满→触发拒绝策略:临时工也占满→按配置的策略拒绝新任务。

💯处理Runnable任务

Runnable 是"无返回值任务"的代表,用 execute() 方法提交给线程池。

  1. 代码示例:提交Runnable任务
java 复制代码
// 定义一个Runnable任务(像"点餐任务")
class OrderTask implements Runnable {
    private int taskId;
    public OrderTask(int taskId) { this.taskId = taskId; }

    @Override
    public 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();
}
  1. 方法总结
方法 作用 适用任务类型
execute(Runnable) 提交无返回值任务 Runnable

💯处理Callable任务

Callable 是"有返回值任务"的代表,用 submit() 提交,通过 Future 获取结果。

  1. 代码示例:计算1~100的和(带返回值)
java 复制代码
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();
    }
}
  1. 方法总结
方法 作用 适用任务类型
submit(Callable<T>) 提交有返回值 任务,返回 Future<T> Callable
Future.get() 获取任务结果(阻塞等待或超时等待) -

💯通过Executors创建线程池

Executors 是线程池"工具类",像"快捷模板",但不适合生产环境(阿里开发手册强制禁用!)。

  1. 常用快捷方法
方法名 特点(技术解释) 餐厅类比 潜在风险
newFixedThreadPool(3) 固定3个核心线程,队列无界 固定3个服务员,排队不限 任务堆积→内存溢出(OOM)
newSingleThreadExecutor() 单线程,队列无界 只有1个服务员,排队不限 同上(队列无界)
newCachedThreadPool() 线程数弹性扩容(最多 Integer.MAX_VALUE 无限招临时工 线程数爆炸→OOM
  1. 代码示例:FixedThreadPool
java 复制代码
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();
    }
}
  1. 为什么"不推荐"?
    阿里巴巴Java开发手册 强制要求:

线程池不允许用 Executors 创建,必须用 ThreadPoolExecutor

原因:

  • FixedThreadPool/SingleThreadExecutor:队列是 Integer.MAX_VALUE(无限排队),任务堆积会撑爆内存(OOM)。
  • CachedThreadPool:线程数上限是 Integer.MAX_VALUE(无限招人),线程太多也会OOM。

💯线程数配置公式

线程池的核心/最大线程数配置,必须根据任务类型调整,否则会严重影响效率。先理解两种任务类型:

  1. 任务类型定义
类型 特点(餐厅类比) 示例任务
CPU密集型 任务疯狂"占用CPU"(如计算、加密),线程几乎不空闲 复杂数学运算、图片压缩
IO密集型 任务大部分时间"等IO"(如读写文件、网络请求),CPU空闲 数据库查询、文件上传、接口调用
  1. 配置公式(基于CPU核心数 N

线程池的核心思路:让CPU尽可能不空闲,同时避免线程过多导致切换开销

任务类型 配置公式 原理说明
CPU密集型 核心线程数 = N + 1 避免线程等待时CPU完全空闲,+1应对偶尔阻塞
IO密集型 核心线程数 = N * 2(或 N * 5 等) 利用IO等待的空闲时间,多线程并行处理任务
  1. 代码示例:根据任务类型配置线程池
java 复制代码
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 + 1
        ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
            cpuCore + 1, 
            cpuCore + 1, // 最大线程数同核心(CPU没空,无需临时工)
            30, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
        );

        // 2. IO密集型任务:核心数 = cpuCore * 2
        ThreadPoolExecutor 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) {}
    }
}
  1. 小结:线程池配置实战指南
场景 任务类型 核心线程数公式 拒绝策略建议 典型案例
计算密集(如加密) CPU密集型 CPU核心数 + 1 AbortPolicy(抛异常提醒) 大数据排序、视频编码
网络/IO 密集(如接口调用) IO密集型 CPU核心数 * 2 CallerRunsPolicy(降级执行) 微服务调用、文件上传

✍️并发与并行

在多线程的世界里,并发并行是经常被混淆的概念,但它们的本质区别直接影响程序的效率。

1.定义:宏观与微观的区别

简单说,两者的核心差异在于 "任务是否真正同时执行",用表格对比更清晰:

概念 技术解释(CPU视角) 生活化比喻
并发 多个任务交替执行(单核CPU也能实现) 餐厅1个服务员交替招呼3桌客人
并行 多个任务同时执行(需要多核CPU支持) 餐厅3个服务员同时招呼3桌客人
  1. 直观示例:处理任务的两种方式

假设你需要完成3件事:

  • 任务A:煮咖啡(需5分钟,大部分时间等水开,CPU空闲)
  • 任务B:煎鸡蛋(需3分钟,全程占用CPU,不能停)
  • 任务C:烤面包(需4分钟,全程占用CPU,不能停)

🌰 并发执行(单核CPU场景)

1个服务员处理3桌客人:

  1. 启动任务A(放水煮咖啡)→ 水开前CPU空闲,切换到任务B煎鸡蛋(3分钟全程占用CPU)→
  2. 任务B完成后,切换到任务C烤面包(4分钟全程占用CPU)→
  3. 最后回到任务A,咖啡煮好收尾。

特点

  • 宏观上:3个任务"同时进行"(你感觉在同步推进);
  • 微观上:CPU核心在任务间快速交替切换,实际同一时间只做一件事。

🌰 并行执行(多核CPU场景)

3个服务员同时开工:

  • 服务员1负责任务A(煮咖啡,等水开时CPU自动空闲)→
  • 服务员2负责任务B(煎鸡蛋,全程占用1个CPU核心)→
  • 服务员3负责任务C(烤面包,全程占用另1个CPU核心)。

特点

  • 宏观+微观上:3个任务真正同时执行,效率翻倍(前提是CPU有多个核心)。
  1. Java多线程中的体现
  • 单核CPU :无论创建多少线程,线程池里的任务都只能并发执行(线程交替占用唯一的CPU核心)。
  • 多核CPU :线程池中的多个线程可以并行执行 (不同核心同时跑任务),剩余线程继续并发交替(充分利用多核+等待时间)。
  1. 小结

理解这两个概念,才能设计出高效的多线程方案:

维度 并发(Concurrency) 并行(Parallelism)
执行本质 交替执行(宏观同时,微观交替) 同时执行(宏观+微观都同时)
CPU依赖 单核即可实现 必须多核支持
典型场景 IO密集型任务(利用等待时间) CPU密集型任务(榨干多核性能)

实战逻辑

  • 处理网络请求、文件读写等IO密集型任务 → 用并发让线程交替利用等待时间(线程池配多线程,哪怕单核也能高效);
  • 处理加密计算、大数据排序等CPU密集型任务 → 用并行让多核同时开工(线程池核心数贴近CPU核心数,避免切换开销)。

这就是线程池配置要区分任务类型的底层逻辑------并发与并行的协同,才是多线程的最大价值

相关推荐
胚芽鞘68141 分钟前
关于java项目中maven的理解
java·数据库·maven
nbsaas-boot2 小时前
Java 正则表达式白皮书:语法详解、工程实践与常用表达式库
开发语言·python·mysql
岁忧2 小时前
(LeetCode 面试经典 150 题 ) 11. 盛最多水的容器 (贪心+双指针)
java·c++·算法·leetcode·面试·go
chao_7892 小时前
二分查找篇——搜索旋转排序数组【LeetCode】两次二分查找
开发语言·数据结构·python·算法·leetcode
CJi0NG2 小时前
【自用】JavaSE--算法、正则表达式、异常
java
风无雨2 小时前
GO 启动 简单服务
开发语言·后端·golang
Hellyc2 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
斯普信专业组2 小时前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
今天又在摸鱼3 小时前
Maven
java·maven
老马啸西风3 小时前
maven 发布到中央仓库常用脚本-02
java·maven