Java多线程开发实战:解锁线程安全与性能优化的关键技术

Java多线程开发实战:解锁线程安全与性能优化的关键技术

在现代Java开发中,多线程是提升程序性能、优化资源利用率的核心技术之一。无论是高并发的电商系统、实时通信应用,还是后台数据处理服务,都离不开多线程的支持。本文将从线程基础概念出发,逐步深入线程创建、常用方法、线程安全、线程池等核心技术,并通过实战案例帮助大家掌握多线程的实际应用。

一、线程与多线程基础认知

1. 什么是线程?

线程(Thread)是程序内部的一条执行流程,一个程序若只有一条执行流程,则为单线程程序。例如,简单的控制台输出程序通常是单线程执行的。

2. 多线程的定义与应用场景

多线程是指从软硬件层面实现多条执行流程的技术,多条线程由CPU调度执行。其核心价值在于提高程序执行效率,让多个任务并行处理。

典型应用场景

  • 12306购票系统:同时处理成千上万用户的查询、购票请求
  • 百度网盘上传下载:后台传输文件的同时,前台可进行其他操作
  • 电商平台:订单处理、库存更新、消息推送等任务并行执行

3. 并发与并行的区别

  • 并发:CPU轮询调度多个线程,由于切换速度极快,给人"同时执行"的错觉(实际同一时刻只有一个线程在执行)
  • 并行:同一时刻多个线程被CPU同时调度执行(需多核CPU支持)

二、Java多线程的三种创建方式

Java提供了三种主流的线程创建方式,各有优劣,适用于不同场景。

1. 继承Thread类

这是最基础的创建方式,步骤简单直接。

实现步骤

  1. 定义子类继承java.lang.Thread
  2. 重写run()方法,编写线程任务逻辑
  3. 创建子类对象,调用start()方法启动线程

代码示例

csharp 复制代码
public class ThreadDemo1 {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t1 = new MyThread();
        // 启动线程(必须调用start(),而非直接调用run())
        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);
        }
    }
}

优缺点

  • 优点:编码简单,可直接使用Thread类的方法
  • 缺点:Java单继承限制,线程类无法继承其他类,扩展性差

2. 实现Runnable接口

推荐使用的创建方式,规避了单继承限制,扩展性更强。

实现步骤

  1. 定义任务类实现Runnable接口
  2. 重写run()方法,编写任务逻辑
  3. 创建任务对象,交给Thread处理
  4. 调用start()方法启动线程

代码示例(含匿名内部类简化写法):

csharp 复制代码
public class ThreadDemo2_2 {
    public static void main(String[] args) {
        // 方式1:常规写法
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(r).start();

        // 方式2:匿名内部类简化
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

        // 方式3:Lambda表达式(JDK8+)
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程3输出:" + i);
            }
        }).start();

        // 主线程任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}

优缺点

  • 优点:任务类可继承其他类、实现其他接口,扩展性强
  • 缺点:无法直接返回线程执行结果

3. 实现Callable接口(JDK5+)

解决了前两种方式无法返回执行结果的问题,适用于需要获取线程执行结果的场景。

实现步骤

  1. 定义任务类实现Callable接口,指定返回值类型
  2. 重写call()方法(可抛出异常),编写任务逻辑并返回结果
  3. 将Callable对象封装为FutureTask(兼具Runnable和结果获取功能)
  4. 交给Thread处理并启动线程
  5. 通过FutureTask.get()方法获取执行结果

代码示例

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    public static void main(String[] args) throws Exception {
        // 创建Callable任务对象
        Callable<String> c1 = new MyCallable(100);
        Callable<String> c2 = new MyCallable(50);

        // 封装为FutureTask
        FutureTask<String> f1 = new FutureTask<>(c1);
        FutureTask<String> f2 = new FutureTask<>(c2);

        // 启动线程
        new Thread(f1).start();
        new Thread(f2).start();

        // 获取执行结果(主线程会阻塞直到结果返回)
        System.out.println(f1.get());
        System.out.println(f2.get());
    }
}

// 自定义Callable任务类
class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "1-" + n + "的和是:" + sum;
    }
}

优缺点

  • 优点:扩展性强,可获取线程执行结果
  • 缺点:编码相对复杂,获取结果时可能阻塞

三、线程常用核心方法

方法名 功能说明
start() 启动线程,JVM调用run()方法
run() 线程任务逻辑所在,不可直接调用
getName()/setName() 获取/设置线程名称
currentThread() 获取当前执行的线程对象
sleep(long millis) 让当前线程休眠指定毫秒数
join() 让调用线程先执行完毕,其他线程再继续

关键方法示例

  1. 线程休眠(sleep)
csharp 复制代码
public class ThreadApiDemo2 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程输出:" + i);
            try {
                // 休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 线程插队(join)
csharp 复制代码
public class ThreadApiDemo3 {
    public static void main(String[] args) throws InterruptedException {
        MyThread2 t1 = new MyThread2();
        t1.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程输出:" + i);
            if (i == 3) {
                // 让t1线程插队,执行完毕后主线程再继续
                t1.join();
            }
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}

四、线程安全问题与解决方案

1. 线程安全问题的产生条件

当满足以下三个条件时,会出现线程安全问题:

  • 多个线程同时执行
  • 线程共享同一个资源
  • 线程对共享资源进行修改操作

模拟线程安全问题(银行取款案例):

typescript 复制代码
// 账户类(共享资源)
class Account {
    private String cardId;
    private double money;

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        if (this.money >= money) {
            System.out.println(name + "取钱成功,吐出" + money + "元");
            this.money -= money;
            System.out.println("余额:" + this.money + "元");
        } else {
            System.out.println(name + "取钱失败,余额不足");
        }
    }
}

// 取款线程
class DrawThread extends Thread {
    private Account account;

    public DrawThread(String name, Account account) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        account.drawMoney(100000);
    }
}

// 测试类
public class ThreadDemo1 {
    public static void main(String[] args) {
        // 同一个账户,初始余额10万
        Account account = new Account("ICBC-110", 100000);
        // 两个线程同时取款10万
        new DrawThread("小明", account).start();
        new DrawThread("小红", account).start();
    }
}

问题现象:可能出现小明和小红同时取款成功,余额变为负数的情况。

2. 线程同步解决方案

核心思想:让多个线程先后依次访问共享资源,避免并发修改。

(1)同步代码块

将访问共享资源的核心代码上锁,每次只允许一个线程进入执行。

语法

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

优化后的账户类

kotlin 复制代码
class Account {
    private String cardId;
    private double money;

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        // 锁对象:推荐使用共享资源本身(this)
        synchronized (this) {
            if (this.money >= money) {
                System.out.println(name + "取钱成功,吐出" + money + "元");
                this.money -= money;
                System.out.println("余额:" + this.money + "元");
            } else {
                System.out.println(name + "取钱失败,余额不足");
            }
        }
    }
}
(2)同步方法

将整个方法上锁,简化同步代码块的写法。

语法

arduino 复制代码
修饰符 synchronized 返回值类型 方法名(参数列表) {
    // 操作共享资源的代码
}

优化后的取款方法

csharp 复制代码
public synchronized void drawMoney(double money) {
    String name = Thread.currentThread().getName();
    if (this.money >= money) {
        System.out.println(name + "取钱成功,吐出" + money + "元");
        this.money -= money;
        System.out.println("余额:" + this.money + "元");
    } else {
        System.out.println(name + "取钱失败,余额不足");
    }
}

底层原理

  • 实例方法:默认使用this作为锁对象
  • 静态方法:默认使用类名.class作为锁对象
(3)Lock锁(JDK5+)

java.util.concurrent.locks.Lock接口提供了更灵活的锁定操作,推荐使用其实现类ReentrantLock

核心方法

  • lock():获取锁
  • unlock():释放锁(建议在finally中执行,确保锁一定释放)

代码示例

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Account {
    private String cardId;
    private double money;
    // 创建Lock锁对象
    private final Lock lock = new ReentrantLock();

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        lock.lock(); // 加锁
        try {
            if (this.money >= money) {
                System.out.println(name + "取钱成功,吐出" + money + "元");
                this.money -= money;
                System.out.println("余额:" + this.money + "元");
            } else {
                System.out.println(name + "取钱失败,余额不足");
            }
        } finally {
            lock.unlock(); // 释放锁(无论是否异常,都必须释放)
        }
    }
}

五、线程池技术(JDK5+)

1. 线程池的核心价值

  • 避免频繁创建和销毁线程的开销(线程创建成本高)
  • 控制线程数量,防止线程过多导致系统资源耗尽
  • 复用线程,提高程序响应速度

2. 线程池的创建方式

(1)通过ThreadPoolExecutor手动创建(推荐)

ThreadPoolExecutor是线程池的核心实现类,可灵活配置参数。

构造器参数说明

arduino 复制代码
public ThreadPoolExecutor(
    int corePoolSize, // 核心线程数(常驻线程)
    int maximumPoolSize, // 最大线程数(核心+临时线程)
    long keepAliveTime, // 临时线程空闲时间
    TimeUnit unit, // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory, // 线程工厂(创建线程)
    RejectedExecutionHandler handler // 任务拒绝策略
)

任务拒绝策略

策略 说明
AbortPolicy 丢弃任务并抛出异常(默认)
DiscardPolicy 丢弃任务,不抛出异常
DiscardOldestPolicy 丢弃队列中最久的任务,加入新任务
CallerRunsPolicy 由提交任务的线程(如主线程)直接执行

代码示例

java 复制代码
import java.util.concurrent.*;

public class ExecutorServiceDemo1 {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(
            3, // 核心线程数3
            5, // 最大线程数5
            10, // 临时线程空闲10秒后销毁
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(3), // 任务队列容量3
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
        );

        // 提交任务(Runnable)
        Runnable task = new MyRunnable();
        for (int i = 0; i < 9; i++) {
            pool.execute(task);
        }

        // 关闭线程池(一般不主动关闭,除非程序结束)
        // pool.shutdown(); // 等待所有任务执行完毕后关闭
    }
}
(2)通过Executors工具类创建(不推荐在大型系统中使用)

Executors提供了简化的线程池创建方法,但存在资源耗尽风险:

  • newFixedThreadPool(int n):固定线程数的线程池
  • newSingleThreadExecutor():单线程线程池
  • newCachedThreadPool():缓存线程池(线程数可无限增长)
  • newScheduledThreadPool(int corePoolSize):定时任务线程池

风险说明(阿里巴巴Java开发手册):

  • FixedThreadPoolSingleThreadExecutor:任务队列长度为Integer.MAX_VALUE,可能堆积大量任务导致OOM
  • CachedThreadPoolScheduledThreadPool:最大线程数为Integer.MAX_VALUE,可能创建大量线程导致OOM

六、实战案例:红包雨游戏

需求说明

  • 100名员工(100个线程)抢200个红包
  • 小红包(1-30元)占80%(160个),大红包(31-100元)占20%(40个)
  • 输出抢红包过程,活动结束后按员工抢到的总金额降序排序

核心思路

  1. 生成符合要求的红包集合(线程共享资源)
  2. 100个线程并发抢红包,通过同步机制保证线程安全
  3. 统计每个员工抢到的总金额,排序后展示

完整代码

csharp 复制代码
import java.util.*;

// 红包抢夺线程
class PeopleGetRedPacket extends Thread {
    private List<Integer> redPackets;
    private static Map<String, Integer> totalMoneyMap = new HashMap<>(); // 统计总金额

    public PeopleGetRedPacket(List<Integer> redPackets, String name) {
        super(name);
        this.redPackets = redPackets;
        totalMoneyMap.put(name, 0); // 初始化总金额为0
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (true) {
            synchronized (redPackets) {
                if (redPackets.isEmpty()) {
                    break;
                }
                // 随机抢夺一个红包
                int index = new Random().nextInt(redPackets.size());
                Integer money = redPackets.remove(index);
                System.out.println(name + "抢到红包:" + money + "元");
                // 更新总金额
                totalMoneyMap.put(name, totalMoneyMap.get(name) + money);
                if (redPackets.isEmpty()) {
                    System.out.println("\n红包雨结束!");
                    // 排序并展示结果
                    showResult();
                    break;
                }
            }
        }
    }

    // 展示抢红包结果(按总金额降序)
    private void showResult() {
        System.out.println("=== 抢红包结果排名 ===");
        // 将map转换为list并排序
        List<Map.Entry<String, Integer>> list = new ArrayList<>(totalMoneyMap.entrySet());
        list.sort((o1, o2) -> o2.getValue() - o1.getValue());

        // 输出排名
        for (int i = 0; i < list.size(); i++) {
            Map.Entry<String, Integer> entry = list.get(i);
            System.out.println((i + 1) + "、" + entry.getKey() + " 总金额:" + entry.getValue() + "元");
        }
    }
}

// 测试类
public class ThreadTest {
    public static void main(String[] args) {
        // 生成200个红包
        List<Integer> redPackets = getRedPackets();
        // 创建100个员工线程抢红包
        for (int i = 1; i <= 100; i++) {
            new PeopleGetRedPacket(redPackets, "员工" + i).start();
        }
    }

    // 生成红包:160个小红包,40个大红包
    private static List<Integer> getRedPackets() {
        List<Integer> redPackets = new ArrayList<>();
        Random random = new Random();
        // 小红包(1-30元)
        for (int i = 0; i < 160; i++) {
            redPackets.add(random.nextInt(30) + 1);
        }
        // 大红包(31-100元)
        for (int i = 0; i < 40; i++) {
            redPackets.add(random.nextInt(70) + 31);
        }
        return redPackets;
    }
}

七、总结

Java多线程技术是一把"双刃剑":合理使用能大幅提升程序性能,但如果处理不当(如线程安全问题、资源耗尽),会导致程序异常甚至崩溃。

核心要点回顾

  1. 线程创建优先选择RunnableCallable接口,规避单继承限制
  2. 线程安全问题需通过同步代码块、同步方法或Lock锁解决
  3. 线程池推荐使用ThreadPoolExecutor手动创建,明确配置参数
  4. 高并发场景需注意资源控制,避免OOM等风险

掌握多线程技术需要不断实践,在实际开发中需根据业务场景选择合适的技术方案,平衡性能与安全性。

相关推荐
12344522 小时前
【MCP入门篇】从0到1教你搭建MCP服务
后端·mcp
HuangYongbiao2 小时前
NestJS 架构设计系列:应用服务与领域服务的区别
后端·架构
技术不打烊2 小时前
MySQL主从延迟飙升?元数据锁可能是“真凶”
后端
Java天梯之路2 小时前
Spring Boot 钩子全集实战(三):`EnvironmentPostProcessor` 详解
java·spring
無量2 小时前
MySQL架构原理与执行流程
后端·mysql
无敌最俊朗@2 小时前
STL-适配器(面试复习4)
java·面试·职场和发展
Han.miracle2 小时前
《Spring MVC 响应机制综合实践:页面、数据、JSON 与响应配置》
java·spring·springboot
JHC0000002 小时前
dy直播间评论保存插件
java·后端·python·spring cloud·信息可视化
SuperherRo2 小时前
JAVA攻防-FastJson专题&面试不出网利用&BCEL字节码&C3P0二次&Impl链&延时判断
java·fastjson·不出网