前端视角 Java Web 入门手册 2.11:Java Core ——多线程编程

109 阅读20分钟

在软件开发中,并发的挑战是一个随着业务扩展和用户增长而出现的“幸福烦恼”。JavaScript 使用事件驱动和异步 I/O 的特性,成功处理了许多并发任务,从而在 Node.js 中开辟了新的应用领域,使其在 Web 服务器开发中占据一席之地。

然而,JavaScript 的单线程模型在面对 CPU 密集型任务类时,比如复杂加密运算或大规模数据排序,可能显得力不从心。这种场景下,由于主线程被繁重的计算任务长时间占用,页面可能会出现响应迟缓或渲染卡顿的问题,影响用户体验。

JavaScript 的早期任务更多是处理简单的表单验证、页面动态效果等,这些任务不需要复杂的并行处理。后面考虑到单线程模型在浏览器环境可以保证 UI 操作的顺序性和确定性,避免多个线程在操作 DOM 时的冲突,JavaScript 一直没有引入多线程支持。为了支持并行计算,JavaScript 引入了 Web Workers,它允许将计算密集型任务放到后台线程中,以免阻塞主线程的 UI 操作

在处理 CPU 密集操作时候,多线程编程模型允许程序并行执行多个任务,充分利用当代多核处理器的性能优势,大幅提高任务处理效率。在 Java 的生态系统中,丰富的线程管理和同步机制简化了并发编程的复杂性,使开发者能够开发出高效且可靠的多线程应用程序,从容应对高并发、大规模数据处理及复杂计算任务的挑战。

线程状态

在操作系统中代表一个正在执行的程序,是资源分配基本单位。线程是程序的执行单位,操作系统中的线程通常包含以下几个主要状态:

  1. 创建(New) :线程被创建,但尚未开始执行
  2. 就绪(Ready) :线程准备好执行,等待操作系统分配资源进行调度
  3. 运行(Running) :线程正在 CPU 上执行
  4. 阻塞(Blocked)/等待(Waiting) :线程因等待某些资源(如I/O操作完成、锁释放等)而暂停执行
  5. 终止(Terminated) :线程完成执行或被操作系统强制终止,资源被释放

Java 通过java.lang.Thread.State枚举类定义了线程的各种状态,虽然 Java 层面对线程状态进行了抽象,但其基本概念与操作系统中的线程状态模型相似

操作系统线程状态Java 线程状态说明
NewNEW线程已创建,但未启动运行
ReadyRUNNABLE线程已准备好运行,等待操作系统调度
RunningRUNNABLE线程正在 CPU 上执行
BlockedBLOCKED线程被阻塞,等待获取锁或其他资源
WaitingWAITING线程无限期地等待某个条件的发生
Timed WaitingTIMED_WAITING线程等待特定条件,并设置了超时限制
TerminatedTERMINATED线程完成执行或被操作系统强制终止,所有资源释放

New 和 Ready 区别

  • 新建状态(New) :当程序通过代码创建一个线程对象时,线程就进入了新建状态。此时线程仅仅是在系统中被声明和初始化,它只是一个存在于内存中的数据结构,包含了线程的一些基本信息,如线程 ID、优先级等,但还未被操作系统真正纳入调度范围,尚未做好执行的准备。
  • 就绪状态(Ready) :处于新建状态的线程完成必要的初始化操作后,会进入就绪状态。处于就绪状态的线程已经具备了执行的条件,它的所有资源(如栈空间、寄存器等)都已分配完毕,只等待操作系统的调度器分配 CPU 时间片,一旦获得 CPU 资源,就可以立即开始执行线程体中的代码。

Block 和 Waiting 区别

  • 阻塞(Block)状态:线程因为等待某些资源(如 I/O 操作完成、锁释放等)而无法继续执行,临时停止运行,等待资源变得可用。一旦所需资源变为可用,线程会被操作系统调度器唤醒,恢复到就绪(Ready)状态,等待 CPU 分配执行。
  • 等待(Waiting)状态:一般是程序主动调用特定方法触发,目的是等待其他线程的特定操作或某个条件满足。需要其它线程发出特定信号或通知来唤醒线程,例如在 Java 中调用 Object.notify() 方法,满足等待的条件后 Waiting 状态的线程会被唤醒继续执行。

WAITING 和 TIMED_WAITING 区别

TIMED_WAITING 是 Java 定义的线程状态,不同于 WAITING,TIMED_WAITING 设有等待的时间限制,防止线程长期处于等待状态。适用于希望线程在等待特定事件的同时,避免无限期阻塞的场景。

线程进入 TIMED_WAITING 状态需要主动调用带有超时参数的方法,使线程在等待特定时间后自动恢复运行,避免无限期等待

  • Thread.sleep(long millis) :使当前线程休眠指定的毫秒数
  • Object.wait(long timeout) :当前线程等待,直到被通知或超时
  • Thread.join(long millis) :当前线程等待,直到被join()的线程完成或超时
  • LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long deadline) :使当前线程等待指定的纳秒或直到指定的绝对时间点

Java 线程的创建与管理

Java 提供了多种创建线程的方式,每种方式适用于不同的场景和需求

继承 Thread 类

最基本的创建线程的方式是通过继承 java.lang.Thread 类,并重写 run() 方法

// 自定义线程类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread is running.");
    }
}

// 主类
public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread(); // 创建线程实例
        thread.start(); // 启动线程,调用 run() 方法
    }
}

调用 start() 方法会使 JVM 分配资源并启动新的线程,随后 JVM 调用 run() 方法。继承 Thread 类虽然简单,但受限于 Java 单继承机制,若继承 Thread 类,便无法继承其它类,限制了类的设计灵活性

实现 Runnable 接口

更推荐的方式是实现 java.lang.Runnable 接口,将线程执行的逻辑封装在 run() 方法中。然后将 Runnable 实例作为参数传递给 Thread 类的构造器。

// 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable is running.");
    }
}

// 主类
public class RunnableExample {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable(); // 创建 Runnable 实例
        Thread thread = new Thread(runnable); // 创建线程,并传入 Runnable
        thread.start(); // 启动线程
    }
}

实现 Callable 接口与 Future

Runnable 接口适用于不需要返回结果的任务,而 java.util.concurrent.Callable 接口则支持任务执行后返回结果,并能够抛出异常

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 实现 Callable 接口
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result.";
    }
}

// 主类
public class CallableExample {
    public static void main(String[] args) {
        Callable<String> callable = new MyCallable(); // 创建 Callable 实例
        FutureTask<String> futureTask = new FutureTask<>(callable); // 创建 FutureTask
        Thread thread = new Thread(futureTask); // 创建线程并传入 FutureTask
        thread.start(); // 启动线程

        try {
            String result = futureTask.get(); // 获取任务结果(阻塞)
            System.out.println("Result from Callable: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

FutureTask 类封装 Callable 对象,作为 Thread 的目标任务,并提供查询任务状态和获取结果的方法。若任务尚未完成, get() 方法则会阻塞等待

和 JavaScript 或 Node.js 不同,Java 在多线程中普遍(不是全部)使用阻塞的方式,这是因为阻塞式编程模型在编程逻辑上更为直观,代码结构清晰,易于编写和维护。对于复杂的业务逻辑,使用同步机制和线程管理可以更好地确保数据一致性和程序稳定性。

线程阻塞在多线程编程中是一种自然且必要的现象,通过现代操作系统的高效调度、多核处理器的并行执行,阻塞操作不会显著影响整体性能。

CompletableFuture 自动回调

使用标准的 java.util.concurrent.Future 接口确实需要通过轮询 (isDone) 或者阻塞 (get) 方法来获取任务的结果。Java 8 引入了 CompletableFuture,在任务完成后可以自动执行某些操作,而不需要轮询或阻塞

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

public class CompletableFutureExample {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 123;
        }, executorService);

        // Attach a callback to be executed when the computation is done
        completableFuture.thenAccept(result -> {
            System.out.println("Task is done! Result: " + result);
        });

        System.out.println("Main thread is not blocked and continues to do other work");

        // Optionally, wait for the result (this is blocking)
        try {
            Integer result = completableFuture.get();
            System.out.println("Blocking wait done! Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}

线程池管理

创建和销毁线程虽然比切换进程开销小,但仍旧是是资源密集型操作,涉及内存分配、上下文切换等过程,频繁创建和销毁线程会导致系统性能下降

线程池(Thread Pool)是一种预先创建一定数量线程的技术,这些线程处于等待任务的状态。当有新任务提交时,线程池会从线程池中选择一个空闲线程来执行该任务。任务执行完毕后,线程不会被销毁,而是返回线程池继续等待下一个任务。通过复用已有的工作线程,避免了重复的创建与销毁操作,显著降低了资源消耗和系统开销

Java 通过java.util.concurrent.Executor 框架可以方便地管理线程池,提交任务并获取结果,简化线程的创建与管理

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

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池,线程数为3
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 提交5个任务
        for(int i = 1; i <= 5; i++) {
            executor.submit(new Task(i));
        }
        
        // 关闭线程池
        executor.shutdown();
    }
}

class Task implements Runnable {
    private int taskId;
    
    public Task(int id) {
        this.taskId = id;
    }
    
    @Override
    public void run() {
        System.out.println("执行任务 " + taskId + " 由线程 " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000); // 模拟任务执行时间
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("任务 " + taskId + " 完成");
    }
}
  • thenAccept:可以在任务完成时执行一个接受结果的操作
  • thenApply:可以在任务完成时处理并转换结果
  • thenRun:可以在任务完成时执行一个不需要结果的操作

输出

执行任务 1 由线程 pool-1-thread-1
执行任务 2 由线程 pool-1-thread-2
执行任务 3 由线程 pool-1-thread-3
任务 1 完成
执行任务 4 由线程 pool-1-thread-1
任务 2 完成
执行任务 5 由线程 pool-1-thread-2
任务 3 完成
任务 4 完成
任务 5 完成

除了固定数量的线程池,Java 还支持多种线程池实现

  • SingleThreadExecutor:线程池中只有一个线程,所有任务按照提交的顺序依次执行。如果线程在执行任务过程中出现异常而终止,线程池会创建一个新的线程继续执行后续任务。适用于需要保证任务顺序执行的场景
  • CachedThreadPool:线程池中的线程数量是动态变化的,当有新任务提交时,如果线程池中存在空闲线程,则分配一个空闲线程执行任务;如果没有空闲线程,线程池会创建一个新的线程来执行任务。如果某个线程在 60 秒内没有被使用,该线程会被销毁,适用于执行大量短期异步任务的场景
  • ScheduledThreadPool:线程池可以在指定的延迟时间后执行任务,或者按照固定的周期执行任务。线程池会根据任务的执行时间和周期进行调度,确保任务按时执行,适用于需要定时执行任务或周期性执行任务的场景
  • WorkStealingPool:使用多个工作队列,每个线程都有自己的工作队列。当一个线程完成了自己队列中的任务后,它可以从其他线程的队列中 “窃取” 任务来执行,从而提高系统的并行度和资源利用率,适用于具有大量独立子任务的场景

同步

线程安全是并发编程中的一个重要概念,在多线程环境下,当多个线程同时访问共享资源时,可能会引发数据不一致、程序出现错误结果等问题,而线程安全就是要保证在这种情况下程序依然能正确执行。

为了保证线程安全,Java 提供了多种同步机制

synchronized 关键字

synchronized 是 Java 中最基本的同步机制,能够确保同一时刻只有一个线程可以执行被修饰的代码块或方法

// 计数器类
class Counter {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }
}

// 主类
public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建多个线程,执行计数器增减操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.decrement();
            }
        });

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

        t1.join();
        t2.join();

        // 预期结果为0
        System.out.println("Final count: " + counter.getCount());
    }
}
  • increment() 方法使用 synchronized 修饰,确保在同一时刻只有一个线程可以执行该方法
  • decrement() 方法内的代码在一个 synchronized 块中执行,同样保证了同步性

当线程尝试获取锁但锁已经被其它线程持有时,线程会被操作系统挂起,进入阻塞状态。在阻塞状态下,线程会被放入锁的等待队列中,不再占用 CPU 资源。操作系统会将 CPU 时间片分配给其它就绪的线程。

操作系统会为每个锁维护一个特定的数据结构,用于记录锁的状态(如锁定、解锁)以及等待该锁的线程队列。当持有锁的线程释放锁后,操作系统会从等待队列中唤醒一个线程,使其有机会再次尝试获取锁。

显式锁 ReentrantLock

synchronized 是一种隐式锁,当使用 synchronized 修饰方法或者代码块时,锁的获取和释放操作是由 Java 编译器和 JVM 自动完成的,开发者无需手动编写获取和释放锁的代码。而 ReentrantLock 是显式锁,意味着开发者需要在代码里手动进行锁的获取和释放操作

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 计数器类使用 ReentrantLock
class LockCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    // 增加计数
    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 减少计数
    public void decrement() {
        lock.lock(); // 获取锁
        try {
            count--;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

// 主类
public class ReentrantLockExample {
    public static void main(String[] args) throws InterruptedException {
        LockCounter counter = new LockCounter();

        // 创建多个线程,执行计数器增减操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.decrement();
            }
        });

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

        t1.join();
        t2.join();

        // 预期结果为0
        System.out.println("Final count: " + counter.getCount());
    }
}

条件变量 Condition

Condition 接口提供了一种线程间协调的方式,允许线程在某个条件不满足时进入 Waiting 状态,而当其他线程改变了这个条件并通知等待的线程时,等待的线程可以被唤醒继续执行

Condition 接口与 ReentrantLock 配合使用,提供了类似于 Object 类中 wait() 和 notify() 的条件等待和通知机制。看一下生产者、消费者模型的实现示例:

使用 notFullnotEmpty 条件,生产者在缓冲区满时等待,消费者在缓冲区空时等待

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 共享缓冲区类
class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    // 生产者放入元素
    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 等待缓冲区有空间
            }
            queue.offer(value);
            System.out.println("Produced: " + value);
            notEmpty.signalAll(); // 通知消费者缓冲区不为空
        } finally {
            lock.unlock();
        }
    }

    // 消费者取出元素
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 等待缓冲区不为空
            }
            int value = queue.poll();
            System.out.println("Consumed: " + value);
            notFull.signalAll(); // 通知生产者缓冲区有空间
            return value;
        } finally {
            lock.unlock();
        }
    }
}

// 主类
public class ConditionExample {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();

        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    buffer.produce(i);
                    Thread.sleep(100); // 模拟生产时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    buffer.consume();
                    Thread.sleep(150); // 模拟消费时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

输出

Produced: 1
Consumed: 1
Produced: 2
Produced: 3
Consumed: 2
Produced: 4
Produced: 5
Consumed: 3
Produced: 6
Consumed: 4
Produced: 7
Consumed: 5
Produced: 8
Consumed: 6
Produced: 9
Consumed: 7
Produced: 10
Consumed: 8
Consumed: 9
Consumed: 10

volatile 关键字

为了进一步简化并提高线程安全性,Java 提供了 volatile 关键字。当一个变量被声明为 volatile 时,它会保证对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值,从而保证了变量在多个线程之间的可见性。

可见性指的是当一个线程修改了共享变量的值后,其它线程能够立即看到这个修改后的值。

在多线程环境中,每个线程可能会有自己的本地缓存(如 CPU 缓存),线程对共享变量的读写操作可能首先在本地缓存中进行,而不是直接操作主内存中的变量。这就可能导致一个线程对共享变量的修改不能及时反映到其它线程的本地缓存中,从而出现其它线程读取到旧值的情况。

volatile 用于声明一个变量在多线程环境下的可见性,确保一个线程修改的变量对其它线程立即可见,但不保证原子性

class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++; // 不保证原子性
    }

    public int getCount() {
        return count;
    }
}

public class VolatileExample {
    public static void main(String[] args) throws InterruptedException {
        VolatileCounter counter = new VolatileCounter();

        // 创建多个线程,执行计数器增操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

        t1.join();
        t2.join();

        // 预期结果为2000,但由于非原子操作,结果可能不准确
        System.out.println("Final count: " + counter.getCount());
    }
}

count++ 是一个复合操作,由读取、增加、写入三步组成,不保证操作的原子性,容易导致计数不准确

可能输出: Final count: 1987

原子变量

原子性指的是一个操作或者一系列操作,要么全部执行并且在执行过程中不会被任何因素打断,要么就完全不执行。这些操作就像一个不可分割的整体,一旦开始执行,就会一直执行到结束,期间不会有其他线程或进程插入并影响其执行结果。

Java 提供了 java.util.concurrent.atomic 包中的原子类,支持原子操作,避免了使用显式同步

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

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

    public int getCount() {
        return count.get();
    }
}

public class AtomicExample {
    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();

        // 创建多个线程,执行计数器增操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

        t1.join();
        t2.join();

        // 预期结果为2000
        System.out.println("Final count: " + counter.getCount());
    }
}

AtomicInteger 的 getAndIncrement() 方法保证了操作的原子性,避免了竞态条件问题

输出: Final count: 2000

原子变量的底层实现依赖于 CAS(Compare-And-Swap)操作,CAS 是一种无锁算法,属于乐观锁的实现方式。

CAS 操作涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。它的工作过程是先读取内存位置 V 的值,将其与预期原值 A 进行比较,如果相等,则将内存位置 V 的值更新为新值 B;如果不相等,则操作失败,通常会重试操作或者放弃。

原子变量基于 CAS 的实现方式避免了线程的阻塞和上下文切换开销,在竞争不激烈的情况下,性能更高。

信号量 Semaphor

Semaphore 控制对特定资源的访问数量,通过许可(permits)机制限制同时访问某一资源的线程数量。信号量本质上是一个计数器,用于表示可用资源的数量。它提供了两个主要操作:

  • P 操作(等待、获取资源) :也称为 acquire() 操作,当一个线程需要访问资源时,会调用该操作。如果信号量的值大于 0,说明有可用资源,信号量的值会减 1,线程可以继续执行并使用资源;如果信号量的值为 0,说明没有可用资源,线程会被阻塞,直到有其它线程释放资源使信号量的值大于 0。
  • V 操作(释放、归还资源) :也称为 release() 操作,当一个线程使用完资源后,会调用该操作。调用此操作会使信号量的值加 1,如果此时有其它线程正在等待资源(即处于阻塞状态),则会唤醒其中一个线程。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        // 定义信号量,最多允许 2 个线程同时访问资源
        Semaphore semaphore = new Semaphore(2);

        // 创建多个线程,尝试获取信号量许可
        for (int i = 1; i <= 5; i++) {
            final int threadId = i;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadId + " is attempting to acquire a permit.");
                    semaphore.acquire(); // 获取许可,可能阻塞
                    System.out.println("Thread " + threadId + " has acquired a permit.");
                    Thread.sleep(2000); // 模拟资源使用时间
                    System.out.println("Thread " + threadId + " is releasing the permit.");
                    semaphore.release(); // 释放许可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

信号量初始化为 2,最多允许 2 个线程同时访问资源。前两个线程成功获取许可,其它线程在等待许可释放,一旦许可被释放,等待的线程可以继续执行

输出

Thread 1 is attempting to acquire a permit.
Thread 1 has acquired a permit.
Thread 2 is attempting to acquire a permit.
Thread 2 has acquired a permit.
Thread 3 is attempting to acquire a permit.
Thread 4 is attempting to acquire a permit.
Thread 5 is attempting to acquire a permit.
Thread 1 is releasing the permit.
Thread 3 has acquired a permit.
Thread 2 is releasing the permit.
Thread 4 has acquired a permit.
Thread 3 is releasing the permit.
Thread 5 has acquired a permit.
Thread 4 is releasing the permit.
Thread 5 is releasing the permit.

CountDownLatch

CountDownLatch 是 Java 并发编程中 java.util.concurrent 包下的一个实用工具类,它能够让一个或多个线程等待其它一组线程完成操作后再继续执行

  • 初始化:创建 CountDownLatch 对象时,需要传入一个整数作为初始计数值,表示需要等待完成的任务数量
  • 计数递减:每个需要完成任务的线程在完成任务后调用 latch.countDown(); 方法,该方法会将计数器的值减 1
  • 等待操作:需要等待的线程调用 latch.await(); 方法进入等待状态。await() 方法会一直阻塞当前线程,直到计数器的值变为 0
  • 唤醒线程:当计数器的值减到 0 时,所有调用 await() 方法而处于等待状态的线程会被唤醒,继续执行后续的代码

CountDownLatch 允许主线程等待所有任务线程完成后再继续执行,常用于协调多个线程的执行,比如等待所有子线程完成后汇总结果。例如,在分布式系统中,某个服务可能依赖于多个其它服务的启动。可以使用 CountDownLatch 让主服务等待所有依赖服务都启动完成后再开始提供服务。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numTasks = 3;
        CountDownLatch latch = new CountDownLatch(numTasks);

        // 创建并启动多个任务线程
        for (int i = 1; i <= numTasks; i++) {
            final int taskId = i;
            new Thread(() -> {
                System.out.println("Task " + taskId + " is starting.");
                try {
                    Thread.sleep(1000 * taskId); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task " + taskId + " is completed.");
                latch.countDown(); // 任务完成,计数器减 1
            }).start();
        }

        System.out.println("Main thread is waiting for tasks to complete.");
        latch.await(); // 等待所有任务完成
        System.out.println("All tasks have completed. Main thread resumes.");
    }
}

输出

Main thread is waiting for tasks to complete.
Task 1 is starting.
Task 2 is starting.
Task 3 is starting.
Task 1 is completed.
Task 2 is completed.
Task 3 is completed.
All tasks have completed. Main thread resumes.

线程安全的集合类

Java 的 java.util.concurrent 提供了多种线程安全的集合实现,涵盖了List、Set、Map和Queue等不同类型。如 ConcurrentHashMap、CopyOnWriteArrayList 等,适用于高并发环境

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> map = new ConcurrentHashMap<>();

        // 创建多个线程,执行插入操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put("Key" + i, i);
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 500; i < 1500; i++) {
                map.put("Key" + i, i);
            }
        });

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

        t1.join();
        t2.join();

        // 打印部分结果
        System.out.println("Size of map: " + map.size());
        System.out.println("Value for Key750: " + map.get("Key750"));
        System.out.println("Value for Key1200: " + map.get("Key1200"));
    }
}