目录
一、单例模式
1. 饿汉模式
2. 懒汉模式
二、阻塞队列
1. 阻塞队列是什么
2. 生产者消费者模型
3. 标准库中的阻塞队列
4. 自实现阻塞队列
三、定时器
1. 定时器是什么
2. 标准库中的定时器
欢迎观看我滴上一篇关于 多线程的博客呀,直达地址:
多线程(续—解决线程不安全问题)(超详细) 感谢各位大佬的支持 💓💓💓
一、单例模式
注意:单例模式是面试中一个最常考的设计模式之一。
啥是设计模式?
设计模式就是大佬们针对一些常见的问题场景,给出的一个固定的解决套路,按照这些套路进行解决问题,就不会进行出错。
设计模式是一种思想,而非是一个固定的代码模式。
这里的单例模式就是能保证某个类在程序中只存在唯一的一个实例,而不会创建出多个实例。
单例模式存在很多种,最常见的就是 “饿汉” 和 “懒汉” 模式。
1. 饿汉模式
饿汉模式,单单从名称中就可以理解其思想,主要突出的就是——急。就好像一个饿汉被饿了好几天,突然看到递过来的食物一样,急切的想要的到它。
在计算机中,需要在JVM启动的时候,即类刚刚加载的时候,就创建这个类的实例。
其次,为了该类只创建一个实例,防止通过构造方法来创建多余的实例,所以我们把构造方法设置为私有。之后要想获取这个类的实例,可以通过一个普通方法来获取类中创建的这个实例。
单线程 下的来代码的实现:
public class Singleton {
private static Singleton instance = new Singleton(); // 在类加载的时候就创建实例
private Singleton (){ // 使用private修饰符来修饰 构造方法使其变为私有的,来避免调用构造方法
}
public static Singleton getInstance() { // 使用普通的方法来返回 构造的实例,需要时静态的
return instance;
}
}
这个饿汉模式即使在 多线程情况下,仍然是线程安全的。因为在创建这个类的时候,这个实例就已经存在了,这个实例的创建比主线程的创建都早,那么主线程只需要只读操作就可以了。
回忆前面说过线程安全问题中,如果是只读操作是不存在线程安全问题的,只有在对同一个变量进行修改的时候容易产生线程安全问题。
2. 懒汉模式
同样这个通过名称也可以理解到这个模式的思想,就是突出 —— 懒。
这个在类加载的时候不会创建实例,只有在第一次调用这个类的实例的时候才会进行创建实例。如果一直没有被调用的话,就一直不创建实例,这个实例就为null。
单线程 下的代码是实现:
public class Singleton {
private static Singleton instance = null;
private Singleton (){ // 使用private修饰符来修饰 构造方法使其变为私有的,来避免调用构造方法
}
public static Singleton getInstance() { // 使用普通的方法来返回 构造的实例,需要时静态的
if (instance == null) { // 判断是否被调用过
instance = new Singleton();
}
return instance;
}
}
多线程 情况:
这个模式下的单线程情况下不会出现什么bug,但是在多线程的情况下,就可能会出现bug。这个bug可能会出现在首次创建实例的情况下,如果多个线程中同时调用 getInstance这个方法,就可能导致创建多个实例,利用图片直观的看一下:
在线程t1和线程t2创建实例的时候,线程t2的判断可能在线程t1创建实例之前,此时线程t1和线程t2都会创还能实例,那么就违背了到哪里模式的初衷。这里产生线程安全问题的原因是原子性问题。
那么通过上一个博客,解决线程安全问题的方法中,对应解决原子性的线程安全问题,可以通过加锁来进行解决,将其打包成一个原子的操作。
代码如下:
public class Singleton {
private static Singleton instance = null;
private Singleton (){ // 使用private修饰符来修饰 构造方法使其变为私有的,来避免调用构造方法
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但是呢,对于上述的代码呢还存在一些问题
1. 如果线程比较多,会产生重复加锁竞争,那就可能会产生阻塞问题。在最初调用 getInstance 的时候会存在线程安全问题,但是后序调用,就只是读取操作,并不存在线程安全问题。所以此时上锁就是多余的,上锁本身也是一个开销比较大的操作。所以就引入了双重校验锁模式。
2.存在内存可见性问题和指令重排序问题导致读取 instance 的偏差。所以便引入了 volatile关键字
那么优化后的代码就是如下:
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
这里的两层判断条件虽然是一样的 判断 instance == null 的,但是它们所起的作用,却不是一样的
第一层的判断是为了在创建好实例之后,在后续获取实例的时候重复加锁,来节省系统的开销。
第二层判断是为了多个线程如果都在创建好实例之前运行,防止同时创建多个实例。
二、阻塞队列
1. 阻塞队列是什么
阻塞队列是一种特殊的队列。也遵守 “先进先出” 的原则。
阻塞队列能是一种线程安全的数据结构,并且具有以下特性:
• 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。
阻塞队列的一个典型应用场景就是 “生产者消费者模型”。这是一种非常典型的开发模型。
2. 生产者消费者模型
注意:生产者消费者模型并不是常见的23种设计模式中的一种。
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。
就如同下述的图片一样:
生产者消费者模型有以下两种好处:
1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。(削峰填谷)
这个情况最常出现在 各种购物软件的 双十一之类的活动中,当双十一的时候,服务器在同一时刻可能收到大量的支付请求,如果直接进行处理这些请求,服务器可能扛不住。
这个时候 阻塞队列 就起到作用了,在生产者和消费之间放入一个阻塞队列,把支付请求放入到阻塞队列中,让消费者线程慢慢的进行处理每一个支付请求。
2. 阻塞队列能使生产者和消费者之间 解耦。
比如在餐厅吃饭,客户就是消费者,后厨就是生产者,消费者在服务员(阻塞队列)这里进行点餐,服务员把菜单(请求) 给到后厨,后厨就行返回做好的菜(响应)。这样呢 客户 不去关心这个菜是谁做的,后厨不用关心这个菜是谁点的。这样就做到了 解耦。
3. 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列。如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可。
• BlockingQueue 是一个接口。真正实现的类是 LinkedBlockingQueue/ArrayBlockingQueue/PriorityBlockingQueue。
• put 方法用于阻塞式的入队列,ake 用于阻塞式的出队列。
• BlockingQueue 也有offer,poll,peek 等方法,但是这些方法不带有阻塞特性。
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// ⼊队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
}
使用标准库进行实现 生产者和消费者模型:
public class Test {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take(); // take 阻塞式的出队列
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("⽣产元素: " + num);
blockingQueue.put(num); // put 阻塞式的入队列
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "⽣产者");
producer.start();
customer.join();
producer.join();
}
}
4. 自实现阻塞队列
• 通过 “循环队列” 的方式实现。
• 使用 synchronized 进行加锁控制。
• put 插入元素的时候,判定如果队列满了,就进行 wait。(注意,要在循环中进行 wait。被唤醒时不一定队列就不满了,因为同时可能唤醒多个线程)。
• take 取出元素的时候,判定如果队列为空,就进行 wait。(也是循环 wait)
public class MyBlockingQueue {
private int[] items = new int[1000];
private volatile int size = 0;
private volatile int head = 0;
private volatile int tail = 0;
public void put(int value) throws InterruptedException {
synchronized (this) {
// 此处最好使⽤ while.
// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了
// 就只能继续等待
while (size == items.length) {
wait();
}
items[tail] = value;
tail = (tail + 1) % items.length;
size++;
notifyAll();
}
}
public int take() throws InterruptedException {
int ret = 0;
synchronized (this) {
while (size == 0) {
wait();
}
ret = items[head];
head = (head + 1) % items.length;
size--;
notifyAll();
}
return ret;
}
public synchronized int size() {
return size;
}
// 测试代码
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue blockingQueue = new MyBlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
blockingQueue.put(random.nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
}
三、定时器
1. 定时器是什么
定时器也是软件开发中的一个重要组件。类似于一个 "闹钟"。达到一个设定的时间之后,就执行某个指定好的代码。
定时器是一种实际开发中非常常用的组件。
比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。
类似这样的场景就需要使用定时器。
2. 标准库中的定时器
• 标准库中提供了一个Timer类。Timer 类的核心方法为 schedule。
• schedule 包含两个参数。第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)。
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Hello");
}
},3000);
}
这次的分享到这里就结束了,感觉文章不错的话,期待你的一键三连哦,你的鼓励就是我的动力,让我们一起加油,顶峰相见。拜拜喽~~我们下次再见💓💓💓💓