你说你熟悉java设计模式,那单例模式的这几种创建方式你都知道吗

单例模式使用案例

  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 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 { 
    INSTANCE; 
    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方法。也就是说同一个加载器下,一个类型只会初始化一次。

所以即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。之所以这里变量定义的时候不需要volatil,因为只有一个线程会执行具体的类的初始化代码clinit。

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

你可能感兴趣的:(你说你熟悉java设计模式,那单例模式的这几种创建方式你都知道吗)