【多线程】单例模式和阻塞队列
目录
一.单例模式
1. 饿汉模式
2. 懒汉模式
二.阻塞队列
1. 阻塞队列的概念
2. BlockingQueue接口
3.生产者-消费者模型
4.模拟生产者-消费者模型
一.单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
为什么要引入单例模式?
单例模式的核心就是一个类中只有一个实例,因为只用管理一个实例,那么就可以更好的对代码进行一个校验和检查,方便高效管理,同时也避免了多个实例可能带来的问题。
如果创建一个实例需要耗费100G的资源,那么创建出多个实例,代价太大。而且在多数情况下,一个实例完全够用,所有没有必要创建出多个实例,这样就避免资源的重复创建和浪费
在编译器中,没有提供类只能创建出多少个实例的方法,但是我们可以通过一些代码逻辑去规定创建实例的要求,下面是常用的几种单例模式。
1. 饿汉模式
核心特点是 在类加载时就立即创建单例实例,并通过静态方法提供全局访问。
class Singleton{private static Singleton singleton = new Singleton();public static Singleton getSingleton(){return singleton;}//核心操作private Singleton(){}}
- 使用static关键字保证唯一实例(在类被加载的时候就会创建出这个唯一实例)
- 构造方法被设为私有,导致构造方法无法被调用
- 如果想要获取这个实例只能使用静态方法getSingleton调用
由于在类被加载的时候,就会创建出实例,创建实例的时机很早(感觉非常的迫切,像一个饿汉),所有叫做饿汉模式
注意:在多线程中,并发调用getSingleton静态方法,由于只有读操作,所以是线程安全
缺点: 如果实例未被使用,实例依然会被创建,可能造成资源浪费(假设实例的大小是100G)。
2. 懒汉模式
其核心特点是 延迟实例的创建,只有在第一次使用时才初始化单例对象,以减少资源浪费。
public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}// 非线程安全public static LazySingleton getInstance() {if (instance == null) {//这里会涉及指令重排序的问题instance = new LazySingleton();}return instance;}
}
在使用调用方法时,只有在第一次使用时才初始化实例,否则都是返回已经存在的实例
注意: 在多线程中,并发调用getInstance方法,由于同时存在两个线程修改一个变量操作,线程不安全
发现出现new两次情况,所以解决这个问题,我们要进行加锁
class LazySingleton {private static LazySingleton instance;private LazySingleton() {}static Object A = new Object();// 非线程安全public static LazySingleton getInstance() {synchronized (A){if (instance == null) {//这里会涉及指令重排序的问题instance = new LazySingleton();}}return instance;}
}
这里我们会发现,每次调用getInstance方法,都要进行加锁和解锁的步骤,这样的步骤开销很大
所以我们需要进行改进
public static LazySingleton getInstance() {if(instance==null){synchronized (A){if (instance == null) {//这里会涉及指令重排序的问题instance = new LazySingleton();}}}return instance;}
- 第一次的 if 语句判断是否需要加锁
- 第二次的 if 语句判断是否为空
写到这里我们的代码还存在一个很严重的问题,由于指令重排序引起的线程安全问题
instance = new LazySingleton();
在创建一个实例的时候,主要有3步骤:正确的步骤顺序是1—>2—>3
1. 在内存中开辟一份空间,2.使用构造方法去创建实例,3. 将空间的地址赋值给引用变量
但是JVM可能会将步骤的执行顺序发送改变1—>3—>2,从而引发线程安全问题
具体原因:t2线程没有进入因为锁阻塞这步,t2线程会在t1线程执行完地址赋值后,刚好执行第一次的 if 判断语句,发现引用变量不为空,会直接返回引用变量(但是引用变量的值是空的)
其实解决也很简单使用volatile关键字
class LazySingleton {private static volatile LazySingleton instance;private LazySingleton() {}static Object A = new Object();// 非线程安全public static LazySingleton getInstance() {if(instance==null){synchronized (A){if (instance == null) {//这里会涉及指令重排序的问题instance = new LazySingleton();}}}return instance;}
}
二.阻塞队列
1. 阻塞队列的概念
阻塞队列可以看成是普通队列的一种扩展,遵循先进先出的原则,核心特性是当队列操作无法立即执行时,线程会被自动阻塞直到条件满足。
- 如果队列是满的,进行入队列操作则会被阻塞,直到队列不满
- 如果队列是空的,进行出队列操作则会被阻塞,直到队列不空
2. BlockingQueue接口
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的
- BlockingQueue接口属于java.util.concurrent包。它是线程安全的队列,支持阻塞操作。
- 常见的实现类有:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue
方法 | 说明 |
---|---|
| 队列未满时插入元素;若队列已满,则阻塞线程直到有空位。 |
| 队列非空时取出元素;若队列为空,则阻塞线程直到有元素可用。 |
| 队列未满时插入元素并返回 |
| 队列非空时取出元素并返回;队列为空时返回 |
| 队列满时等待指定超时时间,超时后返回 |
| 队列空时等待指定超时时间,超时后返回 |
E peek() | 返回队列头部元素但不移除;队列空时返回 null 。 |
其中只有put( )和take( )方法带有阻塞的效果
3.生产者-消费者模型
生产者-消费者模型用于解决多线程环境下的线程同步问题,核心思想是通过 共享缓冲区(阻塞队列) 解耦生产者和消费者,使两者可以独立并发工作
生产者和消费者彼此之间不直接进行联系,而是通过缓冲区(阻塞队列)进行联系
好处
(1)解耦合
- 解耦生产者和消费者的直接依赖,生产者和消费者可独立开发,提高开发效率,方便维护
- 生产者和消费者可并行执行,最大化利用 CPU、I/O 等资源
- 即使生产者挂了,也不会影响消费者的正常工作(反之同理)
解耦合在分布式系统中很常见,比如服务器的整个功能并不是由一个服务器全部完成,而是由多个服务器完成,每个服务器完成一部分功能,最后通过服务器之间的网络通信,实现整个功能
(2)缓冲机制
- 生产者突发大量请求时,队列暂存数据,避免消费者过载崩溃。
- 消费者处理慢时,队列累积任务,避免生产者因等待而阻塞。
- 可以将缓冲区作为“蓄水池”,协调速度差异
public class Demo_3 {public static void main(String[] args) {BlockingDeque<Integer> deque = new LinkedBlockingDeque<>(100);Thread t1 = new Thread(()->{while(true){try {Thread.sleep(300);Random random = new Random();int num = random.nextInt(101);System.out.println("生产数:"+ num);deque.put(num);} catch (InterruptedException e) {e.printStackTrace();}}},"生产者");t1.start();Thread t2 = new Thread(()->{while(true){try {Thread.sleep(500);int num = deque.take();System.out.println("消费数:"+num);} catch (InterruptedException e) {e.printStackTrace();}}},"消费者");t2.start();}
}
4.模拟生产者-消费者模型
核心:阻塞队列的实现
将其看成一个循环队列,其中两个核心的方法:put()和take()
put():入队列操作,如果队列为满则进入阻塞状态,由take()进行唤醒
take():出队列操作,如果队列为空则进入阻塞状态,由put()方法进行唤醒
在判断是否需要进入阻塞状态的时候,使用while语句,进行多次判断,如果使用if语句相当于一锤定音,在阻塞的状态下,可能会被notifyAll()唤醒,但是这时候队列中的空间并不足够(虚假唤醒),也有可能会出现连续唤醒的情况,最好的方式是再进行一次判断
class MyBlockingQueue{Object A = new Object();int[] elems = null;int right ;int tail ;int usedSize;MyBlockingQueue(int capacity){elems = new int[capacity];}//注意锁的位置//放入操作public void put(int elem) throws InterruptedException {synchronized (A){//如果满,不能放出while (usedSize>=elems.length){A.wait();}//没有满,正常放入elems[tail++] = elem;if(tail>=elems.length){tail = 0;}usedSize++;A.notify();}}public int take() throws InterruptedException {synchronized (A){//如果为空,不能取出while (usedSize == 0){A.wait();}//不为空,正常取出int elem = elems[right++];//另一种写法right = right%elems.length;usedSize--;A.notify();return elem;}}}
点赞的宝子今晚自动触发「躺赢锦鲤」buff!