【设计模式】03:单例模式

单例模式


OVERVIOW

  • 单例模式
      • 1.单例模式实现
      • 2.饿汉与懒汉
        • (1)饿汉模式
        • (2)懒汉模式
      • 3.懒汉线程安全1
        • (1)引入互斥锁
        • (2)引入双重检查锁定
        • (3)引入原子变量
      • 4.懒汉线程安全2
        • (1)设置局部静态对象
      • 5.简单案例运用
        • (1)任务队列简单实现
        • (2)用户登录

项目全局范围内,某个类的实例有且仅有一个,通过这个实例向其他模块提供数据的全局访问,这种模式就叫单例模式。

单例模式的典型应用就是任务队列。使用单例模式来替代全局变量(对全局变量进行管理),直接使用全局变量会破坏类的封装(全局变量随意读写),通过单例模式的类提供的成员函数进行访问。

单例模式优点:

  1. 提高性能:避免频繁的创建销毁对象,提高性能,
  2. 节省内存空间:在内存中只有一个对象,节省内存空间,
  3. 避免多重占用:避免对共享资源的多重占用,
  4. 全局访问:可全局访问,利用单例模式避免全局变量的出现

单例模式缺点:

  1. 扩展困难:单例模式中没有抽象层,因此扩展困难,

  2. 不适用于变化的对象:如果同类型的对象总是要在不同的用例场景发生变化,单例就会引起数据错误,不能保存状态。

  3. 职责过重:违背了单一职责原则

  4. 负面问题:

    为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多,而出现连接池溢出。

    如果实例化的单例对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

单利模式使用场景:

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象耗时过多or消耗资源过多,但又经常使用到的对象,
  3. 有状态的工具类对象,
  4. 频繁访问数据库或文件的对象,
  5. 要求只有一个对象的场景

1.单例模式实现

如果使用单例模式,首先要保证这个类的实例有且仅有一个。因此就必须采取一些操作,涉及一个类多对象操作的函数有以下几个:

  • 构造函数:创建一个新的对象
  • 拷贝构造函数:根据已有对象拷贝出一个新的对象
  • 拷贝赋值操作符重载函数:两个对象之间的赋值

为了把一个类可以实例化多个对象的路堵死,需要对以上几个函数做如下处理:

  1. 构造函数私有化,在类内部只调用一次这是可控的。
    • 由于类外部不能使用构造函数,所以在类内部创建的唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装把这个静态对象设置为私有。
    • 在类中只有它的静态成员函数才能访问其静态成员变量,所以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
  2. 拷贝构造函数私有化或者禁用(使用 = delete)
  3. 拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲该函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理)

单例模式就是给类创建一个唯一的实例对象,UML类图如下:

在这里插入图片描述

#include<iostream>
using namespace std;
/*1.关于类创建后的默认提供的函数- 在创建一个新的类之后 会默认提供3个构造函数 1个析构函数- 2个操作符重载(移动赋值操作符重载、拷贝赋值操作符重载)移动构造函数 拷贝构造函数2.关于单例模式下类的实例化- 在通过将 无参构造函数、拷贝构造函数、拷贝赋值操作符重载函数禁用之后 TaskQueue类已经无法在外部创建任何的对象- 要得到TaskQueue的实例无法通过new操作符得到 只能通过类名得到(需要将对象设置为静态对象)- 通过类名访问类内部的属性和方法 其属性和方法一定是静态的(若不是静态需要通过对象来调用)- 能够操作静态成员变量的函数 只有静态成员函数
*///单例模式任务队列
class TaskQueue {
public://无参构造函数//TaskQueue() = delete;//拷贝构造函数TaskQueue(const TaskQueue &t) = delete;//赋值操作符重载函数TaskQueue& operator=(const TaskQueue &t) = delete;// = delete 代表函数禁用, 也可以将其访问权限设置为私有//静态成员公共函数用于获取实例static TaskQueue *getInstance() { return m_taskq; }void printTest() { cout << "i am a public method of a singleton class" << endl; }private://无参构造函数TaskQueue() = default;//拷贝构造函数//TaskQueue(const TaskQueue &t) = default;//赋值操作符重载函数//TaskQueue& operator=(const TaskQueue &t) = default;//通过类名访问静态属性或方法来创建类实例(需要在类外部做初始化处理)static TaskQueue *m_taskq;
};//静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskq = new TaskQueue;int main() {//获取TaskQueue的单例对象 由m_taskq指针指向TaskQueue* m_taskq = TaskQueue::getInstance();//由m_taskq指针调用单例类内部的成员方法m_taskq->printTest();return 0;
}

以上为单例模式中的饿汉模式,在定义单例类的时候就将类对应的单例对象一并创建出来了。

2.饿汉与懒汉

在实现一个单例模式的类的时候,有两种处理模式:

  • 饿汉模式:在将单例类定义出来后实例就已经存在了,饿汉模式没有线程安全问题
  • 懒汉模式:在使用单例对象的时候才会去创建单例对象的实例(节省内存空间),懒汉模式存在线程安全问题(多个线程同时访问单例的实例)
(1)饿汉模式
  1. 多个线程在访问单例对象时,没有线程安全问题,单例对象已经存在,不会出现多个线程创建出多个单例对象的情况。
  2. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),
//饿汉模式
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() { return m_taskq; }void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;static TaskQueue *m_taskq;
};TaskQueue* TaskQueue::m_taskq = new TaskQueue;
//饿汉模式
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() { return &m_taskq; }void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;static TaskQueue m_taskq;//已经创建对象
};TaskQueue* TaskQueue::m_taskq;//改为对象声明
(2)懒汉模式
//懒汉模式
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() {if (m_taskq == nullptr) m_taskq = new TaskQueue;return m_taskq;}void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;static TaskQueue *m_taskq;
};TaskQueue* TaskQueue::m_taskq = nullptr;

3.懒汉线程安全1

在单例模式中饿汉模式下,针对在多线程中可能存在的线程安全问题(创建多个实例),进行问题修改:

(1)引入互斥锁

在多线程环境下,有可能的情况是:多个线程同时进入到getInstance()方法中的if语句判断中,这时对象就可能被同时创建多个,

//懒汉模式 引入互斥锁
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() {m_mutex.lock();if (m_taskq == nullptr) m_taskq = new TaskQueue;m_mutex.unlock();return m_taskq;}void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;static TaskQueue *m_taskq;static mutex m_mutex;
};mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskq = nullptr;

使用互斥锁对new操作创建实例时进行加锁操作,防止同时创建多个实例,但是程序执行的效率太低(多线程访问单例对象时都是顺序访问)

(2)引入双重检查锁定

双重检查锁定,只有第一次访问时是顺序执行的,在TaskQueue被实例化出来之后,其他线程再去访问单例对象就是并行的了(不会进入if内)。

//懒汉模式 引入双重检查锁定
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() {//双重检查锁定if (m_taskq == nullptr) {m_mutex.lock();if (m_taskq == nullptr) m_taskq = new TaskQueue;m_mutex.unlock();	}return m_taskq;}void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;static TaskQueue *m_taskq;static mutex m_mutex;
};mutex TaskQueue::m_mutex;
TaskQueue* TaskQueue::m_taskq = nullptr;
(3)引入原子变量

通过引入双重检查锁定的方式,解决了在懒汉模式下多线程访问单例对象时,出现的线程安全问题,

从表面上观察引入双重检查锁定的方式是十分完美的,但是从底层上依旧存在漏洞:

  1. 对于 m_taskq = new TaskQueue; 操作,其对应的机器指令并不是一条,而有三条(对于计算机来说代码都是二进制指令/机器指令),

    step1:创建一块内存(没有数据)
    step2:创建 TaskQueue 类型的对象,并将数据写入到对象中
    step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
    
  2. 在实际的执行过程中,m_taskq = new TaskQueue; 对应的机器指令可能会被重新排序,成为

    step1:创建一块内存(没有数据)
    step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
    step2:创建 TaskQueue 类型的对象,并将数据写入到对象中
    
  3. 如果线程A执行完成前两步之后失去CPU时间片被挂起,此时线程B在进行指针判断时,发现指针 m_taskq 不为空(但该指针指向内存没有被初始化),导致线程B使用了一个没有被初始化的队列对象,就会出现问题(出现问题是概率性的)

  4. 在C++11中引入原子变量 atomic,在底层控制机器指令的执行顺序,可以实现一种更加安全的懒汉模式,代码如下:

    使用原子变量 atomicstore() 方法来存储单例对象,使用 load() 方法来加载单例对象,

    在原子变量中这两个函数在处理指令的时候,默认的原子顺序是 memory_order_seq_cst 顺序原子操作,

    使用顺序约束原子操作库,整个函数的执行都将保证顺序执行,并且不会出现数据竞态 data races,

    缺点:使用这种方法实现的懒汉模式的单例执行效率更低一些,

    对代码进行以下修改:

    • 通过原子变量将类的实例对象保存起来(m_taskq 指针指向的内存)

    • 类外初始化 指针指向为nullptr

    • 对 getInstance 方法进行相关的修改操作

      多线层在调用 getInstance 方法时 需要从原子变量中加载任务队列的实例

      抢到互斥锁的线程将继续向下执行 创建实例对象

//懒汉模式 引入原子变量
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() {TaskQueue* taskq = m_taskq.load();if (taskq == nullptr) {m_mutex.lock();taskq = m_taskq.load();if (taskq == nullptr) {taskq = new TaskQueue;m_taskq.store(taskq);}m_mutex.unlock();}return m_taskq.load();}void printTest() { cout << "i am a public method of a singleton class" << endl; }private:TaskQueue() = default;// static TaskQueue *m_taskq;static atomic<TaskQueue*> m_taskq;static mutex m_mutex;};mutex TaskQueue::m_mutex;
atomic<TaskQueue*> TaskQueue::m_taskq;
// TaskQueue* TaskQueue::m_taskq = nullptr;

4.懒汉线程安全2

在懒汉模式线程安全问题中,除了可以通过引入双重检查锁定来解决线程安全问题,还可以使用局部静态对象处理线程安全问题,

(1)设置局部静态对象

使用静态的局部对象解决线程安全问题,要求编译器必修支持C++11标准,

  1. getInstance() 局部函数中定义一个静态局部对象 static TaskQueue taskq; (调用无参构造初始化)
  2. 在C++11标准中规定,如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化,

注:使用静态的局部对象没有线程安全问题,已经由C++11标准中的编译器解决,未被初始化的变量,必须等待其完成初始化才能并发执行,

step1:创建一块内存(没有数据)
step2:创建 TaskQueue 类型的对象,并将数据写入到对象中(完成初始化操作)
step3:为 m_taskq 对象指针初始化,将有效的内存地址传递给 m_taskq 对象指针
// 懒汉模式 静态局部对象
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue* getInstance() {static TaskQueue taskq;return &taskq;}void printTest() { cout << "i am a public method of a singleton class" << endl; }
private:TaskQueue() = default;
};

5.简单案例运用

(1)任务队列简单实现
  1. 多线程拿到单例对象后,在访问单例对象内部的数据时,有线程安全问题(多线程共享资源),使用互斥锁保护多线程中共享的资源,

  2. C++11中给互斥锁加/解锁有两种方式,

    方法1:调用mutex对象的 unlock(); lock(); 方法

    方法2:使用lock_gurd自动管理加/解锁 lock_guard<mutex> locker(m_mutex);

    使用 lock_gurd 可以有效的避免死锁的问题,自动加/解锁

// 饿汉模式
class TaskQueue {
public:TaskQueue(const TaskQueue &t) = delete;TaskQueue& operator=(const TaskQueue &t) = delete;static TaskQueue *getInstance() { return m_taskq; }void printTest() { cout << "i am a public method of a singleton class" << endl; }// 判断任务队列是否为空bool isEmpty() {lock_guard<mutex> locker(m_mutex);return m_data.empty();}// 添加任务void addTask(int node) { lock_guard<mutex> locker(m_mutex);m_data.push(node);}// 删除任务bool removeTask() {lock_guard<mutex> locker(m_mutex);if (m_data.empty()) return false;m_data.pop();return true;}// 获取队头任务int takeTask() {lock_guard<mutex> locker(m_mutex);if (m_data.empty()) return -1;return m_data.front();}
private:TaskQueue() = default;static TaskQueue *m_taskq;// 任务队列queue<int> m_data;mutex m_mutex;
};TaskQueue* TaskQueue::m_taskq = new TaskQueue;
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
using namespace std;int main() {// 获取单例对象TaskQueue *taskq = TaskQueue::getInstance();taskq->printTest();// 生产者线程// 使用匿名函数指定线程的处理动作thread t1([=](){for (int i = 0; i < 25; ++i) {taskq->addTask(i + 100);cout << "++push data:" << i + 100 << ", threadId = " << this_thread::get_id() << endl;this_thread::sleep_for(chrono::milliseconds(500));//休眠500ms}});// 消费者线程thread t2([=](){this_thread::sleep_for(chrono::milliseconds(100));while(!taskq->isEmpty()) {// 开始消费cout << "--take data:" << taskq->takeTask() << ", threadId = " << this_thread::get_id() << endl;taskq->removeTask();this_thread::sleep_for(chrono::milliseconds(1000));//休眠500ms}});// 主线程阻塞 只有当t1、t2线程都结束后 主线程解除阻塞t1.join();t2.join();return 0;
}

在这里插入图片描述

(2)用户登录

当用户成功登录之后,用户名和密码就会被存储到内存中,可以创建一个单例类,将用户数据保存到单例对象中,

class Test {
public:static Test* getInstance() { return &m_test; }// m_uservoid setUserName(QString name) {// 多线程下需要加锁解锁(涉及写操作)// lock();m_user = name;// unlock();}QString getUserName(){ return m_user; }// m_passwd// ....// ....// ....
private:Test();Test(const Test& t);static Test* m_test;// static Test m_test;// 定义变量 -> 属于唯一的单例对象QString m_user;QString m_passwd;QString m_ip;QString m_port;QString m_token;
}
Test* Test::m_test = new Test();	// 初始化
// Test Test::m_test;

tips:部分内容参考课程、书籍与网络等,题解、图示及代码内容根据老师课程、二次整理以及自己对知识的理解,进行整理和补充,仅供学习参考使用,不可商业化。

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

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

相关文章

Jmeter接口自动化测试断言之Json断言

json断言可以让我们很快的定位到响应数据中的某一字段&#xff0c;当然前提是响应数据是json格式的&#xff0c;所以如果响应数据为json格式的话&#xff0c;使用json断言还是相当方便的。 还是以之前的接口举例 Url: https://data.cma.cn/weatherGis/web/weather/weatherFcst…

2023/11/30JAVAweb学习

数组json形式 想切换实现类,只需要只在你需要的类上添加 Component 如果在同一层,可以更改扫描范围,但是不推荐这种方法 注入时存在多个同类型bean解决方式

一些好用的12款前端小插件

1. cropper.js Cropper.js 2.0 是一系列用于图像裁剪的 Web 组件。 官网地址&#xff1a;https://fengyuanchen.github.io/cropperjs/v2/zh/ 2. Vditor Vditor是一款浏览器端的 Markdown 编辑器&#xff0c;支持所见即所得、即时渲染&#xff08;类似 Typora&#xff09;和分…

JavaEE——简单认识CSS

文章目录 一、简单了解什么是 CSS二、CSS 选择器1.标签选择器2.类选择器3.ID 选择器4.后代选择器5.子选择器6.伪类选择器 三、字体属性1.设置字体家族2.设置字体大小3.设置字体粗细4.文字倾斜 四、文本属性1.文本对齐2.文本装饰3.文本缩进4.背景设置 五、圆角矩形六、CSS 盒子模…

【编码艺术:掌握String类函数接口的妙用指南】

【本节目标】 1. 为什么要学习string类 2. 标准库中的string类 1. 为什么要学习string类 1.1 C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c; 但是这些库函数与…

【JavaFX漏扫开发基础】screen类的使用

文章目录 一、screen获取屏幕信息二、查看当前屏幕的分辨率DPI三、scene类和两个小知识一个图形化的结构让界面打开一个网页改变鼠标的图案一、screen获取屏幕信息 package javafx.test;import javafx.application.Application; import

SpringBoot项目发送邮件

&#x1f4d1;前言 本文主要是【SpringBoot】——SpringBoot项目发送邮件的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f3…

【研究中】sql server权限用户设置23.11.26

--更新时间2023.11.26 21&#xff1a;30 负责人&#xff1a;jerrysuse DBAliCMSIF EXISTS (select * from sysobjects where namehkcms_user)--判断是否存在此表DROP TABLE hkcms_user CREATE TABLE hkcms_user (id int primary key identity(1, 1),username char(32) NOT N…

半同步复制与MHA高可用架构设计

各位道友好&#xff0c;鼠鼠我呀校招刚通过了移动的面试 &#xff0c;但是安排的岗位是偏远县城里面的岗位&#xff0c;各位能给给建议吗&#xff1f;鼠鼠我啊真不想有时候变成销售员去卖产品&#xff01;&#xff01;&#xff01; 半同步复制与MHA高可用架构设计 一、半同步复…

Set.toArray(new String[size]) 和 Set.toArray(new String[0]) 区别

Set.toArray(new String[size]) 和 Set.toArray(new String[0]) 区别 在Java中&#xff0c;Set.toArray(T[] array) 方法用于将集合&#xff08;Set&#xff09;中的元素转换为数组。两种常见的用法是&#xff1a; Set.toArray(new String[size]) Set.toArray(new String[0])…

记录一次docker搭建tomcat容器的网页不能访问的问题

tomcat Tomcat是Apache软件基金会的Jakarta项目中的一个重要子项目&#xff0c;是一个Web服务器&#xff0c;也是Java应用服务器&#xff0c;是开源免费的软件。它是一个兼容Java Servlet和JavaServer Pages&#xff08;JSP&#xff09;的Web服务器&#xff0c;可以作为独立的W…

故障排查:rpm安装报错事务锁定(can‘t create transaction lock on /var/lib/rpm/.rpm.lock)

博客主页&#xff1a;https://tomcat.blog.csdn.net 博主昵称&#xff1a;农民工老王 主要领域&#xff1a;Java、Linux、K8S 期待大家的关注&#x1f496;点赞&#x1f44d;收藏⭐留言&#x1f4ac; 目录 故障详情故障原因解决办法 故障详情 自己编写spec文件&#xff0c;制…

Redis链表使用

Redis是优秀的非关系型数据库&#xff0c;源码中的链表是很经典&#xff0c;将其提取使用 /* adlist.c - A generic doubly linked list implementation** Copyright (c) 2006-2010, Salvatore Sanfilippo <antirez at gmail dot com>* All rights reserved.** Redistri…

解密Kafka主题的分区策略:提升实时数据处理的关键

目录 一、Kafka主题的分区策略概述1.1 什么是Kafka主题的分区策略&#xff1f;1.2 为什么分区策略重要&#xff1f; 二、Kafka默认分区策略2.1 Round-Robin分区策略 三、自定义分区策略3.1 编写自定义分区器3.2 最佳实践&#xff1a;如何选择分区策略 四、分区策略的性能考量4.…

【开题报告】OFDM雷达捷变波形信号处理方法研究与仿真

选 题 的 目 的 和 意 义 随着现代科技的不断发展&#xff0c;汽车在人们生活中的比重越来越大&#xff0c;人们对汽车安全的要求越来越高。据统计&#xff0c;我国每年有近万人死于交通事故&#xff0c;汽车在行驶过程中容易出现车速过快、方向失控、侧滑等问题&#xff0c;随…

基于SSM健身房管理系统设计与实现

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本健身房管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

C#文件基本操作(判断文件是否存在、创建文件、复制或移动文件、删除文件以及获取文件基本信息)

目录 一、判断文件是否存在 1.File类的Exists()方法 2.FileInfo类的Exists属性 二、创建文件 1.File类的Create()方法 2.FileInfo类的Create()方法 三、复制或移动文件 1.File类的Copy()方法 2.File类的Move()方法 3.FileInfo类的CopyTo()方法 四、删除文件 1.File…

Linux:创建进程 -- fork,到底是什么?

相信大家在初学进程时&#xff0c;对fork函数创建进程一定会有很多的困惑&#xff0c;比如&#xff1a; 1.fork做了什么事情?? 2.为什么fork函数会有两个返回值?3.为什么fork的两个返回值&#xff0c;会给父进程谅回子进程pid&#xff0c;给子进程返回0?4.fork之后:父子进…

Webpack ECMAScript 模块

文章目录 前言标题一导出导入将模块标记为 ESM 后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;webpack &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&a…

新手村之SQL——分组与子查询

1.GROUP BY GROUP BY 函数就是 SQL 中用来实现分组的函数&#xff0c;其用于结合聚合函数&#xff0c;能根据给定数据列的每个成员对查询结果进行分组统计&#xff0c;最终得到一个分组汇总表。 mysql> SELECT country, COUNT(country) AS teacher_count-> FROM teacher…