杰瑞科技汇

Java多线程如何高效协作与同步?

  1. 为什么需要多线程? - 多线程的价值
  2. Java 线程的两种创建方式 - ThreadRunnable
  3. 线程的生命周期 - 从创建到销毁的整个过程
  4. 线程的核心方法 - start(), run(), sleep(), join()
  5. 线程同步 - 解决多线程并发问题(synchronized, Lock
  6. 线程间通信 - wait(), notify(), notifyAll()
  7. 线程池 - ExecutorService - 高效管理线程
  8. 现代异步编程 - CompletableFuture
  9. 最佳实践与注意事项

为什么需要多线程?

多线程是为了提高程序的运行效率和响应速度

Java多线程如何高效协作与同步?-图1
(图片来源网络,侵删)
  • 提高 CPU 利用率:在单核 CPU 中,多线程通过快速切换(时间片轮转)来模拟“执行,当一个线程因 I/O(如读写文件、网络请求)阻塞时,CPU 可以立即切换到其他线程执行,避免了 CPU 闲置。
  • 提升程序响应速度:对于图形界面(GUI)应用,可以将耗时操作(如下载文件、复杂计算)放在后台线程执行,避免主线程(UI线程)被阻塞,从而保持界面的流畅和响应。
  • 简化程序模型:对于某些任务(如服务器处理多个客户端请求),使用多线程可以将每个任务的处理逻辑封装在一个独立的线程中,使程序结构更清晰。

Java 线程的两种创建方式

在 Java 中,创建线程主要有两种方式,并且推荐使用第二种。

继承 Thread

这是最简单直接的方式,你创建一个新类,继承自 java.lang.Thread,并重写其 run() 方法。

// 1. 继承 Thread 类
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程要执行的代码
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread is running: " + i);
            try {
                Thread.sleep(500); // 暂停 500 毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        // 2. 创建线程对象
        MyThread thread = new MyThread();
        // 3. 启动线程 (注意:不是调用 run() 方法)
        thread.start();
        // 主线程的代码
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread is running: " + i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

缺点:Java 是单继承的,如果继承了 Thread 类,就无法再继承其他类了,灵活性较差。

实现 Runnable 接口(推荐)

这种方式更加灵活,符合“面向接口编程”的思想,你只需要创建一个类实现 Runnable 接口,并实现其 run() 方法,然后将这个 Runnable 实例传给 Thread 类。

Java多线程如何高效协作与同步?-图2
(图片来源网络,侵删)
// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程要执行的代码
        for (int i = 0; i < 5; i++) {
            System.out.println("MyRunnable is running: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        // 2. 创建 Runnable 实例
        MyRunnable runnable = new MyRunnable();
        // 3. 创建 Thread 对象,并将 Runnable 实例传入
        Thread thread = new Thread(runnable);
        // 4. 启动线程
        thread.start();
        // 主线程的代码
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread is running: " + i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

优点

  • 避免了单继承的限制。
  • 将线程任务(run() 方法的代码)和线程创建(Thread 对象)解耦,代码结构更清晰。

线程的生命周期

一个线程从创建到销毁,会经历几个不同的状态,可以通过 Thread.getState() 方法查看。

  1. NEW (新建):线程被创建但尚未启动。thread.start() 之前的状态。
  2. RUNNABLE (可运行):线程已经启动,可能正在运行,也可能正在等待 CPU 时间片,在 Java 中,就绪状态和运行状态统称为 RUNNABLE
  3. BLOCKED (阻塞):线程因为等待监视器锁(即 synchronized 代码块或方法)而进入阻塞状态,它不会执行,直到它获取到锁。
  4. WAITING (等待):线程因为调用 wait(), join()LockSupport.park() 而进入无限等待状态,它需要其他线程显式地唤醒(如 notify()notifyAll())。
  5. TIMED_WAITING (计时等待):和 WAITING 类似,但它是在指定的时间内等待。sleep(long millis), wait(long timeout), join(long millis)
  6. TERMINATED (终止):线程已经执行完毕或被中断,生命周期结束。

状态转换图NEW -> start() -> RUNNABLE -> (获取到 CPU) -> 运行中 RUNNABLE -> (失去 CPU/调用 sleep/wait) -> BLOCKED/WAITING/TIMED_WAITING BLOCKED/WAITING/TIMED_WAITING -> (获取锁/被唤醒/sleep时间到) -> RUNNABLE RUNNABLE -> (执行完毕) -> TERMINATED


线程的核心方法

方法 作用 备注
start() 启动线程,JVM 会调用该线程的 run() 方法。 只能调用一次,直接调用 run() 只是在当前线程执行 run() 中的代码,不会启动新线程。
run() 线程执行的主体,包含要执行的任务代码。 通常需要重写。
sleep(long millis) 让当前线程“休眠”指定的毫秒数,进入 TIMED_WAITING 状态。 不会释放锁。
join() 等待该线程终止。 如果在主线程中调用 thread.join(),主线程会阻塞,直到 thread 线程执行完毕。
yield() 提示当前线程已经完成了一次工作,可以“礼让”给同等优先级的其他线程。 不保证一定会礼让,只是建议。
interrupt() 中断线程。 它不会立即停止线程,而是设置一个中断状态,线程可以通过 isInterrupted()InterruptedException 来感知中断。
isAlive() 测试线程是否处于活动状态(RUNNABLE, BLOCKED, WAITING, TIMED_WAITING)。

线程同步

当多个线程同时访问和修改共享资源(如一个变量、一个对象)时,可能会导致数据不一致的问题,这就是线程安全问题

Java多线程如何高效协作与同步?-图3
(图片来源网络,侵删)

问题示例:银行取款

class Account {
    private int balance = 1000;
    public void withdraw(int amount) {
        if (balance >= amount) {
            try {
                // 模拟网络延迟等耗时操作
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " 取款成功,余额: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 余额不足,余额: " + balance);
        }
    }
}
public class UnsafeDemo {
    public static void main(String[] args) {
        Account account = new Account();
        Thread t1 = new Thread(() -> account.withdraw(500), "线程A");
        Thread t2 = new Thread(() -> account.withdraw(500), "线程B");
        t1.start();
        t2.start();
    }
}

可能输出

线程A 取款成功,余额: 500
线程B 取款成功,余额: 0  // 错误!

原因if (balance >= amount)balance -= amount 这两个操作不是一个原子操作,在 t1 检查完余额后,t2 也检查了,然后两个线程都认为余额足够,导致余额被减了两次。

解决方案一:synchronized 关键字

synchronized 可以保证在同一时间,只有一个线程能进入被 synchronized 修饰的代码块或方法,从而保证了原子性。

  1. 同步代码块:更灵活,可以指定锁对象。

    public void withdraw(int amount) {
        synchronized (this) { // 锁对象是 Account 实例
            if (balance >= amount) {
                // ...
            }
        }
    }
  2. 同步方法:锁对象默认是 this(对于实例方法)或 Class 对象(对于静态方法)。

    public synchronized void withdraw(int amount) {
        // ...
    }

    使用 synchronized 后,上面的取款问题就能得到解决。

解决方案二:java.util.concurrent.locks.Lock 接口

Lock 提供了比 synchronized 更强大的功能,例如尝试获取锁可中断地获取锁超时获取锁等。

最常用的实现是 ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SafeAccount {
    private int balance = 1000;
    private final Lock lock = new ReentrantLock(); // 创建锁
    public void withdraw(int amount) {
        lock.lock(); // 加锁
        try {
            if (balance >= amount) {
                Thread.sleep(1);
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " 取款成功,余额: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + " 余额不足,余额: " + balance);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁,必须放在 finally 块中
        }
    }
}

Locksynchronized 更灵活,但使用时必须手动加锁和释放锁,否则会导致死锁。


线程间通信

当多个线程需要协作完成任务时,就需要线程间通信,经典的生产者-消费者模型就是例子。

Object 类提供了三个方法来实现等待/通知机制:

  • wait():让当前线程等待,直到其他线程调用 notify()notifyAll(),调用此方法的线程必须拥有该对象的锁。
  • notify():唤醒一个正在等待该对象锁的线程(是随机的)。
  • notifyAll():唤醒所有正在等待该对象锁的线程。

示例:生产者生产商品,消费者消费商品。

class Product {
    private String name;
    private boolean isEmpty = true; // true表示为空
    // 消费者调用
    public synchronized void consume() throws InterruptedException {
        if (isEmpty) {
            System.out.println("消费者:商品为空,等待生产...");
            wait(); // 等待,并释放锁
        }
        System.out.println("消费者:消费了 " + name);
        isEmpty = true;
        notifyAll(); // 唤醒生产者
    }
    // 生产者调用
    public synchronized void produce(String name) throws InterruptedException {
        if (!isEmpty) {
            System.out.println("生产者:商品已满,等待消费...");
            wait(); // 等待,并释放锁
        }
        this.name = name;
        System.out.println("生产者:生产了 " + name);
        isEmpty = false;
        notifyAll(); // 唤醒消费者
    }
}

注意wait(), notify(), notifyAll() 必须在同步代码块或同步方法中调用,并且操作的是同一个锁对象


线程池

频繁地创建和销毁线程是非常消耗资源的,线程池就是为了解决这个问题而生的,它会预先创建一组线程,当有任务需要执行时,就从池中取出一个线程来执行,任务执行完毕后,线程不会销毁,而是返回池中等待下一次任务。

核心接口:ExecutorService

Java 提供了 java.util.concurrent.ExecutorService 接口来管理线程池。

创建线程池的方式

  1. Executors 工厂类:简单易用,但不适合生产环境。

    • Executors.newFixedThreadPool(int n): 创建一个固定大小的线程池。
    • Executors.newCachedThreadPool(): 创建一个可缓存的线程池,大小不固定。
    • Executors.newSingleThreadExecutor(): 创建一个单线程的线程池。
  2. ThreadPoolExecutor:功能最强大,是线程池的真正实现,推荐在生产环境中使用。

示例:使用 Executors

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建一个固定大小为 3 的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务 " + taskId);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        // 关闭线程池
        executor.shutdown(); // 等待所有任务完成后关闭
        // executor.shutdownNow(); // 立即关闭,尝试停止正在执行的任务
    }
}

使用 ThreadPoolExecutor 的推荐方式

// 核心参数
int corePoolSize = 5;      // 核心线程数
int maximumPoolSize = 10;  // 最大线程数
long keepAliveTime = 60L;  // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    unit,
    workQueue,
    threadFactory,
    handler
);

现代异步编程:CompletableFuture

从 Java 8 开始,CompletableFuture 提供了一种更强大、更灵活的方式来处理异步编程,它结合了 Future 的功能和函数式编程,可以方便地进行链式调用和组合多个异步任务。

示例:异步执行任务并处理结果

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 异步执行一个任务
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, CompletableFuture!";
        });
        // 2. 当任务完成时,执行这个回调
        future.thenAccept(result -> {
            System.out.println("收到结果: " + result);
        });
        // 3. 主线程可以继续做其他事情
        System.out.println("主线程继续执行...");
        // 为了看到结果,让主线程等待一下
        future.get(); // 阻塞,直到 future 完成
    }
}

CompletableFuture 提供了丰富的 API,如 thenApply, thenCompose, thenCombine, exceptionally 等,使得复杂的异步流程处理变得非常优雅。


最佳实践与注意事项

  1. 避免过度使用线程:不是所有场景都需要多线程,对于计算密集型任务,在单核 CPU 上,多线程反而会因为线程切换的开销而降低性能。
  2. 避免 synchronized 滥用synchronized 会影响并发性能,尽量使用 java.util.concurrent 包下的工具类,如 ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger 等,它们是为并发场景设计的。
  3. 注意死锁:当两个或多个线程互相等待对方释放锁时,就会发生死锁,避免在持有锁时调用其他可能阻塞的方法(如 IO 操作),尽量以固定的顺序获取多个锁。
  4. 注意资源泄漏:使用 try-finallytry-with-resources 确保锁、文件、数据库连接等资源被正确释放。
  5. 优先使用线程池:不要手动创建和管理大量线程。
  6. 优先使用 CompletableFutureStream 进行异步和并行处理:它们是现代 Java 中处理并发任务的更高级、更简洁的方式。

希望这份详细的指南能帮助你全面理解 Java 多线程!

分享:
扫描分享到社交APP
上一篇
下一篇