解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(一)?

325 阅读11分钟

要了解在多线程编程方面的理解和应用。讨论如何在Java中确保多个线程按顺序执行,常见的做法包括使用join()CountDownLatchSemaphore、单线程池、synchronizedCompletableFuture等。

关键知识点:

1. 多线程基础知识

希望了解是否熟悉Java中的多线程基本概念,包括:

  • 线程的创建和启动:是否了解如何通过继承Thread类或实现Runnable接口来创建线程,以及如何启动线程(使用start()方法)。
  • 线程的生命周期:是否清楚线程从创建到结束的生命周期以及各种状态,如新建、就绪、运行、阻塞和终止。

2. 同步机制的理解

需要展示对Java中各种同步机制和工具的理解,尤其是如何通过同步工具确保多线程程序中的顺序执行和线程安全。常见的同步工具包括:

  • join() :用来控制线程的顺序执行,确保一个线程完成后才执行下一个线程。
  • CountDownLatch:用来等待其他线程完成任务,常用于控制多个线程的执行顺序。
  • Semaphore:用于控制多个线程对共享资源的访问数量,特别适用于限制并发数的场景。
  • CyclicBarrier:用于协调多个线程之间的同步,直到所有线程都到达屏障点才继续执行。

3. 线程间通信

理解线程之间的通信机制,尤其是在多个线程需要协作时,如何通过协调来实现正确的执行顺序。Java中的常见线程间通信方法有:

  • wait()notify() :这两个方法可以用于线程间的通信,其中wait()让当前线程进入等待状态,直到其他线程调用notify()notifyAll()来唤醒它们。这种机制通常用于生产者-消费者问题等多线程协作场景。

4. 对Java并发包的熟悉程度

Java并发包(java.util.concurrent)提供了丰富的并发工具类,对这些工具有一定的掌握。例如:

  • ExecutorService:用于管理线程池,帮助简化多线程的创建和管理。
  • ReentrantLockReadWriteLock:高级的同步机制,比synchronized更灵活和高效,适用于复杂的并发场景。
  • CompletableFuture:Java 8引入的用于异步编程的工具类,可以帮助简化异步任务的执行和组合。

相关方法应用

1. join()方法

解释: join()方法是Thread类的一部分,它使得当前线程等待调用join()的线程执行完毕后再继续执行。也就是说,通过在一个线程上调用join(),你可以确保它完成后,才开始下一个线程。

场景一: 假设你正在开发一个任务调度系统,其中多个任务需要依次执行,不能同时开始。比如,任务A完成后才会启动任务B,任务B完成后才会启动任务C。

代码示例:

Thread t1 = new Thread(() -> System.out.println("任务T1"));
Thread t2 = new Thread(() -> System.out.println("任务T2"));
Thread t3 = new Thread(() -> System.out.println("任务T3"));

t1.start();
t1.join();  // 等待T1完成
t2.start();
t2.join();  // 等待T2完成
t3.start();
t3.join();  // 等待T3完成

应用场景: 这种方法适用于需要依次执行多个任务的场景,例如一个文件处理系统中,任务A完成后才可以进行任务B的处理。


场景二:想象一下你有一份报告需要生成,分三步:数据收集、数据处理、结果展示。每个步骤必须在前一步完成后才能开始。

代码示例

Thread t1 = new Thread(() -> {
    System.out.println("数据收集");
});

Thread t2 = new Thread(() -> {
    System.out.println("数据处理");
});

Thread t3 = new Thread(() -> {
    System.out.println("结果展示");
});

t1.start();
t1.join(); // 等待t1完成

t2.start();
t2.join(); // 等待t2完成

t3.start();
t3.join(); // 等待t3完成

在这个例子中,通过t1.join(),我们确保t1完成后才开始t2,依此类推。


2. CountDownLatch

解释: CountDownLatch是一个同步工具类,它使用一个计数器来控制线程的执行。当计数器的值为0时,等待的线程会被唤醒并继续执行。你可以通过await()方法让线程在某个特定时刻等待,直到计数器减到0。

场景一: 假设你正在开发一个并行下载系统,其中多个线程需要在下载完成后一起继续执行后续操作,但这些线程的执行顺序是必须按照某个特定顺序的。

代码示例:

CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("任务T1");
    latch1.countDown();  // T1完成,减少计数器
});
Thread t2 = new Thread(() -> {
    try {
        latch1.await();  // 等待T1完成
        System.out.println("任务T2");
        latch2.countDown();  // T2完成,减少计数器
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
Thread t3 = new Thread(() -> {
    try {
        latch2.await();  // 等待T2完成
        System.out.println("任务T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

应用场景: 比如一个分布式系统,多个服务需要在启动时等其他服务完成初始化后才能继续进行。


场景二: 想象一个情况,多个运动员在同一场比赛中,他们只有在裁判宣布“开始”后才能出发。

代码示例

CountDownLatch latch = new CountDownLatch(1);

Thread t1 = new Thread(() -> {
    System.out.println("运动员1准备");
    latch.countDown(); // 裁判说“开始”
});

Thread t2 = new Thread(() -> {
    try {
        latch.await(); // 等待裁判宣布开始
        System.out.println("运动员2出发");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();

CountDownLatcht2等待,直到t1调用countDown(),确保t2t1完成后执行。


3. Semaphore

解释: Semaphore用于控制对共享资源的访问,使用一个计数器来管理许可,线程通过acquire()获取许可,通过release()释放许可。如果没有可用许可,线程会被阻塞,直到获得许可。

场景: 假设你有一个有限的资源池(比如连接池),最多只能同时有几个线程在执行某个任务。你希望线程按顺序执行并限制每次只能一个线程执行。

代码示例:

Semaphore semaphore1 = new Semaphore(0);
Semaphore semaphore2 = new Semaphore(0);

Thread t1 = new Thread(() -> {
    System.out.println("任务T1");
    semaphore1.release();  // 释放一个许可
});
Thread t2 = new Thread(() -> {
    try {
        semaphore1.acquire();  // 等待T1完成
        System.out.println("任务T2");
        semaphore2.release();  // 释放一个许可
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});
Thread t3 = new Thread(() -> {
    try {
        semaphore2.acquire();  // 等待T2完成
        System.out.println("任务T3");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();
t3.start();

应用场景: 比如,生产者-消费者问题中的信号量机制,可以用来控制生产和消费的顺序。


场景二:想象一个办公室,只有一台打印机,员工需要按顺序排队打印文件。

代码示例

Semaphore semaphore = new Semaphore(1);

Thread t1 = new Thread(() -> {
    try {
        semaphore.acquire(); // 获取打印机
        System.out.println("员工1正在打印");
        semaphore.release(); // 打印完成,释放打印机
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread t2 = new Thread(() -> {
    try {
        semaphore.acquire(); // 获取打印机
        System.out.println("员工2正在打印");
        semaphore.release(); // 打印完成,释放打印机
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

t1.start();
t2.start();

Semaphore确保一个线程在另一个释放资源后才能执行。


4. 单线程池(Executors.newSingleThreadExecutor()

解释: 单线程池会确保任务按提交顺序逐个执行,每次只允许一个线程执行任务,保证了任务的顺序性。 单线程池(Executors.newSingleThreadExecutor())可以确保提交的任务按提交顺序执行,但并不涉及线程之间的顺序控制。换句话说,它仅仅是保证提交到线程池的任务依次执行,但你仍然可以在任务之间控制顺序,当然前提是这些任务是通过线程池来执行的。

场景一: 在一些场景中,我们希望提交的多个任务按顺序执行,但不关心它们是否在同一线程中执行。单线程池正好适合这个需求。

代码示例:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("任务T1"));
executor.submit(() -> System.out.println("任务T2"));
executor.submit(() -> System.out.println("任务T3"));
executor.shutdown();

应用场景: 适用于需要顺序执行多个任务且不需要并发的场景。例如,一个日志处理系统,每次只能处理一个日志文件。


场景二:假设你在处理一批银行交易,每笔交易必须按到达顺序处理。

代码示例

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("处理交易1"));
executor.submit(() -> System.out.println("处理交易2"));
executor.submit(() -> System.out.println("处理交易3"));
executor.shutdown();

单线程池会按提交顺序执行任务,确保任务顺序性。

  • Executors.newSingleThreadExecutor() 保证任务按提交顺序执行,但其并不涉及到线程内部控制顺序,因此不能用于严格保证 t1t2t3 执行的顺序。

5. synchronized

解释: synchronized关键字用于确保同一时刻只有一个线程可以执行某个方法或代码块,从而避免多线程并发执行时发生数据不一致问题。

场景: 如果多个线程在执行时共享数据,并且需要确保数据的一致性和线程安全,我们可以使用synchronized来同步访问代码块,确保同一时刻只有一个线程可以执行该代码。

代码示例:

class Task {
    synchronized void executeTask(String taskName) {
        System.out.println(taskName + " 执行");
    }
}

public class Main {
    public static void main(String[] args) {
        Task task = new Task();
        new Thread(() -> task.executeTask("T1")).start();
        new Thread(() -> task.executeTask("T2")).start();
        new Thread(() -> task.executeTask("T3")).start();
    }
}

应用场景: 适用于需要保证线程安全的共享资源访问场景,例如银行账户的余额更新。

  • 这里的 synchronized 确保了任务不被并发执行,但它并不控制执行顺序。每次只有一个线程可以执行 executeTask 方法,但 t1t2t3 的执行顺序是不确定的。

场景二::多个线程需要安全访问一个共享资源,比如存款。

代码示例

class BankAccount {
    private int balance = 100;

    synchronized void deposit(int amount) {
        balance += amount;
        System.out.println("余额:" + balance);
    }
}

BankAccount account = new BankAccount();

new Thread(() -> account.deposit(50)).start();
new Thread(() -> account.deposit(100)).start();

synchronized确保每次只有一个线程可以修改balance,防止数据不一致。


6. CompletableFuture

解释: CompletableFuture是Java 8引入的类,可以用于处理异步编程。它支持通过链式调用来确保任务按顺序执行。CompletableFuture 主要保证任务执行的顺序(即任务依赖关系),而不是线程的执行顺序。底层是通过线程池来调度线程,因此其保证的是任务的执行顺序,而不是线程的顺序。

场景: 如果你需要管理多个异步任务,并希望这些任务按顺序执行,可以使用CompletableFuture。它允许你链式地组合多个任务,并确保每个任务在前一个任务完成后执行。

代码示例:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("任务T1");
});
future = future.thenRunAsync(() -> {
    System.out.println("任务T2");
});
future = future.thenRunAsync(() -> {
    System.out.println("任务T3");
});
future.join();

应用场景: 比如你需要执行多个异步任务,而这些任务之间有明确的执行顺序要求,CompletableFuture非常适合此类需求。

  • CompletableFuture 保证了任务的顺序执行(T1 -> T2 -> T3),但底层依然使用线程池调度线程,所以线程的顺序是不确定的。如果你需要严格控制线程的执行顺序,CompletableFuture 并不适合。

场景二::假设你有一个复杂的订单处理系统,需要一步步确认订单、处理付款、发货。

代码示例

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("确认订单");
});

future = future.thenRunAsync(() -> {
    System.out.println("处理付款");
});

future = future.thenRunAsync(() -> {
    System.out.println("发货");
});

future.join();

CompletableFuture通过链式调用,确保任务按顺序执行,并适用于异步任务。


保证任务顺序执行

  1. 单线程池:  只保证任务按提交顺序执行,但并不保证线程的顺序控制。
  2. synchronized  用于线程安全,而不是控制线程执行顺序。
  3. CompletableFuture  保证任务顺序执行,但底层通过线程池调度线程,无法保证线程顺序。

保证线程顺序控制

  1. join(): 保证线程按顺序执行(t1 -> t2 -> t3),因为调用 join() 会让当前线程阻塞,直到被调用的线程执行完成。
  2. CountDownLatch: 保证的是任务的完成顺序,不是线程的执行顺序。通过在任务之间设置依赖,线程在计数器归零后才能执行下一个任务。
  3. Semaphore: 不能直接保证线程按顺序执行,它更多的是通过限制并发数来控制线程的执行。如果你使用它来实现顺序执行,通常需要通过多个信号量相互依赖的方式来实现。

总结对比

方法顺序控制复杂度灵活性适用场景
join简单顺序执行任务
CountDownLatch多线程同步,等待一组线程完成
Semaphore精细控制线程执行顺序或并发数量
单线程池简单任务按顺序提交到线程池执行
synchronized确保线程安全,非顺序控制
CompletableFuture异步任务的依赖关系

其他方法

解锁Java多线程:如何控制线程T1、T2、T3的执行顺序(二)?

总结

这些方法各有适用的场景,选择合适的工具可以帮助我们更好地管理多线程的执行顺序。在实际开发中,确保线程按顺序执行的需求相对较少,但在某些特定场景下,这些技术依然非常有用。通过这些示例,你可以深入理解不同同步工具在不同应用场景下的优缺点,从而在实际编程中做出更合适的选择。