18个并发场景的设计模式详解,有没有你的盲区
======================
与23种设计模式考虑的场景不同,在分布式并发应用中,还有一些常用的并发模式,V哥今天给大家整理了18种并发下的设计模式,从概念,原理分析,示例代码和应用场景方面来全面介绍,这会帮助你在并发编程中即学即用。
多线程并发设计模式是在多线程程序设计中经常使用的一些解决方案,它们帮助解决特定的问题,提高程序的性能和可维护性。以下是一些常见的多线程并发设计模式:
1. 单例模式(Singleton):
2. 不可变对象模式(Immutable Object):
3. 线程局部存储模式(Thread Local Storage):
4. 生产者-消费者模式(Producer-Consumer):
5. 读者-写者模式(Read-Write Lock):
6. 工作队列模式(Worker Thread):
7. 线程池模式(Thread Pool):
8. future模式(Future):
9. 屏障模式(Barrier):
10. 计数信号量模式(Counting Semaphore):
11. 守护线程模式(Daemon Thread):
12. 枚举线程模式(Thread Enumeration):
13. 线程中断模式(Thread Interruption):
14. 两阶段终止模式(Two-Phase Termination):
15. 工作窃取模式(Work Stealing):
16、保护性暂挂模式(Guarded Suspension)
17、放弃模式(Balking)
18. Thread-Per-Message 模式
这些设计模式是处理多线程并发问题时的重要工具,可以帮助开发者编写出高效、安全、可维护的并发程序。在实际应用中,可能需要根据具体情况选择合适的设计模式,或者将几种模式组合使用。
下面 V 哥一个一个详细来介绍。
1、单例模式(Singleton)
概念
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这意味着一旦创建了一个类的实例,所有对该类的进一步调用都将返回这个已创建的实例,而不是创建一个新的实例,这个跟我们普通写的单例是同一个,只是加入并发场景的需求。
实现原理
- 私有构造函数:防止外部通过new关键字创建类的实例。
- 静态实例变量:保存类的唯一实例。
- 全局访问点:通常是一个静态方法,用于获取类的实例。
并发代码示例
在多线程环境中,实现单例模式需要考虑线程安全问题。以下是使用“双重检查锁定”(Double-Checked Locking)的线程安全单例模式实现:
public class Singleton {
private static volatile Singleton instance; // 使用volatile确保可见性和有序性
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 同步锁
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
在上面的代码中,getInstance方法首先检查实例是否已创建,如果没有,则进入同步块。在同步块内,再次检查实例是否已创建,这样做是为了防止在多线程环境下多次创建实例。使用volatile关键字确保instance变量的可见性和有序性,避免指令重排问题。
应用场景
- 配置管理:应用程序通常需要一个全局的配置管理器来存储和访问配置信息。
- 数据库连接池:数据库连接池通常需要作为一个单例来管理数据库连接。
- 日志系统:日志系统通常需要一个全局的访问点来记录日志信息。
- 驱动管理:操作系统的驱动程序通常也是单例,因为它们需要全局访问和控制硬件资源。
注意事项
- 确保单例类不会被克隆、序列化或反射攻击。
- 如果单例类需要延迟加载,使用双重检查锁定时要确保实例变量的声明是volatile的,以避免指令重排问题。
单例模式比较简单,也是Java设计中非常基础且广泛使用的设计模式,它在许多框架和应用程序中都有应用。正确实现单例模式可以确保资源的有效管理和全局访问的一致性。
2、不可变对象模式(Immutable Object)
概念
不可变对象模式是一种创建型设计模式,它指的是一旦创建了一个对象,其状态就不能被修改。不可变对象在创建后其内部状态保持不变,任何试图修改状态的尝试都会创建一个新的对象。
实现原理
- 所有属性都是final的:这意味着一旦初始化后,属性值不能被改变。
- 不提供修改状态的方法:不可变对象不提供任何可以修改其状态的方法。
- 通过构造函数初始化所有属性:在对象创建时,必须通过构造函数初始化所有属性。
- 深度复制:如果对象包含可变对象,需要确保这些对象在创建时也是不可变的,或者在返回时创建它们的副本。
并发代码示例
不可变对象天然支持并发,因为它们的状态不可变,不会出现线程安全问题。以下是一个简单的不可变对象的示例:
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public ImmutablePerson setName(String newName) {
return new ImmutablePerson(newName, age);
}
public ImmutablePerson setAge(int newAge) {
return new ImmutablePerson(name, newAge);
}
}
在上面的代码中,ImmutablePerson类的属性name和age都是final的,确保了它们在对象创建后不会被修改。如果需要修改这些属性,setName和setAge方法会创建一个新的ImmutablePerson对象,这个一个V哥觉得最简单的并发模式,只要理解其中的含义就可以实现。
应用场景
- 字符串:Java中的String类就是一个典型的不可变对象,它在许多场景下都被使用。
- 枚举:枚举值在创建后也是不可变的,适用于表示一组固定的常量。
- 基本数据类型的包装类:如Integer、Long等,它们都是不可变的。
- 配置对象:在多线程环境中,使用不可变对象作为配置可以确保配置的一致性。
- 函数参数和返回值:不可变对象作为函数参数和返回值可以避免副作用。
注意事项
- 确保所有属性都是不可变的,如果属性是对象,那么这个对象也必须是不可变的。
- 如果对象中包含可变对象,需要确保在返回这些对象时不会暴露它们的可变接口,可以通过返回副本或者使用不可变包装类来实现。
不可变对象模式在多线程环境中非常有用,因为它可以避免并发访问时的同步问题,同时也可以提高程序的安全性和可预测性。
3、线程局部存储模式(Thread Local Storage)
概念
线程局部存储模式是一种行为设计模式,它允许在多线程环境下为每个线程维护一个独立的变量副本。这意味着每个线程都有自己独立的变量实例,从而避免了线程安全问题。
实现原理
- ThreadLocal类:Java提供了ThreadLocal类来支持线程局部存储。ThreadLocal为每个使用该变量的线程提供独立的变量副本,通过这个副本,每个线程都可以独立地改变自己的副本而不影响其他线程的副本。
- 副本的创建和获取:ThreadLocal提供了get和set方法来获取和设置当前线程的变量副本。
并发代码示例
以下是一个使用ThreadLocal的示例,它为每个线程提供了一个独立的变量副本:
public class ThreadLocalExample {
// 定义一个ThreadLocal变量,用于存储线程级别的变量
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void setThreadLocalValue(String value) {
threadLocal.set(value);
}
public static String getThreadLocalValue() {
return threadLocal.get();
}
public static void main(String[] args) {
// 在主线程中设置和获取ThreadLocal变量的值
setThreadLocalValue("Main Thread Value");
System.out.println("Main Thread Value: " + getThreadLocalValue());
// 创建一个新线程并设置和获取ThreadLocal变量的值
Thread thread = new Thread(() -> {
setThreadLocalValue("Child Thread Value");
System.out.println("Child Thread Value: " + getThreadLocalValue());
});
thread.start();
}
}
在这个示例中,主线程和子线程分别设置和获取了ThreadLocal变量的值。由于ThreadLocal为每个线程提供了独立的副本,因此两个线程之间的值是相互独立的。
应用场景
- 数据库连接管理:在多线程应用程序中,为每个线程分配一个独立的数据库连接。
- 线程特定的全局变量:例如,线程级别的用户身份验证信息或事务ID。
- 资源共享:当资源需要在多个方法中被访问,但又不想通过参数传递时,可以使用ThreadLocal来存储资源。
- 线程间的数据隔离:在Web应用中,为每个请求分配一个独立的线程,使用ThreadLocal来存储请求特定的数据。
注意事项
- ThreadLocal变量不会自动清理,如果不恰当地使用,可能会导致内存泄漏。因此,在不需要时应该调用remove方法来清除线程的局部变量。
- ThreadLocal适用于需要线程封闭的场景,如果多个线程需要共享数据,那么ThreadLocal不是正确的选择。
线程局部存储模式在多线程编程中非常有用,它提供了一种简单的方式来管理和维护线程级别的数据,同时避免了同步的开销。
4、生产者-消费者模式(Producer-Consumer)
概念
生产者-消费者模式是一种行为设计模式,它通过将数据的生成(生产者)和数据的处理(消费者)分离来解决这个问题。在这种模式中,生产者负责创建数据,放入一个共享的数据结构中,而消费者则从该数据结构中取出数据进行处理。这种模式可以有效地解决生产者和消费者之间的同步问题,并允许多个生产者和消费者同时操作。
实现原理
- 共享数据结构:通常使用队列作为共享的数据结构,生产者将数据放入队列中,消费者从队列中取出数据。
- 同步:生产者和消费者需要同步对共享数据结构的访问,以避免并发问题。可以使用锁、信号量、阻塞队列等机制来实现同步。
- 阻塞和唤醒:当队列满时,生产者应该阻塞等待;当队列空时,消费者应该阻塞等待。当队列状态改变时,需要唤醒相应的生产者或消费者。
并发代码示例
以下是一个使用BlockingQueue实现的生产者-消费者模式的示例:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> q) {
queue = q;
}
public void run() {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
System.out.println("Produced: " + i);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> q) {
queue = q;
}
public void run() {
try {
while (true) {
int value = queue.take();
System.out.println("Consumed: " + value);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
}
}
在这个示例中,Producer线程将数据放入BlockingQueue中,而Consumer线程从BlockingQueue中取出数据进行处理。BlockingQueue自动处理了同步和阻塞逻辑。
应用场景
- 消息队列:在消息驱动的系统中,生产者-消费者模式被广泛用于消息队列的管理。
- 任务队列:在多线程应用程序中,用于分配和执行任务。
- 异步处理:当需要异步处理数据时,生产者-消费者模式可以有效地分离数据的生成和处理。
- 资源池管理:例如,数据库连接池或线程池的管理。
注意事项
- 确保生产者和消费者之间的同步逻辑正确实现,以避免死锁或数据不一致的问题。
- 选择合适的共享数据结构,例如BlockingQueue、ConcurrentLinkedQueue等,以简化同步逻辑。
- 考虑生产者和消费者的数量和比例,以及队列的大小,以优化系统性能。
生产者-消费者模式在多线程编程中非常重要,它提供了一种高效的方式来处理数据的生成和处理,同时保持了系统组件之间的解耦,这个模式也是经典的多线程协同工作案例,记得 V 哥在基础阶段讲解多线程时就采用了这个案例。
5、读者-写者模式(Read-Write Lock)
概念
读者-写者模式是一种同步机制,允许多个读者同时访问数据,但在写者访问时,其他的读者或写者都会被阻塞。这种模式适用于读操作远多于写操作的场景,它通过减少锁的竞争来提高系统的并发性能。
实现原理
- 读写锁:使用ReadWriteLock接口和ReentrantReadWriteLock类来实现。这种锁有两个锁,一个用于读操作,一个用于写操作。
- 锁的获取和释放:读者获取读锁,写者获取写锁。读锁可以被多个读者同时持有,而写锁是独占的。
- 锁的升级和降级:在某些情况下,可能需要从读锁升级到写锁,或者从写锁降级到读锁。
并发代码示例
以下是一个使用ReentrantReadWriteLock实现的读者-写者模式的示例:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class SharedResource {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
private String data;
public void read() {
readLock.lock();
try {
// 读取数据
System.out.println("Reading data: " + data);
} finally {
readLock.unlock();
}
}
public void write(String newData) {
writeLock.lock();
try {
// 写入数据
data = newData;
System.out.println("Writing data: " + data);
} finally {
writeLock.unlock();
}
}
}
public class ReadWriteLockExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
// 创建多个读者线程
for (int i = 0; i < 5; i++) {
new Thread(resource::read).start();
}
// 创建写者线程
new Thread(() -> resource.write("New Data")).start();
}
}
在这个示例中,SharedResource类包含了一个ReadWriteLock,用于保护数据data。读者线程调用read方法来读取数据,而写者线程调用write方法来写入数据。读锁和写锁保证了数据的一致性和并发访问的控制。
应用场景
- 缓存:当缓存需要被多个读者同时访问,但更新操作较少时。
- 数据共享:在多线程环境中,当数据需要被多个线程读取,但写入操作相对较少时。
- 配置管理:应用程序的配置信息通常读多写少,适合使用读写锁。
- 日志系统:日志系统通常允许并发读取,但写入操作需要独占访问。
注意事项
- 确保在读操作和写操作时正确地获取和释放锁,以避免死锁或数据不一致的问题。
- 考虑锁的竞争和线程的饥饿问题,特别是在写操作频繁的情况下。
- 在某些场景下,可能需要考虑读写锁的性能开销,特别是在读操作非常频繁的情况下。
V哥认为,读者-写者模式在多线程编程中非常有用,它提供了一种高效的方式来处理读多写少
的场景,同时保持了数据的一致性和并发访问的控制。
6、工作队列模式(Worker Thread)
概念
工作队列(工作线程)模式是一种行为设计模式,它将任务的提交与任务的执行分离。在这种模式中,任务被提交到一个工作队列中,而后台线程(工作者线程)从队列中取出任务并执行。这种模式可以有效地管理任务的执行,提高系统的并发性能。
实现原理
- 任务队列:使用一个队列来存储提交的任务。
- 工作者线程:创建一组后台线程作为工作者,它们不断地从任务队列中取出任务并执行。
- 任务提交:客户端将任务提交到队列中,不需要关心任务的执行细节。
并发代码示例
以下是一个使用ExecutorService实现的工作队列模式的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task: " + taskId);
// 执行任务逻辑
}
}
public class WorkerThreadExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池作为工作者线程
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到工作队列
for (int i = 0; i < 10; i++) {
executor.submit(new Task(i));
}
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务完成
executor.awaitTermination(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,ExecutorService创建了一个固定大小的线程池,用于执行提交的任务。客户端通过调用submit方法将Task对象提交到线程池中,工作者线程会从队列中取出任务并执行。
应用场景
- 任务处理:在Web服务器、消息队列处理器等应用中,用于处理大量的短生命周期的任务。
- 异步执行:当需要异步执行任务,而不希望阻塞主线程时。
- 并发执行:当需要并发执行多个任务以提高性能时。
注意事项
- 选择合适的线程池大小,以优化资源利用和性能。
- 管理好任务队列,避免队列过大导致内存溢出。
- 考虑线程池的关闭和任务的超时处理,以确保系统的稳定性和健壮性。
工作队列模式在多线程编程中同样非常有用,它提供了一种高效的方式来管理和执行任务,同时保持了系统组件之间的解耦。 V 哥提醒一下,你是不是隐隐约约想到了消息对列,是的,就是它。
7、线程池模式(Thread Pool)
概念
线程池模式是一种创建型设计模式,它通过复用一组线程来执行多个任务,从而减少了线程创建和销毁的开销。在这种模式中,线程池管理一个线程集合,线程池负责分配任务给这些线程,并在任务完成后将线程返回到池中,以便重新使用。
实现原理
- 线程复用:线程池中的线程是预创建的,它们被重复使用来执行多个任务,而不是每次执行任务时都创建新的线程。
- 任务队列:线程池通常与一个任务队列关联,客户端提交的任务被放入队列中,线程池中的线程从队列中取出任务执行。
- 线程管理:线程池负责管理线程的生命周期,包括线程的创建、销毁和线程数的动态调整。
并发代码示例
以下是一个使用ExecutorService实现的线程池模式的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task: " + taskId);
// 执行任务逻辑
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
executor.submit(new Task(i));
}
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务完成
executor.awaitTermination(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,ExecutorService创建了一个固定大小的线程池,用于执行提交的任务。客户端通过调用submit方法将Task对象提交到线程池中,线程池中的线程会从队列中取出任务并执行。
应用场景
- Web服务器:处理大量的HTTP请求时,使用线程池可以减少线程创建的开销。
- 数据库连接池:管理和复用数据库连接,提高数据库访问效率。
- 消息队列处理器:处理消息队列中的消息时,线程池可以提高消息处理的速度。
- 异步执行:当需要异步执行任务,而不希望阻塞主线程时。
注意事项
- 选择合适的线程池大小,以优化资源利用和性能。
- 管理好任务队列,避免队列过大导致内存溢出。
- 考虑线程池的关闭和任务的超时处理,以确保系统的稳定性和健壮性。
线程池模式在多线程编程中非常有用,它提供了一种高效的方式来管理和执行任务,同时减少了线程创建和销毁的开销,使用线程池非常常见了,V 哥觉得,你在面试时被问到线程池的实现原理时,这个实现原理可以用上。
8、Future模式(Future)
概念
Future模式是一种行为设计模式,它允许异步计算的结果在未来某个时间点获取。在这种模式中,一个任务被提交给一个执行器(如线程池),并返回一个Future对象,该对象可以用来检查任务是否已完成,以及获取任务的结果。
实现原理
- 执行器:使用一个执行器(如ExecutorService)来提交任务。
- Future对象:执行器返回一个Future对象,该对象提供了检查任务是否已完成和获取结果的方法。
- 异步计算:任务在执行器中执行,客户端可以在不等待任务完成的情况下继续执行其他操作。
并发代码示例
以下是一个使用ExecutorService和Future实现的Future模式的示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class LongRunningTask implements Callable<String> {
@Override
public String call() throws Exception {
// 执行耗时任务
Thread.sleep(2000);
return "Task completed";
}
}
public class FutureExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交任务到线程池
Future<String> future = executor.submit(new LongRunningTask());
// 主线程继续执行其他操作
System.out.println("Main thread is doing other work");
// 等待任务完成
try {
String result = future.get();
System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
executor.shutdown();
}
}
在这个示例中,LongRunningTask是一个Callable任务,它执行一个耗时操作并返回一个结果。Future对象future被用来检查任务是否已完成,并获取任务的结果。
应用场景
- 异步计算:当需要执行一个耗时操作,但不希望阻塞主线程时。
- 并行计算:在需要并行执行多个任务时,可以使用Future来异步获取结果。
- 资源池管理:例如,数据库连接池或线程池的管理。
- 消息队列处理器:处理消息队列中的消息时,可以使用Future来异步获取结果。
注意事项
- 确保正确处理Future对象,以避免在任务未完成时尝试获取结果。
- 考虑任务的执行时间,以避免Future.get()方法长时间阻塞主线程。
- 考虑任务的超时处理,以确保系统不会因为等待未完成的任务而挂起。
Future模式在多线程编程中也是常用的不要不要的,它提供了一种高效的方式来执行异步计算,同时保持了系统组件之间的解耦,Future必须拿下。
8、屏障模式(Barrier)
概念
屏障模式是一种同步机制,它允许一组线程在到达一个屏障点时同时阻塞,直到所有线程都到达这个点,屏障才会打开,所有线程才会继续执行。这种模式通常用于多线程任务的分阶段执行,确保所有线程都完成一个阶段后,才能开始下一个阶段。
实现原理
- CyclicBarrier:Java提供了CyclicBarrier类来实现屏障模式。
- 线程到达屏障:当线程到达屏障点时,它会被阻塞,直到所有线程都到达这个点。
- 屏障打开:当所有线程都到达屏障点时,屏障会打开,所有线程继续执行。
并发代码示例
以下是一个使用CyclicBarrier实现的屏障模式的示例:
import java.util.concurrent.CyclicBarrier;
public class BarrierExample {
public static void main(String[] args) {
int numberOfThreads = 5;
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i;
new Thread(() -> {
try {
System.out.println("Thread " + threadId + " is waiting at the barrier");
barrier.await();
System.out.println("Thread " + threadId + " has passed the barrier");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,CyclicBarrier被创建,并设置为当有5个线程到达屏障点时打开。每个线程在到达屏障点时都会被阻塞,直到所有线程都到达这个点。当所有线程都到达屏障点时,屏障会打开,所有线程继续执行。
应用场景
- 多阶段任务执行:当需要将一个任务分为多个阶段,并确保所有线程都完成一个阶段后才能开始下一个阶段时。
- 并行计算:在需要将一个大任务分解为多个小任务,并确保所有小任务都完成后才能开始下一步计算时。
- 多线程测试:在需要测试多线程程序时,可以使用屏障来确保所有线程都执行到某个点。
注意事项
- 确保正确处理CyclicBarrier的异常,以避免线程挂起。
- 考虑屏障的打开次数,CyclicBarrier可以被重用,但需要注意其打开次数的上限。
下面这句话V哥写了很多遍了,但还是要说,下面还会再说,不重要的话V哥写它干毛用:屏障模式在多线程编程中非常有用,它提供了一种高效的方式来确保多线程任务在特定点同步执行,从而提高了系统的并发性能。
10、计数信号量模式(Counting Semaphore)
概念
计数信号量模式是一种同步机制,它允许控制对共享资源的访问。在这种模式中,信号量维护一个许可数,线程必须获得一个许可才能访问资源。当所有许可都被占用时,试图获取许可的线程会被阻塞,直到有许可被释放。
实现原理
- 许可数:信号量维护一个许可数,每个线程在访问资源之前必须获取一个许可。
- 获取许可:线程尝试获取一个许可,如果许可数大于0,线程可以继续执行;如果许可数为0,线程会被阻塞。
- 释放许可:线程完成对资源的访问后,释放一个许可,使得其他阻塞的线程可以获取许可并继续执行。
并发代码示例
以下是一个使用Semaphore实现的计数信号量模式的示例:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int maxThreads = 5; // 最大并发线程数
int permits = 2; // 可用许可数
Semaphore semaphore = new Semaphore(permits, true); // 公平模式
for (int i = 0; i < maxThreads; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("Thread " + Thread.currentThread().getName() + " is working");
Thread.sleep(1000); // 模拟工作
semaphore.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,Semaphore被创建,并设置为有2个可用许可。每个线程在访问资源之前必须获取一个许可,当所有许可都被占用时,其他线程会被阻塞。当线程完成对资源的访问后,会释放一个许可,使得其他阻塞的线程可以获取许可并继续执行。
应用场景
- 资源池管理:例如,数据库连接池或线程池的管理。
- 限制并发访问:当需要限制对某个资源的并发访问时。
- 多线程测试:在需要测试多线程程序时,可以使用信号量来控制对资源的访问。
注意事项
- 确保正确处理信号量的异常,以避免线程挂起。
- 考虑信号量的公平性,即线程是否按照请求许可的顺序获取许可。
计数信号量模式在多线程编程中非常有用,它提供了一种高效的方式来控制对共享资源的并发访问,从而提高了系统的并发性能。
11、守护线程模式(Daemon Thread)
概念
守护线程模式是一种线程配置模式,它指定了线程的守护状态。当一个Java虚拟机中所有非守护线程结束时,Java虚拟机会退出。守护线程的目的是在主线程之外执行一些后台任务,这些任务不必须完成,因为它们不会阻止Java虚拟机退出。
实现原理
- 守护线程:在创建线程时,可以通过调用setDaemon(true)方法将线程设置为守护线程。
- 主线程:Java虚拟机中的主线程是非守护线程,只有当所有非守护线程结束时,Java虚拟机才会退出。
并发代码示例
以下是一个使用守护线程模式的示例:
public class DaemonThreadExample {
public static void main(String[] args) {
// 创建一个非守护线程
Thread nonDaemonThread = new Thread(() -> {
System.out.println("Thread is running");
while (true) {
// 执行非守护线程的任务
}
});
nonDaemonThread.start();
// 创建一个守护线程
Thread daemonThread = new Thread(() -> {
System.out.println("Thread is running as a daemon");
while (true) {
// 执行守护线程的任务
}
}, "Daemon Thread");
daemonThread.setDaemon(true);
daemonThread.start();
}
}
在这个示例中,nonDaemonThread是一个非守护线程,它会无限循环执行任务。daemonThread是一个守护线程,它也会无限循环执行任务,但是当所有非守护线程结束时,Java虚拟机会退出,守护线程也会随之结束。
应用场景
- 后台任务:当需要执行一些后台任务,如垃圾回收、日志记录等时。
- 资源清理:当需要执行一些资源清理任务时,可以使用守护线程来执行。
- 网络监听:当需要监听网络连接时,可以使用守护线程来执行。
注意事项
- 守护线程不保证一定会执行,因为当所有非守护线程结束时,Java虚拟机会退出。
- 守护线程的优先级通常较低,因此在多线程环境中,守护线程可能会被优先级较高的非守护线程抢占。
守护线程模式在多线程编程中非常有用,它提供了一种高效的方式来执行后台任务,同时不会阻止Java虚拟机退出。
12、枚举线程模式(Thread Enumeration)
概念
枚举线程模式是一种操作设计模式,它提供了一种遍历系统中所有线程的方法。在这种模式中,可以获取当前Java虚拟机中所有线程的枚举,并对其进行操作,如获取线程信息、修改线程状态等。
实现原理
- 线程枚举:Java提供了Thread.getAllStackTraces()方法来获取所有线程的堆栈跟踪信息。
- 线程操作:可以通过遍历线程枚举来获取线程信息,并进行操作。
并发代码示例
以下是一个使用Thread.getAllStackTraces()实现的枚举线程模式的示例:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadEnumerationExample {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds());
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("Thread ID: " + threadInfo.getThreadId());
System.out.println("Thread Name: " + threadInfo.getThreadName());
System.out.println("Thread State: " + threadInfo.getThreadState());
System.out.println("Stack Trace: " + threadInfo.getStackTrace());
System.out.println();
}
}
}
在这个示例中,ThreadMXBean被用来获取所有线程的信息。getThreadInfo方法返回一个包含所有线程信息的数组,通过遍历这个数组,可以获取每个线程的ID、名称、状态和堆栈跟踪信息。
应用场景
- 多线程测试:在需要测试多线程程序时,可以使用线程枚举来获取线程信息。
- 资源清理:当需要执行一些资源清理任务时,可以使用线程枚举来获取需要清理的线程。
- 线程监控:在需要监控系统中线程的状态时,可以使用线程枚举来获取线程信息。
注意事项
- 确保正确处理线程枚举,以避免线程挂起。
- 考虑线程枚举的性能开销,因为它可能会对系统性能产生影响。
枚举线程模式在多线程编程中非常有用,它提供了一种高效的方式来获取和操作线程,从而提高了系统的可维护性和健壮性。
13、线程中断模式(Thread Interruption)
概念
线程中断模式是一种行为设计模式,它允许线程在执行过程中被中断,从而使得线程可以优雅地退出。在这种模式中,当一个线程接收到中断信号时,它可以检查是否需要响应中断,并采取相应的操作,如清理资源、保存状态或直接退出。
实现原理
- 中断信号:通过调用Thread.interrupt()方法来发送中断信号。
- 中断响应:线程在执行过程中可以检查是否接收到中断信号,并采取相应的操作。
并发代码示例
以下是一个使用线程中断模式的示例:
public class InterruptibleThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000); // 模拟长时间运行的任务
} catch (InterruptedException e) {
// 响应中断
System.out.println("Thread has been interrupted");
break;
}
}
});
thread.start();
// 主线程等待一段时间后发送中断信号
Thread.sleep(5000);
thread.interrupt();
}
}
在这个示例中,InterruptibleThread类包含了一个无限循环,该循环会持续运行,直到接收到中断信号。当主线程等待一段时间后,它通过调用thread.interrupt()方法来发送中断信号。在循环内部,线程会检查是否接收到中断信号,如果接收到,它会打印一条消息并退出循环。
应用场景
- 长时间运行的任务:当需要执行一个长时间运行的任务时,可以使用线程中断模式来优雅地退出任务。
- 资源清理:当需要执行一些资源清理任务时,可以使用线程中断模式来优雅地退出任务。
- 线程监控:在需要监控系统中线程的状态时,可以使用线程中断模式来优雅地退出任务。
注意事项
- 确保线程正确地响应中断,以避免线程挂起。
- 考虑线程中断的性能开销,因为它可能会对系统性能产生影响。
线程中断模式在多线程编程中非常有用,它提供了一种高效的方式来优雅地退出线程,从而提高了系统的可维护性和健壮性。
14、两阶段终止模式(Two-Phase Termination)
概念
两阶段终止模式是一种线程管理设计模式,它允许一个线程安全地停止另一个线程。这种模式分为两个阶段:准备阶段和终止阶段。在准备阶段,目标线程会做一些必要的清理工作,如释放资源、保存状态等。在终止阶段,目标线程会退出,从而结束执行。
实现原理
- 准备阶段:目标线程在接收到停止信号后,会进入准备阶段。在这个阶段,线程会执行一些清理工作,如释放持有的锁、关闭资源等。
- 终止阶段:清理工作完成后,目标线程会退出,从而结束执行。
并发代码示例
以下是一个使用两阶段终止模式的示例:
public class TwoPhaseTerminationExample {
public static void main(String[] args) throws InterruptedException {
Thread targetThread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000); // 模拟长时间运行的任务
} catch (InterruptedException e) {
// 进入准备阶段
System.out.println("Thread has been interrupted");
break;
}
}
});
targetThread.start();
// 主线程等待一段时间后发送中断信号
Thread.sleep(5000);
targetThread.interrupt();
}
}
在这个示例中,TwoPhaseTerminationExample类包含了一个无限循环,该循环会持续运行,直到接收到中断信号。当主线程等待一段时间后,它通过调用targetThread.interrupt()方法来发送中断信号。在循环内部,线程会检查是否接收到中断信号,如果接收到,它会打印一条消息并退出循环。
应用场景
- 资源清理:当需要执行一些资源清理任务时,可以使用两阶段终止模式来安全地停止任务。
- 线程监控:在需要监控系统中线程的状态时,可以使用两阶段终止模式来安全地停止任务。
注意事项
- 确保线程正确地响应中断,以避免线程挂起。
- 考虑线程中断的性能开销,因为它可能会对系统性能产生影响。
两阶段终止模式在多线程编程中非常有用,它提供了一种高效的方式来安全地停止线程,从而提高了系统的可维护性和健壮性。
15、作窃取模式(Work Stealing)
概念
工作窃取模式是一种线程调度算法,它允许线程从其他线程的队列中窃取任务来执行。这种模式通常用于实现线程池,其中线程会尝试从其他队列中窃取任务,以提高系统的并发性能。
实现原理
- 任务队列:线程池通常与多个任务队列关联,每个队列包含一组任务。
- 任务窃取:当一个线程完成自己的任务队列时,它会尝试从其他队列中窃取任务。
- 任务窃取的公平性:为了确保任务窃取的公平性,可以采用一些策略,如先进先出(FIFO)、随机选择等。
并发代码示例
以下是一个使用工作窃取模式的示例:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task: " + taskId);
// 执行任务逻辑
}
}
public class WorkStealingExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 创建多个任务队列
BlockingQueue<Runnable>[] taskQueues = new BlockingQueue[5];
for (int i = 0; i < taskQueues.length; i++) {
taskQueues[i] = new LinkedBlockingQueue<>();
}
// 提交任务到任务队列
for (int i = 0; i < 10; i++) {
taskQueues[i % taskQueues.length].add(new Task(i));
}
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务完成
executor.awaitTermination(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,WorkStealingExample类创建了一个固定大小的线程池和一个任务队列数组。每个任务队列都包含一个特定的任务队列。当线程池中的线程完成自己的任务队列时,它会尝试从其他队列中窃取任务。
应用场景
- 任务处理:在Web服务器、消息队列处理器等应用中,用于处理大量的短生命周期的任务。
- 异步执行:当需要异步执行任务,而不希望阻塞主线程时。
- 并发执行:当需要并发执行多个任务以提高性能时。
注意事项
- 确保正确管理任务队列,避免队列过大导致内存溢出。
- 考虑线程池的关闭和任务的超时处理,以确保系统的稳定性和健壮性。
工作窃取模式在多线程编程中非常有用,它提供了一种高效的方式来管理和执行任务,同时保持了系统组件之间的解耦。
16、保护性暂挂模式(Guarded Suspension)
概念
保护性暂挂模式是一种同步机制,用于在多线程环境下处理线程的暂挂和唤醒。在这种模式中,线程在等待某个条件满足时会被暂时挂起,当条件满足时,挂起的线程会被唤醒并继续执行。
实现原理
- 等待条件:线程在执行过程中会等待某个条件满足。
- 暂挂线程:当条件不满足时,线程会被暂时挂起。
- 唤醒线程:当条件满足时,挂起的线程会被唤醒并继续执行。
并发代码示例
以下是一个使用保护性暂挂模式的示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class GuardedSuspensionExample {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean conditionMet = false;
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
System.out.println("Condition is met, executing code");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try {
conditionMet = true;
condition.signalAll();
} finally {
lock.unlock();
}
}).start();
}
}
在这个示例中,GuardedSuspensionExample类包含了一个ReentrantLock和一个Condition。conditionMet变量用于表示条件是否满足。主线程和子线程分别等待和设置条件。当子线程设置条件时,主线程会被唤醒并执行后续代码。
应用场景
- 资源访问控制:当需要控制对共享资源的访问时,可以使用保护性暂挂模式。
- 线程间通信:当需要线程间进行通信时,可以使用保护性暂挂模式。
- 任务调度:当需要调度任务时,可以使用保护性暂挂模式。
注意事项
- 确保正确处理暂挂和唤醒操作,以避免线程挂起。
- 考虑线程暂挂的性能开销,因为它可能会对系统性能产生影响。
保护性暂挂模式在多线程编程中非常有用,它提供了一种高效的方式来控制线程的暂挂和唤醒,从而提高了系统的并发性能。
17、放弃模式(Balking)
概念
放弃模式是一种同步机制,用于在多线程环境下避免重复执行某些操作。在这种模式中,当一个线程尝试执行某个操作时,它会检查其他线程是否已经执行了该操作,如果是,则该线程会放弃执行该操作。
实现原理
- 检查操作状态:线程在执行操作之前会检查操作的状态。
- 放弃操作:如果其他线程已经执行了该操作,线程会放弃执行,并执行其他操作。
并发代码示例
以下是一个使用放弃模式的示例:
import java.util.concurrent.atomic.AtomicBoolean;
class BalkingExample {
private static final AtomicBoolean operationCompleted = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(() -> {
if (operationCompleted.get()) {
System.out.println("Operation has already been completed");
return;
}
System.out.println("Executing operation");
operationCompleted.set(true);
}).start();
new Thread(() -> {
if (operationCompleted.get()) {
System.out.println("Operation has already been completed");
return;
}
System.out.println("Executing operation");
operationCompleted.set(true);
}).start();
}
}
在这个示例中,BalkingExample类包含了一个AtomicBoolean变量,用于表示操作是否已经完成。两个线程尝试执行操作,如果操作已经完成,它们会放弃执行。
应用场景
- 资源访问控制:当需要控制对共享资源的访问时,可以使用放弃模式。
- 线程间通信:当需要线程间进行通信时,可以使用放弃模式。
- 任务调度:当需要调度任务时,可以使用放弃模式。
注意事项
- 确保正确处理操作状态,以避免线程挂起。
- 考虑操作状态的性能开销,因为它可能会对系统性能产生影响。
放弃模式在多线程编程中非常有用,它提供了一种高效的方式来避免重复执行某些操作,从而提高了系统的可维护性和健壮性。
18、Thread-Per-Message 模式
概念
Thread-Per-Message模式是一种多线程编程的设计模式,它为每个消息创建一个独立的线程来处理。在这种模式中,每个消息都会启动一个新的线程来执行任务,这样可以确保每个任务都是独立的,不会受到其他任务的影响。
实现原理
- 消息队列:将所有需要处理的消息放入一个队列中。
- 线程池:创建一个线程池,线程池中的线程会不断地从消息队列中取出消息并执行。
- 消息处理:每个线程处理一个消息,执行完成后,线程会返回线程池。
并发代码示例
以下是一个使用线程-每-消息模式的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
class Message {
private final int messageId;
public Message(int messageId) {
this.messageId = messageId;
}
public void process() {
System.out.println("Processing message: " + messageId);
// 执行消息处理逻辑
}
}
public class ThreadPerMessageExample {
public static void main(String[] args) {
LinkedBlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>();
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Message message = new Message(i);
messageQueue.add(message);
}
while (!messageQueue.isEmpty()) {
Message message = messageQueue.poll();
if (message != null) {
executor.submit(() -> message.process());
}
}
executor.shutdown();
try {
executor.awaitTermination(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,ThreadPerMessageExample类创建了一个消息队列和一个线程池。每个消息都会启动一个新的线程来执行任务。当消息队列不为空时,线程池中的线程会不断地从消息队列中取出消息并执行。
应用场景
- 任务处理:在Web服务器、消息队列处理器等应用中,用于处理大量的短生命周期的任务。
- 异步执行:当需要异步执行任务,而不希望阻塞主线程时。
- 并发执行:当需要并发执行多个任务以提高性能时。
注意事项
- 确保正确管理消息队列,避免队列过大导致内存溢出。
- 考虑线程池的关闭和任务的超时处理,以确保系统的稳定性和健壮性。
Thread-Per-Message在多线程编程中非常有用,它提供了一种高效的方式来管理和执行任务,同时保持了系统组件之间的解耦。
最后
最后再调强一下,这些模式在多线程并发编程中非常有用
。在分布式应用中,并发场景无处不在,理解和掌握这些并发模式的编码技巧,有助于我们在开发中解决很多问题,这要把这些与23种设计模式混淆了,虽然像单例模式是同一个,但这个是考虑并发场景下的应用。内容比较多,V哥建议可以收藏起来,即用好查。拜拜了您誒,晚安。
原文链接: https://juejin.cn/post/7358744954615283738
文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17035.html