1.二十三种设计模式 —— 单例模式

概述

单例设计模式(Singleton Pattern) 是一种:确保一个类在任何情况下在内存空间中都绝对只有一个实例并提
供一起全局访问点

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

不管new几次,在内存空间中都使用的是一个内存空间地址

如何做到呢?

我们知道创建一个对象就是通过new,来调用构造函数

所以,我们通过私有构造函数,也就是不能在别的类中new了。 然后暴露出一个对象,来完成上面的功能。

常见场景

  • Windows的任务管理器
  • Windows的回收站
  • 项目中,读取配置文件的类,一般也只有一个对象,没必要每次都去new对象读取
  • 网站的计数器一般也会采用单例模式,可以保证同步
  • 数据库连接池的设计一般也是单例模式
  • 在Servlet编程中,每个Servlet也是单例的
  • 在Spring中,每个Bean默认就是单例的

饿汉单例

普通

特点

  • 是线程安全的
  • 私有构造器

缺点

  • 会浪费内存

代码示例

package com.lcb.creatmodes.singletonpattern;

/**
 * 单例模式 —— 饿汉模式
 *
 *  核心:私有的构造器(即,私有的构造方法)  -->  造成的原因:别人无法new对象,保证内存中只有这一个对象
 *
 *  缺点:会浪费内存  【如:在类加载时就会创建对象hungryMode,如果只是创建但是不去用,就造成浪费空间。(结合JVM的创建一点对象的过程)】
 */
public class HungryMode {

    //1.私有的构造器
    private HungryMode(){

    }

    //2.私有的静态对象:保证该对象的唯一
    private static HungryMode hungryMode = new HungryMode();//如果创建的这个对象不用,就造成了内存的浪费

    //3.抛出一个对外的方法
    public static HungryMode getInstance(){
        return hungryMode;
    }
}

静态块初始的饿汉单例

【枚举底层的实现原理】

特点

  • 是线程安全的
  • 通过静态代码块来初始化对象
  • 解决了普通饿汉模式浪费空间的问题

代码示例

package com.lcb.creatmodes.singletonpattern;

/**
 * 单例模式 —— 饿汉模式
 *
 *  核心:私有的构造器(即,私有的构造方法)  -->  造成的原因:别人无法new对象,保证内存中只有这一个对象
 *
 */
public class HungryMode {

    //1.私有的构造器
    private HungryMode(){

    }

    //2.私有的静态对象:保证该对象的唯一
    private static HungryMode hungryMode;
    
    //2.1 通过静态代码块来创建对象
    static{//静态代码块只有在类第一次调用静态方法的时候才会执行
        hungryMode = new HungryMode();
    }

    //3.抛出一个对外的方法
    public static HungryMode getInstance(){
        return hungryMode;
    }
}

懒汉模式

特点

  • 不安全的
    • 原因:把创建对象放在方法中了,而方法是存在并发的

普通-懒汉模式

特点

  • 避免了空间的浪费
  • 线程不安全

缺点

  • 没有加任何锁。会出现线程安全的隐患。在多线程并发的情况下,无法保证单例。
  • 如果给实例的方法增加synchronized,由于是重量级锁,所有的方法都需要锁占用时间,导致资源浪费。除非在特殊的情况下,否则不建议用此种方式实现单例设计模式。

示例代码

package com.lcb.creatmodes.singletonpattern;

/**
 * 单例模式 —— 单线程懒汉模式
 *
 * 在单线程下可以使用,在多线程下不能使用
 */
public class LazyMode {

    private LazyMode(){

    }

    private static LazyMode lazyMode;

    public static LazyMode getInstance(){
        //1.第一次创建以后
        if (lazyMode == null){
            lazyMode = new LazyMode();
        }

        //2.第二次坐享其成
        return lazyMode;
    }
}

双重检测锁-懒汉模式

特点

  • 又称:DCL-懒汉模式
  • 解决了线程安全问题,比直接增加在方法上的锁,性能更佳

缺点

  • 还是存在锁问题,对程序性能还是存在一定的影响
  • 存在指令重排问题,需要加关键字:volatile

示例代码

package com.lcb.creatmodes.singletonpattern;

/**
 * 单例模式 —— DCL懒汉模式
 *      多重监测锁模式的 单例懒汉模式
 */
public class LazyMode02 {
    private LazyMode02(){

    }

    private static volatile LazyMode02 lazyMode;

    //双重监测锁模式的 懒汉式单例 简称DCI懒汉式
    /**
     * 写法一
     * @return
     */
    public static LazyMode02 getInstance(){
        if (lazyMode == null){
            synchronized (LazyMode02.class){
                if (lazyMode == null){
                    lazyMode = new LazyMode02();

                    /*
                    * lazyMode = new LazyMode02(); 这个过程在极端下是有问题的 不是原子性操作
                    *
                    * 该行代码的执行步骤:
                    *   1. 分配内存空间
                    *   2. 执行构造方法,初始化对象
                    *   3. 把这个对象指向这个空间
                    *
                    * 会发生指令重排的想象,
                    *       如:正确的指向顺序是1、2、3;
                    *          但是可能会指向成1、3、2,即先分配内存空间,再把这个空对象把这个内存空间占用了,占用之后再把创建的这个对象放进去,这在CPU中是可以实现的。
                    *          走1、3、2的这个线程A是没有问题的,但在执行3时,这时候突然又来了一个线程B,它会判断当前lazyMode不为null,就直接return lazyMode了,
                    *          而此时这个lazyMode还没有指向步骤2,未完成构造,这时候这个空间是一个虚无的,就可能产生问题
                    *
                    * 解决方法:
                    *       避免发生指令重排:在private static LazyMode02 lazyMode;代码中加入关键字:volatile
                    *       增加volatile的目的就是解决多线程并发过程中原子性问题,如果不增加可能B线程拿到一个A线程初始化不完整的一个对象实例。
                    *
                    *
                     */
                }
            }
        }

        return lazyMode;
    }

//    /**
//     * 写法二
//     * @return
//     */
//    public static LazyMode02 getInstance(){
//        //如果对象不为空,直接返回
//        if (lazyMode != null){
//            return lazyMode;
//        }
//
//        /*
//        * 如果当前对象为null,则进去一个线程,然后锁住,剩下的线程在此等待,
//        * 当第一个进去的线程创建完对象后,唤醒等待的线程,发现lazyMode对象不为空了,直接跳出,返回lazyMode
//        *   由此这种写法,把同步方法锁要好的多【public static synchronized LazyMode02 getInstance(){ }】
//        */
//        synchronized (LazyMode02.class){
//            if (lazyMode == null){
//                lazyMode = new LazyMode02();
//            }
//        }
//
//        return lazyMode;
//    }
}

静态内部类-懒汉模式

特点

  • 线程安全的
  • 无锁

缺点

  • 难以理解、难以阅读,需要对Java的底层要非常的熟悉

示例代码

package com.lcb.creatmodes.singletonpattern;

/**
 * 单例模式 —— 静态内部类懒汉模式
 *      利用了静态内部类是线程安全的机制
 */
public class LazyMode03 {

    //1.私有的构造方法
    private LazyMode03(){

    }

    //2.定义静态内部类
    private static class LazyInnerHolder{
        //2.1 创建对象
        private static final LazyMode03  LAZY_MODE_03 = new LazyMode03();
    }

    //3. 通过方法进行暴露对象
    public static final LazyMode03 getInstance(){
        return LazyInnerHolder.LAZY_MODE_03;
    }
}

CAS-懒汉模式

概述

除了使用静态内部类的方式解决懒汉的性能问题和线程安全问题。我们也可以使用 JUC 并发编程中的CAS机制来解决此问题。

在Java的并发库中提供很多原子类支持并发问题的数据安全性的原子类,比如:

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong
  • AtomicReference< V >等

使用AtomicReference< V >可以保证一个实例对象在并发访问的时候维持单例的特征

使用CAS的自旋(忙等算法的好处就是不需要使用传统加锁方式,而是依赖CAS的忙等算法,底层硬件的实现保证了多线程安全。相对于其他锁的实现,没有线程的切换和阻塞也就没有额外的开销,并且可以比较大的支持并发。

当然CAS也有一个缺点就是:忙等。如果一致没有获取到,会陷入死循环。

示例代码

package com.lcb.creatmodes.singletonpattern;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 单例模式 —— CAS懒汉模式
 *      CAS会进行自旋(忙等算法)
 */
public class LazyMode04 {

    //1. 私有构造方法
    private  LazyMode04(){

    }

    //2.使用AtomicReference 定义原子对象 -- 定义准备原子的对象
    private static final AtomicReference<LazyMode04> instance
            = new AtomicReference<>();

    //3.使用自旋的方式,抛出一个对象
    public static  final LazyMode04 getInstance(){
        for(;;){
            LazyMode04 lazyMode04 = instance.get();
            if (null != lazyMode04){
                return lazyMode04;
            }
            //如果不存在,就开始创建一个对象和null进行比较和交换
            instance.compareAndSet(null,new LazyMode04());//注:这里会出现性能损耗
            //执行完上边的交换后
            return instance.get();
        }
    }
}

注册表方式

这是一种单例思想

枚举单例模式

特点

  • 属于饿汉模式的衍生
  • 需要注意:要想是单例的,只能定义一个实例(如,此处自定义了一个INSTANCE),要是定义两个(如:INSTANCE,MAN;),那肯定就不是单例的了!!!
  • 是线程安全的
  • 它不能被攻破(如:反射、序列化、克隆),很安全,是一个 JVM底层的实现,把所有问题解决掉了

枚举:

  • 枚举的底层就是一个class类
  • 本身的构造函数就是私有的

示例代码

package com.lcb.creatmodes.singletonpattern;


public enum Registry {

    //对象实例化  ---  静态实例化
    INSTANCE;

    public static Registry getInstance(){
        return INSTANCE;
    }
}

//该类的底层源码
//public class Registry extends Enum{
//    private Registry{
//
//    }
//
//    private static final Registry INSTANCE = null;
//
//    static {
//        INSTANCE = new Registry();
//    }
//
//    public static Registry getInstance(){
//        return INSTANCE;
//    }
//}

容器单例模式

破坏单例模式

破坏不了枚举的单例,因为枚举在JVM底层进行了完善

序列化破坏

准备

  • 要破坏的单例类要实现序列化接口。(如:public class LazyMode02 implements Serializable {})

  • 写出对象的方法

  • 读取写出文件中对象的方法

示例代码

package com.lcb.creatmodes.singletonpattern.destruction;

import com.lcb.creatmodes.singletonpattern.LazyMode02;

import java.io.*;

/**
 * 序列化破坏——单例模式
 */
public class SerializableDestruction {
    public static void main(String[] args) throws Exception{
        //写出对象
        //writeObjectFromFile();


        //读取对象
        LazyMode02 lazyMode02 = readObjectFromFile();
        LazyMode02 lazyMode021 = readObjectFromFile();

        System.out.println(lazyMode02);
        System.out.println(lazyMode021);

    }

    //从文件中读取数据(读取对象)
    public static LazyMode02 readObjectFromFile() throws Exception {
        //1.创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\86159\\Desktop\\1.txt"));

        //读取对象
        LazyMode02 lazyMode02 = (LazyMode02)ois.readObject();

        //释放资源
        ois.close();

        return lazyMode02;
    }

    //把对象写入文件中
    public static void writeObjectFromFile() throws Exception{

        //1.获取对象
        LazyMode02 instance = LazyMode02.getInstance();

        //2.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\86159\\Desktop\\1.txt"));

        //3.写出对象
        oos.writeObject(instance);

        //4.释放资源
        oos.close();

        System.out.println("写出对象成功!");
    }
}

在这里插入图片描述

反射破坏

代码

package com.lcb.creatmodes.singletonpattern.destruction;

import com.lcb.creatmodes.singletonpattern.LazyMode02;

import java.lang.reflect.Constructor;

/**
 * 反射破坏 —— 单例模式
 */
public class ReflexDestruction {
    public static void main(String[] args) throws Exception {

        //1.获取LazyMode02的字节码对象
        Class<LazyMode02> lazyMode02Class = LazyMode02.class;

        //2.通过反射获得无参构造函数
        Constructor declaredConstructor = lazyMode02Class.getDeclaredConstructor();

        //3.取消安全监测
        declaredConstructor.setAccessible(true);
        //4.创建对象
        LazyMode02 lazyMode02 = (LazyMode02) declaredConstructor.newInstance();
        LazyMode02 lazyMode021 = (LazyMode02) declaredConstructor.newInstance();

        System.out.println(lazyMode02);
        System.out.println(lazyMode021);
    }
}

在这里插入图片描述

破坏单例模式的一般解决办法

序列化解决

看源码 —— readObject()

  • 发现会先去判断是否有readResolve()方法。如果有,则调用该方法,如果没有在重新new对象

示例代码

通过在被破坏的单例模式类中添加一个readResolve()方法,通过它来抛出对象

    public Object readResolve(){
        return getInstance();
    }

反射解决

步骤

  • 定义一个标志类
  • 在构造方法中进行判断
  • 加锁是为了保证并发安全

示例代码

    private static boolean flag = false;

    private LazyMode02(){
        synchronized (LazyMode02.class){
            if (flag){
                throw new RuntimeException("不能创建多个对象");
            }

            flag = true;
        }

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值