Java高手的30k之路|面试宝典|精通多线程(二)

线程间通信

等待与通知机制

1. Object类的wait()notify()notifyAll()方法

  • wait()方法

    • 线程调用该方法后,会释放持有的对象锁,进入等待状态,直到被其他线程调用notify()notifyAll()方法唤醒。
    • 必须在同步块或同步方法中调用,否则会抛出IllegalMonitorStateException异常。
  • notify()方法

    • 唤醒在此对象上等待的单个线程(如果有)。
    • 选择具体线程的方式是由JVM决定的,不受程序控制。
    • 必须在同步块或同步方法中调用,否则会抛出IllegalMonitorStateException异常。
  • notifyAll()方法

    • 唤醒在此对象上等待的所有线程。
    • 这些线程会竞争对象的锁,只有一个线程能成功持有锁并继续执行。

2. 使用等待/通知机制实现线程间通信

  • 生产者-消费者模型

    生产者-消费者模型是经典的多线程协作问题,下面是一个使用wait()notifyAll()实现的示例:

    import java.util.LinkedList;
    import java.util.Queue;
    
    public class ProducerConsumerExample {
        private static final int CAPACITY = 5;
        private final Queue<Integer> queue = new LinkedList<>();
        private int value = 0;
    
        public static void main(String[] args) {
            ProducerConsumerExample example = new ProducerConsumerExample();
            Thread producerThread = new Thread(example.new Producer(), "Producer");
            Thread consumerThread = new Thread(example.new Consumer(), "Consumer");
            producerThread.start();
            consumerThread.start();
        }
    
        class Producer implements Runnable {
            @Override
            public void run() {
                while (true) {
                    synchronized (queue) {
                        while (queue.size() == CAPACITY) {
                            try {
                                queue.wait();
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                return;
                            }
                        }
                        queue.offer(value++);
                        queue.notifyAll();
                    }
                }
            }
        }
    
        class Consumer implements Runnable {
            @Override
            public void run() {
                while (true) {
                    synchronized (queue) {
                        while (queue.isEmpty()) {
                            try {
                                queue.wait();
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                return;
                            }
                        }
                        int val = queue.poll();
                        System.out.println("Consumed: " + val);
                        queue.notifyAll();
                    }
                }
            }
        }
    }
    

解释代码:

  1. 同步块中的wait():

    • 在生产者中,当队列已满时调用queue.wait(),让生产者线程等待,直到有空间可以生产新的产品。
    • 在消费者中,当队列为空时调用queue.wait(),让消费者线程等待,直到有新的产品可以消费。
  2. 同步块中的notifyAll():

    • 在生产者中,当生产了新产品后调用queue.notifyAll(),通知等待的消费者线程可以继续消费。
    • 在消费者中,当消费了产品后调用queue.notifyAll(),通知等待的生产者线程可以继续生产。
  3. 线程同步和通信:

    • 生产者和消费者都使用queue对象作为同步锁,确保对队列的访问是线程安全的。
    • 通过wait()notifyAll()方法,生产者和消费者可以有效地进行线程间通信,避免忙等待,提高效率。

这种机制在实际项目中非常重要,能够帮助我们设计出高效且安全的多线程应用程序。通过对这些方法的深入理解和应用,能够有效处理线程间的协调和通信问题。

Condition接口

Condition接口提供了一种更灵活的等待/通知机制,与传统的Objectwait()notify()notifyAll()方法相比,Condition可以与任意类型的锁配合使用,而不仅限于内置的监视器锁Condition常与Lock接口结合使用。

内置监视器锁即使用sychronized时产生的锁,这个锁是被JVM管理的,而Lock接口的锁是用户管理的,比如利用redis管理的锁

Condition接口的使用(与Lock结合)
  • Condition必须与Lock结合使用,常用的Lock实现是ReentrantLock
  • Condition对象由Lock对象创建。
  • 使用await()代替wait(),使用signal()代替notify(),使用signalAll()代替notifyAll()
Condition接口的主要方法
  1. await()

    • 使当前线程等待,直到被通知(signal)或被中断。
    • 当前线程必须持有与Condition关联的锁。
    • 调用await()会释放锁,并且等待其他线程通知。
  2. signal()

    • 唤醒一个等待在Condition上的线程。
    • 唤醒的线程从await()方法返回并重新获取与Condition关联的锁。
  3. signalAll()

    • 唤醒所有等待在Condition上的线程。
    • 唤醒的线程依次从await()方法返回并重新获取与Condition关联的锁。
使用示例

下面是一个使用Condition接口实现生产者-消费者模型的示例:

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;

public class ProducerConsumerWithCondition {
    private static final int CAPACITY = 5;
    private final Queue<Integer> queue = new LinkedList<>();
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private int value = 0;

    public static void main(String[] args) {
        ProducerConsumerWithCondition example = new ProducerConsumerWithCondition();
        Thread producerThread = new Thread(example.new Producer(), "Producer");
        Thread consumerThread = new Thread(example.new Consumer(), "Consumer");
        producerThread.start();
        consumerThread.start();
    }

    class Producer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == CAPACITY) {
                        notFull.await();
                    }
                    queue.offer(value++);
                    System.out.println("Produced: " + (value - 1));
                    notEmpty.signal();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (queue.isEmpty()) {
                        notEmpty.await();
                    }
                    int val = queue.poll();
                    System.out.println("Consumed: " + val);
                    notFull.signal();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}
解释代码
  1. 锁定和解锁

    • 生产者和消费者在操作共享资源(队列)时都使用lock.lock()lock.unlock()来确保线程安全。
  2. 等待和通知

    • 生产者在队列满时调用notFull.await()等待,直到有空余空间。
    • 生产者在生产了新产品后调用notEmpty.signal()通知消费者有新产品可用。
    • 消费者在队列为空时调用notEmpty.await()等待,直到有新产品可消费。
    • 消费者在消费了产品后调用notFull.signal()通知生产者可以继续生产。
  3. 灵活性和可扩展性

    • Condition接口提供了比Objectwait()notify()更灵活的线程通信机制,可以创建多个条件变量(如notFullnotEmpty),使得代码更具可读性和可维护性。

BlockingQueue

之前已经发过

线程调度和任务执行

Executor 框架

Java 的 Executor 框架提供了一种异步执行任务的机制,可以轻松地将任务的提交与执行策略分离。主要接口和类包括 ExecutorExecutorServiceScheduledExecutorService

  1. Executor 接口

    • 定义了一个单一的执行方法 execute(Runnable command),用于执行提交的任务。
    Executor executor = new Executor() {
        @Override
        public void execute(Runnable command) {
            new Thread(command).start();
        }
    };
    executor.execute(() -> System.out.println("Task executed!"));
    
  2. ExecutorService 接口

    • 扩展了 Executor 接口,提供了管理终止的方法以及产生 Future 以跟踪一个或多个异步任务的执行情况。
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    Future<String> future = executorService.submit(() -> {
        Thread.sleep(1000);
        return "Task completed!";
    });
    try {
        String result = future.get(); // 获取任务的结果
        System.out.println(result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        executorService.shutdown(); // 关闭 ExecutorService
    }
    
  3. ScheduledExecutorService 接口

    • 提供了在给定的延迟后运行或定期执行任务的方法。
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    scheduledExecutorService.schedule(() -> System.out.println("Task executed after delay"), 5, TimeUnit.SECONDS);
    scheduledExecutorService.scheduleAtFixedRate(() -> System.out.println("Periodic task executed"), 0, 10, TimeUnit.SECONDS);
    

Fork/Join 框架

处理大任务的分解和并行执行

Fork/Join 框架是 Java 7 引入的,用于并行执行任务,通过将大任务分解成多个小任务并行处理来提高性能。ForkJoinPool 类是核心,使用 ForkJoinTask 的子类 RecursiveTask(有返回值)和 RecursiveAction(无返回值)来实现任务。

  1. ForkJoinPool

    • ForkJoinPool 是一个特殊的线程池,设计用于执行大量的短小任务,使用工作窃取算法来平衡负载。
  2. RecursiveTask(有返回值):

    • 通过继承 RecursiveTask 类来定义任务,并在 compute() 方法中实现任务的分解和合并。
    import java.util.concurrent.RecursiveTask;
    import java.util.concurrent.ForkJoinPool;
    
    public class FibonacciTask extends RecursiveTask<Integer> {
        private final int n;
    
        public FibonacciTask(int n) {
            this.n = n;
        }
    
        @Override
        protected Integer compute() {
            if (n <= 1) {
                return n;
            }
            FibonacciTask f1 = new FibonacciTask(n - 1);
            f1.fork(); // 分解任务并异步执行
            FibonacciTask f2 = new FibonacciTask(n - 2);
            return f2.compute() + f1.join(); // 计算任务并等待结果
        }
    
        public static void main(String[] args) {
            ForkJoinPool pool = new ForkJoinPool();
            FibonacciTask task = new FibonacciTask(10);
            int result = pool.invoke(task);
            System.out.println("Fibonacci(10) = " + result);
        }
    }
    
  3. RecursiveAction(无返回值):

    • 类似于 RecursiveTask,但 compute() 方法没有返回值。
    import java.util.concurrent.RecursiveAction;
    import java.util.concurrent.ForkJoinPool;
    
    public class SimpleAction extends RecursiveAction {
        private final int workload;
    
        public SimpleAction(int workload) {
            this.workload = workload;
        }
    
        @Override
        protected void compute() {
            if (workload > 1) {
                SimpleAction subtask1 = new SimpleAction(workload / 2);
                SimpleAction subtask2 = new SimpleAction(workload / 2);
                invokeAll(subtask1, subtask2);
            } else {
                System.out.println("Workload: " + workload);
            }
        }
    
        public static void main(String[] args) {
            ForkJoinPool pool = new ForkJoinPool();
            SimpleAction action = new SimpleAction(10);
            pool.invoke(action);
        }
    }
    

总结

  1. Executor 框架

    • Executor:提供基础的任务执行机制。
    • ExecutorService:管理线程池和任务提交,提供 Future 接口。
    • ScheduledExecutorService:处理延迟和周期性任务。
  2. Fork/Join 框架

    • 适用于分解和并行处理大任务,通过 ForkJoinPoolForkJoinTask 的子类(如 RecursiveTaskRecursiveAction)实现高效并行计算。

工作窃取算法

工作窃取算法(Work Stealing Algorithm)是一种用于并行计算的负载均衡策略。该算法的基本思想是:当一个线程完成了自己任务队列中的所有任务时,它会窃取其他线程任务队列中的任务来执行。这种方式可以有效地平衡各个线程的工作负载,从而提高多核处理器的利用率。

工作窃取算法的原理

  1. 任务分割

    • 将大任务分割成多个小任务,每个小任务可以独立执行。
  2. 双端队列

    • 每个线程都有一个双端队列(Deque),线程从队列的一端(通常是末尾)取任务来执行。
  3. 窃取任务

    • 当一个线程完成了自己队列中的任务,它会去尝试从其他线程的队列头部窃取任务来执行。
  4. 负载均衡

    • 通过窃取任务,可以使得各个线程的工作量大致均衡,避免某些线程过于忙碌而其他线程空闲的情况。

Java中的工作窃取算法

在Java中,工作窃取算法主要体现在ForkJoinPool类中。ForkJoinPool是一个线程池实现,适用于大规模并行任务的执行。它使用工作窃取算法来实现任务的负载均衡。

ForkJoinPool的实现

ForkJoinPool使用了一个专门设计的双端队列ForkJoinTask来存放任务,每个工作线程都有自己的任务队列。当一个工作线程完成了自己队列中的任务时,它会去窃取其他工作线程的任务队列中的任务。

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class WorkStealingExample {
    static class FibonacciTask extends RecursiveTask<Integer> {
        private final int n;

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

        @Override
        protected Integer compute() {
            if (n <= 1) {
                return n;
            }
            FibonacciTask f1 = new FibonacciTask(n - 1);
            FibonacciTask f2 = new FibonacciTask(n - 2);
            f1.fork(); // 分解任务并异步执行
            return f2.compute() + f1.join(); // 计算任务并等待结果
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        FibonacciTask task = new FibonacciTask(10);
        int result = pool.invoke(task);
        System.out.println("Fibonacci(10) = " + result);
    }
}

工作窃取算法的优势

  1. 高效利用多核处理器

    • 工作窃取算法通过动态负载均衡,充分利用多核处理器的计算能力。
  2. 简化编程模型

    • 通过任务的递归分解和窃取执行,程序员可以更容易地编写并行算法。
  3. 减少线程间的锁争用

    • 每个线程主要操作自己的任务队列,只有在需要窃取任务时才会访问其他线程的队列,从而减少了锁争用和同步开销。

工作窃取算法的挑战

  1. 任务分解的粒度

    • 如果任务分解得过细,会导致任务调度和管理的开销增加;如果任务分解得过粗,会影响并行执行的效率。
  2. 线程间通信开销

    • 虽然工作窃取算法减少了锁争用,但线程间的任务窃取仍然会有一定的通信和同步开销。
  3. 任务窃取的代价

    • 任务窃取的频率和代价需要合理平衡,否则可能会影响整体性能。

并行计算常见负载策略

在并行计算中,负载均衡策略是确保任务在多个处理器上均匀分布,以提高效率和性能。除了工作窃取算法,以下是一些常见的负载均衡策略:

1. 静态负载均衡

定义: 在任务执行之前,任务的分配就已经确定,不会在执行过程中动态调整。
适用场景: 任务数量和执行时间已知且固定。

方法
  • 轮询法(Round Robin):
    • 任务按顺序分配给处理器,每个处理器轮流获取任务。
  • 随机分配(Random Assignment):
    • 任务随机分配给处理器。
  • 任务划分(Task Partitioning):
    • 根据任务的特性进行预先划分,分配到各处理器。

2. 动态负载均衡

定义: 在任务执行过程中,任务的分配可以动态调整,以应对任务负载的不均衡。
适用场景: 任务执行时间不可预测或任务数量动态变化。

方法
  • 集中式调度(Centralized Scheduling):
    • 由一个中央调度器负责监控和分配任务,确保所有处理器负载均衡。
  • 分布式调度(Distributed Scheduling):
    • 每个处理器都有一定的自主权,可以根据自身负载情况向其他处理器请求或转移任务。

3. 递归分割(Recursive Bisection)

定义: 将任务递归地分割成更小的子任务,并分别分配到不同的处理器。
适用场景: 适用于多层次分解的任务,如矩阵乘法和快速傅里叶变换。

4. 图划分(Graph Partitioning)

定义: 将任务表示为图结构,节点表示任务,边表示任务之间的依赖关系,然后对图进行划分以最小化跨处理器边的数量。
适用场景: 任务间有复杂依赖关系的并行计算,如有限元分析。

5. 随机游走(Random Walk)

定义: 通过随机游走的方式动态调整任务分配,使得任务逐步趋向均匀分布。
适用场景: 任务之间相互独立,且每个任务执行时间相似。

6. 区域划分(Domain Decomposition)

定义: 将计算域分成若干子域,每个子域分配给一个处理器,并通过边界条件进行交互。
适用场景: 数值模拟和科学计算,如气象模拟和流体动力学。

7. 基于代理的调度(Agent-Based Scheduling)

定义: 使用智能代理监控和调整任务负载,根据任务和处理器的状态动态进行任务分配。
适用场景: 任务复杂且动态变化的场景,如云计算和分布式计算。

8. 需求驱动调度(Demand-Driven Scheduling)

定义: 任务根据需求动态请求计算资源,资源根据任务负载和优先级进行分配。
适用场景: 资源有限且任务负载不均的场景,如实时系统和分布式数据库。

9. 基于机器学习的负载均衡

定义: 使用机器学习算法预测任务负载和处理器性能,动态调整任务分配以达到最佳负载均衡。
适用场景: 负载模式复杂且变化频繁的场景,如大数据处理和人工智能计算。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值