Java设计模式-3、单例模式

单例模式

单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例, 构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例,对外提供⼀ 个静态公有⽅法获取实例。

优点是内存中只有⼀个实例,减少了开销,尤其是频繁创建和销毁实例的 情况下并且可以避免对资源的多重占⽤。

缺点是没有抽象层,难以扩展, 与单⼀职责原则冲突。

单例模式的设计规则

由定义我们可以很清晰的抽象出: 实现Java单例模式类有哪些通用设计规则?

(1)私有化类构造器。

(2)定义静态私有的类对象。

(3)提供公共静态的获取该私有类对象的方法。

了解了单例模式的概念,以及单例模式的通用设计规则,对于如何实现一个Java单例,应该是没什么阻碍了。这里我们还是要思考下单例模式的优点,或者说有啥好处,使用场景是什么?带着这些问题我们就能更好的设计单例模式。

为什么使用单例

1.Java单例模式解决了什么问题?

答:Java的单例模式主要解决了多线程并发访问共享资源的线程安全问题。

2.Java单例模式主要应用场景有哪些?

答:1.共享资源的访问与操作场景,如Windows系统的资源管理器,Windows系统的回收站,显卡的驱动程序,系统的配置文件,工厂本身(类模板),应用程序的日志对象等。

2.控制资源访问与操作的场景,如数据库的连接池,Java的线程池等。

 单例模式的常⻅写法有哪些?

 1. 饿汉式,线程安全

饿汉式单例模式,顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,
但容易产⽣垃圾对象,浪费内存空间。
优点:线程安全,没有加锁,执⾏效率较⾼
缺点:不是懒加载,类加载时就初始化,浪费内存空间

饿汉式单例是如何保证线程安全的呢?
答:java虚拟机加载类的过程时线程安全的,ClassLoader.loadClass方法中使用Synchronized关键字实现。
核心代码:

它是基于类加载机制避免了多线程
的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。
代码如下:
public class Singleton {
	private static final Singleton instance = new Singleton();

	// 私有的默认构造函数
	public Singleton() {
		
	}

	// 静态工厂方法
	public static Singleton getInstance() {
		return instance;
	}

	public void get() {
        System.out.println("get");
    }
	
	public static void main(String[] args) {
		Singleton.getInstance().get();
	}
}
使⽤反射破坏单例,代码如下:
public class Singleton {

    private static final Singleton instance = new Singleton();

    // 私有的默认构造函数
    private Singleton() {

    }

    // 静态工厂方法
    public static Singleton getInstance() {
        return instance;
    }

    public void get() {
        System.out.println("get");
    }

//	public static void main(String[] args) {
//		Singleton.getInstance().get();
//	}

    public static void main(String[] args) throws Exception {
        // 使⽤反射破坏单例
        // 获取空参构造⽅法
        Constructor<Singleton> declaredConstructor =
                Singleton.class.getDeclaredConstructor(null);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射创建的实例" + singleton);
        System.out.println("正常创建的实例" +
                Singleton.getInstance());
        System.out.println("正常创建的实例" +
                Singleton.getInstance());
    }
}

结果:

 2. 懒汉式,线程不安全

这种⽅式在单线程下使⽤没有问题,对于多线程是⽆法保证单例的,这⾥
列出来是为了和后⾯使⽤锁保证线程安全的单例做对⽐。

优点:懒加载
缺点:线程不安全
代码实现如下:
/**
* 懒汉式单例,线程不安全
*
*/
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton(){ }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public static Singleton getInstance() {
 // 判断为 null 的时候再创建对象
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}
使⽤多线程破坏单例,测试代码如下:
public class Test {
 public static void main(String[] args) {
 for (int i = 0; i < 3; i++) {
 new Thread(() -> {
 System.out.println("多线程创建的单例:" +
Singleton.getInstance());
 }).start();
 }
 }
}

结果: 

 懒汉式,线程安全

懒汉式单例如何保证线程安全呢?通过 synchronized 关键字加锁保证线程 安全, synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这 ⾥演示添加在⽅法上⾯,存在的问题是 每⼀次调⽤ getInstance 获取实例时 都需要加锁和释放锁,这样是⾮常影响性能的。
优点:懒加载,线程安全
缺点:效率较低
代码实现如下

public class Singleton {
 // 1、私有化构造⽅法
 private Singleton(){ }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public synchronized static Singleton getInstance() {
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}
双重检查锁( DCL , 即 double-checked locking )常用
实现代码如下:
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton() {
 }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private volatile static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public static Singleton getInstance() {
 // 第⼀重检查是否为 null
 if (instance == null) {
 // 使⽤ synchronized 加锁
 synchronized (Singleton.class) {
 // 第⼆重检查是否为 null
 if (instance == null) {
 // new 关键字创建对象不是原⼦操作
 instance = new Singleton();
 }
 }
 }
 return instance;
 }
}
优点:懒加载,线程安全,效率较⾼
缺点:实现较复杂
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么 要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就 不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进 ⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调 ⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同 步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创 建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。
关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁 竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null ,会进⾏单例对 象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返 回已创建的单例对象。
双重检查锁中使⽤ volatile 的两个重要特性: 可⻅性、禁⽌指令重排序

 这⾥为什么要使⽤ volatile

这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯
的步骤:
1. 在堆内存开辟内存空间
2. 调⽤构造⽅法,初始化对象
3. 引⽤变量指向堆内存空间
对应字节码指令如下:

为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重 排序,从源码到最终执⾏指令会经历如下流程:
源码编译器优化重排序指令级并⾏重排序内存系统重排序最终执⾏指令序列, 所以经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3  2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存 空间,这个对象不为 null ,但是没有初始化,其他线程有可能这个时候进
⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使 ⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。

当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令 的前后添加内存屏障来 禁⽌指令重排序 ,就可以避免这个问题,⽽且对 volatile 修饰的变量的修改对其他任何线程都是可⻅的。
静态内部类
代码实现如下:
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton() {
 }
 // 2、对外提供获取实例的公共⽅法
 public static Singleton getInstance() {
 return InnerClass.INSTANCE;
 }
 // 定义静态内部类
 private static class InnerClass{
 private final static Singleton INSTANCE = new
Singleton();
 }
}
优点:懒加载,线程安全,效率较⾼,实现简单
静态内部类单例是如何实现懒加载的呢?⾸先,我们先了解下类的加载时 机。
虚拟机规范要求有且只有 5 种情况必须⽴即对类进⾏初始化(加载、验 证、准备需要在此之前开始):
1. 遇到 new getstatic putstatic invokestatic 4 条字节码指令
时。⽣成这 4 条指令最常⻅的 Java 代码场景是:使⽤ new 关键字实
例化对象的时候、读取或设置⼀个类的静态字段( final 修饰除外,被
final 修饰的静态字段是常量,已在编译期把结果放⼊常量池)的时
候,以及调⽤⼀个类的静态⽅法的时候。
这里的逻辑,就是引用static的提前初始化,确定好了单例对象
2. 使⽤ java.lang.reflect 包⽅法对类进⾏反射调⽤的时候。
3. 当初始化⼀个类的时候,如果发现其⽗类还没有进⾏过初始化,则需要
先触发其⽗类的初始化。
4. 当虚拟机启动时,⽤户需要指定⼀个要执⾏的主类(包含 main() 的那
个类),虚拟机会先初始化这个主类。
5. 当使⽤ JDK 1.7 的动态语⾔⽀持时,如果⼀个 java.lang.invoke.MethodHandle 实例最后的解析结果是
REF_getStatic REF_putStatic REF_invokeStatic 的⽅法句柄,则需
要先触发这个⽅法句柄所对应的类的初始化。
getInstance() ⽅法被调⽤时, InnerClass 才在 Singleton 的运⾏时常量 池⾥,把符号引⽤替换为直接引⽤,这时静态对象 INSTANCE 也真正被创 建,然后再被 getInstance() ⽅法返回出去,这点同饿汉模式。

 枚举单例

代码实现如下:
public enum Singleton {
 INSTANCE;
 public void doSomething(String str) {
 System.out.println(str);
 }
}
优点:简单,⾼效,线程安全,可以避免通过反射破坏枚举单例枚举在 java 中与普通类⼀样,都能拥有字段与⽅法,⽽且枚举实例创建是 线程安全的,在任何情况下,它都是⼀个单例,可以直接通过如下⽅式调
⽤获取实例:
Singleton singleton = Singleton.INSTANCE;
使⽤下⾯的javap命令反编译枚举类
javap Singleton.class
得到如下内容
Compiled from "Singleton.java"
public final class com.whm.demo.singleton.Singleton
extends
java.lang.Enum<com.whm.demo.singleton.Singleton> {
 public static final
com.spring.demo.singleton.Singleton INSTANCE;
 public static com.whm.demo.singleton.Singleton[]
values();
 public static com.whm.demo.singleton.Singleton
valueOf(java.lang.String);
 public void doSomething(java.lang.String);
 static {};
从枚举的反编译结果可以看到, INSTANCE static final 修饰,所以可以 通过类名直接调⽤,并且创建对象的实例是在静态代码块中创建的 ,因为 static 类型的属性会在类被加载之后被初始化,当⼀个 Java 类第⼀次被真 正使⽤到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程 安全的,所以创建⼀个 enum 类型是线程安全的。

通过反射验证破坏枚举,实现代码如下:

public class Test {
 public static void main(String[] args) throws
Exception {
 Singleton singleton = Singleton.INSTANCE;
 singleton.doSomething("hello enum");
 // 尝试使⽤反射破坏单例
 // 枚举类没有空参构造⽅法,反编译后可以看到枚举有⼀个两个
参数的构造⽅法
 Constructor<Singleton> declaredConstructor =
Singleton.class.getDeclaredConstructor(String.class,
int.class);
 // 设置强制访问
 declaredConstructor.setAccessible(true);
 // 创建实例,这⾥会报错,因为⽆法通过反射创建枚举的实例
 Singleton enumSingleton =
declaredConstructor.newInstance();
 System.out.println(enumSingleton);
 }
}
运⾏结果报如下错误:

 查看反射创建实例的 newInstance() ⽅法,有如下判断:

 所以⽆法通过反射创建枚举的实例。

单例模式总结:

Singleton 模式中的实例构造器可以设置为 protected 以允许子类派生。

Singleton 模式一般不要实现 Clone 接口,因为这有可能导致多个对象实例,与 Singleton 模式的初衷违背。

如何实现多线程环境下安全的 Singleton?     需注意对双检查锁的正确实现。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录|day26|回溯算法part03● 39. 组合总和● 40.组合总和II● 131.分割回文串

今天的练习基本就是回溯法组合问题&#xff0c;这一节只要看labuladong即可。 组合问题&#xff1a; 39. 组合总和---------------------形式三&#xff0c;元素无重可复选 链接&#xff1a;代码随想录 一次对&#xff0c;同样在进入下次循环时&#xff0c;注意startindex是从j…

欧莱雅校招负责人张泽宇:拥抱Z世代,探索新玩法

作为校招HR&#xff0c;你在雇主品牌创新实践的路上做过什么尝试&#xff1f; 2020年&#xff0c;欧莱雅正式推出了全新的雇主品牌价值主张 —— 敢为敢超越&#xff0c;就是欧莱雅&#xff08;Freedom to go beyond, thats the beauty of L’ORAL&#xff09;&#xff0c;鼓励…

使用ChatGPT进行AI对话

1.ChatGPT简介 ChatGPT是美国人工智能研究实验室OpenAI新推出的一种人工智能技术驱动的自然语言处理工具&#xff0c;使用了Transformer神经网络架构&#xff0c;也是GPT-3.5架构&#xff0c;这是一种用于处理序列数据的模型&#xff0c;拥有语言理解和文本生成能力&#xff0c…

C/C++ 日期 时间 函数总结

使用C标准库 有四个与时间相关的类型&#xff1a;clock_t、time_t、size_t 和 tm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数 头文件 #include <time.h> #include <stdio.h> tm 结构: struct tm {int tm_sec; // 秒&#xff0c;…

隐私计算-TEE执行环境

一、TEE 的定义 论述完 TEE 的概念后&#xff0c;接下来进一步解析 TEE 的深层定义。目前对于 TEE 的定义有很多种形式&#xff0c;针对于不同的安全性需求和平台&#xff0c;TEE 的定义也不尽相同&#xff0c;但在所有 TEE 的定义中都会包含两个最关键的点&#xff1a;独立执…

谈谈分布式一致性机制

分布式中一致性是非常重要的&#xff0c;分为弱一致性和强一致性。 现在主流的一致性协议一般都选择的是弱一致性的特殊版本&#xff1a;最终一致性。下面就从分布式系统的基本原则讲起&#xff0c;再整理一些遵循这些原则的协议或者机制&#xff0c;争取通俗易懂。 但是要真…

【通过代理监听UIScrollView的滚动事件 Objective-C语言】

一、输出,当UIScrollView滚动的时候,实时输出当前UIScrollView滚动的位置, 1.用代理实现吧, contentOffset,代表偏移吧,我需要你当UIScrollView滚动的时候,实时输出UIScrollView滚动的位置, 2.第一,我们如何获得UIScrollView滚动的位置呢,contentOffset,是不是就是…

【创作赢红包】LeetCode:232. 用栈实现队列

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340;算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 一、&#x1f331;232. 用栈实现队列 题目描述&#xff1a;请你仅使用两个栈实现先入先出队…

【论文速递】ACL 2022 - 查询和抽取:将事件抽取细化为面向类型的二元解码

【论文速递】ACL 2022 - 查询和抽取&#xff1a;将事件抽取细化为面向类型的二元解码 【论文原文】&#xff1a;Query and Extract: Refining Event Extraction as Type-oriented Binary Decoding 【作者信息】&#xff1a;Wang, Sijia and Yu, Mo and Chang, Shiyu and Sun,…

IP地址规划方法

一、IP地址规划的基本步骤&#xff1a; &#xff08;1&#xff09;判断用户对网络以及主机数的需求&#xff1b; &#xff08;2&#xff09;计算满足用户需要的基本网络地址结构&#xff1b; &#xff08;3&#xff09;计算地址掩码&#xff1b; &#xff08;4&#xff09;…

React Three Fiber动画入门

使用静态对象和形状构建 3D 场景非常酷&#xff0c;但是当你可以使用动画使场景栩栩如生时&#xff0c;它会更酷。 在 3D 世界中&#xff0c;有一个称为角色装配的过程&#xff0c;它允许你创建称为骨架的特殊对象&#xff0c;其作用类似于骨骼和关节系统。 这些骨架连接到一块…

2023-03-24:音视频mp3和h264混合(muxer)编码为mp4,用go语言编写。

2023-03-24&#xff1a;音视频mp3和h264混合&#xff08;muxer&#xff09;编码为mp4&#xff0c;用go语言编写。 答案2023-03-24&#xff1a; 这是一个使用FFmpeg库将MP3和H.264混合编码为MP4的Go语言程序。程序的大体过程如下&#xff1a; 1.设置FFmpeg库路径和环境变量。…

ChatGPT来了,让我们快速做个AI应用

你好&#xff0c;我是徐文浩。 过去的两讲&#xff0c;我带着你通过OpenAI提供的Embedding接口&#xff0c;完成了文本分类的功能。那么&#xff0c;这一讲里&#xff0c;我们重新回到Completion接口。而且这一讲里&#xff0c;我们还会快速搭建出一个有界面的聊天机器人来给你…

五分钟了解支付、交易、清算、银行等专业名词的含义?

五分钟了解支付、交易、清算、银行等专业名词的含义&#xff1f;1. 支付类名词01 支付应用02 支付场景03 交易类型04 支付类型&#xff08;按通道类型&#xff09;05 支付类型&#xff08;按业务双方类型&#xff09;06 支付方式07 支付产品08 收银台类型09 支付通道10 通道类型…

Unity Avatar Cover System - 如何实现一个Avatar角色的智能掩体系统

文章目录简介变量说明实现动画准备动画状态机State 状态NoneStand To CoverIs CoveringCover To Stand高度适配高度检测脚部IK简介 本文介绍如何在Unity中实现一个Avatar角色的智能掩体系统&#xff0c;效果如图所示&#xff1a; 初版1.0.0代码已上传至SKFramework框架Package…

【Nginx】Nginx的学习(3.Nginx命令和nginx配置文件)

1. Nginx命令 1.1 启动nginx systemctl start nginx1.2 停止nginx systemctl stop nginx1.3 重载nginx # 重新加载配置文件 systemctl reload nginx1.4 查看nginx服务端口 netstat -anpl | grep nginx1.5 查看nginx进程 ps aux | grep nginx2. nginx的配置文件 2.1 查看…

git拉取github上的项目

git拉取github上的项目测试创建bash公钥&#xff0c;拉取代码1.先创建github账号和项目&#xff1b;系统安装git程序2.先配置ssh公钥,为了避免每次远程访问需要输密码&#xff0c;将使用ssh登陆。ssh应该与本机信息绑定&#xff0c;查看自己电脑 C:\Users\lenovo\.ssh 目录下是…

预训练语言模型(GPT,BERT)

文章目录GPT 模型预训练语言模型模型和学习BERT 模型去噪自编码器模型和学习模型特点References在自然语言处理中事先使用大规模语料学习基于 Transformer 等的语言模型&#xff0c;之后用于各种任务的学习和预测&#xff0c;称这种模型为预训练语言模型。代表性的模型有 BERT …

STA环境 - 时钟

目录1. 指定时钟create_clock1.1. 时钟延迟set_clock_latency 1.2. 时钟不确定度&#xff08;时钟抖动&#xff09;set_clock_uncertainty 1.3. 时钟过渡时间set_clock_transition 2. 衍生时钟create_generated_clock3. 划定时钟域set_clock_groupsSTA环境配置中对时钟如何约束…

【总结】爬虫4-selenium

爬虫4-selenium 1. selenium 基本操作 在使用selenium之前必须先配置浏览器对应版本的webdriver。才可以控制浏览器打开网页 1.1 创建浏览器对象 b Chrome()1.2 打开网页 &#xff08;需要哪个网页数据&#xff0c;就打开那个网页对应的网页地址&#xff09; b.get(https…