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

news2024/10/2 22:24:18

目录

单例模式:

什么是单例模式? 

单例模式的实现方式: 

饿汉模式: 

懒汉模式: 

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

阻塞队列:

标准库中的阻塞队列: 

自实现阻塞队列: 

生产者消费者模型: 

生产者消费者模型的实现: 

利用系统的BlockingQueue实现生产者消费者模型:

利用自实现的BlockingQueue实现生产者消费者模型:


 

单例模式:

什么是单例模式? 

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

单例模式的实现方式: 

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

饿汉模式: 

懒汉模式的简单实现:


//单例模式:饿汉模式

class Singleton{
    //第一步实例化对象
   public static Singleton singleton=new Singleton();
    //构造方法为空
    private Singleton(){}
    //获取对象实例
    public static Singleton getInstance(){
        return  singleton;
    }
}


public class ThreadDemo4 {
    public static void main(String[] args) {
        //Singleton singleton1=new Singleton();无法创建对象!!!
        //验证单一性:
        Singleton singleton1=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
        System.out.println(singleton1==singleton2);


    }
}

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

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

懒汉模式: 

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

懒汉模式简单实现:


class Singleton1{
    public static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public static Singleton1 getSingleton1() {
        if(singleton1==null){
            singleton1=new Singleton1();
        }
        return singleton1;
    }
}

public class ThreadDemo5 {
    public static void main(String[] args) {
        //Singleton1 singleton1=new Singleton1();无法创建实例
        Singleton1 s1=Singleton1.getSingleton1();
        Singleton1 s2=Singleton1.getSingleton1();
        System.out.println(s1==s2);

    }
}

  

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

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

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

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

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

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

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

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


class Singleton1{
    public static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public synchronized static Singleton1 getSingleton1() {
        if(singleton1==null){
            singleton1=new Singleton1();
        }
        return singleton1;
    }
}

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

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

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

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


class Singleton1{
    public volatile static Singleton1 singleton1=null;//先为空
    //同样构造方法私有化
    private Singleton1(){}
    //懒汉模式是在获取对象实例的方法中进行创建实例的
    public  static Singleton1 getSingleton1() {
        if(singleton1==null){
            synchronized (Singleton1.class){
                if(singleton1==null){
                    singleton1=new Singleton1();
                }
            }
        }
        return singleton1;
    }
}

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

以下代码在加锁的基础上, 做出了进一步改动:
使用双重 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 等方法, 但是这些方法不带有阻塞特性。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueDemo1 {
    public static void main(String[] args) throws InterruptedException {
        //阻塞队列
        BlockingQueue<String> queue=new LinkedBlockingQueue<>();
        //入队列
        queue.put("abc");
        //出队列,如果没有就进行阻塞
        String elem=queue.take();
        System.out.println(elem);
    }
}

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

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

自实现阻塞队列: 

 实现阻塞队列的关键在于实现其阻塞的功能。其他的和普通的队列差不多。这里主要实现put和take方法:


class MyBlockingQueue{

    //利用数组实现
    private int[] arr=new int[1000];//设定数组长度为1000

    private int size=0;//记录数组的内容长度
    //利用end和begin两个指针使得数组变为循环数组(逻辑上的循环)
    private int end=0;
    private int begin=0;

    //实现put方法
    //阻塞考虑使用wait和notify进行唤醒(sleep不太靠谱)
    public void put(int value) throws InterruptedException {
        //判断是否满了(这里要用循环判断,因为在多线程当中,线程被唤醒的时候不一定不满)
        //加锁保证原子性
        synchronized (this){
            while(size>= arr.length){
                this.wait();
        }
            //不满之后放入元素
            arr[end]=value;
            //调整长度
            end++;
            size++;
            //如果放满了则将end变为0
            if(end>= arr.length){
                end=0;
            }
            //进行唤醒
            this.notify();
        }
    }
    //实现take方法
    public int take() throws InterruptedException {
        synchronized (this){
            //判断是否为空
            while (size==0){
                this.wait();
            }
            //不空之后开始取出元素
            int ret=arr[begin];
            begin++;
            if(begin>= arr.length){
                begin=0;
            }
            size--;
            this.notify();
            return ret;
        }

    }
    //长度
    public synchronized int Size(){
        return size;
    }

}


public class BlockingQueueDemo3 {

    public static void main(String[] args) throws InterruptedException {

        MyBlockingQueue queue=new MyBlockingQueue();
        queue.put(100);
        queue.put(200);
        queue.put(300);
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());

    }
}

 

 显然,当其中没有元素的时候就会阻塞等待。 

生产者消费者模型: 

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

首先,阻塞队列相当于一个缓冲区,它平衡了生产者和消费者之间的处理能力。

其次,阻塞队列使得生产者和消费者之间的进行了解耦合,也就是消费者不再是直接依赖生产者。

对于阻塞队列,生产者是添加元素的一方,消费者是取元素的一方,产品是阻塞队列的元素。生产者和消费者通过阻塞队列相互联系。

画个图演示一下:

我们举向游戏里面氪金的例子吧!!!

这样就会出现一个问题:服务器A和服务器B的耦合太高,一旦其中一个服务器出现了问题,就会导致另一个服务器也无法完成需求。就会出现一个服务器挂了,把另一个服务器也带走的情况。

并且,如果我们需要在此基础上加一个新的服务器参与其他相关的功能,比如日志,也是会有问题的!

那么如何解决这种情况呢?这就用到了当前的生产者消费者模型。生产者生成的资源,我们可以将其放到一个阻塞队列当中去,当有消费者需要消费的时候,就直接从该阻塞队列当中去取,如果队列中没有资源,就阻塞等待,等待生产者进行生产,当阻塞队列满的时候,生产者也要进行阻塞等待。这里的服务器A就是生产者,服务器BC就是消费者。所以我们可以利用该模型进行这样的设计:

生产者消费者模型的实现: 

利用系统的BlockingQueue实现生产者消费者模型:

首先我们利用系统提供的BlockingQueue实现生产者消费者模型: 

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueDemo2 {
    public static void main(String[] args) {
        //创建阻塞队列
        BlockingQueue<Integer>queue=new LinkedBlockingQueue<>();
        //使用两个线程:一个线程充当生产者,一个线程充当消费者
        //生产者
        Thread t1=new Thread(()->{
            int count=0;
            while(true){
                try {
                    queue.put(count);
                    System.out.println("生产者生产:"+count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2=new Thread(()->{

            while(true){
                try {
                    int ret=queue.take();
                    System.out.println("消费者消费:"+ret);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

 

我们可以明确的看到,消费元素和生产元素是成对出现的。这就不会出现生产者没有生产出来的东西被消费的情况。 

利用自实现的BlockingQueue实现生产者消费者模型:


class MyBlockingQueue1{

    //利用数组实现
    private int[] arr=new int[1000];//设定数组长度为1000

    private int size=0;//记录数组的内容长度
    //利用end和begin两个指针使得数组变为循环数组(逻辑上的循环)
    private int end=0;
    private int begin=0;

    //实现put方法
    //阻塞考虑使用wait和notify进行唤醒(sleep不太靠谱)
    public void put(int value) throws InterruptedException {
        //判断是否满了(这里要用循环判断,因为在多线程当中,线程被唤醒的时候不一定不满)
        //加锁保证原子性
        synchronized (this){
            while(size>= arr.length){
                this.wait();
            }
            //不满之后放入元素
            arr[end]=value;
            //调整长度
            end++;
            size++;
            //如果放满了则将end变为0
            if(end>= arr.length){
                end=0;
            }
            //进行唤醒
            this.notify();
        }
    }
    //实现take方法
    public int take() throws InterruptedException {
        synchronized (this){
            //判断是否为空
            while (size==0){
                this.wait();
            }
            //不空之后开始取出元素
            int ret=arr[begin];
            begin++;
            if(begin>= arr.length){
                begin=0;
            }
            size--;
            this.notify();
            return ret;
        }

    }
    //长度
    public synchronized int Size(){
        return size;
    }

}


public class BlockingQueueDemo4 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue1 queue1=new MyBlockingQueue1();
        //生产者
        Thread producer =new Thread(()->{
            int count=0;
            while(true){
                try {
                    queue1.put(count);
                    System.out.println("生产者生产元素:"+count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //消费者
        Thread customer =new Thread(()->{
            while(true){
                try {
                    int ret=queue1.take();
                    System.out.println("消费者消费元素:"+ret);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        customer.start();
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/347866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CPP2022-计算机类-期末考试

6-1 判断素数 分数 5 全屏浏览题目 切换布局 作者 李国瑞 单位 东北大学秦皇岛分校 设计一个函数&#xff0c;判断输入数据是否为素数&#xff0c;返回bool类型结果。 函数接口定义&#xff1a; bool prime(int num); 说明&#xff1a;num为正整数。 裁判测试程序样例&…

基于node.js+vue+mysql考研辅导学习打卡交流网站系统vscode

语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 主要功能包括管理员&#xff1a;首页、个人中心、用户管理、每日打卡管理、考研学校管理、考研专业管理、直通车管理、学习教材管理、…

云平台搭建实例

嗨嗨&#xff0c;每天一更是不是很奈斯&#xff1f;我也觉得&#xff0c;昨天晚上我学校的老师借一天一千的设备&#xff0c;只能用七天&#xff0c;所以我拿出来给你们没有设备和刚用设备的看看吧。操作&#xff1a;首先我们将云平台安装好后&#xff0c;插上网线&#xff0c;…

手机截图如何提取文字?

在当今信息爆炸的时代&#xff0c;图文并茂已经成为了一个广告宣传的常用方式。然而&#xff0c;图片中的文字信息往往难以获取&#xff0c;尤其对于那些需要快速获取信息的人们来说&#xff0c;阅读图片中的文字会是一项繁琐且费时的任务。现在&#xff0c;我们有一个好消息要…

C语言的期末复习

&#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f64f;作者水平很有限&#xff0c;如果发现错误&#xff0c;请留言轰炸哦&#xff01;万分感谢&a…

数据结构:链表基础OJ练习+带头双向循环链表的实现

目录 一.leetcode剑指 Offer II 027. 回文链表 1.问题描述 2.问题分析与求解 (1) 快慢指针法定位链表的中间节点 (2) 将链表后半部分进行反转 附:递归法反转链表 (3) 双指针法判断链表是否回文 二.带头双向循环链表的实现 1.头文件 2.节点内存申请接口和链表初始化接口…

virtuoso数据库介绍

在国内&#xff0c;对海量 RDF 数据的管理有着迫切的实际需求&#xff1b; RDF&#xff1a;Resource Description Framework&#xff0c;是一个使用XML语法来表示的资料模型(Data model)&#xff0c;用来描述Web资源的特性&#xff0c;及资源与资源之间的关系。 Virtuoso可以对…

今天正式上线!虹科汽车免拆诊断云展厅:感受精准修车魅力,畅享汽修领先技术

『虹科汽车免拆诊断云展厅』 2月15日正式上线&#xff01; 在这里&#xff0c;您可以参观了解&#xff1a; 虹科Pico汽车示波器产品模型 全流程专业讲解的视频资料 产品功能和应用场景 全面详细的产品手册 还有虹科首席技术工程师在线连麦答疑&#xff01;&#xff01;&#xf…

硬核!2023最全Java面试八股文,覆盖市面上80%以上的面试考点

就目前大环境来看&#xff0c;跳槽成功的难度比往年高很多。一个明显的感受&#xff1a;今年的面试&#xff0c;无论一面还是二面&#xff0c;都很考验 Java 程序员的技术功底。这不马上又到了面试跳槽的黄金段&#xff0c;成功升职加薪&#xff0c;不成功饱受打击。当然也要注…

影像测量设备都有什么?有哪些影像仪器?

影像测量仪器是广泛应用于机械、电子、仪表的仪器。主要由机械主体、标尺系统、影像探测系统、驱动控制系统和测量软件等与高精密工作台结构组成的光电测量仪器。一般分为三大类&#xff1a;手动影像仪、自动影像仪和闪测影像仪。测量元素主要有&#xff1a;长度、宽度、高度、…

【C语言技能树】浮点数在内存中的存储

Halo&#xff0c;这里是Ppeua。平时主要更新C语言&#xff0c;C&#xff0c;数据结构算法......感兴趣就关注我吧&#xff01;你定不会失望。 &#x1f308;个人主页&#xff1a;主页链接 &#x1f308;算法专栏&#xff1a;专栏链接 我会一直往里填充内容哒&#xff01; &…

CSDN每日一练:小豚鼠搬家

题目名称&#xff1a;小豚鼠搬家 时间限制&#xff1a;1000ms内存限制&#xff1a;256M 题目描述 小豚鼠排排坐。 小艺酱买了一排排格子的小房子n*m&#xff0c;她想让k只小豚鼠每只小豚鼠都有自己的房子。 但是为了不浪费空间&#xff0c;她想要小房子的最外圈尽量每行每列都有…

TCP/IP网络编程——多种 I/O 函数

完整版文章请参考&#xff1a; TCP/IP网络编程完整版文章 文章目录第 13 章 多种 I/O 函数13.1 send & recv 函数13.1.1 Linux 中的 send & recv13.1.2 MSG_OOB&#xff1a;发送紧急消息13.1.3 紧急模式工作原理13.1.4 检查输入缓冲13.2 readv & writev 函数13.2.1…

深入探讨软件定义架构及其意义

在上期文章中&#xff0c;我们了解了现代GNSS模拟中的软件定义架构&#xff0c;并与传统架构进行了对比&#xff0c;本期文章中我们将继续深入探讨软件定义架构及其意义。所谓软件定义架构&#xff0c;其实是用软件去定义系统的功能&#xff0c;用软件给硬件赋能&#xff0c;最…

07 分布式事务Seata使用(2)

1、Seata是什么 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS&#x…

win10 安装 vs2015(社区版本)以及opencv-4.5.5

一、下载vs2015以及opencv-4.5.5从https://msdn.itellyou.cn/ 网站下载vs2015&#xff08;社区版本&#xff09;从https://opencv.org/releases/网站下载opencv-4.5.5二、安装vs2015和opencv-4.5.5解压后双击exe安装文件&#xff0c;完成安装&#xff08;默认&#xff09;双击下…

9.Docker Swarm

Docker Swarm 基本概念 Swarm是使用SwarmKit构建的 Docker 引擎内置&#xff08;原生&#xff09;的集群管理和编排工具。Docker Swarm是 Docker 官方三剑客项目之一&#xff0c;提供 Docker 容器集群服务&#xff0c;是 Docker 官方对容器云生态进行支持的核心方案。 使用它…

【大数据clickhouse】clickhouse 常用查询优化策略详解

一、前言 在上一篇我们分享了clickhouse的常用的语法规则优化策略&#xff0c;这些优化规则更多属于引擎自带的优化策略&#xff0c;开发过程中只需尽量遵守即可&#xff0c;然而&#xff0c;在开发过程中&#xff0c;使用clickhouse更多将面临各种查询sql的编写甚至复杂sql的…

PHP(13)HTTP协议

PHP&#xff08;13&#xff09;HTTP协议一、HTTP请求1. 请求行2. 请求头3. 请求体二、HTTP响应1. 响应行2. 响应头三、设置HTTP响应四、模拟HTTP请求一、HTTP请求 1. 请求行 请求行独占一行。形式&#xff1a;请求方式 资源路径 协议版本号 GET /index.php HTTP/1.1 2. 请求…

vue3:直接修改reative的值,页面却不响应,这是什么情况?

目录 前言 错误示范&#xff1a; 解决办法&#xff1a; 1.使用ref 2.reative多套一层 3.使用Object.assign 前言&#xff1a; 今天看到有人在提问&#xff0c;问题是这样的&#xff0c;我修改了reative的值&#xff0c;数据居然失去了响应性&#xff0c;页面毫无变化&…