Java 多线程编程(一文讲透)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观

Java 多线程编程:从基础到实践

前言:为什么需要多线程编程?

在计算机科学领域,多线程编程如同交响乐团的指挥,能够协调多个“演奏者”(线程)同时工作,从而提升程序的整体效率。对于 Java 开发者而言,掌握多线程编程是迈向高性能应用开发的重要一步。无论是处理复杂的后台任务,还是优化用户界面的响应速度,多线程技术都能为开发者提供强大的工具支持。本文将从核心概念、实现方法到实际案例,系统性地解析 Java 多线程编程的关键知识点。


一、线程与进程:理解多线程的基础

1.1 进程与线程的区别

可以将进程想象为一家工厂,而线程则是工厂内的工人。工厂(进程)是资源分配的基本单位,包含内存、文件句柄等资源;工人(线程)则是在工厂内部执行具体任务的个体。线程共享进程的资源,因此创建线程的开销比进程小得多,这也是多线程能够提升效率的原因。

1.2 多线程的优势

  • 提高资源利用率:多个线程可以共享同一进程的内存空间,避免资源重复分配。
  • 提升程序响应性:例如在 GUI 应用中,主线程负责界面渲染,其他线程处理耗时操作,避免界面冻结。
  • 充分利用多核 CPU:现代处理器的多核特性使得并行执行线程成为可能。

二、Java 中创建线程的三种方法

2.1 方法一:继承 Thread 类

这是最直观的方式,通过继承 java.lang.Thread 类并重写 run() 方法来定义线程的行为。

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getId() + " 正在执行");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

2.2 方法二:实现 Runnable 接口

这种方式解决了 Java 单继承的限制,允许对象同时继承其他类并实现线程功能。

public class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("通过 Runnable 接口实现线程逻辑");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.start();
    }
}

2.3 方法三:Lambda 表达式(Java 8+)

利用 Lambda 表达式简化代码,体现函数式编程的简洁性。

public class LambdaExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Lambda 表达式创建线程");
        });
        thread.start();
    }
}

对比总结
| 方法 | 优点 | 缺点 | |---------------|-------------------------------|-------------------------------| | 继承 Thread | 直观易懂 | 单继承限制 | | 实现 Runnable | 解决单继承问题 | 需要额外 Thread 对象包装 | | Lambda 表达式 | 代码简洁,适合短任务 | 需要 Java 8 及以上版本 |


三、线程的生命周期与状态转换

Java 线程的生命周期包含以下状态:

  1. 新建(New):通过 new Thread() 创建线程对象,但尚未调用 start() 方法。
  2. 就绪(Runnable):调用 start() 后,线程进入就绪队列等待 CPU 时间片。
  3. 运行(Running):获得 CPU 资源并执行 run() 方法中的代码。
  4. 阻塞(Blocked):因等待 I/O 操作或锁资源而暂停。
  5. 等待(Waiting):调用 wait()join() 等方法进入等待状态。
  6. 终止(Terminated)run() 方法执行完毕或因异常结束。

状态转换图示

新建 → 就绪 → 运行 → [阻塞/等待] → 就绪 → 运行 → 终止

四、线程同步:解决并发问题的关键

4.1 为什么需要同步?

想象一个银行账户的场景:两个线程同时尝试从账户中扣除 100 元,若不进行同步,可能会出现“余额不足”的错误。这种因多个线程访问共享资源导致的不一致问题,称为竞态条件(Race Condition)。

4.2 同步工具与机制

4.2.1 synchronized 关键字

通过关键字 synchronized 可以对方法或代码块加锁,确保同一时间只有一个线程访问。

public class BankAccount {
    private double balance;
    
    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            balance -= amount;
        }
    }
}
4.2.2 ReentrantLock 类

java.util.concurrent.locks.ReentrantLock 提供更灵活的锁机制,支持尝试获取锁、超时等待等高级功能。

import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 确保锁被释放
        }
    }
}

4.3 线程通信:wait()、notify() 和 notifyAll()

当线程需要等待某个条件满足时,可以使用 wait() 暂停自身,并通过 notify()notifyAll() 唤醒其他线程。

生产者-消费者案例

class Buffer {
    private int value;
    private boolean available = false;

    public synchronized void put(int val) {
        while (available) {
            try { wait(); } catch (InterruptedException e) {}
        }
        this.value = val;
        available = true;
        notifyAll();
    }

    public synchronized int get() {
        while (!available) {
            try { wait(); } catch (InterruptedException e) {}
        }
        available = false;
        notifyAll();
        return value;
    }
}

五、死锁与线程安全设计

5.1 死锁的定义与特征

死锁如同两个人互相握着手试图挣脱,却陷入僵局。Java 程序中若出现以下四要素,即可能发生死锁:

  1. 互斥:资源不可共享。
  2. 占有且等待:线程持有资源同时等待其他资源。
  3. 不可抢占:资源只能由持有者主动释放。
  4. 循环等待:线程间形成环形等待链。

5.2 避免死锁的策略

  • 减少锁的粒度:缩短锁的持有时间。
  • 按顺序获取锁:预先定义锁的获取顺序。
  • 使用超时机制:通过 tryLock() 方法避免无限等待。

案例分析

// 错误示例:可能导致死锁
public class DeadlockExample {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 holds lock1");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("Thread 1 holds lock2");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 holds lock2");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("Thread 2 holds lock1");
                }
            }
        }).start();
    }
}

六、线程池:优化资源管理的利器

6.1 线程池的作用

线程池(ExecutorService)如同工厂的“工人调度系统”,通过复用线程减少创建和销毁的开销,同时控制并发线程数量。

6.2 常用线程池类型

类型特点
newFixedThreadPool(int nThreads)固定大小的线程池,适合任务量稳定的场景
newCachedThreadPool()线程数量动态调整,适合短生命周期任务
newSingleThreadExecutor()单线程池,确保任务顺序执行
newScheduledThreadPool(int corePoolSize)支持定时和周期性任务,如延迟执行或每秒执行一次

示例代码

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
            });
        }
        
        executor.shutdown();
    }
}

七、高级主题:volatile 关键字与原子类

7.1 volatile 的作用

volatile 关键字确保变量的修改对所有线程可见,避免 Java 虚拟机(JVM)的指令重排序优化。但它不保证操作的原子性,例如 count++ 需要配合 synchronized 或原子类。

7.2 原子类(Atomic 类)

java.util.concurrent.atomic 包中的类(如 AtomicInteger)提供了无锁的原子操作,性能优于 synchronized

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子性操作
    }
}

八、实战案例:模拟多线程下载器

8.1 需求分析

实现一个支持分块下载的多线程下载器,利用多个线程并行下载文件片段,最后合并结果。

8.2 实现步骤

  1. 分割文件为多个块。
  2. 每个线程负责下载一个块。
  3. 使用线程池管理下载任务。
  4. 合并下载完成的块。

代码示例

import java.io.*;
import java.net.URL;
import java.util.concurrent.*;

public class MultiThreadDownloader {
    private static final int THREAD_POOL_SIZE = 4;
    private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    private static final int CHUNK_SIZE = 1024 * 1024; // 1MB 分块

    public static void download(String url, String outputFile) throws Exception {
        URL website = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) website.openConnection();
        int fileSize = connection.getContentLength();
        int chunkCount = (fileSize + CHUNK_SIZE - 1) / CHUNK_SIZE;

        RandomAccessFile file = new RandomAccessFile(outputFile, "rw");
        file.setLength(fileSize);

        CountDownLatch latch = new CountDownLatch(chunkCount);
        
        for (int i = 0; i < chunkCount; i++) {
            int startPos = i * CHUNK_SIZE;
            int endPos = Math.min(startPos + CHUNK_SIZE - 1, fileSize - 1);
            
            executor.execute(() -> {
                try {
                    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
                    conn.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
                    InputStream in = conn.getInputStream();
                    
                    file.seek(startPos);
                    byte[] buffer = new byte[CHUNK_SIZE];
                    int read;
                    while ((read = in.read(buffer)) != -1) {
                        file.write(buffer, 0, read);
                    }
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        
        latch.await();
        executor.shutdown();
        file.close();
    }
}

结论:多线程编程的实践与展望

通过本文的讲解,我们系统性地梳理了 Java 多线程编程的核心概念与实践方法。从线程创建、同步机制到线程池优化,每个知识点都通过代码示例和比喻进行了深入浅出的解析。对于初学者,建议从简单案例入手,逐步理解线程间的协作与竞争;中级开发者则可进一步探索 java.util.concurrent 包中的高级工具,如 CompletableFutureForkJoinPool

多线程编程既是提升程序性能的利器,也是引发复杂问题的根源。掌握其原理并遵循“同步最小化、资源有序化”的设计原则,才能真正发挥多线程的优势。希望读者通过本文的指导,能够将所学知识应用到实际项目中,开发出高效、稳定的并发程序。

最新发布