🔥个人主页: 中草药
🕯️一.设计模式
在Java中,设计模式(Design Patterns)是指在软件工程和面向对象编程中,针对特定问题和常见情境的一种经过验证的解决方案模板。设计模式不是具体的代码,而是一种描述问题和解决方案的通用形式,它提供了一种在软件设计中重复使用的方法,一种固定套路,帮助开发者以更优雅、更灵活的方式解决常见的设计问题,从而提高代码的可读性、可维护性和可扩展性。
设计模式通常基于以下几个方面:
- 目的:解决某一类问题或实现特定的功能。
- 参与者:涉及的类和对象。
- 协作:这些类和对象之间的交互方式。
- 效果:使用模式后带来的好处和可能的权衡。
设计模式通常被分类为三种类型:
-
创建型模式(Creational Patterns):关注对象的创建机制,试图创建对象的过程能够满足一定的约束条件,例如单例模式(Singleton)、工厂模式(Factory)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。
-
结构型模式(Structural Patterns):关注类和对象的组合,以达到更灵活和可复用的结构,例如适配器模式(Adapter)、装饰模式(Decorator)、代理模式(Proxy)、桥接模式(Bridge)、组合模式(Composite)和外观模式(Facade)。
-
行为型模式(Behavioral Patterns):关注类和对象之间的职责分配和交互,涉及算法的封装和对象之间的通信,例如策略模式(Strategy)、模板方法模式(Template Method)、观察者模式(Observer)、命令模式(Command)、迭代器模式(Iterator)、访问者模式(Visitor)、中介者模式(Mediator)和状态模式(State)。
不同的设计模式的使用可以带来以下优势:
- 代码复用:通过模式,可以复用解决问题的方案,避免重复造轮子。
- 可维护性:模式往往伴随着良好的代码组织,使得代码更易于理解和维护。
- 可扩展性:模式鼓励使用接口和抽象类,使得系统更易于扩展和修改。
- 灵活性:模式强调松耦合,使得系统更加灵活,能够适应变化。
设计模式并非万能药,过度使用或不恰当使用设计模式可能导致代码过度复杂,增加理解难度。因此,在应用设计模式时,应根据实际情况和项目需求,灵活选择最合适的模式。
在这里,我们着重来学习单例模式和阻塞队列
📽️二.单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。这种模式在需要频繁创建和销毁对象开销较大,或者某个资源只能由一个实例独占使用的场景中非常有用。单例模式通常用于创建日志对象、对话框、数据库连接、配置管理器等。
1.单例模式的实现
单例模式的主要目标是控制一个类的实例化过程,确保任何时候都只有一个实例存在,并且提供一个全局访问点。以下是几种常见的实现方式:
-
懒汉式(Lazy Initialization): 这是最直观的实现方式,只有在首次请求实例时才创建实例。
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
但是,上面的代码在多线程环境下可能不是线程安全的,因为多个线程可能同时进入
if
语句判断,导致创建多个实例。改进的版本可以使用双重检查锁定(Double-Checked Locking):class SingletonLan{ private static volatile SingletonLan instance=null;//1.volatile内存可见性问题 private SingletonLan() {} public static SingletonLan getInstance() { if (instance==null){//2.判断是否要加锁 synchronized (locker) {//3.加锁,把if判定和new赋值操作打包成原子操作 if (instance == null) {//4.是否要创建对象 instance = new SingletonLan(); } } } return instance; } }
-
饿汉式(Eager Initialization): 这种方式在类加载时就创建实例,因此不存在多线程安全问题。
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
除此之外还有静态内部类实现(这种方式结合了懒汉式的延迟加载和饿汉式的线程安全,利用了Java类加载机制保证初始化实例时只有一个线程。) 以及《Effective Java》作者Joshua Bloch推荐的枚举方式,简洁并且线程安全。
2.优缺点
优点:
- 确保一个类只有一个实例,节省内存和资源。
- 全局唯一访问点,便于控制和扩展。
缺点:
- 单例模式可能会隐藏类之间的依赖关系,因为单例类的实例是静态的,不容易在代码中显式表达。
- 单例模式可能违反单一职责原则,一个类负责实例的创建和管理,同时也负责业务逻辑。
- 在多线程环境下,实现线程安全的单例模式需要额外的注意和开销。
单例模式是一种强大的设计模式,可以有效地控制资源的使用,特别是在多线程和网络环境中。然而,它也有可能引入一些不易察觉的问题,因此在使用时应当谨慎,并充分考虑具体的应用场景和潜在的影响。
💡 三.阻塞队列
1.生产者消费者模型
生产者消费者模型是计算机科学中用于解决多线程或并发编程中资源分配和数据共享问题的经典模型。这个模型主要用于描述一组生产数据的进程(或线程)和一组消费这些数据的进程(或线程)之间的交互。它是实现资源管理、数据缓冲和流程控制的有效方式,特别是在多线程和分布式系统中。
基本概念
在生产者消费者模型中,有两组主要的角色:
- 生产者(Producer):负责生成数据或资源,并将它们放入一个共享容器(如队列、缓冲区)中。
- 消费者(Consumer):负责从共享容器中取出数据或资源,并对其进行处理。
这两个角色通过一个共享的缓冲区或队列进行通信和数据交换。生产者将数据放入队列,而消费者从队列中取出数据进行处理。
工作流程
生产者消费者模型的工作流程如下:
-
生产者向队列中添加数据:当生产者生成数据时,它会尝试将数据放入队列中。如果队列已满,生产者可能需要等待,直到有空间可用。
-
消费者从队列中移除数据:当消费者准备好处理数据时,它会从队列中取出数据。如果队列为空,消费者可能需要等待,直到队列中有数据可用。
-
同步和通信:为了确保数据的正确处理,生产者和消费者必须通过某种机制(如锁、信号量或条件变量)进行同步,以避免数据竞争和不一致性。
解决的问题
生产者消费者模型解决了以下问题:
- 资源竞争:通过使用同步机制,可以防止生产者和消费者同时访问共享资源,从而避免数据的混乱或丢失。
- 缓冲区管理:队列或缓冲区充当了生产者和消费者之间的中间层,可以吸收生产速率和消费速率之间的差异,避免生产者和消费者的直接耦合。
- 并发控制:模型允许并发执行,提高系统的吞吐量和响应速度。
实现细节
在实际编程中,实现生产者消费者模型通常涉及到以下技术:
- 线程和进程:生产者和消费者可以是不同的线程或进程。
- 同步原语:使用互斥锁(mutex)、信号量(semaphore)、条件变量(condition variable)等来确保数据的正确性和一致性。
- 队列和缓冲区:实现数据的存储和传递,可以是基于数组、链表或其他数据结构的队列。
Java中的实现
在Java中,可以使用java.util.concurrent
包中的BlockingQueue
接口来实现生产者消费者模型,该接口提供了线程安全的队列实现,如ArrayBlockingQueue
、LinkedBlockingQueue
等,它们内置了阻塞机制,可以简化同步和通信的实现。
应用场景
生产者消费者模型广泛应用于各种场景,包括但不限于:
- 消息队列:如RabbitMQ、Kafka等,用于在微服务架构中解耦服务间的通信。
- 任务调度和执行框架:如Apache Airflow、Apache Beam等,用于大规模数据处理和分析任务的调度。
- 多媒体处理:在视频编码、音频流处理等场景中,用于处理连续的数据流。
- 游戏开发:在渲染引擎中,生产者可能负责生成游戏世界的帧,而消费者则负责渲染这些帧。
总之,生产者消费者模型是解决多线程和并发问题的一个强大工具,它通过分离数据的生产和消费过程,提高了系统的可扩展性和可靠性。
2.阻塞队列
在Java中,阻塞队列(Blocking Queue)是一种特殊类型的队列,它提供了额外的阻塞行为。通常,队列是一种先进先出(FIFO)的数据结构,其中元素的插入操作在队列的尾部进行,而移除操作在队列的头部进行。阻塞队列在标准队列的基础上增加了线程安全性和阻塞能力,使其成为多线程环境中处理任务的理想选择。
特点
// 创建一个容量为5的阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
阻塞队列的主要特点如下:
-
线程安全性:阻塞队列的所有方法都是线程安全的,这意味着多个线程可以同时访问队列而不会引起数据不一致的问题。
-
阻塞行为:当队列满时,
put
方法会阻塞调用线程,直到队列中有可用空间为止。同样,当队列空时,take
方法会阻塞调用线程,直到队列中有新元素被加入。这种阻塞行为有助于线程间的同步和通信。 -
容量限制:阻塞队列通常具有固定的容量限制,这有助于防止无限的内存消耗。
常见的阻塞队列类型
Java并发工具包(java.util.concurrent
包)提供了几种不同类型的阻塞队列实现,每种实现都有其独特的特性和适用场景:
-
ArrayBlockingQueue
:基于数组的有界阻塞队列。它使用公平锁和非公平锁两种模式,并且是线程安全的。 -
LinkedBlockingQueue
:基于链表的阻塞队列。它有两个构造器,一个是有界的,另一个是无界的。当使用无界构造器时,队列的大小只受限于系统可用的内存。 -
PriorityBlockingQueue
:基于优先级堆的无界阻塞队列。元素按优先级排序,当多个元素具有相同优先级时,它们按照FIFO顺序排列。 -
SynchronousQueue
:一种特殊的阻塞队列,它不存储元素,而是在生产者线程和消费者线程之间直接传递元素。这使得它非常轻量级,但不适合存储元素。 -
DelayQueue
:一种特殊类型的队列,它只保存延期元素。元素只有在其延迟过期后才能从队列中取出。
使用场景
阻塞队列广泛应用于多线程编程中,尤其在以下场景中:
-
线程池:线程池使用阻塞队列来管理待处理的任务,当线程空闲时,它们会从队列中获取任务来执行。
-
生产者-消费者模型:阻塞队列是实现生产者-消费者模式的理想选择,生产者向队列中添加元素,而消费者从队列中取出元素。
-
任务调度:阻塞队列可以用于任务的调度,比如定时任务或基于事件的任务。
-
资源池管理:例如数据库连接池或缓存池,阻塞队列可以用来管理有限的资源。
总结
阻塞队列是Java并发编程中一个非常重要的概念,它提供了一种线程安全且高效的机制来管理多线程环境下的任务调度和资源分配。通过选择合适的阻塞队列类型,可以有效地控制并发级别,提高系统的响应能力和吞吐量。
3.模拟实现
我们可以模拟一个基于数组的阻塞队列
实现代码
class MyBlockingQueue{
private volatile int head = 0;
private volatile int tail = 0;
private volatile int size = 0;
private String[] data=null;
public MyBlockingQueue(int capacity){
data=new String[capacity];
}
public void put(String s) throws InterruptedException {
synchronized(this){
if (size==data.length){
this.wait();//如果wait在try catch里面,
// 此时应该用while 在wait唤醒之后判断是否继续执行 如t2
}
data[tail]=s;
tail++;
if (tail>=data.length){
tail=0;
}
size++;
this.notify();
}
}
public String take() throws InterruptedException{
String ret=null;
synchronized(this){
while (size==0){
this.wait();
}
ret=data[head];
head++;
if (head>=data.length){
head=0;
}
size--;
this.notify();
}
return ret;
}
}
测试代码
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue(1000);
Thread t1=new Thread(()->{
int i=1;
while(true){
try {
queue.put(i+"");
System.out.println("生产元素:"+i);
i++;
Thread.sleep(1000);
//生产慢点
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
while(true){
try {
int cur=Integer.parseInt(queue.take());
System.out.println("取出元素:"+ cur);
//Thread.sleep(1000);
//生产快点,消费慢点
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
测试结果
1.当生产慢于消费者时
2.当生产快于消费者时
🏮四.总结与反思
梦想家命长,实干家寿短。——约.奥赖利
在深入探索软件设计模式与多线程编程技术的过程中,Java中的单例模式与阻塞队列成为了我关注的焦点。这两个概念虽然分别属于设计模式和并发控制领域,但它们都在提高代码质量和系统性能方面扮演着关键角色。下面是对这两项技术的学习总结与个人反思。
单例模式(Singleton Pattern)
定义与目的: 单例模式是一种常用的软件设计模式,其核心目标是在整个系统中保证一个类只有一个实例,并提供一个全局访问点。这有助于节省资源,确保共享资源的一致性,比如数据库连接、配置管理器等。
实现方式:
- 懒汉式(Lazy Initialization):在首次使用时创建实例,适用于延迟加载的情况。
- 饿汉式(Eager Initialization):在类加载时就创建实例,适合于系统启动时就需要初始化的情况。
- 双重检查锁定(Double Checked Locking):结合懒汉式的延迟加载和同步控制,确保线程安全的同时减少锁的竞争。
反射与序列化挑战: 单例模式通过私有构造函数和静态工厂方法或枚举来实现,但反射和序列化可能会破坏单例性质。解决办法是重写clone
方法和readResolve
方法来保持单例的唯一性。
个人反思: 单例模式虽然简单,但在复杂系统中应用时需谨慎。过度使用可能导致系统变得难以测试和维护。同时,随着微服务架构的流行,单例模式的全局性也需要重新审视,因为它可能不再适用于分布式环境。
阻塞队列(Blocking Queue)
定义与目的: 阻塞队列是一种特殊的队列,它在队列满时阻止生产者线程继续添加元素,在队列空时阻止消费者线程取出元素,直到条件满足。这样可以有效控制线程间的同步,避免资源竞争和死锁问题。
应用场景:
- 生产者消费者模型:用于协调不同线程之间的数据传递,如任务调度、消息队列等。
- 限流与缓冲:在高并发场景下,阻塞队列可以作为缓冲区,防止后端系统过载。
Java中的实现:
ArrayBlockingQueue
:基于数组的阻塞队列,固定大小。LinkedBlockingQueue
:基于链表的阻塞队列,可选择固定或无限大小。PriorityBlockingQueue
:基于优先级堆的阻塞队列,适用于需要按优先级处理任务的场景。
个人反思: 阻塞队列的使用提升了系统的健壮性和可扩展性,尤其是在多线程环境下。然而,正确配置队列的大小和理解队列的阻塞机制至关重要,否则可能会导致性能瓶颈或资源浪费。此外,阻塞队列的灵活性也意味着开发者需要对线程的交互和队列的行为有深入的理解。
总结
学习单例模式和阻塞队列,不仅加深了我对Java语言特性的理解,也让我认识到在设计高效、健壮的系统时,选择合适的设计模式和并发控制策略的重要性。未来在开发项目时,我会更加注重模式的适用性和潜在的副作用,努力构建既灵活又稳定的软件架构。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸