用一个通俗易懂的例子彻底说清楚单例模式
时间:2022-07-23
本文章向大家介绍用一个通俗易懂的例子彻底说清楚单例模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
一、背景
- 在企业网站后台系统中,一般会将网站统计单元进行独立设计,比如登录人数的统计、IP数量的计数等。在这类需要完成全局统计的过程中,就会用到单例模式,即整个系统只需要拥有一个计数的全局对象。
- 在网站登录这个高并发场景下,由这个全局对象负责统计当前网站的登录人数、IP等,即节约了网站服务器的资源,又能保证计数的准确性。
二、单例模式
1、概念
单例模式是最常见的设计模式之一,也是整个设计模式中最简单的模式之一。
单例模式需确保这个类只有一个实例,而且自行实例化并向整个系统提供这个实例;这个类也称为单例类,提供全局访问的方法。
单例模式有三大要点:
- 构造方法私有化; -- private Singleton() { }
- 实例化的变量引用私有化; -- private static final Singleton APP_INSTANCE = new Singleton();
- 获取实例的方法共有 -- public static SimpleSingleton getInstance() { -- return APP_INSTANCE; -- }
2、网站计数的单例实现
实现单例模式有多种写法,这里我们只列举其中最常用的三种实现方式,且考虑到网站登录高并发场景下,将重点关注多线程环境下的安全问题。
- 登录线程的实现 我们先创建一个登录线程类,用于登录及登录成功后调用单例对象进行计数。
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
// 登录名称
private String loginName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public void run() {
// TODO
// 登录成功后调用单例对象进行计数
}
}
- 主程序的实现 编写一个主程序,利用多线程技术模拟10个用户并发登录,完成登录后输出登录人次计数。
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
// TODO
// 调用单例对象输出登录人数统计
}
2.1 饿汉模式
- 在程序启动之初就进行创建( 不管三七二十一,先创建出来再说)。
- 天生的线程安全。
- 无论程序中是否用到该单例类都会存在。
/**
* 饿汉式单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public class SimpleSingleton implements Serializable {
// 单例对象
private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
我们将饿汉模式的单例对象加入进登录线程及主程序中进行测试:
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
// 登录名称
private String loginName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public void run() {
// 饿汉式单例
SimpleSingleton simpleSingleton= SimpleSingleton.getInstance();
simpleSingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+simpleSingleton.toString());
}
}
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有"+SimpleSingleton.getInstance().getCount()+"个用户登录");
}
}
输出如下: 10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式是有效的。
2.2 懒汉模式
- 在初始化时只进行定义。
- 只有在程序中调用了该单例类,才会完成实例化( 没人动我,我才懒得动)。
- 需通过线程同步技术才能保证线程安全。
我们先看下未使用线程同步技术的例子:
/**
* 懒汉式单例模式--未应用线程同步技术
*
* @author zhuhuix
* @date 2020-06-01
*/
public class LazySingleton {
// 单例对象
private static LazySingleton APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
....
@Override
public void run() {
// 饿汉式单例
LazySingleton lazySingleton =LazySingleton.getInstance();
lazySingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+lazySingleton);
}
}
/**
* 单例模式--主程序-
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有" + LazySingleton.getInstance().getCount() + "个用户登录");
}
}
输出结果: 10个线程并发登录过程中,获取到了四个对象引用地址,该单例模式失效了。
对代码进行分析:
// 未使用线程同步
public static LazySingleton getInstance() {
// 在多个线程并发时,可能会有多个线程同时进入 if 语句,导致产生多个实例
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
return APP_INSTANCE;
}
我们使用线程同步技术对懒汉式模式进行改进:
/**
* 懒汉式单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public class LazySingleton {
// 单例对象 ,加入volatile关键字进行修饰
private static volatile LazySingleton APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (APP_INSTANCE == null) {
// 对类进行加锁,并进行双重检查
synchronized (LazySingleton.class) {
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
}
}
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
再测试运行: 10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式有效了。
2.3 枚举类实现单例模式
《Effective Java》 推荐使用枚举的方式解决单例模式。这种方式解决了最主要的;线程安全、自由串行化、单一实例。
/**
* 利用枚举类实现单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public enum EnumSingleton implements Serializable {
// 单例对象
APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private EnumSingleton() {
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
...
@Override
public void run() {
EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE;
enumSingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+enumSingleton.toString());
}
}
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有"+EnumSingleton.APP_INSTANCE.getCount()+"个用户登录");
}
}
输出如下: 10个线程并发登录过程中,该单例模式是有效的。
三、总结
- 文中首先说明了单例模式在网站计数的应用:创建唯一的全局对象实现统计单元的计数。
- 根据该需求,建立了Login登录线程类及App主程序,模拟多用户同步并发登录。
- 分别设计了饿汉模式、懒汉模式、枚举类三种不同的实现单例模式的方式。
- 在设计单例模式的过程中,特别要注意线程同步安全的问题,文中以懒汉模式列出了线程不同步的实际例子。
- 延伸思考:《Effective Java》为什么说实现单例模式的最佳方案是单元素枚举类型?
- .NET Core的日志[3]:将日志写入Debug窗口
- Code2Cloud:比ALM中断更大
- .NET Core的日志[4]:将日志写入EventLog
- 微信小程序不行了?看小马哥带你忆童年
- ASP.NET MVC三个重要的描述对象:ControllerDescriptor和ActionDescriptor的创建
- .NET Core的日志[5]:利用TraceSource写日志
- 物联网芯片正在积极开发 明年将得到爆发
- 韩国全球首测5G网络下自动驾驶 为汽车安全保驾护航的竟是路灯
- 通过与Quickbuild和Mist.io的持续集成实现云管理和使用监控
- .NET Core的文件系统[1]:读取并监控文件的变化
- ASP.NET MVC以ValueProvider为核心的值提供系统: ValueProviderFactory
- 云本机应用程序成熟度的模型
- 如何利用ETW(Event Tracing for Windows)记录日志
- 如何利用ETW(Event Tracing for Windows)记录日志
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Three.js学习|代码详解 个人见解
- Linux进程详解
- Face_Recognition_v3a
- Building_a_Recurrent_Neural_Network_Step_by_Step_v3b
- gym 搭建 RL 环境
- MNIST练习
- Dinosaurus_Island_Character_level_language_model_final_v3b
- Trigger_word_detection_v1a
- 《深入浅出SQL》问答录(二)
- 《深入浅出SQL》问答录(四)
- 《深入浅出MySQL》问答录(五)
- 《深入浅出SQL》问答录(七)
- 《深入浅出SQL》问答录(八)
- Improvise_a_Jazz_Solo_with_an_LSTM_Network_v3a-2
- 《深入浅出SQL》问答录(九)