设计模式之:单例模式的几种创建方式

单例模式使用案例

  1. 数据库的连接池;
  2. Spring中的Bean默认也是单例的;

单例模式的特性

  1. 将构造函数私有化
  2. 在类的内部创建实例
  3. 提供获取唯一实例的方法

编写单例模式的代码

饿汉式
public class Singleton {
    // 1.将构造函数私有化,不可以通过new的方式来创建对象
    private Singleton() {
    }
    // 2.在类的内部创建自行实例
    private static Singleton instance = new Singleton();
    // 3.提供获取唯一实例的方法
    public static Singleton getInstance() {
        return instance;
    }
}

这是实现一个安全的单例模式的最简单粗暴的写法,这种实现方式我们称之为饿汉式。之所以称之为饿汉式,是因为肚子很饿了,想马上吃到东西,不想等待生产时间。这种写法,在类被加载的时候就把Singleton实例给创建出来了,所以也不存在多线程竞争问题
饿汉式的缺点就是,一上来就创建对象了,如果该实例从始至终都没被使用过,则会造成内存浪费,没起到lazy loading的效果。

简单懒汉式
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上边方式在单线程环境下是行的,在多线程环境下就不行了,在并发获取实例的时候,可能会存在构建了多个实例的情况。所以,需要对此代码进行下改进,要解决也很简单,我们只要加锁就行。

public class SingletonSafe {
    private static volatile SingletonSafe singleton;
    private SingletonSafe() {
    }
    public static SingletonSafe getSingleton() {
        if (singleton == null) {  //这个判空主要是为了提升性能
            synchronized (SingletonSafe.class) {
                if (singleton == null) {    //不考虑性能只需要这一个判空就行
                    singleton = new SingletonSafe();
                }
            }
        }
        return singleton;
    }
}

这里采用了双重校验的方式,对懒汉式单例模式做了线程安全处理。通过加锁,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。

静态内部类方式
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。
似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。且看如下代码:

//反射方式获取
public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);   //结果为false,这两个实例不是同一个,违背了单例模式的原则
}

//序列化测试,需要类实现serialize接口
public static void main(String[] args) {
    Singleton instance = Singleton.getInstance();
    byte[] serialize = SerializationUtils.serialize(instance);
    Singleton newInstance = SerializationUtils.deserialize(serialize);
    System.out.println(instance == newInstance);  //结果为false,这两个实例不是同一个,违背了单例模式的原则
}
枚举类型实现

在effective java 中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

//第一种方式
public enum DataSourceEnum { 
    DATASOURCE; 
    private DBConnection connection = null; 
    private DataSourceEnum() { 
        connection = new DBConnection(); 
    } 
    public DBConnection getConnection() { 
        return connection; 
    } 
} 
//调用
public static void main(String[] args) { 
        DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection(); 
        DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection(); 
        System.out.println(con1 == con2); 
    }

//第二种方式在类里调用
public class User {
    //私有化构造函数
    private User(){ } 
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

总结:

- 创建单例的方式:
(1)饿汉式
(2)简单懒汉式(在方法加锁)
(3)DCL双重检测加锁(进阶懒汉式)
(4)静态内部类实现懒汉式
(5)枚举方式
- 为什么饿汉式是线程安全的呢?
  饿汉模式下,调用static方法去获取单例,这个时候,会触发初始化类,作为static field会首先被初始化,然后再被方法调用,不存在多线程竞争,更不存在线程安全的问题。
- 为什么静态内部类是线程安全的呢?
  首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的方法。
  <clinit>方法这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>方法。也就是说同一个加载器下,一个类型只会初始化一次。
  所以即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码厚炮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值