实现单例设计模式的多种方式
1. 关于单例设计模式
Singleton:在Java中即指单例设计模式,它是软件开发中最常用的设计模式之一。
单:唯一
例:实例
单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
例如:代表JVM运行环境的Runtime类
要点:
(1)某个类只能有一个实例:构造器私有化
(2)它必须能够创建该类实例:使用该类的静态变量来保存这个唯一的实例。
(3)它必须能返回该实例:通过get方法返回该实例或直接暴露给外部.
2. 单例设计模式的种类
2.1 饿汉式:直接创建对象,不存在线程安全问题
2.1.1 直接实例化饿汉式(简洁直观)
/*
* 饿汉式:
* 在类初始化时直接创建实例对象,不管是否需要这个对象都会创建
* 单例创建要求:
* (1)构造器私有化
* (2)自行创建,并且用静态变量保存
* (3)向外提供这个实例
* (4)强调这是一个单例,我们可以用final修饰
*/
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static void main(String[] args) {
Singleton1 s = Singleton1.INSTANCE;
System.out.println(s);
}
}
2.1.2 枚举式(最简洁)
package com.bigdata.juc.singleton;
/*
* 枚举类型:表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
enum EnumSingleton{
INSTANCE
}
public class Singleton2 {
public static void main(String[] args) {
EnumSingleton s = EnumSingleton.INSTANCE;
System.out.println(s);
}
}
2.1.3 静态代码块饿汉式(适合复杂实例化)
//静态代码块饿汉式
public class Singleton3 {
public static final Singleton3 INSTANCE;
public String info;
private Singleton3(String info){
this.info = info;
}
static{
try {
Properties pro = new Properties();
pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton3(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Singleton3 s = Singleton3.INSTANCE;
System.out.println(s.info);
}
}
2.2 懒汉式:延迟创建对象
2.2.1 线程不安全(适用于单线程)
import java.util.concurrent.*;
/*
* 懒汉式:
* 延迟创建这个实例对象,可能会出现线程不安全的情况
*
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if(instance == null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton4();
}
return instance;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
//检查这种创建方式的线程安全性
Callable<Singleton4> c = new Callable<Singleton4>() {
@Override
public Singleton4 call() throws Exception {
return Singleton4.getInstance();
}
};
//使用newFixedThreadPool线程池,设置线程数为2
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton4> f1 = es.submit(c);
Future<Singleton4> f2 = es.submit(c);
Singleton4 s1 = f1.get();
Singleton4 s2 = f2.get();
System.out.println(s1 == s2);//false 说明这种创建单例的方式,在多线程的方式下并不安全
System.out.println(s1);
System.out.println(s2);
es.shutdown();
// new Thread(()->{
// System.out.println(Thread.currentThread().getName()+":"+Singleton4.getInstance());
// },"A").start();
// new Thread(()->{
// System.out.println(Thread.currentThread().getName()+":"+Singleton4.getInstance());
// },"B").start();
}
}
2.2.2 线程安全(适用于多线程)
/*
* 懒汉式:
* 延迟创建这个实例对象,针对于线程不安全的问题,可以在创建的时候,使用lock或synchronize来解决
*
* (1)构造器私有化
* (2)用一个静态变量保存这个唯一的实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){
}
//并不推荐使用同步方法的方式,来完成单例模式设计,因为它锁了整个方法,其他线程想要获取实例只能等待,显然效率比较低,推荐使用DCL(Double Check Lock 双端检锁机制)来完成单例设计
// public synchronized static Singleton5 getInstance(){
public static Singleton5 getInstance(){
if(instance == null){
//使用同步代码块来解决单例创建过程中的线程不安全问题,也可以使用同步方法来实现
synchronized (Singleton5.class) {
if(instance == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton5();
}
}
}
return instance;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
//验证多线程环境下,这种创建线程的方式是否安全
Callable<Singleton5> c = new Callable<Singleton5>() {
@Override
public Singleton5 call() throws Exception {
return Singleton5.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton5> f1 = es.submit(c);
Future<Singleton5> f2 = es.submit(c);
Singleton5 s1 = f1.get();
Singleton5 s2 = f2.get();
System.out.println(s1 == s2);//true
System.out.println(s1);
System.out.println(s2);
es.shutdown();
}
}
注意这里在实现多线程下的单例设计时,尽管使用DCL(Double Check Lock 双端检锁机制),但仍然存在一种问题,是由于指令重排导致的,所以为了防止执行重排,需要在instance上添加volatile修饰符
//添加volatile禁止指令重排
private static volatile Singleton5 instance;
所说的问题也就是这样的,由于指令重排,程序未必按照上面的编写顺序执行,当某一个线程在执行时,读取到的instance不为null时,但instanc对象可能没有完成初始化,最终所得到的instance可能还是null,使用时导致空指针异常。
关于这点介绍,在周志明的《深入理解Java虚拟机 第二版》的P370,DCL单例模式中有详细的解释
这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机
引用链接:https://www.cnblogs.com/zhanqing/p/11076646.html
2.2.3 静态内部类形式(适用于多线程)
/*
* 在内部类被加载和初始化时,才创建INSTANCE实例对象
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
* 因为是在内部类加载和初始化时,创建的,因此是线程安全的
*/
public class Singleton6 {
private Singleton6(){
}
private static class Inner{
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance(){
return Inner.INSTANCE;
}
}
3.总结:
- 如果是饿汉式,枚举形式最简单
- 如果是懒汉式,静态内部类形式最简单
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程