【JavaEE】多线程代码实例:单例模式与阻塞队列BlockingQueue(一)

简介: 【JavaEE】多线程代码实例:单例模式与阻塞队列BlockingQueue

单例模式:

什么是单例模式?

单例模式能保证某个类只能存在唯一的实例,不能创建多个实例。这种设计模式是需要在特定业务场景进行使用的。

单例模式的实现方式:

单例模式的实现方式有很多种,主要的方式是饿汉模式懒汉模式

饿汉模式:

懒汉模式的简单实现:

1. 
2. //单例模式:饿汉模式
3. 
4. class Singleton{
5. //第一步实例化对象
6. public static Singleton singleton=new Singleton();
7. //构造方法为空
8. private Singleton(){}
9. //获取对象实例
10. public static Singleton getInstance(){
11. return  singleton;
12.     }
13. }
14. 
15. 
16. public class ThreadDemo4 {
17. public static void main(String[] args) {
18. //Singleton singleton1=new Singleton();无法创建对象!!!
19. //验证单一性:
20.         Singleton singleton1=Singleton.getInstance();
21.         Singleton singleton2=Singleton.getInstance();
22.         System.out.println(singleton1==singleton2);
23. 
24. 
25.     }
26. }

通过比较我们发现得到的实例是同一个实例,且在该模式下不能再进行实例的创建。从代码我们可以知道,该模式实例的创建要比一般类的实例创建要早,所以我们形象的称为饿汉模式(饿的等不及了),该对象的实例在类加载阶段就进行了创建。

饿汉模式如何确保创建对象是单例的?类定义时创建静态对象+私有构造方法,公开接口get实例,并且设置成静态确保可利用类名直接调用。

懒汉模式:

懒汉模式之所以被称为这样也是很形象的说法,这种模式下的单例模式,只有在需要实例的时候才会进行创建实例,并且只会创建这一次。

懒汉模式简单实现:

1. 
2. class Singleton1{
3. public static Singleton1 singleton1=null;//先为空
4. //同样构造方法私有化
5. private Singleton1(){}
6. //懒汉模式是在获取对象实例的方法中进行创建实例的
7. public static Singleton1 getSingleton1() {
8. if(singleton1==null){
9.             singleton1=new Singleton1();
10.         }
11. return singleton1;
12.     }
13. }
14. 
15. public class ThreadDemo5 {
16. public static void main(String[] args) {
17. //Singleton1 singleton1=new Singleton1();无法创建实例
18.         Singleton1 s1=Singleton1.getSingleton1();
19.         Singleton1 s2=Singleton1.getSingleton1();
20.         System.out.println(s1==s2);
21. 
22.     }
23. }

 

很显然,我们的实例是在第一次获取实例的时候进行创建的。

懒汉模式是通过创建静态对象变量+需要时创建对象+提供公开的接口并且设置成静态方法,私有化构造方法实现的。

这里需要注意:上述的单例模式在单线程的模式下运行时没有安全问题的,但是放到并发编程中就会出现问题!!!

基于并发编程对单例模式线程安全问题的讨论:

我们可以看到:在饿汉模式下,我们一上来就把对象实例化了,在多线程当中只会有读的操作,所以不会出现线程安全问题,所以我们说饿汉模式下的单例模式是线程安全的。但是对于懒汉模式而言,在获取实例的时候创建了实例,这样就即涉及到读,又涉及到写的操作了。

线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致
创建出多个实例.一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改singleton1 了)

所以使用synchronized可以改善这里的线程安全问题

懒汉模式多线程改进1.0版本:

1. 
2. class Singleton1{
3. public static Singleton1 singleton1=null;//先为空
4. //同样构造方法私有化
5. private Singleton1(){}
6. //懒汉模式是在获取对象实例的方法中进行创建实例的
7. public synchronized static Singleton1 getSingleton1() {
8. if(singleton1==null){
9.             singleton1=new Singleton1();
10.         }
11. return singleton1;
12.     }
13. }

但是,你以为这样就结束了吗?NO!!!

这里面还有一些问题!比如锁竞争,内存可见性问题等等。加锁 / 解锁是一件开销比较高的事情。 而懒汉模式的线程不安全只是发生在首次创建实例的时候。因此后续使用的时候, 不必再进行加锁了。所以我们考虑使用一个if判定下看当前是否已经把singleton1实例创建出来了。同时为了避免 "内存可见性" 导致读取的singleton1出现偏差, 于是补充上volatile。当多线程首次调用getInstance, 大家可能都发现instance为null, 于是又继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作。当这个实例创建完了之后, 我们还需要用一个if来判断是否创建完毕了,如果创建完毕了,其他竞争到锁的线程就被该层的if 挡住,也就不会继续创建其他实例。

所以我们将对懒汉模式进行二次改进:

懒汉模式多线程改进2.0版本:

1. 
2. class Singleton1{
3. public volatile static Singleton1 singleton1=null;//先为空
4. //同样构造方法私有化
5. private Singleton1(){}
6. //懒汉模式是在获取对象实例的方法中进行创建实例的
7. public  static Singleton1 getSingleton1() {
8. if(singleton1==null){
9. synchronized (Singleton1.class){
10. if(singleton1==null){
11.                     singleton1=new Singleton1();
12.                 }
13.             }
14.         }
15. return singleton1;
16.     }
17. }

这样我们的懒汉模式才算是完善了。

以下代码在加锁的基础上, 做出了进一步改动:

使用双重 if 判定, 降低锁竞争的频率,给singleton1加上了 volatile

我们举个例子

1) 有三个线程, 开始执行getInstance, 通过外层的if (singleton1 == null) 知道了实例还没有创建的消息,于是开始竞争同一把锁

2) 其中线程1率先获取到锁, 此时线程1通过里层的if (singleton1 == null) 进一步确认实例是否已经创建,如果没创建, 就把这个实例创建出来

3) 当线程1释放锁之后, 线程2和线程3也拿到锁, 也通过里层的 if (singleton1 == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了

4) 后续的线程, 不必加锁, 直接就通过外层if (singleton1 == null) 就知道实例已经创建了, 从而不再尝试获取锁了,降低了开销

试着理解一下吧。

阻塞队列

阻塞队列是一种特殊的队列,也遵守 "先进先出" 的原则。

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素。
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型

标准库中的阻塞队列:

在 Java 标准库中内置了阻塞队列.。如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可。

标准库中提供了实现阻塞队列功能的类和接口。虽然说,阻塞队列本质上还是一个队列,也就是说实现了Queue接口,也有普通队列方法,但是我们使用阻塞队列主要使用的不是这些,而是它特有的阻塞功能,此时对应的入队和出队操作的方法分别对应的是put和take方法

同时BlockingQueue还有这些比较常用的实现Queue接口的类,背后的数据结构看名字就知道是什么了。此外,后面是Deque的是双端阻塞队列。

注意点:

BlockingQueue 是一个接口, 真正实现的类是 LinkedBlockingQueue。

put 方法用于阻塞式的入队列, take 用于阻塞式的出队列。

BlockingQueue也有offer, poll, peek 等方法, 但是这些方法不带有阻塞特性。

1. import java.util.concurrent.BlockingQueue;
2. import java.util.concurrent.LinkedBlockingQueue;
3. 
4. public class BlockingQueueDemo1 {
5. public static void main(String[] args) throws InterruptedException {
6. //阻塞队列
7.         BlockingQueue<String> queue=new LinkedBlockingQueue<>();
8. //入队列
9.         queue.put("abc");
10. //出队列,如果没有就进行阻塞
11.         String elem=queue.take();
12.         System.out.println(elem);
13.     }
14. }

在阻塞队列中,如果队列放满了或者没有出的元素都会进入阻塞状态。这里演示一下没有元素的情况:

此时队列中没有元素,程序进行了阻塞。


相关文章
|
3月前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
2月前
|
安全 Java 关系型数据库
单例模式下引发的线程安全问题
单例模式确保类在进程中仅有一个实例,适用于如数据库连接等场景。分为饿汉式与懒汉式:饿汉式在类加载时创建实例,简单但可能浪费资源;懒汉式延迟创建实例,需注意线程安全问题,常采用双重检查锁定(Double-Checked Locking)模式,并使用 `volatile` 关键字避免指令重排序导致的问题。
56 2
单例模式下引发的线程安全问题
|
23天前
|
消息中间件 NoSQL 关系型数据库
【多线程-从零开始-捌】阻塞队列,消费者生产者模型
【多线程-从零开始-捌】阻塞队列,消费者生产者模型
20 0
|
23天前
|
设计模式 安全 Java
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
【多线程-从零开始-柒】单例模式,饿汉和懒汉模式
30 0
|
3月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
38 0
|
20天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
36 1
C++ 多线程之初识多线程
|
4天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
10 3
|
4天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
8 2