Java设计模式之单例模式示例详解

 更新时间:2021年12月27日 10:02:03   作者:LL.LEBRON  
单例模式是最简单也是最基础的设计模式之一,单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。本文将通过一些示例代码为大家详细介绍一下单例模式,感兴趣的可以了解一下

0.概述

为什么要使用单例模式?

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。因此这里需要用到单例模式

使用单例模式的好处?

对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销

由于new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间

1.饿汉式

1.1 饿汉式单例实现

实例会提前创建:

/**
* 饿汉式
*
* @author xppll
* @date 2021/12/24 21:21
*/
public class Singleton1 implements Serializable {
    //构造私有
    private Singleton1() {
        System.out.println("private Singleton1()");
    }

    //唯一实例
    private static final Singleton1 INSTANCE = new Singleton1();

    //获得实例方法
    public static Singleton1 getINSTANCE() {
        return INSTANCE;
    }

    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

/**
 * @author xppll
 * @date 2021/12/24 21:28
 */
public class TestSingleton {
    public static void main(String[] args) {
        //触发Singleton1类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton1.otherMethod();
        System.out.println("-----------------------------------");
        System.out.println(Singleton1.getINSTANCE());
        System.out.println(Singleton1.getINSTANCE());
    }
}
//输出:
private Singleton1()
otherMethod()
-----------------------------------
singleton.Singleton1@10bedb4
singleton.Singleton1@10bedb4

1.2 破坏单例的几种情况

1.反射破坏单例

2.反序列化破坏单例

3.Unsafe破坏单例

演示:

/**
 * @author xppll
 * @date 2021/12/24 21:28
 */
public class TestSingleton {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        //触发Singleton1类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton1.otherMethod();
        System.out.println("-----------------------------------");
        System.out.println(Singleton1.getINSTANCE());
        System.out.println(Singleton1.getINSTANCE());

        //反射破坏单例
        reflection(Singleton1.class);

        //反序列化破坏单例
        serializable(Singleton1.getINSTANCE());

        //Unsafe破坏单例
        unsafe(Singleton1.class);

    }
	//反射破坏单例
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //得到无参
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        //将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
        constructor.setAccessible(true);
        //创建实例
        System.out.println("反射创建实例:" + constructor.newInstance());
    }
	//反序列化破坏单例
    private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        //序列化
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        //反序列化
        System.out.println("反序列化创建示例:" + ois.readObject());
    }
	//Unsafe破坏单例
    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
        System.out.println("Unsafe 创建实例:" + o);
    }

}

结果:

可以看出三种方式都会破坏单例!

1.3 预防单例的破坏

预防反射破坏单例

在构造方法中加个判断即可:

//构造私有
private Singleton1() {
    //防止反射破坏单例
    if(INSTANCE!=null){
        throw new RuntimeException("单例对象不能重复创建");
    }
    System.out.println("private Singleton1()");
}

预防反序列化破坏单例

Singleton1()中重写readResolve方法:

//重写这个方法,如果序列化了,就会返回这个,不会返回反序列化的对象
public Object readResolve(){
    return  INSTANCE;
}

Unsafe破坏单例无法预防

2.枚举饿汉式

2.1 枚举单例实现

枚举实现单例:

/**
 * 枚举实现单例
 *
 * @author xppll
 * @date 2021/12/24 22:23
 */
public enum Singleton2 {
    INSTANCE;

    //枚举的构造方法默认是private的,可以不写
    Singleton2() {
        System.out.println("private Singleton2()");
    }

    //重写toString方法
    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    //获得实例方法(这个可以不要,枚举变量都是public的)
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

/**
 * @author xppll
 * @date 2021/12/24 21:28
 */
public class TestSingleton {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        //触发Singleton2类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton2.otherMethod();
        System.out.println("-----------------------------------");
        System.out.println(Singleton2.getInstance());
        System.out.println(Singleton2.getInstance());
    }
}
//输出:
private Singleton2()
otherMethod()
-----------------------------------
singleton.Singleton2@2de80c
singleton.Singleton2@2de80c

可以看出当调用otherMethod()时,就会触发类的加载,枚举对象就会创建,所以枚举实现单例是饿汉式的

2.2 破坏单例

枚举类实现单例的好处:

1.反序列化无法破坏枚举单例

2.反射无法破坏枚举单例

栗子:

需要先修改反射破坏代码,枚举需要有参构造

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        Singleton5.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());

        //反序列化破坏单例
        serializable(Singleton2.getInstance());

        //Unsafe破坏单例
        unsafe(Singleton2.class);

        //反射破坏单例
        reflection(Singleton2.class);
    }

    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
        System.out.println("Unsafe 创建实例:" + o);
    }

    private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:" + ois.readObject());
    }

    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        System.out.println("反射创建实例:" + constructor.newInstance());
    }
}

结果:

可以看出

1.反射是无法创建枚举对象!无法破坏枚举单例

2.反序列化也不会破坏枚举单例!

3.Unsafe依然会破坏!

3.懒汉式

实现代码如下:

/**
 * 懒汉式
 *
 * @author xppll
 * @date 2021/12/25 08:34
 */
public class Singleton3 implements Serializable {
    //构造私有
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    //唯一实例
    private static Singleton3 INSTANCE = null;

    public static Singleton3 getInstance() {
        //第一次调用的时候才创建
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

/**
 * @author xppll
 * @date 2021/12/24 21:28
 */
public class TestSingleton {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton3.otherMethod();
        System.out.println("-----------------------------------");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());
    }
}

结果:

可以看出只有在第一次调用getInstance()时才会创建唯一的单例对象,因此是懒汉式的。

但是这种方式在多线程环境下是会有问题的,可能多个线程会同时执行INSTANCE = new Singleton3();。因此这里需要在getInstance()方法上加上synchronized关键字保证多线程下的正确性:

public static synchronized Singleton3 getInstance() {
    //第一次调用的时候才创建
    if (INSTANCE == null) {
        INSTANCE = new Singleton3();
    }
    return INSTANCE;
}

但是这种方法是有问题的,第一次创建完对象后,以后的操作是不需要在加锁的,所以这种方式会影响性能!

我们的目标应该是第一次创建单例的时候给予保护,后续操作则不需要加锁保护!

4.双检锁懒汉式

针对上面的问题,这里给出第四种方法双检锁懒汉式进行优化:

/**
 * 双检锁懒汉式
 *
 * @author xppll
 * @date 2021/12/25 08:53
 */
public class Singleton4 {
    //构造私有
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    //唯一实例
    //这里volatile的作用是保证共享变量有序性!
    private static volatile Singleton4 INSTANCE = null;

    //双检锁优化
    public static synchronized Singleton4 getInstance() {
        //实例没创建,才会进入内部的 synchronized 代码块,提高性能,防止每次都加锁
        if (INSTANCE == null) {
            //可能第一个线程在synchronized 代码块还没创建完对象时,第二个线程已经到了这一步,所以里面还需要加上判断
            synchronized (Singleton4.class) {
                //也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

5.内部类懒汉式

内部类懒汉式单例实现:

/**
 * 内部类懒汉式
 *
 * @author xppll
 * @date 2021/12/25 09:24
 */
public class Singleton5 {

    //构造私有
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    //静态内部类实现懒汉式单例,静态变量的创建会放在静态代码块里执行,jvm会保证其线程安全
    //只有第一次用到内部类时,才会初始化创建单例
    private static class Holder {
        static Singleton5 INSTANCE = new Singleton5();
    }

    //获得实例方法
    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

/**
 * @author xppll
 * @date 2021/12/24 21:28
 */
public class TestSingleton {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton5.otherMethod();
        System.out.println("-----------------------------------");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());
    }
}

结果:

可以看出内部类实现单例也是懒汉式的!

6.JDK中单例的体现

Runtime 体现了饿汉式单例

System类下的Console 体现了双检锁懒汉式单例

Collections 中的 EmptyNavigableSet内部类懒汉式单例

Collections 中的ReverseComparator.REVERSE_ORDER 内部类懒汉式单例

Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例

到此这篇关于Java设计模式之单例模式示例详解的文章就介绍到这了,更多相关Java单例模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java实现身份证号码验证的示例代码

    java实现身份证号码验证的示例代码

    这篇文章主要为大家详细介绍了如何利用java语言实现身份证号码验证的功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-09-09
  • java自定义日志输出文件(log4j日志文件输出多个自定义日志文件)

    java自定义日志输出文件(log4j日志文件输出多个自定义日志文件)

    打印日志的在程序中是必不可少的,如果需要将不同的日志打印到不同的地方,则需要定义不同的Appender,然后定义每一个Appender的日志级别、打印形式和日志的输出路径,下面看一个示例吧
    2014-01-01
  • Mybatis批量修改时出现报错问题解决方案

    Mybatis批量修改时出现报错问题解决方案

    这篇文章主要介绍了Mybatis批量修改时出现报错问题解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • java8之lambda表达式用法总结

    java8之lambda表达式用法总结

    这篇文章主要介绍了java8之lambda表达式用法总结,需要的朋友可以参考下
    2020-02-02
  • Java之获取客户端真实IP地址的实现

    Java之获取客户端真实IP地址的实现

    在开发工作中,我们常常需要获取客户端的IP,本文主要介绍了Jav之获取客户端真实IP地址的实现,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12
  • 轻松掌握Java观察者模式

    轻松掌握Java观察者模式

    这篇文章主要帮助大家轻松掌握Java观察者模式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-09-09
  • Springboot 读取自定义pro文件注入static静态变量方式

    Springboot 读取自定义pro文件注入static静态变量方式

    这篇文章主要介绍了Springboot 读取自定义pro文件注入static静态变量方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Java web网站访问量的统计

    Java web网站访问量的统计

    这篇文章主要为大家详细介绍了Java web网站访问量的统计,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • Java Stream 流中 Collectors.toMap 的用法详解

    Java Stream 流中 Collectors.toMap 的用法详解

    这篇文章主要介绍了Stream 流中 Collectors.toMap 的用法,Collectors.toMap()方法是把List转Map的操作,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2024-01-01
  • Javas使用Redlock实现分布式锁过程解析

    Javas使用Redlock实现分布式锁过程解析

    这篇文章主要介绍了Javas使用Redlock实现分布式锁过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08

最新评论