目录
一.单例模式
单例模式(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!