设计模式实践——单例模式

一、为什么要使用单例模式

二、单例模式的分类

1、懒汉模式

懒汉模式是指当对象第一次被请求获取实例时,才进行对象的创建。

1)普通单例


public class Singleton {

    //懒汉式--实际调用的时候才创建对象
    private static  Singleton lazyInstance;

    //私有化构造函数,避免在外部被实例化。
    private Singleton(){
        
    }
	//唯一对外暴露可以获取对象实例的方法
    public synchronized static Singleton getLazyInstance(){
        if(null == lazyInstance){
            lazyInstance = new Singleton();
        }
        return lazyInstance;
    }
    
}

这种是最简单的饿汉单例模式,但是有一个很大的缺陷就是每次想要获取lazyInstance都要竞争锁。

这样完全没有必要,因为lazyInstance在实例化后在JVM内存全局中只存在一个,任何方法调用都应该直接返回。反复竞争锁太消耗资源了。

为了解决普通单例锁竞争消耗资源的问题,我们一般采用DCL模式来获取单例。

2)DCL单例(double check lock)

public class Singleton {

    //懒汉式--实际调用的时候才创建对象
    //使用volatile关键字修饰是为了禁止指令重排序,以免线程获取到半初始化的对象
    //例如:线程A在 lazyInstance = new Singleton(); 时发生了指令重排序,导致lazyInstance里面全是空值,而不是初始化后的值。
    //     这时线程B调用getLazyInstance()方法,会把还没有完全初始化的lazyInstance取走,导致后续业务出现问题。
    //     所以DCL创建单例,单例对象需要用volatile修饰。
    private static volatile Singleton lazyInstance;

    //私有化构造函数,避免在外部被实例化。
    private Singleton(){
      // 初始化逻辑...
      // 初始化逻辑...
      // 初始化逻辑...
    }

   public static Singleton getLazyInstance(){
        //第一次判断,如果单例对象已经实例化,则直接返回,不需要再竞争锁
        if(null ==lazyInstance){
            //如果lazyInstance没有实例化,那么线程竞争锁进入实例化代码块
            synchronized (Singleton.class) {
                System.out.println(Thread.currentThread().getName()+"竞争到锁了!");
                //第二次判断,所有竞争锁的线程都会尝试对lazyInstance进行实例化。
                //所以需要第二次判断,只有第一个取到锁的线程才能进行实例化。
                if(null == lazyInstance) {
                    System.out.println(Thread.currentThread().getName()+"进行了初始化!");
                    lazyInstance = new Singleton();
                }else{
                    System.out.println(Thread.currentThread().getName()+"竞争到锁了,但是没有进行初始化。。。");
                }
            }
        }
        return lazyInstance;
    }
    
}

测试代码:

public static void main(String[] args) {
        int i = 0;
        //1000个线程调用getLazyInstance()方法
        while (i<1000){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Singleton.getLazyInstance());
                }
            }).start();
            i++;
        }
    }

输出结果:
在这里插入图片描述

2、饿汉模式

1)普通饿汉式

public class HungrySingleton {

    //普通饿汉式 类加载时就创建好对象
    private static  HungrySingleton hungryInstance = new HungrySingleton();

    private HungrySingleton(){
        
    }

    public static HungrySingleton getHungryInstance(){
         return hungryInstance;
    }


}

从上面代码可以看出,普通饿汉式就是在类加载阶段创建好对象。之后每次返回的都是一开始就创建好的对象,线程安全。

但是有个缺点就是如果单例对象比较多的话,会在类加载阶段消耗大量时间。
针对上述问题,可以采用内部类饿汉式加载。

2)内部类饿汉式实现懒加载

public class HungrySingleton {
	//使用内部类进行饿汉式单例
    private static class Singleton{
        private static HungrySingleton instance = new HungrySingleton();
    }

    private HungrySingleton(){
        
    }
	//调用获取单例方法时,才加载内部类,凭此实现懒加载
    public static HungrySingleton getHungryInstance(){
        return  Singleton.instance;
    }
}

3、枚举实现单例

以上不管饿汉式还是懒汉式都有缺陷,就是可以破坏单例。

例如:
使用反射调用私有的构造方法。
使用序列化/反序列化创建对象。

如果要解决上述两个问题,可以采用枚举实现单例,这也是目前最被推荐的实现单例的方法。

//不实现接口也可以用枚举实现单例,这里只是按实现接口的方式写出而已
public enum EnumSingleton implements Resource {

    SINGLETON{
        @Override
        public void doSomething() {
            System.out.println("执行了接口方法!");
        }
    }
}

interface Resource {
    void doSomething();
}

在我还没有了解枚举类型时,我不太明白枚举实现单例的应用场景是什么 其实现在也没太明白枚举

直到知道了枚举类型也可以继承,可以有自己的方法和内部变量。大体用法和普通java类类似。

我才明枚举实现单例和普通单例使用起来其实是一样的,只不过他有以下优点:

1、JVM会保证enum不能被反射并且构造器方法只执行一次。
2、此方法无偿提供了序列化机制,绝对防止反序列化时多次实例化。
3、运行时(compile-time )创建对象(懒加载)

和一个缺点:

不支持继承

单例模式资料传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值