设计模式学习04(Java实现)——单例模式

康小庄 2021-06-23 20:34:15
java spring Mysql SpringBoot intellij-idea


写在前面

  • 记录学习设计模式的笔记
  • 提高对设计模式的灵活运用

学习地址

https://www.bilibili.com/vide...

https://www.bilibili.com/vide...

参考文章

http://c.biancheng.net/view/1...

项目源码
https://gitee.com/zhuang-kang/DesignPattern

4,创建者模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

创建型模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工程模式
  • 原型模式
  • 建造者模式

5,单例模式

5.1 单例模式的定义和特点

单例(Singleton)模式的定义: 指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletgContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

5.2 单例模式的结构与实现

5.2.1 单例模式的结构

  1. 单例类:包含一个实例且能自行创建这个实例的类。
  2. 访问类:使用单例的类。

5.2 代码实现

单例设计模式分类两种:

饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式(静态变量)

package com.zhuang.singleton.type1;
/**
* @Classname SingletonTest01
* @Description 饿汉式(静态变量)
* @Date 2021/3/17 9:26
* @Created by dell
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
//1.构造器私有化,外部能new、
private Singleton() {
}
//本类内部创建对象实例
private final static Singleton instance = new Singleton();
//对外部提供一个公有的静态方法
public static Singleton getInstance() {
return instance;
}
}

<font color='red'>说明:</font>

该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

静态代码块

package com.zhuang.singleton.type2;
/**
* @Classname SingletonTest02
* @Description 静态代码块
* @Date 2021/3/17 9:35
* @Created by dell
*/
public class SingletonTest02 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton2 {
//1.构造器私有化,外部能new、
private Singleton2() {
}
//本类内部创建对象实例
private static Singleton2 instance;
/*
在静态代码块中创建对象
*/
static {
instance = new Singleton2();
}
//对外部提供一个公有的静态方法
public static Singleton2 getInstance() {
return instance;
}
}

<font color='red'>说明:</font>

该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,**当然该方式也存在内存浪费问题。**

懒汉式 线程不安全

package com.zhuang.singleton.type3;
/**
* @Classname SingletonTest03
* @Description 懒汉式 线程不安全
* @Date 2021/3/17 9:39
* @Created by dell
*/
public class SingletonTest03 {
public static void main(String[] args) {
System.out.println("懒汉式,线程不安全!!!");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法 当使用到该方法时,才去创建instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

<font color='red'>说明:</font>

从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

懒汉式(线程安全 , 同步方法)

package com.zhuang.singleton.type4;
/**
* @Classname SingletonTest04
* @Description 懒汉式(线程安全 , 同步方法)
* @Date 2021/3/17 9:46
* @Created by dell
*/
public class SingletonTest04 {
public static void main(String[] args) {
System.out.println("懒汉式,线程安全!!!");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

<font color='red'>说明:</font>

该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

懒汉式(线程安全 , 同步代码块)

package com.zhuang.singleton.type5;
/**
* @Classname SingletonTest05
* @Description 懒汉式(线程安全 , 同步代码块)
* @Date 2021/3/17 9:50
* @Created by dell
*/
public class SingletonTest05 {
public static void main(String[] args) {
System.out.println("懒汉式,线程安全!,同步代码块");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

package com.zhuang.singleton.type6;
/**
* @Classname SingletonTest06
* @Description 双重检查,推荐使用
* @Date 2021/3/17 9:54
* @Created by dell
*/
public class SingletonTest06 {
public static void main(String[] args) {
System.out.println("懒汉式,双重检查,推荐使用");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入双重检查代码,加入同步处理的代码,解决懒加载的问题
//保证效率。推荐使用
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}

<font color="red">小结:</font>

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

静态内部类实现单例模式!

package com.zhuang.singleton.type7;
/**
* @Classname SingletonTest07
* @Description 静态内部类实现单例模式!
* @Date 2021/3/17 9:59
* @Created by dell
*/
public class SingletonTest07 {
public static void main(String[] args) {
System.out.println("静态内部类实现单例模式");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//写一个静态内部类,该类中有个静态属性,Singleton
public static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE;
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}

<font color='red'>说明:</font>

第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder

并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

<font color="red">小结:</font>

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举的方式实现单例模式

package com.zhuang.singleton.type8;
/**
* @Classname SingletonTest08
* @Description 枚举的方式实现单例模式
* @Date 2021/3/17 10:06
* @Created by dell
*/
public class SingletonTest08 {
public static void main(String[] args) {
System.out.println("枚举的方式实现单例模式,推荐使用");
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
/*
枚举
*/
enum Singleton {
INSTANCE;//属性
public void method() {
System.out.println("method()方法被调用...");
}
}

说明:

枚举方式属于恶汉式方式。

5.3 单例模式的应用场景

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

5.4 存在的问题

破坏单例模式:

使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。

  • 序列化反序列化

    Singleton类:

    public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    }

Test类:

public class Test {
public static void main(String[] args) throws Exception {
//往文件中写对象
//writeObject2File();
//从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\dell\\Desktop\\a.txt"));
//第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
//获取Singleton类的对象
Singleton instance = Singleton.getInstance();
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\dell\\Desktop\\a.txt"));
//将instance对象写出到文件中
oos.writeObject(instance);
}
}
上面代码运行结果是 false,表明序列化和反序列化已经破坏了单例设计模式。
  • 反射

    Singleton类:

    public class Singleton {
    //私有构造方法
    private Singleton() {}
    private static volatile Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    if(instance != null) {
    return instance;
    }
    synchronized (Singleton.class) {
    if(instance != null) {
    return instance;
    }
    instance = new Singleton();
    return instance;
    }
    }
    }

Test类:

public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}

上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

<font color="red">注意:</font>枚举方式不会出现这两个问题。

问题的解决

  • 序列化、反序列方式破坏单例模式的解决方法

    在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    Singleton类:

    public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
    }
    /**
    * 下面是为了解决序列化反序列化破解单例模式
    */
    private Object readResolve() {
    return SingletonHolder.INSTANCE;
    }
    }

源码解析:

ObjectInputStream类

public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}
  • 反射方式破解单例的解决方法

    public class Singleton {
    //私有构造方法
    private Singleton() {
    /*
    反射破解单例模式需要添加的代码
    */
    if(instance != null) {
    throw new RuntimeException();
    }
    }
    private static volatile Singleton instance;
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
    if(instance != null) {
    return instance;
    }
    synchronized (Singleton.class) {
    if(instance != null) {
    return instance;
    }
    instance = new Singleton();
    return instance;
    }
    }
    }

<font color="red">说明:</font>

 这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。

5.5 JDK源码解析-Runtime类

Runtime类就是使用的单例设计模式。

  1. 通过源代码查看使用的是哪儿种单例模式

    public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    /**
    * Returns the runtime object associated with the current Java application.
    * Most of the methods of class <code>Runtime</code> are instance
    * methods and must be invoked with respect to the current runtime object.
    *
    * @return the <code>Runtime</code> object associated with the current
    * Java application.
    */
    public static Runtime getRuntime() {
    return currentRuntime;
    }
    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
    }

从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。

  1. 使用Runtime类中的方法

    public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
    //获取Runtime类对象
    Runtime runtime = Runtime.getRuntime();
    //返回 Java 虚拟机中的内存总量。
    System.out.println(runtime.totalMemory());
    //返回 Java 虚拟机试图使用的最大内存量。
    System.out.println(runtime.maxMemory());
    //创建一个新的进程执行指定的字符串命令,返回进程对象
    Process process = runtime.exec("ipconfig");
    //获取命令执行后的结果,通过输入流获取
    InputStream inputStream = process.getInputStream();
    byte[] arr = new byte[1024 * 1024* 100];
    int b = inputStream.read(arr);
    System.out.println(new String(arr,0,b,"gbk"));
    }
    }

写在最后

  • 如果我的文章对你有用,请给我点个,感谢你!
  • 有问题,欢迎在评论区指出!
版权声明
本文为[康小庄]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000040228567

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云